Skip to content

Commit 2bf55d8

Browse files
committed
Allow predefined ids for encrypted saved objects
1 parent 3ba7758 commit 2bf55d8

7 files changed

+139
-8
lines changed

x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,18 @@ it('correctly determines attribute properties', () => {
111111
}
112112
}
113113
});
114+
115+
it('it correctly sets allowPredefinedID', () => {
116+
const defaultTypeDefinition = new EncryptedSavedObjectAttributesDefinition({
117+
type: 'so-type',
118+
attributesToEncrypt: new Set(['attr#1', 'attr#2']),
119+
});
120+
expect(defaultTypeDefinition.allowPredefinedID).toBeFalsy();
121+
122+
const typeDefinitionWithPredefinedIDAllowed = new EncryptedSavedObjectAttributesDefinition({
123+
type: 'so-type',
124+
attributesToEncrypt: new Set(['attr#1', 'attr#2']),
125+
allowPredefinedID: true,
126+
});
127+
expect(typeDefinitionWithPredefinedIDAllowed.allowPredefinedID).toBe(true);
128+
});

x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_object_type_definition.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export class EncryptedSavedObjectAttributesDefinition {
1515
public readonly attributesToEncrypt: ReadonlySet<string>;
1616
private readonly attributesToExcludeFromAAD: ReadonlySet<string> | undefined;
1717
private readonly attributesToStrip: ReadonlySet<string>;
18+
public readonly allowPredefinedID?: boolean;
1819

1920
constructor(typeRegistration: EncryptedSavedObjectTypeRegistration) {
2021
const attributesToEncrypt = new Set<string>();
@@ -34,6 +35,7 @@ export class EncryptedSavedObjectAttributesDefinition {
3435
this.attributesToEncrypt = attributesToEncrypt;
3536
this.attributesToStrip = attributesToStrip;
3637
this.attributesToExcludeFromAAD = typeRegistration.attributesToExcludeFromAAD;
38+
this.allowPredefinedID = typeRegistration.allowPredefinedID;
3739
}
3840

3941
/**

x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.mocks.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ export const encryptedSavedObjectsServiceMock = {
5252
mock.isRegistered.mockImplementation(
5353
(type) => registrations.findIndex((r) => r.type === type) >= 0
5454
);
55+
mock.allowPredefinedID.mockImplementation((type) => {
56+
const registration = registrations.find((r) => r.type === type);
57+
if (!registration) {
58+
return true;
59+
}
60+
return registration.allowPredefinedID === true;
61+
});
5562
mock.encryptAttributes.mockImplementation(async (descriptor, attrs) =>
5663
processAttributes(
5764
descriptor,

x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,35 @@ describe('#isRegistered', () => {
8989
});
9090
});
9191

92+
describe('#allowPredefinedID', () => {
93+
it('returns true for unknown types', () => {
94+
expect(service.allowPredefinedID('unknown-type')).toBe(true);
95+
});
96+
97+
it('returns true for types registered setting allowPredefinedID to true', () => {
98+
service.registerType({
99+
type: 'known-type-1',
100+
attributesToEncrypt: new Set(['attr-1']),
101+
allowPredefinedID: true,
102+
});
103+
expect(service.allowPredefinedID('known-type-1')).toBe(true);
104+
});
105+
106+
it('returns false for types registered without setting allowPredefinedID', () => {
107+
service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attr-1']) });
108+
expect(service.allowPredefinedID('known-type-1')).toBe(false);
109+
});
110+
111+
it('returns false for types registered setting allowPredefinedID to false', () => {
112+
service.registerType({
113+
type: 'known-type-1',
114+
attributesToEncrypt: new Set(['attr-1']),
115+
allowPredefinedID: false,
116+
});
117+
expect(service.allowPredefinedID('known-type-1')).toBe(false);
118+
});
119+
});
120+
92121
describe('#stripOrDecryptAttributes', () => {
93122
it('does not strip attributes from unknown types', async () => {
94123
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };

x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface EncryptedSavedObjectTypeRegistration {
3131
readonly type: string;
3232
readonly attributesToEncrypt: ReadonlySet<string | AttributeToEncrypt>;
3333
readonly attributesToExcludeFromAAD?: ReadonlySet<string>;
34+
readonly allowPredefinedID?: boolean;
3435
}
3536

3637
/**
@@ -144,6 +145,19 @@ export class EncryptedSavedObjectsService {
144145
return this.typeDefinitions.has(type);
145146
}
146147

148+
/**
149+
* Checks whether specified saved object type supports predefined IDs. If the type isn't registered
150+
* as an encrypted saved object type, this will return "true".
151+
* @param type Saved object type.
152+
*/
153+
public allowPredefinedID(type: string) {
154+
const typeDefinitions = this.typeDefinitions.get(type);
155+
if (typeDefinitions === undefined) {
156+
return true;
157+
}
158+
return typeDefinitions.allowPredefinedID === true;
159+
}
160+
147161
/**
148162
* Takes saved object attributes for the specified type and, depending on the type definition,
149163
* either decrypts or strips encrypted attributes (e.g. in case AAD or encryption key has changed

x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ beforeEach(() => {
3030
{ key: 'attrNotSoSecret', dangerouslyExposeValue: true },
3131
]),
3232
},
33+
{
34+
type: 'known-type-predefined-id',
35+
attributesToEncrypt: new Set(['attrSecret']),
36+
allowPredefinedID: true,
37+
},
3338
]);
3439

3540
wrapper = new EncryptedSavedObjectsClientWrapper({
@@ -72,7 +77,7 @@ describe('#create', () => {
7277
expect(mockBaseClient.create).toHaveBeenCalledWith('unknown-type', attributes, options);
7378
});
7479

75-
it('fails if type is registered and ID is specified', async () => {
80+
it('fails if type is registered without allowPredefinedID and ID is specified', async () => {
7681
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
7782

7883
await expect(wrapper.create('known-type', attributes, { id: 'some-id' })).rejects.toThrowError(
@@ -82,6 +87,26 @@ describe('#create', () => {
8287
expect(mockBaseClient.create).not.toHaveBeenCalled();
8388
});
8489

90+
it('succeeds if type is registered with allowPredefinedID and ID is specified', async () => {
91+
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
92+
const mockedResponse = {
93+
id: 'some-id',
94+
type: 'known-type-predefined-id',
95+
attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
96+
references: [],
97+
};
98+
99+
mockBaseClient.create.mockResolvedValue(mockedResponse);
100+
await expect(
101+
wrapper.create('known-type-predefined-id', attributes, { id: 'some-id' })
102+
).resolves.toEqual({
103+
...mockedResponse,
104+
attributes: { attrOne: 'one', attrThree: 'three' },
105+
});
106+
107+
expect(mockBaseClient.create).toHaveBeenCalled();
108+
});
109+
85110
it('allows a specified ID when overwriting an existing object', async () => {
86111
const attributes = {
87112
attrOne: 'one',
@@ -299,7 +324,7 @@ describe('#bulkCreate', () => {
299324
);
300325
});
301326

302-
it('fails if ID is specified for registered type', async () => {
327+
it('fails if ID is specified for registered type without allowPredefinedID', async () => {
303328
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
304329

305330
const bulkCreateParams = [
@@ -314,6 +339,42 @@ describe('#bulkCreate', () => {
314339
expect(mockBaseClient.bulkCreate).not.toHaveBeenCalled();
315340
});
316341

342+
it('succeeds if ID is specified for registered type with allowPredefinedID', async () => {
343+
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
344+
const options = { namespace: 'some-namespace' };
345+
const mockedResponse = {
346+
saved_objects: [
347+
{
348+
id: 'some-id',
349+
type: 'known-type-predefined-id',
350+
attributes,
351+
references: [],
352+
},
353+
{
354+
id: 'some-id',
355+
type: 'unknown-type',
356+
attributes,
357+
references: [],
358+
},
359+
],
360+
};
361+
mockBaseClient.bulkCreate.mockResolvedValue(mockedResponse);
362+
363+
const bulkCreateParams = [
364+
{ id: 'some-id', type: 'known-type-predefined-id', attributes },
365+
{ type: 'unknown-type', attributes },
366+
];
367+
368+
await expect(wrapper.bulkCreate(bulkCreateParams, options)).resolves.toEqual({
369+
saved_objects: [
370+
{ ...mockedResponse.saved_objects[0], attributes: { attrOne: 'one', attrThree: 'three' } },
371+
mockedResponse.saved_objects[1],
372+
],
373+
});
374+
375+
expect(mockBaseClient.bulkCreate).toHaveBeenCalled();
376+
});
377+
317378
it('allows a specified ID when overwriting an existing object', async () => {
318379
const attributes = {
319380
attrOne: 'one',

x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,17 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
6868
}
6969

7070
// Saved objects with encrypted attributes should have IDs that are hard to guess especially
71-
// since IDs are part of the AAD used during encryption, that's why we control them within this
72-
// wrapper and don't allow consumers to specify their own IDs directly.
71+
// since IDs are part of the AAD used during encryption. Types can opt-out of this restriction,
72+
// when necessary, but it's much safer for this wrapper to generate them.
7373

7474
// only allow a specified ID if we're overwriting an existing ESO with a Version
7575
// this helps us ensure that the document really was previously created using ESO
7676
// and not being used to get around the specified ID limitation
77-
const canSpecifyID = options.overwrite && options.version;
77+
const canSpecifyID =
78+
(options.overwrite && options.version) || this.options.service.allowPredefinedID(type);
7879
if (options.id && !canSpecifyID) {
7980
throw new Error(
80-
'Predefined IDs are not allowed for saved objects with encrypted attributes.'
81+
`Predefined IDs are not allowed for encrypted saved objects of type "${type}".`
8182
);
8283
}
8384

@@ -118,10 +119,12 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
118119
// Saved objects with encrypted attributes should have IDs that are hard to guess especially
119120
// since IDs are part of the AAD used during encryption, that's why we control them within this
120121
// wrapper and don't allow consumers to specify their own IDs directly unless overwriting the original document.
121-
const canSpecifyID = options?.overwrite && object.version;
122+
const canSpecifyID =
123+
(options?.overwrite && object.version) ||
124+
this.options.service.allowPredefinedID(object.type);
122125
if (object.id && !canSpecifyID) {
123126
throw new Error(
124-
'Predefined IDs are not allowed for saved objects with encrypted attributes.'
127+
`Predefined IDs are not allowed for encrypted saved objects of type "${object.type}".`
125128
);
126129
}
127130

0 commit comments

Comments
 (0)