Skip to content

Commit 7d929fe

Browse files
authored
Allow predefined ids for encrypted saved objects (#83482)
* Allow predefined ids for encrypted saved objects * Fix mock * fix tests * Added suggestions from code review * added jsdocs params * Fixed jsdocs
1 parent d51437e commit 7d929fe

7 files changed

+160
-16
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
@@ -113,3 +113,18 @@ it('correctly determines attribute properties', () => {
113113
}
114114
}
115115
});
116+
117+
it('it correctly sets allowPredefinedID', () => {
118+
const defaultTypeDefinition = new EncryptedSavedObjectAttributesDefinition({
119+
type: 'so-type',
120+
attributesToEncrypt: new Set(['attr#1', 'attr#2']),
121+
});
122+
expect(defaultTypeDefinition.allowPredefinedID).toBe(false);
123+
124+
const typeDefinitionWithPredefinedIDAllowed = new EncryptedSavedObjectAttributesDefinition({
125+
type: 'so-type',
126+
attributesToEncrypt: new Set(['attr#1', 'attr#2']),
127+
allowPredefinedID: true,
128+
});
129+
expect(typeDefinitionWithPredefinedIDAllowed.allowPredefinedID).toBe(true);
130+
});

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
@@ -13,6 +13,7 @@ import {
1313
function createEncryptedSavedObjectsServiceMock() {
1414
return ({
1515
isRegistered: jest.fn(),
16+
canSpecifyID: jest.fn(),
1617
stripOrDecryptAttributes: jest.fn(),
1718
encryptAttributes: jest.fn(),
1819
decryptAttributes: jest.fn(),
@@ -52,6 +53,12 @@ export const encryptedSavedObjectsServiceMock = {
5253
mock.isRegistered.mockImplementation(
5354
(type) => registrations.findIndex((r) => r.type === type) >= 0
5455
);
56+
mock.canSpecifyID.mockImplementation((type, version, overwrite) => {
57+
const registration = registrations.find((r) => r.type === type);
58+
return (
59+
registration === undefined || registration.allowPredefinedID || !!(version && overwrite)
60+
);
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: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,45 @@ describe('#isRegistered', () => {
8989
});
9090
});
9191

92+
describe('#canSpecifyID', () => {
93+
it('returns true for unknown types', () => {
94+
expect(service.canSpecifyID('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.canSpecifyID('known-type-1')).toBe(true);
104+
});
105+
106+
it('returns true when overwriting a saved object with a version specified even when allowPredefinedID is not set', () => {
107+
service.registerType({
108+
type: 'known-type-1',
109+
attributesToEncrypt: new Set(['attr-1']),
110+
});
111+
expect(service.canSpecifyID('known-type-1', '2', true)).toBe(true);
112+
expect(service.canSpecifyID('known-type-1', '2', false)).toBe(false);
113+
expect(service.canSpecifyID('known-type-1', undefined, true)).toBe(false);
114+
});
115+
116+
it('returns false for types registered without setting allowPredefinedID', () => {
117+
service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attr-1']) });
118+
expect(service.canSpecifyID('known-type-1')).toBe(false);
119+
});
120+
121+
it('returns false for types registered setting allowPredefinedID to false', () => {
122+
service.registerType({
123+
type: 'known-type-1',
124+
attributesToEncrypt: new Set(['attr-1']),
125+
allowPredefinedID: false,
126+
});
127+
expect(service.canSpecifyID('known-type-1')).toBe(false);
128+
});
129+
});
130+
92131
describe('#stripOrDecryptAttributes', () => {
93132
it('does not strip attributes from unknown types', async () => {
94133
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };

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

Lines changed: 20 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,25 @@ export class EncryptedSavedObjectsService {
144145
return this.typeDefinitions.has(type);
145146
}
146147

148+
/**
149+
* Checks whether ID can be specified for the provided saved object.
150+
*
151+
* If the type isn't registered as an encrypted saved object, or when overwriting an existing
152+
* saved object with a version specified, this will return "true".
153+
*
154+
* @param type Saved object type.
155+
* @param version Saved object version number which changes on each successful write operation.
156+
* Can be used in conjunction with `overwrite` for implementing optimistic concurrency
157+
* control.
158+
* @param overwrite Overwrite existing documents.
159+
*/
160+
public canSpecifyID(type: string, version?: string, overwrite?: boolean) {
161+
const typeDefinition = this.typeDefinitions.get(type);
162+
return (
163+
typeDefinition === undefined || typeDefinition.allowPredefinedID || !!(version && overwrite)
164+
);
165+
}
166+
147167
/**
148168
* Takes saved object attributes for the specified type and, depending on the type definition,
149169
* 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: 65 additions & 4 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,16 +77,36 @@ 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(
79-
'Predefined IDs are not allowed for saved objects with encrypted attributes.'
84+
'Predefined IDs are not allowed for encrypted saved objects of type "known-type".'
8085
);
8186

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 = [
@@ -308,12 +333,48 @@ describe('#bulkCreate', () => {
308333
];
309334

310335
await expect(wrapper.bulkCreate(bulkCreateParams)).rejects.toThrowError(
311-
'Predefined IDs are not allowed for saved objects with encrypted attributes.'
336+
'Predefined IDs are not allowed for encrypted saved objects of type "known-type".'
312337
);
313338

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: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,14 @@ 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.
73-
74-
// only allow a specified ID if we're overwriting an existing ESO with a Version
75-
// this helps us ensure that the document really was previously created using ESO
76-
// and not being used to get around the specified ID limitation
77-
const canSpecifyID = options.overwrite && options.version;
78-
if (options.id && !canSpecifyID) {
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.
73+
if (
74+
options.id &&
75+
!this.options.service.canSpecifyID(type, options.version, options.overwrite)
76+
) {
7977
throw new Error(
80-
'Predefined IDs are not allowed for saved objects with encrypted attributes.'
78+
`Predefined IDs are not allowed for encrypted saved objects of type "${type}".`
8179
);
8280
}
8381

@@ -118,10 +116,12 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
118116
// Saved objects with encrypted attributes should have IDs that are hard to guess especially
119117
// since IDs are part of the AAD used during encryption, that's why we control them within this
120118
// 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-
if (object.id && !canSpecifyID) {
119+
if (
120+
object.id &&
121+
!this.options.service.canSpecifyID(object.type, object.version, options?.overwrite)
122+
) {
123123
throw new Error(
124-
'Predefined IDs are not allowed for saved objects with encrypted attributes.'
124+
`Predefined IDs are not allowed for encrypted saved objects of type "${object.type}".`
125125
);
126126
}
127127

0 commit comments

Comments
 (0)