Allow encrypted saved-object properties to be accessed by end-users (#64941)
Co-authored-by: kobelb <brandon.kobel@elastic.co>
This commit is contained in:
parent
0fd5311a82
commit
65186b3393
|
@ -3,6 +3,7 @@
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"kibanaVersion": "kibana",
|
"kibanaVersion": "kibana",
|
||||||
"configPath": ["xpack", "encryptedSavedObjects"],
|
"configPath": ["xpack", "encryptedSavedObjects"],
|
||||||
|
"optionalPlugins": ["security"],
|
||||||
"server": true,
|
"server": true,
|
||||||
"ui": false
|
"ui": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EncryptedSavedObjectsAuditLogger } from './audit_logger';
|
import { EncryptedSavedObjectsAuditLogger } from './audit_logger';
|
||||||
|
import { mockAuthenticatedUser } from '../../../security/common/model/authenticated_user.mock';
|
||||||
|
|
||||||
test('properly logs audit events', () => {
|
it('properly logs audit events', () => {
|
||||||
const mockInternalAuditLogger = { log: jest.fn() };
|
const mockInternalAuditLogger = { log: jest.fn() };
|
||||||
const audit = new EncryptedSavedObjectsAuditLogger(() => mockInternalAuditLogger);
|
const audit = new EncryptedSavedObjectsAuditLogger(() => mockInternalAuditLogger);
|
||||||
|
|
||||||
|
@ -19,6 +20,11 @@ test('properly logs audit events', () => {
|
||||||
id: 'object-id-ns',
|
id: 'object-id-ns',
|
||||||
namespace: 'object-ns',
|
namespace: 'object-ns',
|
||||||
});
|
});
|
||||||
|
audit.encryptAttributesSuccess(
|
||||||
|
['one', 'two'],
|
||||||
|
{ type: 'known-type-ns', id: 'object-id-ns', namespace: 'object-ns' },
|
||||||
|
mockAuthenticatedUser()
|
||||||
|
);
|
||||||
|
|
||||||
audit.decryptAttributesSuccess(['three', 'four'], {
|
audit.decryptAttributesSuccess(['three', 'four'], {
|
||||||
type: 'known-type-1',
|
type: 'known-type-1',
|
||||||
|
@ -29,6 +35,11 @@ test('properly logs audit events', () => {
|
||||||
id: 'object-id-1-ns',
|
id: 'object-id-1-ns',
|
||||||
namespace: 'object-ns',
|
namespace: 'object-ns',
|
||||||
});
|
});
|
||||||
|
audit.decryptAttributesSuccess(
|
||||||
|
['three', 'four'],
|
||||||
|
{ type: 'known-type-1-ns', id: 'object-id-1-ns', namespace: 'object-ns' },
|
||||||
|
mockAuthenticatedUser()
|
||||||
|
);
|
||||||
|
|
||||||
audit.encryptAttributeFailure('five', {
|
audit.encryptAttributeFailure('five', {
|
||||||
type: 'known-type-2',
|
type: 'known-type-2',
|
||||||
|
@ -39,6 +50,11 @@ test('properly logs audit events', () => {
|
||||||
id: 'object-id-2-ns',
|
id: 'object-id-2-ns',
|
||||||
namespace: 'object-ns',
|
namespace: 'object-ns',
|
||||||
});
|
});
|
||||||
|
audit.encryptAttributeFailure(
|
||||||
|
'five',
|
||||||
|
{ type: 'known-type-2-ns', id: 'object-id-2-ns', namespace: 'object-ns' },
|
||||||
|
mockAuthenticatedUser()
|
||||||
|
);
|
||||||
|
|
||||||
audit.decryptAttributeFailure('six', {
|
audit.decryptAttributeFailure('six', {
|
||||||
type: 'known-type-3',
|
type: 'known-type-3',
|
||||||
|
@ -49,8 +65,13 @@ test('properly logs audit events', () => {
|
||||||
id: 'object-id-3-ns',
|
id: 'object-id-3-ns',
|
||||||
namespace: 'object-ns',
|
namespace: 'object-ns',
|
||||||
});
|
});
|
||||||
|
audit.decryptAttributeFailure(
|
||||||
|
'six',
|
||||||
|
{ type: 'known-type-3-ns', id: 'object-id-3-ns', namespace: 'object-ns' },
|
||||||
|
mockAuthenticatedUser()
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockInternalAuditLogger.log).toHaveBeenCalledTimes(8);
|
expect(mockInternalAuditLogger.log).toHaveBeenCalledTimes(12);
|
||||||
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith(
|
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith(
|
||||||
'encrypt_success',
|
'encrypt_success',
|
||||||
'Successfully encrypted attributes "[one,two]" for saved object "[known-type,object-id]".',
|
'Successfully encrypted attributes "[one,two]" for saved object "[known-type,object-id]".',
|
||||||
|
@ -66,6 +87,17 @@ test('properly logs audit events', () => {
|
||||||
attributesNames: ['one', 'two'],
|
attributesNames: ['one', 'two'],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith(
|
||||||
|
'encrypt_success',
|
||||||
|
'Successfully encrypted attributes "[one,two]" for saved object "[object-ns,known-type-ns,object-id-ns]".',
|
||||||
|
{
|
||||||
|
id: 'object-id-ns',
|
||||||
|
type: 'known-type-ns',
|
||||||
|
namespace: 'object-ns',
|
||||||
|
attributesNames: ['one', 'two'],
|
||||||
|
username: 'user',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith(
|
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith(
|
||||||
'decrypt_success',
|
'decrypt_success',
|
||||||
|
@ -82,6 +114,17 @@ test('properly logs audit events', () => {
|
||||||
attributesNames: ['three', 'four'],
|
attributesNames: ['three', 'four'],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith(
|
||||||
|
'decrypt_success',
|
||||||
|
'Successfully decrypted attributes "[three,four]" for saved object "[object-ns,known-type-1-ns,object-id-1-ns]".',
|
||||||
|
{
|
||||||
|
id: 'object-id-1-ns',
|
||||||
|
type: 'known-type-1-ns',
|
||||||
|
namespace: 'object-ns',
|
||||||
|
attributesNames: ['three', 'four'],
|
||||||
|
username: 'user',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith(
|
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith(
|
||||||
'encrypt_failure',
|
'encrypt_failure',
|
||||||
|
@ -93,6 +136,17 @@ test('properly logs audit events', () => {
|
||||||
'Failed to encrypt attribute "five" for saved object "[object-ns,known-type-2-ns,object-id-2-ns]".',
|
'Failed to encrypt attribute "five" for saved object "[object-ns,known-type-2-ns,object-id-2-ns]".',
|
||||||
{ id: 'object-id-2-ns', type: 'known-type-2-ns', namespace: 'object-ns', attributeName: 'five' }
|
{ id: 'object-id-2-ns', type: 'known-type-2-ns', namespace: 'object-ns', attributeName: 'five' }
|
||||||
);
|
);
|
||||||
|
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith(
|
||||||
|
'encrypt_failure',
|
||||||
|
'Failed to encrypt attribute "five" for saved object "[object-ns,known-type-2-ns,object-id-2-ns]".',
|
||||||
|
{
|
||||||
|
id: 'object-id-2-ns',
|
||||||
|
type: 'known-type-2-ns',
|
||||||
|
namespace: 'object-ns',
|
||||||
|
attributeName: 'five',
|
||||||
|
username: 'user',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith(
|
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith(
|
||||||
'decrypt_failure',
|
'decrypt_failure',
|
||||||
|
@ -104,4 +158,15 @@ test('properly logs audit events', () => {
|
||||||
'Failed to decrypt attribute "six" for saved object "[object-ns,known-type-3-ns,object-id-3-ns]".',
|
'Failed to decrypt attribute "six" for saved object "[object-ns,known-type-3-ns,object-id-3-ns]".',
|
||||||
{ id: 'object-id-3-ns', type: 'known-type-3-ns', namespace: 'object-ns', attributeName: 'six' }
|
{ id: 'object-id-3-ns', type: 'known-type-3-ns', namespace: 'object-ns', attributeName: 'six' }
|
||||||
);
|
);
|
||||||
|
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith(
|
||||||
|
'decrypt_failure',
|
||||||
|
'Failed to decrypt attribute "six" for saved object "[object-ns,known-type-3-ns,object-id-3-ns]".',
|
||||||
|
{
|
||||||
|
id: 'object-id-3-ns',
|
||||||
|
type: 'known-type-3-ns',
|
||||||
|
namespace: 'object-ns',
|
||||||
|
attributeName: 'six',
|
||||||
|
username: 'user',
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
import { SavedObjectDescriptor, descriptorToArray } from '../crypto';
|
import { SavedObjectDescriptor, descriptorToArray } from '../crypto';
|
||||||
import { LegacyAPI } from '../plugin';
|
import { LegacyAPI } from '../plugin';
|
||||||
|
import { AuthenticatedUser } from '../../../security/common/model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents all audit events the plugin can log.
|
* Represents all audit events the plugin can log.
|
||||||
|
@ -13,49 +14,59 @@ import { LegacyAPI } from '../plugin';
|
||||||
export class EncryptedSavedObjectsAuditLogger {
|
export class EncryptedSavedObjectsAuditLogger {
|
||||||
constructor(private readonly getAuditLogger: () => LegacyAPI['auditLogger']) {}
|
constructor(private readonly getAuditLogger: () => LegacyAPI['auditLogger']) {}
|
||||||
|
|
||||||
public encryptAttributeFailure(attributeName: string, descriptor: SavedObjectDescriptor) {
|
public encryptAttributeFailure(
|
||||||
|
attributeName: string,
|
||||||
|
descriptor: SavedObjectDescriptor,
|
||||||
|
user?: AuthenticatedUser
|
||||||
|
) {
|
||||||
this.getAuditLogger().log(
|
this.getAuditLogger().log(
|
||||||
'encrypt_failure',
|
'encrypt_failure',
|
||||||
`Failed to encrypt attribute "${attributeName}" for saved object "[${descriptorToArray(
|
`Failed to encrypt attribute "${attributeName}" for saved object "[${descriptorToArray(
|
||||||
descriptor
|
descriptor
|
||||||
)}]".`,
|
)}]".`,
|
||||||
{ ...descriptor, attributeName }
|
{ ...descriptor, attributeName, username: user?.username }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public decryptAttributeFailure(attributeName: string, descriptor: SavedObjectDescriptor) {
|
public decryptAttributeFailure(
|
||||||
|
attributeName: string,
|
||||||
|
descriptor: SavedObjectDescriptor,
|
||||||
|
user?: AuthenticatedUser
|
||||||
|
) {
|
||||||
this.getAuditLogger().log(
|
this.getAuditLogger().log(
|
||||||
'decrypt_failure',
|
'decrypt_failure',
|
||||||
`Failed to decrypt attribute "${attributeName}" for saved object "[${descriptorToArray(
|
`Failed to decrypt attribute "${attributeName}" for saved object "[${descriptorToArray(
|
||||||
descriptor
|
descriptor
|
||||||
)}]".`,
|
)}]".`,
|
||||||
{ ...descriptor, attributeName }
|
{ ...descriptor, attributeName, username: user?.username }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public encryptAttributesSuccess(
|
public encryptAttributesSuccess(
|
||||||
attributesNames: readonly string[],
|
attributesNames: readonly string[],
|
||||||
descriptor: SavedObjectDescriptor
|
descriptor: SavedObjectDescriptor,
|
||||||
|
user?: AuthenticatedUser
|
||||||
) {
|
) {
|
||||||
this.getAuditLogger().log(
|
this.getAuditLogger().log(
|
||||||
'encrypt_success',
|
'encrypt_success',
|
||||||
`Successfully encrypted attributes "[${attributesNames}]" for saved object "[${descriptorToArray(
|
`Successfully encrypted attributes "[${attributesNames}]" for saved object "[${descriptorToArray(
|
||||||
descriptor
|
descriptor
|
||||||
)}]".`,
|
)}]".`,
|
||||||
{ ...descriptor, attributesNames }
|
{ ...descriptor, attributesNames, username: user?.username }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public decryptAttributesSuccess(
|
public decryptAttributesSuccess(
|
||||||
attributesNames: readonly string[],
|
attributesNames: readonly string[],
|
||||||
descriptor: SavedObjectDescriptor
|
descriptor: SavedObjectDescriptor,
|
||||||
|
user?: AuthenticatedUser
|
||||||
) {
|
) {
|
||||||
this.getAuditLogger().log(
|
this.getAuditLogger().log(
|
||||||
'decrypt_success',
|
'decrypt_success',
|
||||||
`Successfully decrypted attributes "[${attributesNames}]" for saved object "[${descriptorToArray(
|
`Successfully decrypted attributes "[${attributesNames}]" for saved object "[${descriptorToArray(
|
||||||
descriptor
|
descriptor
|
||||||
)}]".`,
|
)}]".`,
|
||||||
{ ...descriptor, attributesNames }
|
{ ...descriptor, attributesNames, username: user?.username }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EncryptedSavedObjectTypeRegistration } from './encrypted_saved_objects_service';
|
||||||
|
import { EncryptedSavedObjectAttributesDefinition } from './encrypted_saved_object_type_definition';
|
||||||
|
|
||||||
|
it('correctly determines attribute properties', () => {
|
||||||
|
const attributes = ['attr#1', 'attr#2', 'attr#3', 'attr#4'];
|
||||||
|
const cases: Array<[
|
||||||
|
EncryptedSavedObjectTypeRegistration,
|
||||||
|
{
|
||||||
|
shouldBeEncrypted: boolean[];
|
||||||
|
shouldBeExcludedFromAAD: boolean[];
|
||||||
|
shouldBeStripped: boolean[];
|
||||||
|
}
|
||||||
|
]> = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'so-type',
|
||||||
|
attributesToEncrypt: new Set(['attr#1', 'attr#2', 'attr#3', 'attr#4']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shouldBeEncrypted: [true, true, true, true],
|
||||||
|
shouldBeExcludedFromAAD: [true, true, true, true],
|
||||||
|
shouldBeStripped: [true, true, true, true],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'so-type',
|
||||||
|
attributesToEncrypt: new Set(['attr#1', 'attr#2']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shouldBeEncrypted: [true, true, false, false],
|
||||||
|
shouldBeExcludedFromAAD: [true, true, false, false],
|
||||||
|
shouldBeStripped: [true, true, false, false],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'so-type',
|
||||||
|
attributesToEncrypt: new Set([{ key: 'attr#1' }, { key: 'attr#2' }]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shouldBeEncrypted: [true, true, false, false],
|
||||||
|
shouldBeExcludedFromAAD: [true, true, false, false],
|
||||||
|
shouldBeStripped: [true, true, false, false],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'so-type',
|
||||||
|
attributesToEncrypt: new Set(['attr#1', 'attr#2']),
|
||||||
|
attributesToExcludeFromAAD: new Set(['attr#3']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shouldBeEncrypted: [true, true, false, false],
|
||||||
|
shouldBeExcludedFromAAD: [true, true, true, false],
|
||||||
|
shouldBeStripped: [true, true, false, false],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'so-type',
|
||||||
|
attributesToEncrypt: new Set([
|
||||||
|
'attr#1',
|
||||||
|
'attr#2',
|
||||||
|
{ key: 'attr#4', dangerouslyExposeValue: true },
|
||||||
|
]),
|
||||||
|
attributesToExcludeFromAAD: new Set(['attr#3']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shouldBeEncrypted: [true, true, false, true],
|
||||||
|
shouldBeExcludedFromAAD: [true, true, true, true],
|
||||||
|
shouldBeStripped: [true, true, false, false],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'so-type',
|
||||||
|
attributesToEncrypt: new Set([
|
||||||
|
{ key: 'attr#1', dangerouslyExposeValue: true },
|
||||||
|
'attr#2',
|
||||||
|
{ key: 'attr#4', dangerouslyExposeValue: true },
|
||||||
|
]),
|
||||||
|
attributesToExcludeFromAAD: new Set(['some-other-attribute']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shouldBeEncrypted: [true, true, false, true],
|
||||||
|
shouldBeExcludedFromAAD: [true, true, false, true],
|
||||||
|
shouldBeStripped: [false, true, false, false],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [typeRegistration, asserts] of cases) {
|
||||||
|
const typeDefinition = new EncryptedSavedObjectAttributesDefinition(typeRegistration);
|
||||||
|
for (const [attributeIndex, attributeName] of attributes.entries()) {
|
||||||
|
expect(typeDefinition.shouldBeEncrypted(attributeName)).toBe(
|
||||||
|
asserts.shouldBeEncrypted[attributeIndex]
|
||||||
|
);
|
||||||
|
expect(typeDefinition.shouldBeStripped(attributeName)).toBe(
|
||||||
|
asserts.shouldBeStripped[attributeIndex]
|
||||||
|
);
|
||||||
|
expect(typeDefinition.shouldBeExcludedFromAAD(attributeName)).toBe(
|
||||||
|
asserts.shouldBeExcludedFromAAD[attributeIndex]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EncryptedSavedObjectTypeRegistration } from './encrypted_saved_objects_service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the definition of the attributes of the specific saved object that are supposed to be
|
||||||
|
* encrypted. The definition also dictates which attributes should be excluded from AAD and/or
|
||||||
|
* stripped from response.
|
||||||
|
*/
|
||||||
|
export class EncryptedSavedObjectAttributesDefinition {
|
||||||
|
public readonly attributesToEncrypt: ReadonlySet<string>;
|
||||||
|
private readonly attributesToExcludeFromAAD: ReadonlySet<string> | undefined;
|
||||||
|
private readonly attributesToStrip: ReadonlySet<string>;
|
||||||
|
|
||||||
|
constructor(typeRegistration: EncryptedSavedObjectTypeRegistration) {
|
||||||
|
const attributesToEncrypt = new Set<string>();
|
||||||
|
const attributesToStrip = new Set<string>();
|
||||||
|
for (const attribute of typeRegistration.attributesToEncrypt) {
|
||||||
|
if (typeof attribute === 'string') {
|
||||||
|
attributesToEncrypt.add(attribute);
|
||||||
|
attributesToStrip.add(attribute);
|
||||||
|
} else {
|
||||||
|
attributesToEncrypt.add(attribute.key);
|
||||||
|
if (!attribute.dangerouslyExposeValue) {
|
||||||
|
attributesToStrip.add(attribute.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.attributesToEncrypt = attributesToEncrypt;
|
||||||
|
this.attributesToStrip = attributesToStrip;
|
||||||
|
this.attributesToExcludeFromAAD = typeRegistration.attributesToExcludeFromAAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether particular attribute should be encrypted. Full list of attributes that
|
||||||
|
* should be encrypted can be retrieved via `attributesToEncrypt` property.
|
||||||
|
* @param attributeName Name of the attribute.
|
||||||
|
*/
|
||||||
|
public shouldBeEncrypted(attributeName: string) {
|
||||||
|
return this.attributesToEncrypt.has(attributeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether particular attribute should be excluded from AAD.
|
||||||
|
* @param attributeName Name of the attribute.
|
||||||
|
*/
|
||||||
|
public shouldBeExcludedFromAAD(attributeName: string) {
|
||||||
|
return (
|
||||||
|
this.shouldBeEncrypted(attributeName) ||
|
||||||
|
(this.attributesToExcludeFromAAD != null &&
|
||||||
|
this.attributesToExcludeFromAAD.has(attributeName))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether particular attribute should be stripped from the attribute list.
|
||||||
|
* @param attributeName Name of the attribute.
|
||||||
|
*/
|
||||||
|
public shouldBeStripped(attributeName: string) {
|
||||||
|
return this.attributesToStrip.has(attributeName);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,8 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { mockAuthenticatedUser } from '../../../security/common/model/authenticated_user.mock';
|
||||||
|
|
||||||
jest.mock('@elastic/node-crypto', () => jest.fn());
|
jest.mock('@elastic/node-crypto', () => jest.fn());
|
||||||
|
|
||||||
import { EncryptedSavedObjectsAuditLogger } from '../audit';
|
import { EncryptedSavedObjectsAuditLogger } from '../audit';
|
||||||
|
@ -72,40 +74,26 @@ describe('#isRegistered', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#stripEncryptedAttributes', () => {
|
describe('#stripOrDecryptAttributes', () => {
|
||||||
it('does not strip attributes from unknown types', () => {
|
it('does not strip attributes from unknown types', async () => {
|
||||||
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
|
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
|
||||||
|
|
||||||
expect(service.stripEncryptedAttributes('unknown-type', attributes)).toEqual({
|
await expect(
|
||||||
attrOne: 'one',
|
service.stripOrDecryptAttributes({ id: 'unknown-id', type: 'unknown-type' }, attributes)
|
||||||
attrTwo: 'two',
|
).resolves.toEqual({ attributes: { attrOne: 'one', attrTwo: 'two', attrThree: 'three' } });
|
||||||
attrThree: 'three',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not strip attributes from known, but not registered types', () => {
|
it('does not strip any attributes if none of them are supposed to be encrypted', async () => {
|
||||||
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
|
|
||||||
|
|
||||||
expect(service.stripEncryptedAttributes('known-type-1', attributes)).toEqual({
|
|
||||||
attrOne: 'one',
|
|
||||||
attrTwo: 'two',
|
|
||||||
attrThree: 'three',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not strip any attributes if none of them are supposed to be encrypted', () => {
|
|
||||||
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
|
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
|
||||||
|
|
||||||
service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attrFour']) });
|
service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attrFour']) });
|
||||||
|
|
||||||
expect(service.stripEncryptedAttributes('known-type-1', attributes)).toEqual({
|
await expect(
|
||||||
attrOne: 'one',
|
service.stripOrDecryptAttributes({ id: 'known-id', type: 'known-type-1' }, attributes)
|
||||||
attrTwo: 'two',
|
).resolves.toEqual({ attributes: { attrOne: 'one', attrTwo: 'two', attrThree: 'three' } });
|
||||||
attrThree: 'three',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('strips only attributes that are supposed to be encrypted', () => {
|
it('strips only attributes that are supposed to be encrypted', async () => {
|
||||||
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
|
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
|
||||||
|
|
||||||
service.registerType({
|
service.registerType({
|
||||||
|
@ -113,8 +101,113 @@ describe('#stripEncryptedAttributes', () => {
|
||||||
attributesToEncrypt: new Set(['attrOne', 'attrThree']),
|
attributesToEncrypt: new Set(['attrOne', 'attrThree']),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(service.stripEncryptedAttributes('known-type-1', attributes)).toEqual({
|
await expect(
|
||||||
attrTwo: 'two',
|
service.stripOrDecryptAttributes({ id: 'known-id', type: 'known-type-1' }, attributes)
|
||||||
|
).resolves.toEqual({ attributes: { attrTwo: 'two' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with `dangerouslyExposeValue`', () => {
|
||||||
|
it('decrypts and exposes values with `dangerouslyExposeValue` set to `true`', async () => {
|
||||||
|
service.registerType({
|
||||||
|
type: 'known-type-1',
|
||||||
|
attributesToEncrypt: new Set([
|
||||||
|
'attrOne',
|
||||||
|
{ key: 'attrThree', dangerouslyExposeValue: true },
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
|
||||||
|
const encryptedAttributes = await service.encryptAttributes(
|
||||||
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
|
attributes
|
||||||
|
);
|
||||||
|
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
|
await expect(
|
||||||
|
service.stripOrDecryptAttributes(
|
||||||
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
|
encryptedAttributes,
|
||||||
|
undefined,
|
||||||
|
{ user: mockUser }
|
||||||
|
)
|
||||||
|
).resolves.toEqual({ attributes: { attrTwo: 'two', attrThree: 'three' } });
|
||||||
|
|
||||||
|
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(
|
||||||
|
['attrThree'],
|
||||||
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
|
mockUser
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exposes values with `dangerouslyExposeValue` set to `true` using original attributes if provided', async () => {
|
||||||
|
service.registerType({
|
||||||
|
type: 'known-type-1',
|
||||||
|
attributesToEncrypt: new Set([
|
||||||
|
'attrOne',
|
||||||
|
{ key: 'attrThree', dangerouslyExposeValue: true },
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
|
||||||
|
const encryptedAttributes = {
|
||||||
|
attrOne: 'fake-enc-one',
|
||||||
|
attrTwo: 'two',
|
||||||
|
attrThree: 'fake-enc-three',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.stripOrDecryptAttributes(
|
||||||
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
|
encryptedAttributes,
|
||||||
|
attributes
|
||||||
|
)
|
||||||
|
).resolves.toEqual({ attributes: { attrTwo: 'two', attrThree: 'three' } });
|
||||||
|
|
||||||
|
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
||||||
|
expect(mockAuditLogger.decryptAttributeFailure).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips attributes with `dangerouslyExposeValue` set to `true` if failed to decrypt', async () => {
|
||||||
|
service.registerType({
|
||||||
|
type: 'known-type-1',
|
||||||
|
attributesToEncrypt: new Set([
|
||||||
|
'attrOne',
|
||||||
|
{ key: 'attrThree', dangerouslyExposeValue: true },
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const attributes = {
|
||||||
|
attrZero: 'zero',
|
||||||
|
attrOne: 'one',
|
||||||
|
attrTwo: 'two',
|
||||||
|
attrThree: 'three',
|
||||||
|
attrFour: 'four',
|
||||||
|
};
|
||||||
|
const encryptedAttributes = await service.encryptAttributes(
|
||||||
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
|
attributes
|
||||||
|
);
|
||||||
|
|
||||||
|
encryptedAttributes.attrThree = 'some-undecryptable-value';
|
||||||
|
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
|
const { attributes: decryptedAttributes, error } = await service.stripOrDecryptAttributes(
|
||||||
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
|
encryptedAttributes,
|
||||||
|
undefined,
|
||||||
|
{ user: mockUser }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(decryptedAttributes).toEqual({ attrZero: 'zero', attrTwo: 'two', attrFour: 'four' });
|
||||||
|
expect(error).toMatchInlineSnapshot(`[Error: Unable to decrypt attribute "attrThree"]`);
|
||||||
|
|
||||||
|
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
||||||
|
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith(
|
||||||
|
'attrThree',
|
||||||
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
|
mockUser
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -183,8 +276,11 @@ describe('#encryptAttributes', () => {
|
||||||
attributesToEncrypt: new Set(['attrOne', 'attrThree', 'attrFour']),
|
attributesToEncrypt: new Set(['attrOne', 'attrThree', 'attrFour']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.encryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributes)
|
service.encryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributes, {
|
||||||
|
user: mockUser,
|
||||||
|
})
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
attrOne: '|one|["known-type-1","object-id",{"attrTwo":"two"}]|',
|
attrOne: '|one|["known-type-1","object-id",{"attrTwo":"two"}]|',
|
||||||
attrTwo: 'two',
|
attrTwo: 'two',
|
||||||
|
@ -194,7 +290,8 @@ describe('#encryptAttributes', () => {
|
||||||
expect(mockAuditLogger.encryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
expect(mockAuditLogger.encryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
||||||
expect(mockAuditLogger.encryptAttributesSuccess).toHaveBeenCalledWith(
|
expect(mockAuditLogger.encryptAttributesSuccess).toHaveBeenCalledWith(
|
||||||
['attrOne', 'attrThree'],
|
['attrOne', 'attrThree'],
|
||||||
{ type: 'known-type-1', id: 'object-id' }
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
|
mockUser
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -206,17 +303,21 @@ describe('#encryptAttributes', () => {
|
||||||
attributesToEncrypt: new Set(['attrOne', 'attrThree']),
|
attributesToEncrypt: new Set(['attrOne', 'attrThree']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.encryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributes)
|
service.encryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributes, {
|
||||||
|
user: mockUser,
|
||||||
|
})
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
attrTwo: 'two',
|
attrTwo: 'two',
|
||||||
attrThree: '|three|["known-type-1","object-id",{"attrTwo":"two"}]|',
|
attrThree: '|three|["known-type-1","object-id",{"attrTwo":"two"}]|',
|
||||||
});
|
});
|
||||||
expect(mockAuditLogger.encryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
expect(mockAuditLogger.encryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
||||||
expect(mockAuditLogger.encryptAttributesSuccess).toHaveBeenCalledWith(['attrThree'], {
|
expect(mockAuditLogger.encryptAttributesSuccess).toHaveBeenCalledWith(
|
||||||
type: 'known-type-1',
|
['attrThree'],
|
||||||
id: 'object-id',
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
});
|
mockUser
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('includes `namespace` into AAD if provided', async () => {
|
it('includes `namespace` into AAD if provided', async () => {
|
||||||
|
@ -227,21 +328,23 @@ describe('#encryptAttributes', () => {
|
||||||
attributesToEncrypt: new Set(['attrOne', 'attrThree']),
|
attributesToEncrypt: new Set(['attrOne', 'attrThree']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.encryptAttributes(
|
service.encryptAttributes(
|
||||||
{ type: 'known-type-1', id: 'object-id', namespace: 'object-ns' },
|
{ type: 'known-type-1', id: 'object-id', namespace: 'object-ns' },
|
||||||
attributes
|
attributes,
|
||||||
|
{ user: mockUser }
|
||||||
)
|
)
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
attrTwo: 'two',
|
attrTwo: 'two',
|
||||||
attrThree: '|three|["object-ns","known-type-1","object-id",{"attrTwo":"two"}]|',
|
attrThree: '|three|["object-ns","known-type-1","object-id",{"attrTwo":"two"}]|',
|
||||||
});
|
});
|
||||||
expect(mockAuditLogger.encryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
expect(mockAuditLogger.encryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
||||||
expect(mockAuditLogger.encryptAttributesSuccess).toHaveBeenCalledWith(['attrThree'], {
|
expect(mockAuditLogger.encryptAttributesSuccess).toHaveBeenCalledWith(
|
||||||
type: 'known-type-1',
|
['attrThree'],
|
||||||
id: 'object-id',
|
{ type: 'known-type-1', id: 'object-id', namespace: 'object-ns' },
|
||||||
namespace: 'object-ns',
|
mockUser
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not include specified attributes to AAD', async () => {
|
it('does not include specified attributes to AAD', async () => {
|
||||||
|
@ -300,8 +403,11 @@ describe('#encryptAttributes', () => {
|
||||||
.mockResolvedValueOnce('Successfully encrypted attrOne')
|
.mockResolvedValueOnce('Successfully encrypted attrOne')
|
||||||
.mockRejectedValueOnce(new Error('Something went wrong with attrThree...'));
|
.mockRejectedValueOnce(new Error('Something went wrong with attrThree...'));
|
||||||
|
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.encryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributes)
|
service.encryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributes, {
|
||||||
|
user: mockUser,
|
||||||
|
})
|
||||||
).rejects.toThrowError(EncryptionError);
|
).rejects.toThrowError(EncryptionError);
|
||||||
|
|
||||||
expect(attributes).toEqual({
|
expect(attributes).toEqual({
|
||||||
|
@ -311,10 +417,11 @@ describe('#encryptAttributes', () => {
|
||||||
});
|
});
|
||||||
expect(mockAuditLogger.encryptAttributesSuccess).not.toHaveBeenCalled();
|
expect(mockAuditLogger.encryptAttributesSuccess).not.toHaveBeenCalled();
|
||||||
expect(mockAuditLogger.encryptAttributeFailure).toHaveBeenCalledTimes(1);
|
expect(mockAuditLogger.encryptAttributeFailure).toHaveBeenCalledTimes(1);
|
||||||
expect(mockAuditLogger.encryptAttributeFailure).toHaveBeenCalledWith('attrThree', {
|
expect(mockAuditLogger.encryptAttributeFailure).toHaveBeenCalledWith(
|
||||||
type: 'known-type-1',
|
'attrThree',
|
||||||
id: 'object-id',
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
});
|
mockUser
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -379,8 +486,11 @@ describe('#decryptAttributes', () => {
|
||||||
attrFour: null,
|
attrFour: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes)
|
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes, {
|
||||||
|
user: mockUser,
|
||||||
|
})
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
attrOne: 'one',
|
attrOne: 'one',
|
||||||
attrTwo: 'two',
|
attrTwo: 'two',
|
||||||
|
@ -390,7 +500,8 @@ describe('#decryptAttributes', () => {
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(
|
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(
|
||||||
['attrOne', 'attrThree'],
|
['attrOne', 'attrThree'],
|
||||||
{ type: 'known-type-1', id: 'object-id' }
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
|
mockUser
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -411,17 +522,21 @@ describe('#decryptAttributes', () => {
|
||||||
attrThree: expect.not.stringMatching(/^three$/),
|
attrThree: expect.not.stringMatching(/^three$/),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes)
|
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes, {
|
||||||
|
user: mockUser,
|
||||||
|
})
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
attrTwo: 'two',
|
attrTwo: 'two',
|
||||||
attrThree: 'three',
|
attrThree: 'three',
|
||||||
});
|
});
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(['attrThree'], {
|
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(
|
||||||
type: 'known-type-1',
|
['attrThree'],
|
||||||
id: 'object-id',
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
});
|
mockUser
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('decrypts if all attributes that contribute to AAD are present', async () => {
|
it('decrypts if all attributes that contribute to AAD are present', async () => {
|
||||||
|
@ -445,17 +560,21 @@ describe('#decryptAttributes', () => {
|
||||||
|
|
||||||
const attributesWithoutAttr = { attrTwo: 'two', attrThree: encryptedAttributes.attrThree };
|
const attributesWithoutAttr = { attrTwo: 'two', attrThree: encryptedAttributes.attrThree };
|
||||||
|
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributesWithoutAttr)
|
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributesWithoutAttr, {
|
||||||
|
user: mockUser,
|
||||||
|
})
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
attrTwo: 'two',
|
attrTwo: 'two',
|
||||||
attrThree: 'three',
|
attrThree: 'three',
|
||||||
});
|
});
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(['attrThree'], {
|
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(
|
||||||
type: 'known-type-1',
|
['attrThree'],
|
||||||
id: 'object-id',
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
});
|
mockUser
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('decrypts even if attributes in AAD are defined in a different order', async () => {
|
it('decrypts even if attributes in AAD are defined in a different order', async () => {
|
||||||
|
@ -482,10 +601,12 @@ describe('#decryptAttributes', () => {
|
||||||
attrOne: 'one',
|
attrOne: 'one',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.decryptAttributes(
|
service.decryptAttributes(
|
||||||
{ type: 'known-type-1', id: 'object-id' },
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
attributesInDifferentOrder
|
attributesInDifferentOrder,
|
||||||
|
{ user: mockUser }
|
||||||
)
|
)
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
attrOne: 'one',
|
attrOne: 'one',
|
||||||
|
@ -493,10 +614,11 @@ describe('#decryptAttributes', () => {
|
||||||
attrThree: 'three',
|
attrThree: 'three',
|
||||||
});
|
});
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(['attrThree'], {
|
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(
|
||||||
type: 'known-type-1',
|
['attrThree'],
|
||||||
id: 'object-id',
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
});
|
mockUser
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('decrypts if correct namespace is provided', async () => {
|
it('decrypts if correct namespace is provided', async () => {
|
||||||
|
@ -517,10 +639,12 @@ describe('#decryptAttributes', () => {
|
||||||
attrThree: expect.not.stringMatching(/^three$/),
|
attrThree: expect.not.stringMatching(/^three$/),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.decryptAttributes(
|
service.decryptAttributes(
|
||||||
{ type: 'known-type-1', id: 'object-id', namespace: 'object-ns' },
|
{ type: 'known-type-1', id: 'object-id', namespace: 'object-ns' },
|
||||||
encryptedAttributes
|
encryptedAttributes,
|
||||||
|
{ user: mockUser }
|
||||||
)
|
)
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
attrOne: 'one',
|
attrOne: 'one',
|
||||||
|
@ -528,11 +652,11 @@ describe('#decryptAttributes', () => {
|
||||||
attrThree: 'three',
|
attrThree: 'three',
|
||||||
});
|
});
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(['attrThree'], {
|
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(
|
||||||
type: 'known-type-1',
|
['attrThree'],
|
||||||
id: 'object-id',
|
{ type: 'known-type-1', id: 'object-id', namespace: 'object-ns' },
|
||||||
namespace: 'object-ns',
|
mockUser
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('decrypts even if no attributes are included into AAD', async () => {
|
it('decrypts even if no attributes are included into AAD', async () => {
|
||||||
|
@ -551,8 +675,11 @@ describe('#decryptAttributes', () => {
|
||||||
attrThree: expect.not.stringMatching(/^three$/),
|
attrThree: expect.not.stringMatching(/^three$/),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes)
|
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes, {
|
||||||
|
user: mockUser,
|
||||||
|
})
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
attrOne: 'one',
|
attrOne: 'one',
|
||||||
attrThree: 'three',
|
attrThree: 'three',
|
||||||
|
@ -560,7 +687,8 @@ describe('#decryptAttributes', () => {
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(
|
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(
|
||||||
['attrOne', 'attrThree'],
|
['attrOne', 'attrThree'],
|
||||||
{ type: 'known-type-1', id: 'object-id' }
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
|
mockUser
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -592,8 +720,11 @@ describe('#decryptAttributes', () => {
|
||||||
attrSix: expect.any(String),
|
attrSix: expect.any(String),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes)
|
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes, {
|
||||||
|
user: mockUser,
|
||||||
|
})
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
attrOne: 'one',
|
attrOne: 'one',
|
||||||
attrTwo: 'two',
|
attrTwo: 'two',
|
||||||
|
@ -605,7 +736,8 @@ describe('#decryptAttributes', () => {
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(
|
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(
|
||||||
['attrOne', 'attrThree', 'attrFive', 'attrSix'],
|
['attrOne', 'attrThree', 'attrFive', 'attrSix'],
|
||||||
{ type: 'known-type-1', id: 'object-id' }
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
|
mockUser
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -632,39 +764,53 @@ describe('#decryptAttributes', () => {
|
||||||
|
|
||||||
it('fails to decrypt if not all attributes that contribute to AAD are present', async () => {
|
it('fails to decrypt if not all attributes that contribute to AAD are present', async () => {
|
||||||
const attributesWithoutAttr = { attrTwo: 'two', attrThree: encryptedAttributes.attrThree };
|
const attributesWithoutAttr = { attrTwo: 'two', attrThree: encryptedAttributes.attrThree };
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributesWithoutAttr)
|
service.decryptAttributes(
|
||||||
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
|
attributesWithoutAttr,
|
||||||
|
{ user: mockUser }
|
||||||
|
)
|
||||||
).rejects.toThrowError(EncryptionError);
|
).rejects.toThrowError(EncryptionError);
|
||||||
|
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
||||||
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith('attrThree', {
|
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith(
|
||||||
type: 'known-type-1',
|
'attrThree',
|
||||||
id: 'object-id',
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
});
|
mockUser
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails to decrypt if ID does not match', async () => {
|
it('fails to decrypt if ID does not match', async () => {
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.decryptAttributes({ type: 'known-type-1', id: 'object-id*' }, encryptedAttributes)
|
service.decryptAttributes({ type: 'known-type-1', id: 'object-id*' }, encryptedAttributes, {
|
||||||
|
user: mockUser,
|
||||||
|
})
|
||||||
).rejects.toThrowError(EncryptionError);
|
).rejects.toThrowError(EncryptionError);
|
||||||
|
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
||||||
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith('attrThree', {
|
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith(
|
||||||
type: 'known-type-1',
|
'attrThree',
|
||||||
id: 'object-id*',
|
{ type: 'known-type-1', id: 'object-id*' },
|
||||||
});
|
mockUser
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails to decrypt if type does not match', async () => {
|
it('fails to decrypt if type does not match', async () => {
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.decryptAttributes({ type: 'known-type-2', id: 'object-id' }, encryptedAttributes)
|
service.decryptAttributes({ type: 'known-type-2', id: 'object-id' }, encryptedAttributes, {
|
||||||
|
user: mockUser,
|
||||||
|
})
|
||||||
).rejects.toThrowError(EncryptionError);
|
).rejects.toThrowError(EncryptionError);
|
||||||
|
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
||||||
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith('attrThree', {
|
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith(
|
||||||
type: 'known-type-2',
|
'attrThree',
|
||||||
id: 'object-id',
|
{ type: 'known-type-2', id: 'object-id' },
|
||||||
});
|
mockUser
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails to decrypt if namespace does not match', async () => {
|
it('fails to decrypt if namespace does not match', async () => {
|
||||||
|
@ -673,19 +819,21 @@ describe('#decryptAttributes', () => {
|
||||||
{ attrOne: 'one', attrTwo: 'two', attrThree: 'three' }
|
{ attrOne: 'one', attrTwo: 'two', attrThree: 'three' }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.decryptAttributes(
|
service.decryptAttributes(
|
||||||
{ type: 'known-type-1', id: 'object-id', namespace: 'object-NS' },
|
{ type: 'known-type-1', id: 'object-id', namespace: 'object-NS' },
|
||||||
encryptedAttributes
|
encryptedAttributes,
|
||||||
|
{ user: mockUser }
|
||||||
)
|
)
|
||||||
).rejects.toThrowError(EncryptionError);
|
).rejects.toThrowError(EncryptionError);
|
||||||
|
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
||||||
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith('attrThree', {
|
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith(
|
||||||
type: 'known-type-1',
|
'attrThree',
|
||||||
id: 'object-id',
|
{ type: 'known-type-1', id: 'object-id', namespace: 'object-NS' },
|
||||||
namespace: 'object-NS',
|
mockUser
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails to decrypt if namespace is expected, but is not provided', async () => {
|
it('fails to decrypt if namespace is expected, but is not provided', async () => {
|
||||||
|
@ -694,71 +842,75 @@ describe('#decryptAttributes', () => {
|
||||||
{ attrOne: 'one', attrTwo: 'two', attrThree: 'three' }
|
{ attrOne: 'one', attrTwo: 'two', attrThree: 'three' }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes)
|
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes, {
|
||||||
|
user: mockUser,
|
||||||
|
})
|
||||||
).rejects.toThrowError(EncryptionError);
|
).rejects.toThrowError(EncryptionError);
|
||||||
|
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
||||||
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith('attrThree', {
|
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith(
|
||||||
type: 'known-type-1',
|
'attrThree',
|
||||||
id: 'object-id',
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
});
|
mockUser
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails to decrypt if encrypted attribute is defined, but not a string', async () => {
|
it('fails to decrypt if encrypted attribute is defined, but not a string', async () => {
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.decryptAttributes(
|
service.decryptAttributes(
|
||||||
{ type: 'known-type-1', id: 'object-id' },
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
{
|
{ ...encryptedAttributes, attrThree: 2 },
|
||||||
...encryptedAttributes,
|
{ user: mockUser }
|
||||||
attrThree: 2,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
).rejects.toThrowError(
|
).rejects.toThrowError(
|
||||||
'Encrypted "attrThree" attribute should be a string, but found number'
|
'Encrypted "attrThree" attribute should be a string, but found number'
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
||||||
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith('attrThree', {
|
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith(
|
||||||
type: 'known-type-1',
|
'attrThree',
|
||||||
id: 'object-id',
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
});
|
mockUser
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails to decrypt if encrypted attribute is not correct', async () => {
|
it('fails to decrypt if encrypted attribute is not correct', async () => {
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.decryptAttributes(
|
service.decryptAttributes(
|
||||||
{ type: 'known-type-1', id: 'object-id' },
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
{
|
{ ...encryptedAttributes, attrThree: 'some-unknown-string' },
|
||||||
...encryptedAttributes,
|
{ user: mockUser }
|
||||||
attrThree: 'some-unknown-string',
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
).rejects.toThrowError(EncryptionError);
|
).rejects.toThrowError(EncryptionError);
|
||||||
|
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
||||||
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith('attrThree', {
|
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith(
|
||||||
type: 'known-type-1',
|
'attrThree',
|
||||||
id: 'object-id',
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
});
|
mockUser
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails to decrypt if the AAD attribute has changed', async () => {
|
it('fails to decrypt if the AAD attribute has changed', async () => {
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.decryptAttributes(
|
service.decryptAttributes(
|
||||||
{ type: 'known-type-1', id: 'object-id' },
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
{
|
{ ...encryptedAttributes, attrOne: 'oNe' },
|
||||||
...encryptedAttributes,
|
{ user: mockUser }
|
||||||
attrOne: 'oNe',
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
).rejects.toThrowError(EncryptionError);
|
).rejects.toThrowError(EncryptionError);
|
||||||
|
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
||||||
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith('attrThree', {
|
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith(
|
||||||
type: 'known-type-1',
|
'attrThree',
|
||||||
id: 'object-id',
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
});
|
mockUser
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails if encrypted with another encryption key', async () => {
|
it('fails if encrypted with another encryption key', async () => {
|
||||||
|
@ -773,15 +925,19 @@ describe('#decryptAttributes', () => {
|
||||||
attributesToEncrypt: new Set(['attrThree']),
|
attributesToEncrypt: new Set(['attrThree']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockUser = mockAuthenticatedUser();
|
||||||
await expect(
|
await expect(
|
||||||
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes)
|
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes, {
|
||||||
|
user: mockUser,
|
||||||
|
})
|
||||||
).rejects.toThrowError(EncryptionError);
|
).rejects.toThrowError(EncryptionError);
|
||||||
|
|
||||||
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
|
||||||
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith('attrThree', {
|
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith(
|
||||||
type: 'known-type-1',
|
'attrThree',
|
||||||
id: 'object-id',
|
{ type: 'known-type-1', id: 'object-id' },
|
||||||
});
|
mockUser
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,8 +8,20 @@ import nodeCrypto, { Crypto } from '@elastic/node-crypto';
|
||||||
import stringify from 'json-stable-stringify';
|
import stringify from 'json-stable-stringify';
|
||||||
import typeDetect from 'type-detect';
|
import typeDetect from 'type-detect';
|
||||||
import { Logger } from 'src/core/server';
|
import { Logger } from 'src/core/server';
|
||||||
|
import { AuthenticatedUser } from '../../../security/common/model';
|
||||||
import { EncryptedSavedObjectsAuditLogger } from '../audit';
|
import { EncryptedSavedObjectsAuditLogger } from '../audit';
|
||||||
import { EncryptionError } from './encryption_error';
|
import { EncryptionError, EncryptionErrorOperation } from './encryption_error';
|
||||||
|
import { EncryptedSavedObjectAttributesDefinition } from './encrypted_saved_object_type_definition';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the attributes to encrypt. By default, attribute values won't be exposed to end-users
|
||||||
|
* and can only be consumed by the internal Kibana server. If end-users should have access to the
|
||||||
|
* encrypted values use `dangerouslyExposeValue: true`
|
||||||
|
*/
|
||||||
|
export interface AttributeToEncrypt {
|
||||||
|
readonly key: string;
|
||||||
|
readonly dangerouslyExposeValue?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes the registration entry for the saved object type that contain attributes that need to
|
* Describes the registration entry for the saved object type that contain attributes that need to
|
||||||
|
@ -17,7 +29,7 @@ import { EncryptionError } from './encryption_error';
|
||||||
*/
|
*/
|
||||||
export interface EncryptedSavedObjectTypeRegistration {
|
export interface EncryptedSavedObjectTypeRegistration {
|
||||||
readonly type: string;
|
readonly type: string;
|
||||||
readonly attributesToEncrypt: ReadonlySet<string>;
|
readonly attributesToEncrypt: ReadonlySet<string | AttributeToEncrypt>;
|
||||||
readonly attributesToExcludeFromAAD?: ReadonlySet<string>;
|
readonly attributesToExcludeFromAAD?: ReadonlySet<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +42,16 @@ export interface SavedObjectDescriptor {
|
||||||
readonly namespace?: string;
|
readonly namespace?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes parameters that are common for all EncryptedSavedObjectsService public methods.
|
||||||
|
*/
|
||||||
|
interface CommonParameters {
|
||||||
|
/**
|
||||||
|
* User on behalf of the method is called if determined.
|
||||||
|
*/
|
||||||
|
user?: AuthenticatedUser;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility function that gives array representation of the saved object descriptor respecting
|
* Utility function that gives array representation of the saved object descriptor respecting
|
||||||
* optional `namespace` property.
|
* optional `namespace` property.
|
||||||
|
@ -52,9 +74,12 @@ export class EncryptedSavedObjectsService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map of all registered saved object types where the `key` is saved object type and the `value`
|
* Map of all registered saved object types where the `key` is saved object type and the `value`
|
||||||
* is the registration parameters (names of attributes that need to be encrypted etc.).
|
* is the definition (names of attributes that need to be encrypted etc.).
|
||||||
*/
|
*/
|
||||||
private readonly typeRegistrations: Map<string, EncryptedSavedObjectTypeRegistration> = new Map();
|
private readonly typeDefinitions: Map<
|
||||||
|
string,
|
||||||
|
EncryptedSavedObjectAttributesDefinition
|
||||||
|
> = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param encryptionKey The key used to encrypt and decrypt saved objects attributes.
|
* @param encryptionKey The key used to encrypt and decrypt saved objects attributes.
|
||||||
|
@ -81,11 +106,14 @@ export class EncryptedSavedObjectsService {
|
||||||
throw new Error(`The "attributesToEncrypt" array for "${typeRegistration.type}" is empty.`);
|
throw new Error(`The "attributesToEncrypt" array for "${typeRegistration.type}" is empty.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.typeRegistrations.has(typeRegistration.type)) {
|
if (this.typeDefinitions.has(typeRegistration.type)) {
|
||||||
throw new Error(`The "${typeRegistration.type}" saved object type is already registered.`);
|
throw new Error(`The "${typeRegistration.type}" saved object type is already registered.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.typeRegistrations.set(typeRegistration.type, typeRegistration);
|
this.typeDefinitions.set(
|
||||||
|
typeRegistration.type,
|
||||||
|
new EncryptedSavedObjectAttributesDefinition(typeRegistration)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,32 +122,75 @@ export class EncryptedSavedObjectsService {
|
||||||
* @param type Saved object type.
|
* @param type Saved object type.
|
||||||
*/
|
*/
|
||||||
public isRegistered(type: string) {
|
public isRegistered(type: string) {
|
||||||
return this.typeRegistrations.has(type);
|
return this.typeDefinitions.has(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes saved object attributes for the specified type and strips any of them that are supposed
|
* Takes saved object attributes for the specified type and, depending on the type definition,
|
||||||
* to be encrypted and returns that __NEW__ attributes dictionary back.
|
* either decrypts or strips encrypted attributes (e.g. in case AAD or encryption key has changed
|
||||||
* @param type Type of the saved object to strip encrypted attributes from.
|
* and decryption is no longer possible).
|
||||||
* @param attributes Dictionary of __ALL__ saved object attributes.
|
* @param descriptor Saved object descriptor (ID, type and optional namespace)
|
||||||
|
* @param attributes Object that includes a dictionary of __ALL__ saved object attributes stored
|
||||||
|
* in Elasticsearch.
|
||||||
|
* @param [originalAttributes] An optional dictionary of __ALL__ saved object original attributes
|
||||||
|
* that were used to create that saved object (i.e. values are NOT encrypted).
|
||||||
|
* @param [params] Parameters that control the way encrypted attributes are handled.
|
||||||
*/
|
*/
|
||||||
public stripEncryptedAttributes<T extends Record<string, unknown>>(
|
public async stripOrDecryptAttributes<T extends Record<string, unknown>>(
|
||||||
type: string,
|
descriptor: SavedObjectDescriptor,
|
||||||
attributes: T
|
attributes: T,
|
||||||
): Record<string, unknown> {
|
originalAttributes?: T,
|
||||||
const typeRegistration = this.typeRegistrations.get(type);
|
params?: CommonParameters
|
||||||
if (typeRegistration === undefined) {
|
) {
|
||||||
return attributes;
|
const typeDefinition = this.typeDefinitions.get(descriptor.type);
|
||||||
|
if (typeDefinition === undefined) {
|
||||||
|
return { attributes };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let decryptedAttributes: T | null = null;
|
||||||
|
let decryptionError: Error | undefined;
|
||||||
const clonedAttributes: Record<string, unknown> = {};
|
const clonedAttributes: Record<string, unknown> = {};
|
||||||
for (const [attributeName, attributeValue] of Object.entries(attributes)) {
|
for (const [attributeName, attributeValue] of Object.entries(attributes)) {
|
||||||
if (!typeRegistration.attributesToEncrypt.has(attributeName)) {
|
// We should strip encrypted attribute if definition explicitly mandates that or decryption
|
||||||
|
// failed.
|
||||||
|
if (
|
||||||
|
typeDefinition.shouldBeStripped(attributeName) ||
|
||||||
|
(!!decryptionError && typeDefinition.shouldBeEncrypted(attributeName))
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If attribute isn't supposed to be encrypted, just copy it to the resulting attribute set.
|
||||||
|
if (!typeDefinition.shouldBeEncrypted(attributeName)) {
|
||||||
clonedAttributes[attributeName] = attributeValue;
|
clonedAttributes[attributeName] = attributeValue;
|
||||||
|
} else if (originalAttributes) {
|
||||||
|
// If attribute should be decrypted, but we have original attributes used to create object
|
||||||
|
// we should get raw unencrypted value from there to avoid performance penalty.
|
||||||
|
clonedAttributes[attributeName] = originalAttributes[attributeName];
|
||||||
|
} else {
|
||||||
|
// Otherwise just try to decrypt attribute. We decrypt all attributes at once, cache it and
|
||||||
|
// reuse for any other attributes.
|
||||||
|
if (decryptedAttributes === null) {
|
||||||
|
try {
|
||||||
|
decryptedAttributes = await this.decryptAttributes(
|
||||||
|
descriptor,
|
||||||
|
// Decrypt only attributes that are supposed to be exposed.
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(attributes).filter(([key]) => !typeDefinition.shouldBeStripped(key))
|
||||||
|
) as T,
|
||||||
|
{ user: params?.user }
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
decryptionError = err;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clonedAttributes[attributeName] = decryptedAttributes[attributeName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return clonedAttributes;
|
return { attributes: clonedAttributes as T, error: decryptionError };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -128,20 +199,22 @@ export class EncryptedSavedObjectsService {
|
||||||
* attributes were encrypted original attributes dictionary is returned.
|
* attributes were encrypted original attributes dictionary is returned.
|
||||||
* @param descriptor Descriptor of the saved object to encrypt attributes for.
|
* @param descriptor Descriptor of the saved object to encrypt attributes for.
|
||||||
* @param attributes Dictionary of __ALL__ saved object attributes.
|
* @param attributes Dictionary of __ALL__ saved object attributes.
|
||||||
|
* @param [params] Additional parameters.
|
||||||
* @throws Will throw if encryption fails for whatever reason.
|
* @throws Will throw if encryption fails for whatever reason.
|
||||||
*/
|
*/
|
||||||
public async encryptAttributes<T extends Record<string, unknown>>(
|
public async encryptAttributes<T extends Record<string, unknown>>(
|
||||||
descriptor: SavedObjectDescriptor,
|
descriptor: SavedObjectDescriptor,
|
||||||
attributes: T
|
attributes: T,
|
||||||
|
params?: CommonParameters
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const typeRegistration = this.typeRegistrations.get(descriptor.type);
|
const typeDefinition = this.typeDefinitions.get(descriptor.type);
|
||||||
if (typeRegistration === undefined) {
|
if (typeDefinition === undefined) {
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptionAAD = this.getAAD(typeRegistration, descriptor, attributes);
|
const encryptionAAD = this.getAAD(typeDefinition, descriptor, attributes);
|
||||||
const encryptedAttributes: Record<string, string> = {};
|
const encryptedAttributes: Record<string, string> = {};
|
||||||
for (const attributeName of typeRegistration.attributesToEncrypt) {
|
for (const attributeName of typeDefinition.attributesToEncrypt) {
|
||||||
const attributeValue = attributes[attributeName];
|
const attributeValue = attributes[attributeName];
|
||||||
if (attributeValue != null) {
|
if (attributeValue != null) {
|
||||||
try {
|
try {
|
||||||
|
@ -153,11 +226,12 @@ export class EncryptedSavedObjectsService {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Failed to encrypt "${attributeName}" attribute: ${err.message || err}`
|
`Failed to encrypt "${attributeName}" attribute: ${err.message || err}`
|
||||||
);
|
);
|
||||||
this.audit.encryptAttributeFailure(attributeName, descriptor);
|
this.audit.encryptAttributeFailure(attributeName, descriptor, params?.user);
|
||||||
|
|
||||||
throw new EncryptionError(
|
throw new EncryptionError(
|
||||||
`Unable to encrypt attribute "${attributeName}"`,
|
`Unable to encrypt attribute "${attributeName}"`,
|
||||||
attributeName,
|
attributeName,
|
||||||
|
EncryptionErrorOperation.Encryption,
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -167,12 +241,12 @@ export class EncryptedSavedObjectsService {
|
||||||
// Normally we expect all registered to-be-encrypted attributes to be defined, but if it's
|
// Normally we expect all registered to-be-encrypted attributes to be defined, but if it's
|
||||||
// not the case we should collect and log them to make troubleshooting easier.
|
// not the case we should collect and log them to make troubleshooting easier.
|
||||||
const encryptedAttributesKeys = Object.keys(encryptedAttributes);
|
const encryptedAttributesKeys = Object.keys(encryptedAttributes);
|
||||||
if (encryptedAttributesKeys.length !== typeRegistration.attributesToEncrypt.size) {
|
if (encryptedAttributesKeys.length !== typeDefinition.attributesToEncrypt.size) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`The following attributes of saved object "${descriptorToArray(
|
`The following attributes of saved object "${descriptorToArray(
|
||||||
descriptor
|
descriptor
|
||||||
)}" should have been encrypted: ${Array.from(
|
)}" should have been encrypted: ${Array.from(
|
||||||
typeRegistration.attributesToEncrypt
|
typeDefinition.attributesToEncrypt
|
||||||
)}, but found only: ${encryptedAttributesKeys}`
|
)}, but found only: ${encryptedAttributesKeys}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -181,7 +255,7 @@ export class EncryptedSavedObjectsService {
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.audit.encryptAttributesSuccess(encryptedAttributesKeys, descriptor);
|
this.audit.encryptAttributesSuccess(encryptedAttributesKeys, descriptor, params?.user);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...attributes,
|
...attributes,
|
||||||
|
@ -195,28 +269,30 @@ export class EncryptedSavedObjectsService {
|
||||||
* attributes were decrypted original attributes dictionary is returned.
|
* attributes were decrypted original attributes dictionary is returned.
|
||||||
* @param descriptor Descriptor of the saved object to decrypt attributes for.
|
* @param descriptor Descriptor of the saved object to decrypt attributes for.
|
||||||
* @param attributes Dictionary of __ALL__ saved object attributes.
|
* @param attributes Dictionary of __ALL__ saved object attributes.
|
||||||
|
* @param [params] Additional parameters.
|
||||||
* @throws Will throw if decryption fails for whatever reason.
|
* @throws Will throw if decryption fails for whatever reason.
|
||||||
* @throws Will throw if any of the attributes to decrypt is not a string.
|
* @throws Will throw if any of the attributes to decrypt is not a string.
|
||||||
*/
|
*/
|
||||||
public async decryptAttributes<T extends Record<string, unknown>>(
|
public async decryptAttributes<T extends Record<string, unknown>>(
|
||||||
descriptor: SavedObjectDescriptor,
|
descriptor: SavedObjectDescriptor,
|
||||||
attributes: T
|
attributes: T,
|
||||||
|
params?: CommonParameters
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const typeRegistration = this.typeRegistrations.get(descriptor.type);
|
const typeDefinition = this.typeDefinitions.get(descriptor.type);
|
||||||
if (typeRegistration === undefined) {
|
if (typeDefinition === undefined) {
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptionAAD = this.getAAD(typeRegistration, descriptor, attributes);
|
const encryptionAAD = this.getAAD(typeDefinition, descriptor, attributes);
|
||||||
const decryptedAttributes: Record<string, string> = {};
|
const decryptedAttributes: Record<string, string> = {};
|
||||||
for (const attributeName of typeRegistration.attributesToEncrypt) {
|
for (const attributeName of typeDefinition.attributesToEncrypt) {
|
||||||
const attributeValue = attributes[attributeName];
|
const attributeValue = attributes[attributeName];
|
||||||
if (attributeValue == null) {
|
if (attributeValue == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof attributeValue !== 'string') {
|
if (typeof attributeValue !== 'string') {
|
||||||
this.audit.decryptAttributeFailure(attributeName, descriptor);
|
this.audit.decryptAttributeFailure(attributeName, descriptor, params?.user);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Encrypted "${attributeName}" attribute should be a string, but found ${typeDetect(
|
`Encrypted "${attributeName}" attribute should be a string, but found ${typeDetect(
|
||||||
attributeValue
|
attributeValue
|
||||||
|
@ -231,11 +307,12 @@ export class EncryptedSavedObjectsService {
|
||||||
)) as string;
|
)) as string;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(`Failed to decrypt "${attributeName}" attribute: ${err.message || err}`);
|
this.logger.error(`Failed to decrypt "${attributeName}" attribute: ${err.message || err}`);
|
||||||
this.audit.decryptAttributeFailure(attributeName, descriptor);
|
this.audit.decryptAttributeFailure(attributeName, descriptor, params?.user);
|
||||||
|
|
||||||
throw new EncryptionError(
|
throw new EncryptionError(
|
||||||
`Unable to decrypt attribute "${attributeName}"`,
|
`Unable to decrypt attribute "${attributeName}"`,
|
||||||
attributeName,
|
attributeName,
|
||||||
|
EncryptionErrorOperation.Decryption,
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -244,12 +321,12 @@ export class EncryptedSavedObjectsService {
|
||||||
// Normally we expect all registered to-be-encrypted attributes to be defined, but if it's
|
// Normally we expect all registered to-be-encrypted attributes to be defined, but if it's
|
||||||
// not the case we should collect and log them to make troubleshooting easier.
|
// not the case we should collect and log them to make troubleshooting easier.
|
||||||
const decryptedAttributesKeys = Object.keys(decryptedAttributes);
|
const decryptedAttributesKeys = Object.keys(decryptedAttributes);
|
||||||
if (decryptedAttributesKeys.length !== typeRegistration.attributesToEncrypt.size) {
|
if (decryptedAttributesKeys.length !== typeDefinition.attributesToEncrypt.size) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`The following attributes of saved object "${descriptorToArray(
|
`The following attributes of saved object "${descriptorToArray(
|
||||||
descriptor
|
descriptor
|
||||||
)}" should have been decrypted: ${Array.from(
|
)}" should have been decrypted: ${Array.from(
|
||||||
typeRegistration.attributesToEncrypt
|
typeDefinition.attributesToEncrypt
|
||||||
)}, but found only: ${decryptedAttributesKeys}`
|
)}, but found only: ${decryptedAttributesKeys}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -258,7 +335,7 @@ export class EncryptedSavedObjectsService {
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.audit.decryptAttributesSuccess(decryptedAttributesKeys, descriptor);
|
this.audit.decryptAttributesSuccess(decryptedAttributesKeys, descriptor, params?.user);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...attributes,
|
...attributes,
|
||||||
|
@ -269,23 +346,19 @@ export class EncryptedSavedObjectsService {
|
||||||
/**
|
/**
|
||||||
* Generates string representation of the Additional Authenticated Data based on the specified saved
|
* Generates string representation of the Additional Authenticated Data based on the specified saved
|
||||||
* object type and attributes.
|
* object type and attributes.
|
||||||
* @param typeRegistration Saved object type registration parameters.
|
* @param typeDefinition Encrypted saved object type definition.
|
||||||
* @param descriptor Descriptor of the saved object to get AAD for.
|
* @param descriptor Descriptor of the saved object to get AAD for.
|
||||||
* @param attributes All attributes of the saved object instance of the specified type.
|
* @param attributes All attributes of the saved object instance of the specified type.
|
||||||
*/
|
*/
|
||||||
private getAAD(
|
private getAAD(
|
||||||
typeRegistration: EncryptedSavedObjectTypeRegistration,
|
typeDefinition: EncryptedSavedObjectAttributesDefinition,
|
||||||
descriptor: SavedObjectDescriptor,
|
descriptor: SavedObjectDescriptor,
|
||||||
attributes: Record<string, unknown>
|
attributes: Record<string, unknown>
|
||||||
) {
|
) {
|
||||||
// Collect all attributes (both keys and values) that should contribute to AAD.
|
// Collect all attributes (both keys and values) that should contribute to AAD.
|
||||||
const attributesAAD: Record<string, unknown> = {};
|
const attributesAAD: Record<string, unknown> = {};
|
||||||
for (const [attributeKey, attributeValue] of Object.entries(attributes)) {
|
for (const [attributeKey, attributeValue] of Object.entries(attributes)) {
|
||||||
if (
|
if (!typeDefinition.shouldBeExcludedFromAAD(attributeKey)) {
|
||||||
!typeRegistration.attributesToEncrypt.has(attributeKey) &&
|
|
||||||
(typeRegistration.attributesToExcludeFromAAD == null ||
|
|
||||||
!typeRegistration.attributesToExcludeFromAAD.has(attributeKey))
|
|
||||||
) {
|
|
||||||
attributesAAD[attributeKey] = attributeValue;
|
attributesAAD[attributeKey] = attributeValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,18 +4,38 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EncryptionError } from './encryption_error';
|
import { EncryptionError, EncryptionErrorOperation } from './encryption_error';
|
||||||
|
|
||||||
test('#EncryptionError is correctly constructed', () => {
|
test('#EncryptionError is correctly constructed', () => {
|
||||||
const cause = new TypeError('Some weird error');
|
const cause = new TypeError('Some weird error');
|
||||||
const encryptionError = new EncryptionError(
|
const encryptionError = new EncryptionError(
|
||||||
'Unable to encrypt attribute "someAttr"',
|
'Unable to encrypt attribute "someAttr"',
|
||||||
'someAttr',
|
'someAttr',
|
||||||
|
EncryptionErrorOperation.Encryption,
|
||||||
cause
|
cause
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(encryptionError).toBeInstanceOf(EncryptionError);
|
expect(encryptionError).toBeInstanceOf(EncryptionError);
|
||||||
expect(encryptionError.message).toBe('Unable to encrypt attribute "someAttr"');
|
expect(encryptionError.message).toBe('Unable to encrypt attribute "someAttr"');
|
||||||
expect(encryptionError.attributeName).toBe('someAttr');
|
expect(encryptionError.attributeName).toBe('someAttr');
|
||||||
|
expect(encryptionError.operation).toBe(EncryptionErrorOperation.Encryption);
|
||||||
expect(encryptionError.cause).toBe(cause);
|
expect(encryptionError.cause).toBe(cause);
|
||||||
|
expect(JSON.stringify(encryptionError)).toMatchInlineSnapshot(
|
||||||
|
`"{\\"message\\":\\"Unable to encrypt attribute \\\\\\"someAttr\\\\\\"\\"}"`
|
||||||
|
);
|
||||||
|
|
||||||
|
const decryptionErrorWithoutCause = new EncryptionError(
|
||||||
|
'Unable to decrypt attribute "someAttr"',
|
||||||
|
'someAttr',
|
||||||
|
EncryptionErrorOperation.Decryption
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(decryptionErrorWithoutCause).toBeInstanceOf(EncryptionError);
|
||||||
|
expect(decryptionErrorWithoutCause.message).toBe('Unable to decrypt attribute "someAttr"');
|
||||||
|
expect(decryptionErrorWithoutCause.attributeName).toBe('someAttr');
|
||||||
|
expect(decryptionErrorWithoutCause.operation).toBe(EncryptionErrorOperation.Decryption);
|
||||||
|
expect(decryptionErrorWithoutCause.cause).toBeUndefined();
|
||||||
|
expect(JSON.stringify(decryptionErrorWithoutCause)).toMatchInlineSnapshot(
|
||||||
|
`"{\\"message\\":\\"Unable to decrypt attribute \\\\\\"someAttr\\\\\\"\\"}"`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,10 +4,19 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines operation (encryption or decryption) during which error occurred.
|
||||||
|
*/
|
||||||
|
export enum EncryptionErrorOperation {
|
||||||
|
Encryption,
|
||||||
|
Decryption,
|
||||||
|
}
|
||||||
|
|
||||||
export class EncryptionError extends Error {
|
export class EncryptionError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
public readonly attributeName: string,
|
public readonly attributeName: string,
|
||||||
|
public readonly operation: EncryptionErrorOperation,
|
||||||
public readonly cause?: Error
|
public readonly cause?: Error
|
||||||
) {
|
) {
|
||||||
super(message);
|
super(message);
|
||||||
|
@ -16,4 +25,8 @@ export class EncryptionError extends Error {
|
||||||
// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
||||||
Object.setPrototypeOf(this, EncryptionError.prototype);
|
Object.setPrototypeOf(this, EncryptionError.prototype);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return { message: this.message };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ export const encryptedSavedObjectsServiceMock = {
|
||||||
function processAttributes<T extends Record<string, any>>(
|
function processAttributes<T extends Record<string, any>>(
|
||||||
descriptor: Pick<SavedObjectDescriptor, 'type'>,
|
descriptor: Pick<SavedObjectDescriptor, 'type'>,
|
||||||
attrs: T,
|
attrs: T,
|
||||||
action: (attrs: T, attrName: string) => void
|
action: (attrs: T, attrName: string, shouldExpose: boolean) => void
|
||||||
) {
|
) {
|
||||||
const registration = registrations.find(r => r.type === descriptor.type);
|
const registration = registrations.find(r => r.type === descriptor.type);
|
||||||
if (!registration) {
|
if (!registration) {
|
||||||
|
@ -27,9 +27,13 @@ export const encryptedSavedObjectsServiceMock = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const clonedAttrs = { ...attrs };
|
const clonedAttrs = { ...attrs };
|
||||||
for (const attrName of registration.attributesToEncrypt) {
|
for (const attr of registration.attributesToEncrypt) {
|
||||||
|
const [attrName, shouldExpose] =
|
||||||
|
typeof attr === 'string'
|
||||||
|
? [attr, false]
|
||||||
|
: [attr.key, attr.dangerouslyExposeValue === true];
|
||||||
if (attrName in clonedAttrs) {
|
if (attrName in clonedAttrs) {
|
||||||
action(clonedAttrs, attrName);
|
action(clonedAttrs, attrName, shouldExpose);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return clonedAttrs;
|
return clonedAttrs;
|
||||||
|
@ -53,8 +57,16 @@ export const encryptedSavedObjectsServiceMock = {
|
||||||
(clonedAttrs[attrName] = (clonedAttrs[attrName] as string).slice(1, -1))
|
(clonedAttrs[attrName] = (clonedAttrs[attrName] as string).slice(1, -1))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
mock.stripEncryptedAttributes.mockImplementation((type, attrs) =>
|
mock.stripOrDecryptAttributes.mockImplementation((descriptor, attrs) =>
|
||||||
processAttributes({ type }, attrs, (clonedAttrs, attrName) => delete clonedAttrs[attrName])
|
Promise.resolve({
|
||||||
|
attributes: processAttributes(descriptor, attrs, (clonedAttrs, attrName, shouldExpose) => {
|
||||||
|
if (shouldExpose) {
|
||||||
|
clonedAttrs[attrName] = (clonedAttrs[attrName] as string).slice(1, -1);
|
||||||
|
} else {
|
||||||
|
delete clonedAttrs[attrName];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return mock;
|
return mock;
|
||||||
|
|
|
@ -7,12 +7,14 @@
|
||||||
import { Plugin } from './plugin';
|
import { Plugin } from './plugin';
|
||||||
|
|
||||||
import { coreMock } from 'src/core/server/mocks';
|
import { coreMock } from 'src/core/server/mocks';
|
||||||
|
import { securityMock } from '../../security/server/mocks';
|
||||||
|
|
||||||
describe('EncryptedSavedObjects Plugin', () => {
|
describe('EncryptedSavedObjects Plugin', () => {
|
||||||
describe('setup()', () => {
|
describe('setup()', () => {
|
||||||
it('exposes proper contract', async () => {
|
it('exposes proper contract', async () => {
|
||||||
const plugin = new Plugin(coreMock.createPluginInitializerContext());
|
const plugin = new Plugin(coreMock.createPluginInitializerContext());
|
||||||
await expect(plugin.setup(coreMock.createSetup())).resolves.toMatchInlineSnapshot(`
|
await expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() }))
|
||||||
|
.resolves.toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"__legacyCompat": Object {
|
"__legacyCompat": Object {
|
||||||
"registerLegacyAPI": [Function],
|
"registerLegacyAPI": [Function],
|
||||||
|
@ -27,7 +29,7 @@ describe('EncryptedSavedObjects Plugin', () => {
|
||||||
describe('start()', () => {
|
describe('start()', () => {
|
||||||
it('exposes proper contract', async () => {
|
it('exposes proper contract', async () => {
|
||||||
const plugin = new Plugin(coreMock.createPluginInitializerContext());
|
const plugin = new Plugin(coreMock.createPluginInitializerContext());
|
||||||
await plugin.setup(coreMock.createSetup());
|
await plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() });
|
||||||
await expect(plugin.start()).toMatchInlineSnapshot(`
|
await expect(plugin.start()).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"getDecryptedAsInternalUser": [Function],
|
"getDecryptedAsInternalUser": [Function],
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
CoreSetup,
|
CoreSetup,
|
||||||
} from 'src/core/server';
|
} from 'src/core/server';
|
||||||
import { first } from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
|
import { SecurityPluginSetup } from '../../security/server';
|
||||||
import { createConfig$ } from './config';
|
import { createConfig$ } from './config';
|
||||||
import {
|
import {
|
||||||
EncryptedSavedObjectsService,
|
EncryptedSavedObjectsService,
|
||||||
|
@ -20,6 +21,10 @@ import {
|
||||||
import { EncryptedSavedObjectsAuditLogger } from './audit';
|
import { EncryptedSavedObjectsAuditLogger } from './audit';
|
||||||
import { SavedObjectsSetup, setupSavedObjects } from './saved_objects';
|
import { SavedObjectsSetup, setupSavedObjects } from './saved_objects';
|
||||||
|
|
||||||
|
export interface PluginsSetup {
|
||||||
|
security?: SecurityPluginSetup;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EncryptedSavedObjectsPluginSetup {
|
export interface EncryptedSavedObjectsPluginSetup {
|
||||||
registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => void;
|
registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => void;
|
||||||
__legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => void };
|
__legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => void };
|
||||||
|
@ -59,7 +64,10 @@ export class Plugin {
|
||||||
this.logger = this.initializerContext.logger.get();
|
this.logger = this.initializerContext.logger.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setup(core: CoreSetup): Promise<EncryptedSavedObjectsPluginSetup> {
|
public async setup(
|
||||||
|
core: CoreSetup,
|
||||||
|
deps: PluginsSetup
|
||||||
|
): Promise<EncryptedSavedObjectsPluginSetup> {
|
||||||
const { config, usingEphemeralEncryptionKey } = await createConfig$(this.initializerContext)
|
const { config, usingEphemeralEncryptionKey } = await createConfig$(this.initializerContext)
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.toPromise();
|
.toPromise();
|
||||||
|
@ -75,6 +83,7 @@ export class Plugin {
|
||||||
this.savedObjectsSetup = setupSavedObjects({
|
this.savedObjectsSetup = setupSavedObjects({
|
||||||
service,
|
service,
|
||||||
savedObjects: core.savedObjects,
|
savedObjects: core.savedObjects,
|
||||||
|
security: deps.security,
|
||||||
getStartServices: core.getStartServices,
|
getStartServices: core.getStartServices,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,15 +4,17 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('uuid-v4-id') }));
|
import { EncryptionErrorOperation } from '../crypto/encryption_error';
|
||||||
|
|
||||||
import { SavedObjectsClientContract } from 'src/core/server';
|
import { SavedObjectsClientContract } from 'src/core/server';
|
||||||
import { EncryptedSavedObjectsService } from '../crypto';
|
import { EncryptedSavedObjectsService, EncryptionError } from '../crypto';
|
||||||
import { EncryptedSavedObjectsClientWrapper } from './encrypted_saved_objects_client_wrapper';
|
import { EncryptedSavedObjectsClientWrapper } from './encrypted_saved_objects_client_wrapper';
|
||||||
|
|
||||||
import { savedObjectsClientMock, savedObjectsTypeRegistryMock } from 'src/core/server/mocks';
|
import { savedObjectsClientMock, savedObjectsTypeRegistryMock } from 'src/core/server/mocks';
|
||||||
|
import { mockAuthenticatedUser } from '../../../security/common/model/authenticated_user.mock';
|
||||||
import { encryptedSavedObjectsServiceMock } from '../crypto/index.mock';
|
import { encryptedSavedObjectsServiceMock } from '../crypto/index.mock';
|
||||||
|
|
||||||
|
jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('uuid-v4-id') }));
|
||||||
|
|
||||||
let wrapper: EncryptedSavedObjectsClientWrapper;
|
let wrapper: EncryptedSavedObjectsClientWrapper;
|
||||||
let mockBaseClient: jest.Mocked<SavedObjectsClientContract>;
|
let mockBaseClient: jest.Mocked<SavedObjectsClientContract>;
|
||||||
let mockBaseTypeRegistry: ReturnType<typeof savedObjectsTypeRegistryMock.create>;
|
let mockBaseTypeRegistry: ReturnType<typeof savedObjectsTypeRegistryMock.create>;
|
||||||
|
@ -23,7 +25,10 @@ beforeEach(() => {
|
||||||
encryptedSavedObjectsServiceMockInstance = encryptedSavedObjectsServiceMock.create([
|
encryptedSavedObjectsServiceMockInstance = encryptedSavedObjectsServiceMock.create([
|
||||||
{
|
{
|
||||||
type: 'known-type',
|
type: 'known-type',
|
||||||
attributesToEncrypt: new Set(['attrSecret']),
|
attributesToEncrypt: new Set([
|
||||||
|
'attrSecret',
|
||||||
|
{ key: 'attrNotSoSecret', dangerouslyExposeValue: true },
|
||||||
|
]),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -31,6 +36,7 @@ beforeEach(() => {
|
||||||
service: encryptedSavedObjectsServiceMockInstance,
|
service: encryptedSavedObjectsServiceMockInstance,
|
||||||
baseClient: mockBaseClient,
|
baseClient: mockBaseClient,
|
||||||
baseTypeRegistry: mockBaseTypeRegistry,
|
baseTypeRegistry: mockBaseTypeRegistry,
|
||||||
|
getCurrentUser: () => mockAuthenticatedUser(),
|
||||||
} as any);
|
} as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -63,13 +69,23 @@ describe('#create', () => {
|
||||||
expect(mockBaseClient.create).not.toHaveBeenCalled();
|
expect(mockBaseClient.create).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('generates ID, encrypts attributes and strips them from response', async () => {
|
it('generates ID, encrypts attributes and strips them from response except for ones with `dangerouslyExposeValue` set to `true`', async () => {
|
||||||
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
|
const attributes = {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: 'secret',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
|
attrThree: 'three',
|
||||||
|
};
|
||||||
const options = { overwrite: true };
|
const options = { overwrite: true };
|
||||||
const mockedResponse = {
|
const mockedResponse = {
|
||||||
id: 'uuid-v4-id',
|
id: 'uuid-v4-id',
|
||||||
type: 'known-type',
|
type: 'known-type',
|
||||||
attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
|
attributes: {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
references: [],
|
references: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -77,19 +93,30 @@ describe('#create', () => {
|
||||||
|
|
||||||
expect(await wrapper.create('known-type', attributes, options)).toEqual({
|
expect(await wrapper.create('known-type', attributes, options)).toEqual({
|
||||||
...mockedResponse,
|
...mockedResponse,
|
||||||
attributes: { attrOne: 'one', attrThree: 'three' },
|
attributes: { attrOne: 'one', attrNotSoSecret: 'not-so-secret', attrThree: 'three' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
|
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
|
||||||
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
|
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
|
||||||
{ type: 'known-type', id: 'uuid-v4-id' },
|
{ type: 'known-type', id: 'uuid-v4-id' },
|
||||||
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
|
{
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: 'secret',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
{ user: mockAuthenticatedUser() }
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockBaseClient.create).toHaveBeenCalledTimes(1);
|
expect(mockBaseClient.create).toHaveBeenCalledTimes(1);
|
||||||
expect(mockBaseClient.create).toHaveBeenCalledWith(
|
expect(mockBaseClient.create).toHaveBeenCalledWith(
|
||||||
'known-type',
|
'known-type',
|
||||||
{ attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
|
{
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
{ id: 'uuid-v4-id', overwrite: true }
|
{ id: 'uuid-v4-id', overwrite: true }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -119,7 +146,8 @@ describe('#create', () => {
|
||||||
id: 'uuid-v4-id',
|
id: 'uuid-v4-id',
|
||||||
namespace: expectNamespaceInDescriptor ? namespace : undefined,
|
namespace: expectNamespaceInDescriptor ? namespace : undefined,
|
||||||
},
|
},
|
||||||
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
|
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
|
||||||
|
{ user: mockAuthenticatedUser() }
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockBaseClient.create).toHaveBeenCalledTimes(1);
|
expect(mockBaseClient.create).toHaveBeenCalledTimes(1);
|
||||||
|
@ -221,14 +249,19 @@ describe('#bulkCreate', () => {
|
||||||
expect(mockBaseClient.bulkCreate).not.toHaveBeenCalled();
|
expect(mockBaseClient.bulkCreate).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('generates ID, encrypts attributes and strips them from response', async () => {
|
it('generates ID, encrypts attributes and strips them from response except for ones with `dangerouslyExposeValue` set to `true`', async () => {
|
||||||
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
|
const attributes = {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: 'secret',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
|
attrThree: 'three',
|
||||||
|
};
|
||||||
const mockedResponse = {
|
const mockedResponse = {
|
||||||
saved_objects: [
|
saved_objects: [
|
||||||
{
|
{
|
||||||
id: 'uuid-v4-id',
|
id: 'uuid-v4-id',
|
||||||
type: 'known-type',
|
type: 'known-type',
|
||||||
attributes,
|
attributes: { ...attributes, attrSecret: '*secret*', attrNotSoSecret: '*not-so-secret*' },
|
||||||
references: [],
|
references: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -249,7 +282,10 @@ describe('#bulkCreate', () => {
|
||||||
|
|
||||||
await expect(wrapper.bulkCreate(bulkCreateParams)).resolves.toEqual({
|
await expect(wrapper.bulkCreate(bulkCreateParams)).resolves.toEqual({
|
||||||
saved_objects: [
|
saved_objects: [
|
||||||
{ ...mockedResponse.saved_objects[0], attributes: { attrOne: 'one', attrThree: 'three' } },
|
{
|
||||||
|
...mockedResponse.saved_objects[0],
|
||||||
|
attributes: { attrOne: 'one', attrNotSoSecret: 'not-so-secret', attrThree: 'three' },
|
||||||
|
},
|
||||||
mockedResponse.saved_objects[1],
|
mockedResponse.saved_objects[1],
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -257,7 +293,13 @@ describe('#bulkCreate', () => {
|
||||||
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
|
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
|
||||||
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
|
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
|
||||||
{ type: 'known-type', id: 'uuid-v4-id' },
|
{ type: 'known-type', id: 'uuid-v4-id' },
|
||||||
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
|
{
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: 'secret',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
{ user: mockAuthenticatedUser() }
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockBaseClient.bulkCreate).toHaveBeenCalledTimes(1);
|
expect(mockBaseClient.bulkCreate).toHaveBeenCalledTimes(1);
|
||||||
|
@ -266,7 +308,12 @@ describe('#bulkCreate', () => {
|
||||||
{
|
{
|
||||||
...bulkCreateParams[0],
|
...bulkCreateParams[0],
|
||||||
id: 'uuid-v4-id',
|
id: 'uuid-v4-id',
|
||||||
attributes: { attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
|
attributes: {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
bulkCreateParams[1],
|
bulkCreateParams[1],
|
||||||
],
|
],
|
||||||
|
@ -301,7 +348,8 @@ describe('#bulkCreate', () => {
|
||||||
id: 'uuid-v4-id',
|
id: 'uuid-v4-id',
|
||||||
namespace: expectNamespaceInDescriptor ? namespace : undefined,
|
namespace: expectNamespaceInDescriptor ? namespace : undefined,
|
||||||
},
|
},
|
||||||
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
|
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
|
||||||
|
{ user: mockAuthenticatedUser() }
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockBaseClient.bulkCreate).toHaveBeenCalledTimes(1);
|
expect(mockBaseClient.bulkCreate).toHaveBeenCalledTimes(1);
|
||||||
|
@ -377,7 +425,7 @@ describe('#bulkUpdate', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('encrypts attributes and strips them from response', async () => {
|
it('encrypts attributes and strips them from response except for ones with `dangerouslyExposeValue` set to `true`', async () => {
|
||||||
const docs = [
|
const docs = [
|
||||||
{
|
{
|
||||||
id: 'some-id',
|
id: 'some-id',
|
||||||
|
@ -385,6 +433,7 @@ describe('#bulkUpdate', () => {
|
||||||
attributes: {
|
attributes: {
|
||||||
attrOne: 'one',
|
attrOne: 'one',
|
||||||
attrSecret: 'secret',
|
attrSecret: 'secret',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
attrThree: 'three',
|
attrThree: 'three',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -394,13 +443,22 @@ describe('#bulkUpdate', () => {
|
||||||
attributes: {
|
attributes: {
|
||||||
attrOne: 'one 2',
|
attrOne: 'one 2',
|
||||||
attrSecret: 'secret 2',
|
attrSecret: 'secret 2',
|
||||||
|
attrNotSoSecret: 'not-so-secret 2',
|
||||||
attrThree: 'three 2',
|
attrThree: 'three 2',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const mockedResponse = {
|
const mockedResponse = {
|
||||||
saved_objects: docs.map(doc => ({ ...doc, references: undefined })),
|
saved_objects: docs.map(doc => ({
|
||||||
|
...doc,
|
||||||
|
attributes: {
|
||||||
|
...doc.attributes,
|
||||||
|
attrSecret: `*${doc.attributes.attrSecret}*`,
|
||||||
|
attrNotSoSecret: `*${doc.attributes.attrNotSoSecret}*`,
|
||||||
|
},
|
||||||
|
references: undefined,
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
mockBaseClient.bulkUpdate.mockResolvedValue(mockedResponse);
|
mockBaseClient.bulkUpdate.mockResolvedValue(mockedResponse);
|
||||||
|
@ -417,6 +475,7 @@ describe('#bulkUpdate', () => {
|
||||||
type: 'known-type',
|
type: 'known-type',
|
||||||
attributes: {
|
attributes: {
|
||||||
attrOne: 'one',
|
attrOne: 'one',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
attrThree: 'three',
|
attrThree: 'three',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -425,6 +484,7 @@ describe('#bulkUpdate', () => {
|
||||||
type: 'known-type',
|
type: 'known-type',
|
||||||
attributes: {
|
attributes: {
|
||||||
attrOne: 'one 2',
|
attrOne: 'one 2',
|
||||||
|
attrNotSoSecret: 'not-so-secret 2',
|
||||||
attrThree: 'three 2',
|
attrThree: 'three 2',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -434,11 +494,23 @@ describe('#bulkUpdate', () => {
|
||||||
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(2);
|
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(2);
|
||||||
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
|
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
|
||||||
{ type: 'known-type', id: 'some-id' },
|
{ type: 'known-type', id: 'some-id' },
|
||||||
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
|
{
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: 'secret',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
{ user: mockAuthenticatedUser() }
|
||||||
);
|
);
|
||||||
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
|
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
|
||||||
{ type: 'known-type', id: 'some-id-2' },
|
{ type: 'known-type', id: 'some-id-2' },
|
||||||
{ attrOne: 'one 2', attrSecret: 'secret 2', attrThree: 'three 2' }
|
{
|
||||||
|
attrOne: 'one 2',
|
||||||
|
attrSecret: 'secret 2',
|
||||||
|
attrNotSoSecret: 'not-so-secret 2',
|
||||||
|
attrThree: 'three 2',
|
||||||
|
},
|
||||||
|
{ user: mockAuthenticatedUser() }
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledTimes(1);
|
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledTimes(1);
|
||||||
|
@ -450,6 +522,7 @@ describe('#bulkUpdate', () => {
|
||||||
attributes: {
|
attributes: {
|
||||||
attrOne: 'one',
|
attrOne: 'one',
|
||||||
attrSecret: '*secret*',
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
attrThree: 'three',
|
attrThree: 'three',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -459,6 +532,7 @@ describe('#bulkUpdate', () => {
|
||||||
attributes: {
|
attributes: {
|
||||||
attrOne: 'one 2',
|
attrOne: 'one 2',
|
||||||
attrSecret: '*secret 2*',
|
attrSecret: '*secret 2*',
|
||||||
|
attrNotSoSecret: '*not-so-secret 2*',
|
||||||
attrThree: 'three 2',
|
attrThree: 'three 2',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -509,7 +583,8 @@ describe('#bulkUpdate', () => {
|
||||||
id: 'some-id',
|
id: 'some-id',
|
||||||
namespace: expectNamespaceInDescriptor ? namespace : undefined,
|
namespace: expectNamespaceInDescriptor ? namespace : undefined,
|
||||||
},
|
},
|
||||||
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
|
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
|
||||||
|
{ user: mockAuthenticatedUser() }
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledTimes(1);
|
expect(mockBaseClient.bulkUpdate).toHaveBeenCalledTimes(1);
|
||||||
|
@ -635,19 +710,29 @@ describe('#find', () => {
|
||||||
expect(mockBaseClient.find).toHaveBeenCalledWith(options);
|
expect(mockBaseClient.find).toHaveBeenCalledWith(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('redirects request to underlying base client and strips encrypted attributes if type is registered', async () => {
|
it('redirects request to underlying base client and strips encrypted attributes except for ones with `dangerouslyExposeValue` set to `true` if type is registered', async () => {
|
||||||
const mockedResponse = {
|
const mockedResponse = {
|
||||||
saved_objects: [
|
saved_objects: [
|
||||||
{
|
{
|
||||||
id: 'some-id',
|
id: 'some-id',
|
||||||
type: 'unknown-type',
|
type: 'unknown-type',
|
||||||
attributes: { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
|
attributes: {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: 'secret',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
references: [],
|
references: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'some-id-2',
|
id: 'some-id-2',
|
||||||
type: 'known-type',
|
type: 'known-type',
|
||||||
attributes: { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
|
attributes: {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
references: [],
|
references: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -664,16 +749,118 @@ describe('#find', () => {
|
||||||
saved_objects: [
|
saved_objects: [
|
||||||
{
|
{
|
||||||
...mockedResponse.saved_objects[0],
|
...mockedResponse.saved_objects[0],
|
||||||
attributes: { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
|
attributes: {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: 'secret',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...mockedResponse.saved_objects[1],
|
...mockedResponse.saved_objects[1],
|
||||||
attributes: { attrOne: 'one', attrThree: 'three' },
|
attributes: { attrOne: 'one', attrNotSoSecret: 'not-so-secret', attrThree: 'three' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
expect(mockBaseClient.find).toHaveBeenCalledTimes(1);
|
expect(mockBaseClient.find).toHaveBeenCalledTimes(1);
|
||||||
expect(mockBaseClient.find).toHaveBeenCalledWith(options);
|
expect(mockBaseClient.find).toHaveBeenCalledWith(options);
|
||||||
|
|
||||||
|
expect(encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes).toHaveBeenCalledTimes(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes).toHaveBeenCalledWith(
|
||||||
|
{ type: 'known-type', id: 'some-id-2' },
|
||||||
|
{
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ user: mockAuthenticatedUser() }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes both attributes and error if decryption fails.', async () => {
|
||||||
|
const mockedResponse = {
|
||||||
|
saved_objects: [
|
||||||
|
{
|
||||||
|
id: 'some-id',
|
||||||
|
type: 'unknown-type',
|
||||||
|
attributes: {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: 'secret',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
references: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'some-id-2',
|
||||||
|
type: 'known-type',
|
||||||
|
attributes: {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
references: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 2,
|
||||||
|
per_page: 2,
|
||||||
|
page: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockBaseClient.find.mockResolvedValue(mockedResponse);
|
||||||
|
|
||||||
|
const decryptionError = new EncryptionError(
|
||||||
|
'something failed',
|
||||||
|
'attrNotSoSecret',
|
||||||
|
EncryptionErrorOperation.Decryption
|
||||||
|
);
|
||||||
|
encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes.mockResolvedValue({
|
||||||
|
attributes: { attrOne: 'one', attrThree: 'three' },
|
||||||
|
error: decryptionError,
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = { type: ['unknown-type', 'known-type'], search: 'query' };
|
||||||
|
await expect(wrapper.find(options)).resolves.toEqual({
|
||||||
|
...mockedResponse,
|
||||||
|
saved_objects: [
|
||||||
|
{
|
||||||
|
...mockedResponse.saved_objects[0],
|
||||||
|
attributes: {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: 'secret',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...mockedResponse.saved_objects[1],
|
||||||
|
attributes: { attrOne: 'one', attrThree: 'three' },
|
||||||
|
error: decryptionError,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(mockBaseClient.find).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockBaseClient.find).toHaveBeenCalledWith(options);
|
||||||
|
|
||||||
|
expect(encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes).toHaveBeenCalledTimes(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes).toHaveBeenCalledWith(
|
||||||
|
{ type: 'known-type', id: 'some-id-2' },
|
||||||
|
{
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ user: mockAuthenticatedUser() }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails if base client fails', async () => {
|
it('fails if base client fails', async () => {
|
||||||
|
@ -734,19 +921,29 @@ describe('#bulkGet', () => {
|
||||||
expect(mockBaseClient.bulkGet).toHaveBeenCalledWith(bulkGetParams, options);
|
expect(mockBaseClient.bulkGet).toHaveBeenCalledWith(bulkGetParams, options);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('redirects request to underlying base client and strips encrypted attributes if type is registered', async () => {
|
it('redirects request to underlying base client and strips encrypted attributes except for ones with `dangerouslyExposeValue` set to `true` if type is registered', async () => {
|
||||||
const mockedResponse = {
|
const mockedResponse = {
|
||||||
saved_objects: [
|
saved_objects: [
|
||||||
{
|
{
|
||||||
id: 'some-id',
|
id: 'some-id',
|
||||||
type: 'unknown-type',
|
type: 'unknown-type',
|
||||||
attributes: { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
|
attributes: {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: 'secret',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
references: [],
|
references: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'some-id-2',
|
id: 'some-id-2',
|
||||||
type: 'known-type',
|
type: 'known-type',
|
||||||
attributes: { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
|
attributes: {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
references: [],
|
references: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -768,16 +965,123 @@ describe('#bulkGet', () => {
|
||||||
saved_objects: [
|
saved_objects: [
|
||||||
{
|
{
|
||||||
...mockedResponse.saved_objects[0],
|
...mockedResponse.saved_objects[0],
|
||||||
attributes: { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
|
attributes: {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: 'secret',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...mockedResponse.saved_objects[1],
|
...mockedResponse.saved_objects[1],
|
||||||
attributes: { attrOne: 'one', attrThree: 'three' },
|
attributes: { attrOne: 'one', attrNotSoSecret: 'not-so-secret', attrThree: 'three' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
expect(mockBaseClient.bulkGet).toHaveBeenCalledTimes(1);
|
expect(mockBaseClient.bulkGet).toHaveBeenCalledTimes(1);
|
||||||
expect(mockBaseClient.bulkGet).toHaveBeenCalledWith(bulkGetParams, options);
|
expect(mockBaseClient.bulkGet).toHaveBeenCalledWith(bulkGetParams, options);
|
||||||
|
|
||||||
|
expect(encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes).toHaveBeenCalledTimes(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes).toHaveBeenCalledWith(
|
||||||
|
{ type: 'known-type', id: 'some-id-2', namespace: 'some-ns' },
|
||||||
|
{
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ user: mockAuthenticatedUser() }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes both attributes and error if decryption fails.', async () => {
|
||||||
|
const mockedResponse = {
|
||||||
|
saved_objects: [
|
||||||
|
{
|
||||||
|
id: 'some-id',
|
||||||
|
type: 'unknown-type',
|
||||||
|
attributes: {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: 'secret',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
references: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'some-id-2',
|
||||||
|
type: 'known-type',
|
||||||
|
attributes: {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
references: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 2,
|
||||||
|
per_page: 2,
|
||||||
|
page: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockBaseClient.bulkGet.mockResolvedValue(mockedResponse);
|
||||||
|
|
||||||
|
const decryptionError = new EncryptionError(
|
||||||
|
'something failed',
|
||||||
|
'attrNotSoSecret',
|
||||||
|
EncryptionErrorOperation.Decryption
|
||||||
|
);
|
||||||
|
encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes.mockResolvedValue({
|
||||||
|
attributes: { attrOne: 'one', attrThree: 'three' },
|
||||||
|
error: decryptionError,
|
||||||
|
});
|
||||||
|
|
||||||
|
const bulkGetParams = [
|
||||||
|
{ type: 'unknown-type', id: 'some-id' },
|
||||||
|
{ type: 'known-type', id: 'some-id-2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const options = { namespace: 'some-ns' };
|
||||||
|
await expect(wrapper.bulkGet(bulkGetParams, options)).resolves.toEqual({
|
||||||
|
...mockedResponse,
|
||||||
|
saved_objects: [
|
||||||
|
{
|
||||||
|
...mockedResponse.saved_objects[0],
|
||||||
|
attributes: {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: 'secret',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...mockedResponse.saved_objects[1],
|
||||||
|
attributes: { attrOne: 'one', attrThree: 'three' },
|
||||||
|
error: decryptionError,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(mockBaseClient.bulkGet).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockBaseClient.bulkGet).toHaveBeenCalledWith(bulkGetParams, options);
|
||||||
|
|
||||||
|
expect(encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes).toHaveBeenCalledTimes(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes).toHaveBeenCalledWith(
|
||||||
|
{ type: 'known-type', id: 'some-id-2', namespace: 'some-ns' },
|
||||||
|
{
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ user: mockAuthenticatedUser() }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails if base client fails', async () => {
|
it('fails if base client fails', async () => {
|
||||||
|
@ -814,11 +1118,7 @@ describe('#bulkGet', () => {
|
||||||
const options = { namespace: 'some-ns' };
|
const options = { namespace: 'some-ns' };
|
||||||
await expect(wrapper.bulkGet(bulkGetParams, options)).resolves.toEqual({
|
await expect(wrapper.bulkGet(bulkGetParams, options)).resolves.toEqual({
|
||||||
...mockedResponse,
|
...mockedResponse,
|
||||||
saved_objects: [
|
saved_objects: [{ ...mockedResponse.saved_objects[0] }],
|
||||||
{
|
|
||||||
...mockedResponse.saved_objects[0],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
expect(mockBaseClient.bulkGet).toHaveBeenCalledTimes(1);
|
expect(mockBaseClient.bulkGet).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
@ -844,11 +1144,16 @@ describe('#get', () => {
|
||||||
expect(mockBaseClient.get).toHaveBeenCalledWith('unknown-type', 'some-id', options);
|
expect(mockBaseClient.get).toHaveBeenCalledWith('unknown-type', 'some-id', options);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('redirects request to underlying base client and strips encrypted attributes if type is registered', async () => {
|
it('redirects request to underlying base client and strips encrypted attributes except for ones with `dangerouslyExposeValue` set to `true` if type is registered', async () => {
|
||||||
const mockedResponse = {
|
const mockedResponse = {
|
||||||
id: 'some-id',
|
id: 'some-id',
|
||||||
type: 'known-type',
|
type: 'known-type',
|
||||||
attributes: { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
|
attributes: {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
references: [],
|
references: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -857,10 +1162,75 @@ describe('#get', () => {
|
||||||
const options = { namespace: 'some-ns' };
|
const options = { namespace: 'some-ns' };
|
||||||
await expect(wrapper.get('known-type', 'some-id', options)).resolves.toEqual({
|
await expect(wrapper.get('known-type', 'some-id', options)).resolves.toEqual({
|
||||||
...mockedResponse,
|
...mockedResponse,
|
||||||
attributes: { attrOne: 'one', attrThree: 'three' },
|
attributes: { attrOne: 'one', attrNotSoSecret: 'not-so-secret', attrThree: 'three' },
|
||||||
});
|
});
|
||||||
expect(mockBaseClient.get).toHaveBeenCalledTimes(1);
|
expect(mockBaseClient.get).toHaveBeenCalledTimes(1);
|
||||||
expect(mockBaseClient.get).toHaveBeenCalledWith('known-type', 'some-id', options);
|
expect(mockBaseClient.get).toHaveBeenCalledWith('known-type', 'some-id', options);
|
||||||
|
|
||||||
|
expect(encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes).toHaveBeenCalledTimes(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes).toHaveBeenCalledWith(
|
||||||
|
{ type: 'known-type', id: 'some-id', namespace: 'some-ns' },
|
||||||
|
{
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ user: mockAuthenticatedUser() }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes both attributes and error if decryption fails.', async () => {
|
||||||
|
const mockedResponse = {
|
||||||
|
id: 'some-id',
|
||||||
|
type: 'known-type',
|
||||||
|
attributes: {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
references: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockBaseClient.get.mockResolvedValue(mockedResponse);
|
||||||
|
|
||||||
|
const decryptionError = new EncryptionError(
|
||||||
|
'something failed',
|
||||||
|
'attrNotSoSecret',
|
||||||
|
EncryptionErrorOperation.Decryption
|
||||||
|
);
|
||||||
|
encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes.mockResolvedValue({
|
||||||
|
attributes: { attrOne: 'one', attrThree: 'three' },
|
||||||
|
error: decryptionError,
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = { namespace: 'some-ns' };
|
||||||
|
await expect(wrapper.get('known-type', 'some-id', options)).resolves.toEqual({
|
||||||
|
...mockedResponse,
|
||||||
|
attributes: { attrOne: 'one', attrThree: 'three' },
|
||||||
|
error: decryptionError,
|
||||||
|
});
|
||||||
|
expect(mockBaseClient.get).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockBaseClient.get).toHaveBeenCalledWith('known-type', 'some-id', options);
|
||||||
|
|
||||||
|
expect(encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes).toHaveBeenCalledTimes(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(encryptedSavedObjectsServiceMockInstance.stripOrDecryptAttributes).toHaveBeenCalledWith(
|
||||||
|
{ type: 'known-type', id: 'some-id', namespace: 'some-ns' },
|
||||||
|
{
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ user: mockAuthenticatedUser() }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails if base client fails', async () => {
|
it('fails if base client fails', async () => {
|
||||||
|
@ -895,29 +1265,54 @@ describe('#update', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('encrypts attributes and strips them from response', async () => {
|
it('encrypts attributes and strips them from response except for ones with `dangerouslyExposeValue` set to `true`', async () => {
|
||||||
const attributes = { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' };
|
const attributes = {
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: 'secret',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
|
attrThree: 'three',
|
||||||
|
};
|
||||||
const options = { version: 'some-version' };
|
const options = { version: 'some-version' };
|
||||||
const mockedResponse = { id: 'some-id', type: 'known-type', attributes, references: [] };
|
const mockedResponse = {
|
||||||
|
id: 'some-id',
|
||||||
|
type: 'known-type',
|
||||||
|
attributes: {
|
||||||
|
...attributes,
|
||||||
|
attrSecret: `*${attributes.attrSecret}*`,
|
||||||
|
attrNotSoSecret: `*${attributes.attrNotSoSecret}*`,
|
||||||
|
},
|
||||||
|
references: [],
|
||||||
|
};
|
||||||
|
|
||||||
mockBaseClient.update.mockResolvedValue(mockedResponse);
|
mockBaseClient.update.mockResolvedValue(mockedResponse);
|
||||||
|
|
||||||
await expect(wrapper.update('known-type', 'some-id', attributes, options)).resolves.toEqual({
|
await expect(wrapper.update('known-type', 'some-id', attributes, options)).resolves.toEqual({
|
||||||
...mockedResponse,
|
...mockedResponse,
|
||||||
attributes: { attrOne: 'one', attrThree: 'three' },
|
attributes: { attrOne: 'one', attrNotSoSecret: 'not-so-secret', attrThree: 'three' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
|
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
|
||||||
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
|
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
|
||||||
{ type: 'known-type', id: 'some-id' },
|
{ type: 'known-type', id: 'some-id' },
|
||||||
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
|
{
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: 'secret',
|
||||||
|
attrNotSoSecret: 'not-so-secret',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
|
{ user: mockAuthenticatedUser() }
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockBaseClient.update).toHaveBeenCalledTimes(1);
|
expect(mockBaseClient.update).toHaveBeenCalledTimes(1);
|
||||||
expect(mockBaseClient.update).toHaveBeenCalledWith(
|
expect(mockBaseClient.update).toHaveBeenCalledWith(
|
||||||
'known-type',
|
'known-type',
|
||||||
'some-id',
|
'some-id',
|
||||||
{ attrOne: 'one', attrSecret: '*secret*', attrThree: 'three' },
|
{
|
||||||
|
attrOne: 'one',
|
||||||
|
attrSecret: '*secret*',
|
||||||
|
attrNotSoSecret: '*not-so-secret*',
|
||||||
|
attrThree: 'three',
|
||||||
|
},
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -942,7 +1337,8 @@ describe('#update', () => {
|
||||||
id: 'some-id',
|
id: 'some-id',
|
||||||
namespace: expectNamespaceInDescriptor ? namespace : undefined,
|
namespace: expectNamespaceInDescriptor ? namespace : undefined,
|
||||||
},
|
},
|
||||||
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
|
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' },
|
||||||
|
{ user: mockAuthenticatedUser() }
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockBaseClient.update).toHaveBeenCalledTimes(1);
|
expect(mockBaseClient.update).toHaveBeenCalledTimes(1);
|
||||||
|
|
|
@ -23,12 +23,14 @@ import {
|
||||||
SavedObjectsDeleteFromNamespacesOptions,
|
SavedObjectsDeleteFromNamespacesOptions,
|
||||||
ISavedObjectTypeRegistry,
|
ISavedObjectTypeRegistry,
|
||||||
} from 'src/core/server';
|
} from 'src/core/server';
|
||||||
|
import { AuthenticatedUser } from '../../../security/common/model';
|
||||||
import { EncryptedSavedObjectsService } from '../crypto';
|
import { EncryptedSavedObjectsService } from '../crypto';
|
||||||
|
|
||||||
interface EncryptedSavedObjectsClientOptions {
|
interface EncryptedSavedObjectsClientOptions {
|
||||||
baseClient: SavedObjectsClientContract;
|
baseClient: SavedObjectsClientContract;
|
||||||
baseTypeRegistry: ISavedObjectTypeRegistry;
|
baseTypeRegistry: ISavedObjectTypeRegistry;
|
||||||
service: Readonly<EncryptedSavedObjectsService>;
|
service: Readonly<EncryptedSavedObjectsService>;
|
||||||
|
getCurrentUser: () => AuthenticatedUser | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,7 +51,7 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
|
||||||
private getDescriptorNamespace = (type: string, namespace?: string) =>
|
private getDescriptorNamespace = (type: string, namespace?: string) =>
|
||||||
this.options.baseTypeRegistry.isSingleNamespace(type) ? namespace : undefined;
|
this.options.baseTypeRegistry.isSingleNamespace(type) ? namespace : undefined;
|
||||||
|
|
||||||
public async create<T = unknown>(
|
public async create<T>(
|
||||||
type: string,
|
type: string,
|
||||||
attributes: T = {} as T,
|
attributes: T = {} as T,
|
||||||
options: SavedObjectsCreateOptions = {}
|
options: SavedObjectsCreateOptions = {}
|
||||||
|
@ -69,19 +71,22 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
|
||||||
|
|
||||||
const id = generateID();
|
const id = generateID();
|
||||||
const namespace = this.getDescriptorNamespace(type, options.namespace);
|
const namespace = this.getDescriptorNamespace(type, options.namespace);
|
||||||
return this.stripEncryptedAttributesFromResponse(
|
return await this.handleEncryptedAttributesInResponse(
|
||||||
await this.options.baseClient.create(
|
await this.options.baseClient.create(
|
||||||
type,
|
type,
|
||||||
await this.options.service.encryptAttributes(
|
(await this.options.service.encryptAttributes(
|
||||||
{ type, id, namespace },
|
{ type, id, namespace },
|
||||||
attributes as Record<string, unknown>
|
attributes as Record<string, unknown>,
|
||||||
),
|
{ user: this.options.getCurrentUser() }
|
||||||
|
)) as T,
|
||||||
{ ...options, id }
|
{ ...options, id }
|
||||||
)
|
),
|
||||||
) as SavedObject<T>;
|
attributes,
|
||||||
|
namespace
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async bulkCreate<T = unknown>(
|
public async bulkCreate<T>(
|
||||||
objects: Array<SavedObjectsBulkCreateObject<T>>,
|
objects: Array<SavedObjectsBulkCreateObject<T>>,
|
||||||
options?: SavedObjectsBaseOptions
|
options?: SavedObjectsBaseOptions
|
||||||
) {
|
) {
|
||||||
|
@ -110,19 +115,22 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
|
||||||
id,
|
id,
|
||||||
attributes: await this.options.service.encryptAttributes(
|
attributes: await this.options.service.encryptAttributes(
|
||||||
{ type: object.type, id, namespace },
|
{ type: object.type, id, namespace },
|
||||||
object.attributes as Record<string, unknown>
|
object.attributes as Record<string, unknown>,
|
||||||
|
{ user: this.options.getCurrentUser() }
|
||||||
),
|
),
|
||||||
} as SavedObjectsBulkCreateObject<T>;
|
} as SavedObjectsBulkCreateObject<T>;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.stripEncryptedAttributesFromBulkResponse(
|
return await this.handleEncryptedAttributesInBulkResponse(
|
||||||
await this.options.baseClient.bulkCreate<T>(encryptedObjects, options)
|
await this.options.baseClient.bulkCreate<T>(encryptedObjects, options),
|
||||||
|
objects,
|
||||||
|
options?.namespace
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async bulkUpdate(
|
public async bulkUpdate<T>(
|
||||||
objects: SavedObjectsBulkUpdateObject[],
|
objects: Array<SavedObjectsBulkUpdateObject<T>>,
|
||||||
options?: SavedObjectsBaseOptions
|
options?: SavedObjectsBaseOptions
|
||||||
) {
|
) {
|
||||||
// We encrypt attributes for every object in parallel and that can potentially exhaust libuv or
|
// We encrypt attributes for every object in parallel and that can potentially exhaust libuv or
|
||||||
|
@ -139,14 +147,17 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
|
||||||
...object,
|
...object,
|
||||||
attributes: await this.options.service.encryptAttributes(
|
attributes: await this.options.service.encryptAttributes(
|
||||||
{ type, id, namespace },
|
{ type, id, namespace },
|
||||||
attributes
|
attributes,
|
||||||
|
{ user: this.options.getCurrentUser() }
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.stripEncryptedAttributesFromBulkResponse(
|
return await this.handleEncryptedAttributesInBulkResponse(
|
||||||
await this.options.baseClient.bulkUpdate(encryptedObjects, options)
|
await this.options.baseClient.bulkUpdate(encryptedObjects, options),
|
||||||
|
objects,
|
||||||
|
options?.namespace
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,28 +165,34 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
|
||||||
return await this.options.baseClient.delete(type, id, options);
|
return await this.options.baseClient.delete(type, id, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async find<T = unknown>(options: SavedObjectsFindOptions) {
|
public async find<T>(options: SavedObjectsFindOptions) {
|
||||||
return this.stripEncryptedAttributesFromBulkResponse(
|
return await this.handleEncryptedAttributesInBulkResponse(
|
||||||
await this.options.baseClient.find<T>(options)
|
await this.options.baseClient.find<T>(options),
|
||||||
|
undefined,
|
||||||
|
options.namespace
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async bulkGet<T = unknown>(
|
public async bulkGet<T>(
|
||||||
objects: SavedObjectsBulkGetObject[] = [],
|
objects: SavedObjectsBulkGetObject[] = [],
|
||||||
options?: SavedObjectsBaseOptions
|
options?: SavedObjectsBaseOptions
|
||||||
) {
|
) {
|
||||||
return this.stripEncryptedAttributesFromBulkResponse(
|
return await this.handleEncryptedAttributesInBulkResponse(
|
||||||
await this.options.baseClient.bulkGet<T>(objects, options)
|
await this.options.baseClient.bulkGet<T>(objects, options),
|
||||||
|
undefined,
|
||||||
|
options?.namespace
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions) {
|
public async get<T>(type: string, id: string, options?: SavedObjectsBaseOptions) {
|
||||||
return this.stripEncryptedAttributesFromResponse(
|
return await this.handleEncryptedAttributesInResponse(
|
||||||
await this.options.baseClient.get<T>(type, id, options)
|
await this.options.baseClient.get<T>(type, id, options),
|
||||||
|
undefined as unknown,
|
||||||
|
this.getDescriptorNamespace(type, options?.namespace)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update<T = unknown>(
|
public async update<T>(
|
||||||
type: string,
|
type: string,
|
||||||
id: string,
|
id: string,
|
||||||
attributes: Partial<T>,
|
attributes: Partial<T>,
|
||||||
|
@ -185,13 +202,17 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
|
||||||
return await this.options.baseClient.update(type, id, attributes, options);
|
return await this.options.baseClient.update(type, id, attributes, options);
|
||||||
}
|
}
|
||||||
const namespace = this.getDescriptorNamespace(type, options?.namespace);
|
const namespace = this.getDescriptorNamespace(type, options?.namespace);
|
||||||
return this.stripEncryptedAttributesFromResponse(
|
return this.handleEncryptedAttributesInResponse(
|
||||||
await this.options.baseClient.update(
|
await this.options.baseClient.update(
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
await this.options.service.encryptAttributes({ type, id, namespace }, attributes),
|
await this.options.service.encryptAttributes({ type, id, namespace }, attributes, {
|
||||||
|
user: this.options.getCurrentUser(),
|
||||||
|
}),
|
||||||
options
|
options
|
||||||
)
|
),
|
||||||
|
attributes,
|
||||||
|
namespace
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,15 +238,28 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
|
||||||
* Strips encrypted attributes from any non-bulk Saved Objects API response. If type isn't
|
* Strips encrypted attributes from any non-bulk Saved Objects API response. If type isn't
|
||||||
* registered, response is returned as is.
|
* registered, response is returned as is.
|
||||||
* @param response Raw response returned by the underlying base client.
|
* @param response Raw response returned by the underlying base client.
|
||||||
|
* @param [originalAttributes] Optional list of original attributes of the saved object.
|
||||||
|
* @param [namespace] Optional namespace that was used for the saved objects operation.
|
||||||
*/
|
*/
|
||||||
private stripEncryptedAttributesFromResponse<T extends SavedObjectsUpdateResponse | SavedObject>(
|
private async handleEncryptedAttributesInResponse<
|
||||||
response: T
|
T,
|
||||||
): T {
|
R extends SavedObjectsUpdateResponse<T> | SavedObject<T>
|
||||||
if (this.options.service.isRegistered(response.type) && response.attributes) {
|
>(response: R, originalAttributes?: T, namespace?: string): Promise<R> {
|
||||||
response.attributes = this.options.service.stripEncryptedAttributes(
|
if (response.attributes && this.options.service.isRegistered(response.type)) {
|
||||||
response.type,
|
// Error is returned when decryption fails, and in this case encrypted attributes will be
|
||||||
response.attributes as Record<string, unknown>
|
// stripped from the returned attributes collection. That will let consumer decide whether to
|
||||||
|
// fail or handle recovery gracefully.
|
||||||
|
const { attributes, error } = await this.options.service.stripOrDecryptAttributes(
|
||||||
|
{ id: response.id, type: response.type, namespace },
|
||||||
|
response.attributes as Record<string, unknown>,
|
||||||
|
originalAttributes as Record<string, unknown>,
|
||||||
|
{ user: this.options.getCurrentUser() }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
response.attributes = attributes as T;
|
||||||
|
if (error) {
|
||||||
|
response.error = error as any;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
@ -235,17 +269,23 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon
|
||||||
* Strips encrypted attributes from any bulk Saved Objects API response. If type for any bulk
|
* Strips encrypted attributes from any bulk Saved Objects API response. If type for any bulk
|
||||||
* response portion isn't registered, it is returned as is.
|
* response portion isn't registered, it is returned as is.
|
||||||
* @param response Raw response returned by the underlying base client.
|
* @param response Raw response returned by the underlying base client.
|
||||||
|
* @param [objects] Optional list of saved objects with original attributes.
|
||||||
|
* @param [namespace] Optional namespace that was used for the saved objects operation.
|
||||||
*/
|
*/
|
||||||
private stripEncryptedAttributesFromBulkResponse<
|
private async handleEncryptedAttributesInBulkResponse<
|
||||||
T extends SavedObjectsBulkResponse | SavedObjectsFindResponse | SavedObjectsBulkUpdateResponse
|
T,
|
||||||
>(response: T): T {
|
R extends
|
||||||
for (const savedObject of response.saved_objects) {
|
| SavedObjectsBulkResponse<T>
|
||||||
if (this.options.service.isRegistered(savedObject.type) && savedObject.attributes) {
|
| SavedObjectsFindResponse<T>
|
||||||
savedObject.attributes = this.options.service.stripEncryptedAttributes(
|
| SavedObjectsBulkUpdateResponse<T>,
|
||||||
savedObject.type,
|
O extends Array<SavedObjectsBulkCreateObject<T>> | Array<SavedObjectsBulkUpdateObject<T>>
|
||||||
savedObject.attributes as Record<string, unknown>
|
>(response: R, objects?: O, namespace?: string) {
|
||||||
);
|
for (const [index, savedObject] of response.saved_objects.entries()) {
|
||||||
}
|
await this.handleEncryptedAttributesInResponse(
|
||||||
|
savedObject,
|
||||||
|
objects?.[index].attributes ?? undefined,
|
||||||
|
this.getDescriptorNamespace(savedObject.type, namespace)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SavedObjectsSetup, setupSavedObjects } from '.';
|
||||||
|
|
||||||
|
import {
|
||||||
|
coreMock,
|
||||||
|
httpServerMock,
|
||||||
|
savedObjectsClientMock,
|
||||||
|
savedObjectsRepositoryMock,
|
||||||
|
savedObjectsTypeRegistryMock,
|
||||||
|
} from '../../../../../src/core/server/mocks';
|
||||||
|
import { securityMock } from '../../../security/server/mocks';
|
||||||
|
import { encryptedSavedObjectsServiceMock } from '../crypto/index.mock';
|
||||||
|
import { EncryptedSavedObjectsClientWrapper } from './encrypted_saved_objects_client_wrapper';
|
||||||
|
import {
|
||||||
|
ISavedObjectsRepository,
|
||||||
|
ISavedObjectTypeRegistry,
|
||||||
|
SavedObject,
|
||||||
|
} from '../../../../../src/core/server';
|
||||||
|
import { EncryptedSavedObjectsService } from '../crypto';
|
||||||
|
|
||||||
|
describe('#setupSavedObjects', () => {
|
||||||
|
let setupContract: SavedObjectsSetup;
|
||||||
|
let coreSetupMock: ReturnType<typeof coreMock.createSetup>;
|
||||||
|
let mockSavedObjectsRepository: jest.Mocked<ISavedObjectsRepository>;
|
||||||
|
let mockSavedObjectTypeRegistry: jest.Mocked<ISavedObjectTypeRegistry>;
|
||||||
|
let mockEncryptedSavedObjectsService: jest.Mocked<EncryptedSavedObjectsService>;
|
||||||
|
beforeEach(() => {
|
||||||
|
const coreStartMock = coreMock.createStart();
|
||||||
|
|
||||||
|
mockSavedObjectsRepository = savedObjectsRepositoryMock.create();
|
||||||
|
coreStartMock.savedObjects.createInternalRepository.mockReturnValue(mockSavedObjectsRepository);
|
||||||
|
|
||||||
|
mockSavedObjectTypeRegistry = savedObjectsTypeRegistryMock.create();
|
||||||
|
coreStartMock.savedObjects.getTypeRegistry.mockReturnValue(mockSavedObjectTypeRegistry);
|
||||||
|
|
||||||
|
coreSetupMock = coreMock.createSetup();
|
||||||
|
coreSetupMock.getStartServices.mockResolvedValue([coreStartMock, {}, {}]);
|
||||||
|
|
||||||
|
mockEncryptedSavedObjectsService = encryptedSavedObjectsServiceMock.create([
|
||||||
|
{ type: 'known-type', attributesToEncrypt: new Set(['attrSecret']) },
|
||||||
|
]);
|
||||||
|
setupContract = setupSavedObjects({
|
||||||
|
service: mockEncryptedSavedObjectsService,
|
||||||
|
savedObjects: coreSetupMock.savedObjects,
|
||||||
|
security: securityMock.createSetup(),
|
||||||
|
getStartServices: coreSetupMock.getStartServices,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('properly registers client wrapper factory', () => {
|
||||||
|
expect(coreSetupMock.savedObjects.addClientWrapper).toHaveBeenCalledTimes(1);
|
||||||
|
expect(coreSetupMock.savedObjects.addClientWrapper).toHaveBeenCalledWith(
|
||||||
|
Number.MAX_SAFE_INTEGER,
|
||||||
|
'encryptedSavedObjects',
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
|
||||||
|
const [[, , clientFactory]] = coreSetupMock.savedObjects.addClientWrapper.mock.calls;
|
||||||
|
expect(
|
||||||
|
clientFactory({
|
||||||
|
client: savedObjectsClientMock.create(),
|
||||||
|
typeRegistry: savedObjectsTypeRegistryMock.create(),
|
||||||
|
request: httpServerMock.createKibanaRequest(),
|
||||||
|
})
|
||||||
|
).toBeInstanceOf(EncryptedSavedObjectsClientWrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#getDecryptedAsInternalUser', () => {
|
||||||
|
it('includes `namespace` for single-namespace saved objects', async () => {
|
||||||
|
const mockSavedObject: SavedObject = {
|
||||||
|
id: 'some-id',
|
||||||
|
type: 'known-type',
|
||||||
|
attributes: { attrOne: 'one', attrSecret: '*secret*' },
|
||||||
|
references: [],
|
||||||
|
};
|
||||||
|
mockSavedObjectsRepository.get.mockResolvedValue(mockSavedObject);
|
||||||
|
mockSavedObjectTypeRegistry.isSingleNamespace.mockReturnValue(true);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
setupContract.getDecryptedAsInternalUser(mockSavedObject.type, mockSavedObject.id, {
|
||||||
|
namespace: 'some-ns',
|
||||||
|
})
|
||||||
|
).resolves.toEqual({
|
||||||
|
...mockSavedObject,
|
||||||
|
attributes: { attrOne: 'one', attrSecret: 'secret' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockEncryptedSavedObjectsService.decryptAttributes).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockEncryptedSavedObjectsService.decryptAttributes).toHaveBeenCalledWith(
|
||||||
|
{ type: mockSavedObject.type, id: mockSavedObject.id, namespace: 'some-ns' },
|
||||||
|
mockSavedObject.attributes
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockSavedObjectsRepository.get).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockSavedObjectsRepository.get).toHaveBeenCalledWith(
|
||||||
|
mockSavedObject.type,
|
||||||
|
mockSavedObject.id,
|
||||||
|
{ namespace: 'some-ns' }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not include `namespace` for multiple-namespace saved objects', async () => {
|
||||||
|
const mockSavedObject: SavedObject = {
|
||||||
|
id: 'some-id',
|
||||||
|
type: 'known-type',
|
||||||
|
attributes: { attrOne: 'one', attrSecret: '*secret*' },
|
||||||
|
references: [],
|
||||||
|
};
|
||||||
|
mockSavedObjectsRepository.get.mockResolvedValue(mockSavedObject);
|
||||||
|
mockSavedObjectTypeRegistry.isSingleNamespace.mockReturnValue(false);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
setupContract.getDecryptedAsInternalUser(mockSavedObject.type, mockSavedObject.id, {
|
||||||
|
namespace: 'some-ns',
|
||||||
|
})
|
||||||
|
).resolves.toEqual({
|
||||||
|
...mockSavedObject,
|
||||||
|
attributes: { attrOne: 'one', attrSecret: 'secret' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockEncryptedSavedObjectsService.decryptAttributes).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockEncryptedSavedObjectsService.decryptAttributes).toHaveBeenCalledWith(
|
||||||
|
{ type: mockSavedObject.type, id: mockSavedObject.id, namespace: undefined },
|
||||||
|
mockSavedObject.attributes
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockSavedObjectsRepository.get).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockSavedObjectsRepository.get).toHaveBeenCalledWith(
|
||||||
|
mockSavedObject.type,
|
||||||
|
mockSavedObject.id,
|
||||||
|
{ namespace: 'some-ns' }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -9,13 +9,17 @@ import {
|
||||||
SavedObject,
|
SavedObject,
|
||||||
SavedObjectsBaseOptions,
|
SavedObjectsBaseOptions,
|
||||||
SavedObjectsServiceSetup,
|
SavedObjectsServiceSetup,
|
||||||
|
ISavedObjectsRepository,
|
||||||
|
ISavedObjectTypeRegistry,
|
||||||
} from 'src/core/server';
|
} from 'src/core/server';
|
||||||
|
import { SecurityPluginSetup } from '../../../security/server';
|
||||||
import { EncryptedSavedObjectsService } from '../crypto';
|
import { EncryptedSavedObjectsService } from '../crypto';
|
||||||
import { EncryptedSavedObjectsClientWrapper } from './encrypted_saved_objects_client_wrapper';
|
import { EncryptedSavedObjectsClientWrapper } from './encrypted_saved_objects_client_wrapper';
|
||||||
|
|
||||||
interface SetupSavedObjectsParams {
|
interface SetupSavedObjectsParams {
|
||||||
service: PublicMethodsOf<EncryptedSavedObjectsService>;
|
service: PublicMethodsOf<EncryptedSavedObjectsService>;
|
||||||
savedObjects: SavedObjectsServiceSetup;
|
savedObjects: SavedObjectsServiceSetup;
|
||||||
|
security?: SecurityPluginSetup;
|
||||||
getStartServices: StartServicesAccessor;
|
getStartServices: StartServicesAccessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +34,7 @@ export interface SavedObjectsSetup {
|
||||||
export function setupSavedObjects({
|
export function setupSavedObjects({
|
||||||
service,
|
service,
|
||||||
savedObjects,
|
savedObjects,
|
||||||
|
security,
|
||||||
getStartServices,
|
getStartServices,
|
||||||
}: SetupSavedObjectsParams): SavedObjectsSetup {
|
}: SetupSavedObjectsParams): SavedObjectsSetup {
|
||||||
// Register custom saved object client that will encrypt, decrypt and strip saved object
|
// Register custom saved object client that will encrypt, decrypt and strip saved object
|
||||||
|
@ -40,25 +45,39 @@ export function setupSavedObjects({
|
||||||
savedObjects.addClientWrapper(
|
savedObjects.addClientWrapper(
|
||||||
Number.MAX_SAFE_INTEGER,
|
Number.MAX_SAFE_INTEGER,
|
||||||
'encryptedSavedObjects',
|
'encryptedSavedObjects',
|
||||||
({ client: baseClient, typeRegistry: baseTypeRegistry }) =>
|
({ client: baseClient, typeRegistry: baseTypeRegistry, request }) =>
|
||||||
new EncryptedSavedObjectsClientWrapper({ baseClient, baseTypeRegistry, service })
|
new EncryptedSavedObjectsClientWrapper({
|
||||||
|
baseClient,
|
||||||
|
baseTypeRegistry,
|
||||||
|
service,
|
||||||
|
getCurrentUser: () => security?.authc.getCurrentUser(request) ?? undefined,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const internalRepositoryPromise = getStartServices().then(([core]) =>
|
const internalRepositoryAndTypeRegistryPromise = getStartServices().then(
|
||||||
core.savedObjects.createInternalRepository()
|
([core]) =>
|
||||||
|
[core.savedObjects.createInternalRepository(), core.savedObjects.getTypeRegistry()] as [
|
||||||
|
ISavedObjectsRepository,
|
||||||
|
ISavedObjectTypeRegistry
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getDecryptedAsInternalUser: async <T = unknown>(
|
getDecryptedAsInternalUser: async <T = unknown>(
|
||||||
type: string,
|
type: string,
|
||||||
id: string,
|
id: string,
|
||||||
options?: SavedObjectsBaseOptions
|
options?: SavedObjectsBaseOptions
|
||||||
): Promise<SavedObject<T>> => {
|
): Promise<SavedObject<T>> => {
|
||||||
const internalRepository = await internalRepositoryPromise;
|
const [internalRepository, typeRegistry] = await internalRepositoryAndTypeRegistryPromise;
|
||||||
const savedObject = await internalRepository.get(type, id, options);
|
const savedObject = await internalRepository.get(type, id, options);
|
||||||
return {
|
return {
|
||||||
...savedObject,
|
...savedObject,
|
||||||
attributes: (await service.decryptAttributes(
|
attributes: (await service.decryptAttributes(
|
||||||
{ type, id, namespace: options && options.namespace },
|
{
|
||||||
|
type,
|
||||||
|
id,
|
||||||
|
namespace: typeRegistry.isSingleNamespace(type) ? options?.namespace : undefined,
|
||||||
|
},
|
||||||
savedObject.attributes as Record<string, unknown>
|
savedObject.attributes as Record<string, unknown>
|
||||||
)) as T,
|
)) as T,
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,8 +4,12 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CoreSetup, PluginInitializer } from '../../../../../../src/core/server';
|
import {
|
||||||
import { deepFreeze } from '../../../../../../src/core/server';
|
deepFreeze,
|
||||||
|
CoreSetup,
|
||||||
|
PluginInitializer,
|
||||||
|
SavedObjectsNamespaceType,
|
||||||
|
} from '../../../../../../src/core/server';
|
||||||
import {
|
import {
|
||||||
EncryptedSavedObjectsPluginSetup,
|
EncryptedSavedObjectsPluginSetup,
|
||||||
EncryptedSavedObjectsPluginStart,
|
EncryptedSavedObjectsPluginStart,
|
||||||
|
@ -13,6 +17,9 @@ import {
|
||||||
import { SpacesPluginSetup } from '../../../../../plugins/spaces/server';
|
import { SpacesPluginSetup } from '../../../../../plugins/spaces/server';
|
||||||
|
|
||||||
const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret';
|
const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret';
|
||||||
|
const SAVED_OBJECT_WITH_SECRET_AND_MULTIPLE_SPACES_TYPE =
|
||||||
|
'saved-object-with-secret-and-multiple-spaces';
|
||||||
|
const SAVED_OBJECT_WITHOUT_SECRET_TYPE = 'saved-object-without-secret';
|
||||||
|
|
||||||
interface PluginsSetup {
|
interface PluginsSetup {
|
||||||
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup;
|
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup;
|
||||||
|
@ -26,28 +33,44 @@ interface PluginsStart {
|
||||||
|
|
||||||
export const plugin: PluginInitializer<void, void, PluginsSetup, PluginsStart> = () => ({
|
export const plugin: PluginInitializer<void, void, PluginsSetup, PluginsStart> = () => ({
|
||||||
setup(core: CoreSetup<PluginsStart>, deps) {
|
setup(core: CoreSetup<PluginsStart>, deps) {
|
||||||
|
for (const [name, namespaceType] of [
|
||||||
|
[SAVED_OBJECT_WITH_SECRET_TYPE, 'single'],
|
||||||
|
[SAVED_OBJECT_WITH_SECRET_AND_MULTIPLE_SPACES_TYPE, 'multiple'],
|
||||||
|
] as Array<[string, SavedObjectsNamespaceType]>) {
|
||||||
|
core.savedObjects.registerType({
|
||||||
|
name,
|
||||||
|
hidden: false,
|
||||||
|
namespaceType,
|
||||||
|
mappings: deepFreeze({
|
||||||
|
properties: {
|
||||||
|
publicProperty: { type: 'keyword' },
|
||||||
|
publicPropertyExcludedFromAAD: { type: 'keyword' },
|
||||||
|
publicPropertyStoredEncrypted: { type: 'binary' },
|
||||||
|
privateProperty: { type: 'binary' },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
deps.encryptedSavedObjects.registerType({
|
||||||
|
type: name,
|
||||||
|
attributesToEncrypt: new Set([
|
||||||
|
'privateProperty',
|
||||||
|
{ key: 'publicPropertyStoredEncrypted', dangerouslyExposeValue: true },
|
||||||
|
]),
|
||||||
|
attributesToExcludeFromAAD: new Set(['publicPropertyExcludedFromAAD']),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
core.savedObjects.registerType({
|
core.savedObjects.registerType({
|
||||||
name: SAVED_OBJECT_WITH_SECRET_TYPE,
|
name: SAVED_OBJECT_WITHOUT_SECRET_TYPE,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
namespaceType: 'single',
|
namespaceType: 'single',
|
||||||
mappings: deepFreeze({
|
mappings: deepFreeze({ properties: { publicProperty: { type: 'keyword' } } }),
|
||||||
properties: {
|
|
||||||
publicProperty: { type: 'keyword' },
|
|
||||||
publicPropertyExcludedFromAAD: { type: 'keyword' },
|
|
||||||
privateProperty: { type: 'binary' },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
deps.encryptedSavedObjects.registerType({
|
|
||||||
type: SAVED_OBJECT_WITH_SECRET_TYPE,
|
|
||||||
attributesToEncrypt: new Set(['privateProperty']),
|
|
||||||
attributesToExcludeFromAAD: new Set(['publicPropertyExcludedFromAAD']),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
core.http.createRouter().get(
|
core.http.createRouter().get(
|
||||||
{
|
{
|
||||||
path: '/api/saved_objects/get-decrypted-as-internal-user/{id}',
|
path: '/api/saved_objects/get-decrypted-as-internal-user/{type}/{id}',
|
||||||
validate: { params: value => ({ value }) },
|
validate: { params: value => ({ value }) },
|
||||||
},
|
},
|
||||||
async (context, request, response) => {
|
async (context, request, response) => {
|
||||||
|
@ -58,7 +81,7 @@ export const plugin: PluginInitializer<void, void, PluginsSetup, PluginsStart> =
|
||||||
try {
|
try {
|
||||||
return response.ok({
|
return response.ok({
|
||||||
body: await encryptedSavedObjects.getDecryptedAsInternalUser(
|
body: await encryptedSavedObjects.getDecryptedAsInternalUser(
|
||||||
SAVED_OBJECT_WITH_SECRET_TYPE,
|
request.params.type,
|
||||||
request.params.id,
|
request.params.id,
|
||||||
{ namespace }
|
{ namespace }
|
||||||
),
|
),
|
||||||
|
|
|
@ -14,13 +14,20 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
const supertest = getService('supertest');
|
const supertest = getService('supertest');
|
||||||
|
|
||||||
const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret';
|
const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret';
|
||||||
|
const SAVED_OBJECT_WITH_SECRET_AND_MULTIPLE_SPACES_TYPE =
|
||||||
|
'saved-object-with-secret-and-multiple-spaces';
|
||||||
|
const SAVED_OBJECT_WITHOUT_SECRET_TYPE = 'saved-object-without-secret';
|
||||||
|
|
||||||
function runTests(getURLAPIBaseURL: () => string, generateRawID: (id: string) => string) {
|
function runTests(
|
||||||
async function getRawSavedObjectAttributes(id: string) {
|
encryptedSavedObjectType: string,
|
||||||
|
getURLAPIBaseURL: () => string,
|
||||||
|
generateRawID: (id: string, type: string) => string
|
||||||
|
) {
|
||||||
|
async function getRawSavedObjectAttributes({ id, type }: SavedObject) {
|
||||||
const {
|
const {
|
||||||
_source: { [SAVED_OBJECT_WITH_SECRET_TYPE]: savedObject },
|
_source: { [type]: savedObject },
|
||||||
} = await es.get({
|
} = await es.get({
|
||||||
id: generateRawID(id),
|
id: generateRawID(id, type),
|
||||||
index: '.kibana',
|
index: '.kibana',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -29,20 +36,22 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
|
|
||||||
let savedObjectOriginalAttributes: {
|
let savedObjectOriginalAttributes: {
|
||||||
publicProperty: string;
|
publicProperty: string;
|
||||||
publicPropertyExcludedFromAAD: string;
|
publicPropertyStoredEncrypted: string;
|
||||||
privateProperty: string;
|
privateProperty: string;
|
||||||
|
publicPropertyExcludedFromAAD: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
let savedObject: SavedObject;
|
let savedObject: SavedObject;
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
savedObjectOriginalAttributes = {
|
savedObjectOriginalAttributes = {
|
||||||
publicProperty: randomness.string(),
|
publicProperty: randomness.string(),
|
||||||
publicPropertyExcludedFromAAD: randomness.string(),
|
publicPropertyStoredEncrypted: randomness.string(),
|
||||||
privateProperty: randomness.string(),
|
privateProperty: randomness.string(),
|
||||||
|
publicPropertyExcludedFromAAD: randomness.string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { body } = await supertest
|
const { body } = await supertest
|
||||||
.post(`${getURLAPIBaseURL()}${SAVED_OBJECT_WITH_SECRET_TYPE}`)
|
.post(`${getURLAPIBaseURL()}${encryptedSavedObjectType}`)
|
||||||
.set('kbn-xsrf', 'xxx')
|
.set('kbn-xsrf', 'xxx')
|
||||||
.send({ attributes: savedObjectOriginalAttributes })
|
.send({ attributes: savedObjectOriginalAttributes })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
@ -54,14 +63,19 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
expect(savedObject.attributes).to.eql({
|
expect(savedObject.attributes).to.eql({
|
||||||
publicProperty: savedObjectOriginalAttributes.publicProperty,
|
publicProperty: savedObjectOriginalAttributes.publicProperty,
|
||||||
publicPropertyExcludedFromAAD: savedObjectOriginalAttributes.publicPropertyExcludedFromAAD,
|
publicPropertyExcludedFromAAD: savedObjectOriginalAttributes.publicPropertyExcludedFromAAD,
|
||||||
|
publicPropertyStoredEncrypted: savedObjectOriginalAttributes.publicPropertyStoredEncrypted,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rawAttributes = await getRawSavedObjectAttributes(savedObject.id);
|
const rawAttributes = await getRawSavedObjectAttributes(savedObject);
|
||||||
expect(rawAttributes.publicProperty).to.be(savedObjectOriginalAttributes.publicProperty);
|
expect(rawAttributes.publicProperty).to.be(savedObjectOriginalAttributes.publicProperty);
|
||||||
expect(rawAttributes.publicPropertyExcludedFromAAD).to.be(
|
expect(rawAttributes.publicPropertyExcludedFromAAD).to.be(
|
||||||
savedObjectOriginalAttributes.publicPropertyExcludedFromAAD
|
savedObjectOriginalAttributes.publicPropertyExcludedFromAAD
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expect(rawAttributes.publicPropertyStoredEncrypted).to.not.be.empty();
|
||||||
|
expect(rawAttributes.publicPropertyStoredEncrypted).to.not.be(
|
||||||
|
savedObjectOriginalAttributes.publicPropertyStoredEncrypted
|
||||||
|
);
|
||||||
expect(rawAttributes.privateProperty).to.not.be.empty();
|
expect(rawAttributes.privateProperty).to.not.be.empty();
|
||||||
expect(rawAttributes.privateProperty).to.not.be(
|
expect(rawAttributes.privateProperty).to.not.be(
|
||||||
savedObjectOriginalAttributes.privateProperty
|
savedObjectOriginalAttributes.privateProperty
|
||||||
|
@ -71,18 +85,20 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
it('#bulkCreate encrypts attributes and strips them from response', async () => {
|
it('#bulkCreate encrypts attributes and strips them from response', async () => {
|
||||||
const bulkCreateParams = [
|
const bulkCreateParams = [
|
||||||
{
|
{
|
||||||
type: SAVED_OBJECT_WITH_SECRET_TYPE,
|
type: encryptedSavedObjectType,
|
||||||
attributes: {
|
attributes: {
|
||||||
publicProperty: randomness.string(),
|
publicProperty: randomness.string(),
|
||||||
publicPropertyExcludedFromAAD: randomness.string(),
|
publicPropertyExcludedFromAAD: randomness.string(),
|
||||||
|
publicPropertyStoredEncrypted: randomness.string(),
|
||||||
privateProperty: randomness.string(),
|
privateProperty: randomness.string(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: SAVED_OBJECT_WITH_SECRET_TYPE,
|
type: encryptedSavedObjectType,
|
||||||
attributes: {
|
attributes: {
|
||||||
publicProperty: randomness.string(),
|
publicProperty: randomness.string(),
|
||||||
publicPropertyExcludedFromAAD: randomness.string(),
|
publicPropertyExcludedFromAAD: randomness.string(),
|
||||||
|
publicPropertyStoredEncrypted: randomness.string(),
|
||||||
privateProperty: randomness.string(),
|
privateProperty: randomness.string(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -100,30 +116,120 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
for (let index = 0; index < savedObjects.length; index++) {
|
for (let index = 0; index < savedObjects.length; index++) {
|
||||||
const attributesFromResponse = savedObjects[index].attributes;
|
const attributesFromResponse = savedObjects[index].attributes;
|
||||||
const attributesFromRequest = bulkCreateParams[index].attributes;
|
const attributesFromRequest = bulkCreateParams[index].attributes;
|
||||||
const rawAttributes = await getRawSavedObjectAttributes(savedObjects[index].id);
|
const rawAttributes = await getRawSavedObjectAttributes(savedObjects[index]);
|
||||||
|
|
||||||
expect(attributesFromResponse).to.eql({
|
expect(attributesFromResponse).to.eql({
|
||||||
publicProperty: attributesFromRequest.publicProperty,
|
publicProperty: attributesFromRequest.publicProperty,
|
||||||
publicPropertyExcludedFromAAD: attributesFromRequest.publicPropertyExcludedFromAAD,
|
publicPropertyExcludedFromAAD: attributesFromRequest.publicPropertyExcludedFromAAD,
|
||||||
|
publicPropertyStoredEncrypted: attributesFromRequest.publicPropertyStoredEncrypted,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(rawAttributes.publicProperty).to.be(attributesFromRequest.publicProperty);
|
expect(rawAttributes.publicProperty).to.be(attributesFromRequest.publicProperty);
|
||||||
expect(rawAttributes.publicPropertyExcludedFromAAD).to.be(
|
expect(rawAttributes.publicPropertyExcludedFromAAD).to.be(
|
||||||
attributesFromRequest.publicPropertyExcludedFromAAD
|
attributesFromRequest.publicPropertyExcludedFromAAD
|
||||||
);
|
);
|
||||||
|
expect(rawAttributes.publicPropertyStoredEncrypted).to.not.be.empty();
|
||||||
|
expect(rawAttributes.publicPropertyStoredEncrypted).to.not.be(
|
||||||
|
attributesFromRequest.publicPropertyStoredEncrypted
|
||||||
|
);
|
||||||
expect(rawAttributes.privateProperty).to.not.be.empty();
|
expect(rawAttributes.privateProperty).to.not.be.empty();
|
||||||
expect(rawAttributes.privateProperty).to.not.be(attributesFromRequest.privateProperty);
|
expect(rawAttributes.privateProperty).to.not.be(attributesFromRequest.privateProperty);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('#bulkCreate with different types encrypts attributes and strips them from response when necessary', async () => {
|
||||||
|
const bulkCreateParams = [
|
||||||
|
{
|
||||||
|
type: encryptedSavedObjectType,
|
||||||
|
attributes: {
|
||||||
|
publicProperty: randomness.string(),
|
||||||
|
publicPropertyExcludedFromAAD: randomness.string(),
|
||||||
|
publicPropertyStoredEncrypted: randomness.string(),
|
||||||
|
privateProperty: randomness.string(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: SAVED_OBJECT_WITHOUT_SECRET_TYPE,
|
||||||
|
attributes: {
|
||||||
|
publicProperty: randomness.string(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const {
|
||||||
|
body: { saved_objects: savedObjects },
|
||||||
|
} = await supertest
|
||||||
|
.post(`${getURLAPIBaseURL()}_bulk_create`)
|
||||||
|
.set('kbn-xsrf', 'xxx')
|
||||||
|
.send(bulkCreateParams)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(savedObjects).to.have.length(bulkCreateParams.length);
|
||||||
|
for (let index = 0; index < savedObjects.length; index++) {
|
||||||
|
const attributesFromResponse = savedObjects[index].attributes;
|
||||||
|
const attributesFromRequest = bulkCreateParams[index].attributes;
|
||||||
|
|
||||||
|
const type = savedObjects[index].type;
|
||||||
|
expect(type).to.be.eql(bulkCreateParams[index].type);
|
||||||
|
|
||||||
|
const rawAttributes = await getRawSavedObjectAttributes(savedObjects[index]);
|
||||||
|
if (type === SAVED_OBJECT_WITHOUT_SECRET_TYPE) {
|
||||||
|
expect(attributesFromResponse).to.eql(attributesFromRequest);
|
||||||
|
expect(attributesFromRequest).to.eql(rawAttributes);
|
||||||
|
} else {
|
||||||
|
expect(attributesFromResponse).to.eql({
|
||||||
|
publicProperty: attributesFromRequest.publicProperty,
|
||||||
|
publicPropertyExcludedFromAAD: attributesFromRequest.publicPropertyExcludedFromAAD,
|
||||||
|
publicPropertyStoredEncrypted: attributesFromRequest.publicPropertyStoredEncrypted,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rawAttributes.publicProperty).to.be(attributesFromRequest.publicProperty);
|
||||||
|
expect(rawAttributes.publicPropertyExcludedFromAAD).to.be(
|
||||||
|
attributesFromRequest.publicPropertyExcludedFromAAD
|
||||||
|
);
|
||||||
|
expect(rawAttributes.publicPropertyStoredEncrypted).to.not.be.empty();
|
||||||
|
expect(rawAttributes.publicPropertyStoredEncrypted).to.not.be(
|
||||||
|
attributesFromRequest.publicPropertyStoredEncrypted
|
||||||
|
);
|
||||||
|
expect(rawAttributes.privateProperty).to.not.be.empty();
|
||||||
|
expect(rawAttributes.privateProperty).to.not.be(attributesFromRequest.privateProperty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('#get strips encrypted attributes from response', async () => {
|
it('#get strips encrypted attributes from response', async () => {
|
||||||
const { body: response } = await supertest
|
const { body: response } = await supertest
|
||||||
.get(`${getURLAPIBaseURL()}${SAVED_OBJECT_WITH_SECRET_TYPE}/${savedObject.id}`)
|
.get(`${getURLAPIBaseURL()}${encryptedSavedObjectType}/${savedObject.id}`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.attributes).to.eql({
|
expect(response.attributes).to.eql({
|
||||||
publicProperty: savedObjectOriginalAttributes.publicProperty,
|
publicProperty: savedObjectOriginalAttributes.publicProperty,
|
||||||
publicPropertyExcludedFromAAD: savedObjectOriginalAttributes.publicPropertyExcludedFromAAD,
|
publicPropertyExcludedFromAAD: savedObjectOriginalAttributes.publicPropertyExcludedFromAAD,
|
||||||
|
publicPropertyStoredEncrypted: savedObjectOriginalAttributes.publicPropertyStoredEncrypted,
|
||||||
|
});
|
||||||
|
expect(response.error).to.be(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('#get strips all encrypted attributes from response if decryption fails', async () => {
|
||||||
|
// Update non-encrypted property that is included into AAD to make it impossible to decrypt
|
||||||
|
// encrypted attributes.
|
||||||
|
const updatedPublicProperty = randomness.string();
|
||||||
|
await supertest
|
||||||
|
.put(`${getURLAPIBaseURL()}${encryptedSavedObjectType}/${savedObject.id}`)
|
||||||
|
.set('kbn-xsrf', 'xxx')
|
||||||
|
.send({ attributes: { publicProperty: updatedPublicProperty } })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const { body: response } = await supertest
|
||||||
|
.get(`${getURLAPIBaseURL()}${encryptedSavedObjectType}/${savedObject.id}`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.attributes).to.eql({
|
||||||
|
publicProperty: updatedPublicProperty,
|
||||||
|
publicPropertyExcludedFromAAD: savedObjectOriginalAttributes.publicPropertyExcludedFromAAD,
|
||||||
|
});
|
||||||
|
expect(response.error).to.eql({
|
||||||
|
message: 'Unable to decrypt attribute "publicPropertyStoredEncrypted"',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -131,7 +237,7 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
const {
|
const {
|
||||||
body: { saved_objects: savedObjects },
|
body: { saved_objects: savedObjects },
|
||||||
} = await supertest
|
} = await supertest
|
||||||
.get(`${getURLAPIBaseURL()}_find?type=${SAVED_OBJECT_WITH_SECRET_TYPE}`)
|
.get(`${getURLAPIBaseURL()}_find?type=${encryptedSavedObjectType}`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(savedObjects).to.have.length(1);
|
expect(savedObjects).to.have.length(1);
|
||||||
|
@ -139,6 +245,35 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
expect(savedObjects[0].attributes).to.eql({
|
expect(savedObjects[0].attributes).to.eql({
|
||||||
publicProperty: savedObjectOriginalAttributes.publicProperty,
|
publicProperty: savedObjectOriginalAttributes.publicProperty,
|
||||||
publicPropertyExcludedFromAAD: savedObjectOriginalAttributes.publicPropertyExcludedFromAAD,
|
publicPropertyExcludedFromAAD: savedObjectOriginalAttributes.publicPropertyExcludedFromAAD,
|
||||||
|
publicPropertyStoredEncrypted: savedObjectOriginalAttributes.publicPropertyStoredEncrypted,
|
||||||
|
});
|
||||||
|
expect(savedObjects[0].error).to.be(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('#find strips all encrypted attributes from response if decryption fails', async () => {
|
||||||
|
// Update non-encrypted property that is included into AAD to make it impossible to decrypt
|
||||||
|
// encrypted attributes.
|
||||||
|
const updatedPublicProperty = randomness.string();
|
||||||
|
await supertest
|
||||||
|
.put(`${getURLAPIBaseURL()}${encryptedSavedObjectType}/${savedObject.id}`)
|
||||||
|
.set('kbn-xsrf', 'xxx')
|
||||||
|
.send({ attributes: { publicProperty: updatedPublicProperty } })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const {
|
||||||
|
body: { saved_objects: savedObjects },
|
||||||
|
} = await supertest
|
||||||
|
.get(`${getURLAPIBaseURL()}_find?type=${encryptedSavedObjectType}`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(savedObjects).to.have.length(1);
|
||||||
|
expect(savedObjects[0].id).to.be(savedObject.id);
|
||||||
|
expect(savedObjects[0].attributes).to.eql({
|
||||||
|
publicProperty: updatedPublicProperty,
|
||||||
|
publicPropertyExcludedFromAAD: savedObjectOriginalAttributes.publicPropertyExcludedFromAAD,
|
||||||
|
});
|
||||||
|
expect(savedObjects[0].error).to.eql({
|
||||||
|
message: 'Unable to decrypt attribute "publicPropertyStoredEncrypted"',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -156,6 +291,37 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
expect(savedObjects[0].attributes).to.eql({
|
expect(savedObjects[0].attributes).to.eql({
|
||||||
publicProperty: savedObjectOriginalAttributes.publicProperty,
|
publicProperty: savedObjectOriginalAttributes.publicProperty,
|
||||||
publicPropertyExcludedFromAAD: savedObjectOriginalAttributes.publicPropertyExcludedFromAAD,
|
publicPropertyExcludedFromAAD: savedObjectOriginalAttributes.publicPropertyExcludedFromAAD,
|
||||||
|
publicPropertyStoredEncrypted: savedObjectOriginalAttributes.publicPropertyStoredEncrypted,
|
||||||
|
});
|
||||||
|
expect(savedObjects[0].error).to.be(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('#bulkGet strips all encrypted attributes from response if decryption fails', async () => {
|
||||||
|
// Update non-encrypted property that is included into AAD to make it impossible to decrypt
|
||||||
|
// encrypted attributes.
|
||||||
|
const updatedPublicProperty = randomness.string();
|
||||||
|
await supertest
|
||||||
|
.put(`${getURLAPIBaseURL()}${encryptedSavedObjectType}/${savedObject.id}`)
|
||||||
|
.set('kbn-xsrf', 'xxx')
|
||||||
|
.send({ attributes: { publicProperty: updatedPublicProperty } })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const {
|
||||||
|
body: { saved_objects: savedObjects },
|
||||||
|
} = await supertest
|
||||||
|
.post(`${getURLAPIBaseURL()}_bulk_get`)
|
||||||
|
.set('kbn-xsrf', 'xxx')
|
||||||
|
.send([{ type: savedObject.type, id: savedObject.id }])
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(savedObjects).to.have.length(1);
|
||||||
|
expect(savedObjects[0].id).to.be(savedObject.id);
|
||||||
|
expect(savedObjects[0].attributes).to.eql({
|
||||||
|
publicProperty: updatedPublicProperty,
|
||||||
|
publicPropertyExcludedFromAAD: savedObjectOriginalAttributes.publicPropertyExcludedFromAAD,
|
||||||
|
});
|
||||||
|
expect(savedObjects[0].error).to.eql({
|
||||||
|
message: 'Unable to decrypt attribute "publicPropertyStoredEncrypted"',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -163,11 +329,12 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
const updatedAttributes = {
|
const updatedAttributes = {
|
||||||
publicProperty: randomness.string(),
|
publicProperty: randomness.string(),
|
||||||
publicPropertyExcludedFromAAD: randomness.string(),
|
publicPropertyExcludedFromAAD: randomness.string(),
|
||||||
|
publicPropertyStoredEncrypted: randomness.string(),
|
||||||
privateProperty: randomness.string(),
|
privateProperty: randomness.string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { body: response } = await supertest
|
const { body: response } = await supertest
|
||||||
.put(`${getURLAPIBaseURL()}${SAVED_OBJECT_WITH_SECRET_TYPE}/${savedObject.id}`)
|
.put(`${getURLAPIBaseURL()}${encryptedSavedObjectType}/${savedObject.id}`)
|
||||||
.set('kbn-xsrf', 'xxx')
|
.set('kbn-xsrf', 'xxx')
|
||||||
.send({ attributes: updatedAttributes })
|
.send({ attributes: updatedAttributes })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
@ -175,13 +342,18 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
expect(response.attributes).to.eql({
|
expect(response.attributes).to.eql({
|
||||||
publicProperty: updatedAttributes.publicProperty,
|
publicProperty: updatedAttributes.publicProperty,
|
||||||
publicPropertyExcludedFromAAD: updatedAttributes.publicPropertyExcludedFromAAD,
|
publicPropertyExcludedFromAAD: updatedAttributes.publicPropertyExcludedFromAAD,
|
||||||
|
publicPropertyStoredEncrypted: updatedAttributes.publicPropertyStoredEncrypted,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rawAttributes = await getRawSavedObjectAttributes(savedObject.id);
|
const rawAttributes = await getRawSavedObjectAttributes(savedObject);
|
||||||
expect(rawAttributes.publicProperty).to.be(updatedAttributes.publicProperty);
|
expect(rawAttributes.publicProperty).to.be(updatedAttributes.publicProperty);
|
||||||
expect(rawAttributes.publicPropertyExcludedFromAAD).to.be(
|
expect(rawAttributes.publicPropertyExcludedFromAAD).to.be(
|
||||||
updatedAttributes.publicPropertyExcludedFromAAD
|
updatedAttributes.publicPropertyExcludedFromAAD
|
||||||
);
|
);
|
||||||
|
expect(rawAttributes.publicPropertyStoredEncrypted).to.not.be.empty();
|
||||||
|
expect(rawAttributes.publicPropertyStoredEncrypted).to.not.be(
|
||||||
|
updatedAttributes.publicPropertyStoredEncrypted
|
||||||
|
);
|
||||||
|
|
||||||
expect(rawAttributes.privateProperty).to.not.be.empty();
|
expect(rawAttributes.privateProperty).to.not.be.empty();
|
||||||
expect(rawAttributes.privateProperty).to.not.be(updatedAttributes.privateProperty);
|
expect(rawAttributes.privateProperty).to.not.be(updatedAttributes.privateProperty);
|
||||||
|
@ -189,7 +361,11 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
|
|
||||||
it('#getDecryptedAsInternalUser decrypts and returns all attributes', async () => {
|
it('#getDecryptedAsInternalUser decrypts and returns all attributes', async () => {
|
||||||
const { body: decryptedResponse } = await supertest
|
const { body: decryptedResponse } = await supertest
|
||||||
.get(`${getURLAPIBaseURL()}get-decrypted-as-internal-user/${savedObject.id}`)
|
.get(
|
||||||
|
`${getURLAPIBaseURL()}get-decrypted-as-internal-user/${encryptedSavedObjectType}/${
|
||||||
|
savedObject.id
|
||||||
|
}`
|
||||||
|
)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(decryptedResponse.attributes).to.eql(savedObjectOriginalAttributes);
|
expect(decryptedResponse.attributes).to.eql(savedObjectOriginalAttributes);
|
||||||
|
@ -199,7 +375,7 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
const updatedAttributes = { publicPropertyExcludedFromAAD: randomness.string() };
|
const updatedAttributes = { publicPropertyExcludedFromAAD: randomness.string() };
|
||||||
|
|
||||||
const { body: response } = await supertest
|
const { body: response } = await supertest
|
||||||
.put(`${getURLAPIBaseURL()}${SAVED_OBJECT_WITH_SECRET_TYPE}/${savedObject.id}`)
|
.put(`${getURLAPIBaseURL()}${encryptedSavedObjectType}/${savedObject.id}`)
|
||||||
.set('kbn-xsrf', 'xxx')
|
.set('kbn-xsrf', 'xxx')
|
||||||
.send({ attributes: updatedAttributes })
|
.send({ attributes: updatedAttributes })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
@ -209,7 +385,11 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const { body: decryptedResponse } = await supertest
|
const { body: decryptedResponse } = await supertest
|
||||||
.get(`${getURLAPIBaseURL()}get-decrypted-as-internal-user/${savedObject.id}`)
|
.get(
|
||||||
|
`${getURLAPIBaseURL()}get-decrypted-as-internal-user/${encryptedSavedObjectType}/${
|
||||||
|
savedObject.id
|
||||||
|
}`
|
||||||
|
)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(decryptedResponse.attributes).to.eql({
|
expect(decryptedResponse.attributes).to.eql({
|
||||||
|
@ -222,7 +402,7 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
const updatedAttributes = { publicProperty: randomness.string() };
|
const updatedAttributes = { publicProperty: randomness.string() };
|
||||||
|
|
||||||
const { body: response } = await supertest
|
const { body: response } = await supertest
|
||||||
.put(`${getURLAPIBaseURL()}${SAVED_OBJECT_WITH_SECRET_TYPE}/${savedObject.id}`)
|
.put(`${getURLAPIBaseURL()}${encryptedSavedObjectType}/${savedObject.id}`)
|
||||||
.set('kbn-xsrf', 'xxx')
|
.set('kbn-xsrf', 'xxx')
|
||||||
.send({ attributes: updatedAttributes })
|
.send({ attributes: updatedAttributes })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
@ -233,7 +413,11 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
|
|
||||||
// Bad request means that we successfully detected "EncryptionError" (not unexpected one).
|
// Bad request means that we successfully detected "EncryptionError" (not unexpected one).
|
||||||
await supertest
|
await supertest
|
||||||
.get(`${getURLAPIBaseURL()}get-decrypted-as-internal-user/${savedObject.id}`)
|
.get(
|
||||||
|
`${getURLAPIBaseURL()}get-decrypted-as-internal-user/${encryptedSavedObjectType}/${
|
||||||
|
savedObject.id
|
||||||
|
}`
|
||||||
|
)
|
||||||
.expect(400, {
|
.expect(400, {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
error: 'Bad Request',
|
error: 'Bad Request',
|
||||||
|
@ -243,19 +427,38 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('encrypted saved objects API', () => {
|
describe('encrypted saved objects API', () => {
|
||||||
|
function generateRawId(id: string, type: string, spaceId?: string) {
|
||||||
|
return `${
|
||||||
|
spaceId && type !== SAVED_OBJECT_WITH_SECRET_AND_MULTIPLE_SPACES_TYPE
|
||||||
|
? `${spaceId}:${type}`
|
||||||
|
: type
|
||||||
|
}:${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await es.deleteByQuery({
|
await es.deleteByQuery({
|
||||||
index: '.kibana',
|
index: '.kibana',
|
||||||
q: `type:${SAVED_OBJECT_WITH_SECRET_TYPE}`,
|
q: `type:${SAVED_OBJECT_WITH_SECRET_TYPE} OR type:${SAVED_OBJECT_WITH_SECRET_AND_MULTIPLE_SPACES_TYPE} OR type:${SAVED_OBJECT_WITHOUT_SECRET_TYPE}`,
|
||||||
refresh: true,
|
refresh: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('within a default space', () => {
|
describe('within a default space', () => {
|
||||||
runTests(
|
describe('with `single` namespace saved object', () => {
|
||||||
() => '/api/saved_objects/',
|
runTests(
|
||||||
id => `${SAVED_OBJECT_WITH_SECRET_TYPE}:${id}`
|
SAVED_OBJECT_WITH_SECRET_TYPE,
|
||||||
);
|
() => '/api/saved_objects/',
|
||||||
|
(id, type) => generateRawId(id, type)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with `multiple` namespace saved object', () => {
|
||||||
|
runTests(
|
||||||
|
SAVED_OBJECT_WITH_SECRET_AND_MULTIPLE_SPACES_TYPE,
|
||||||
|
() => '/api/saved_objects/',
|
||||||
|
(id, type) => generateRawId(id, type)
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('within a custom space', () => {
|
describe('within a custom space', () => {
|
||||||
|
@ -276,10 +479,21 @@ export default function({ getService }: FtrProviderContext) {
|
||||||
.expect(204);
|
.expect(204);
|
||||||
});
|
});
|
||||||
|
|
||||||
runTests(
|
describe('with `single` namespace saved object', () => {
|
||||||
() => `/s/${SPACE_ID}/api/saved_objects/`,
|
runTests(
|
||||||
id => `${SPACE_ID}:${SAVED_OBJECT_WITH_SECRET_TYPE}:${id}`
|
SAVED_OBJECT_WITH_SECRET_TYPE,
|
||||||
);
|
() => `/s/${SPACE_ID}/api/saved_objects/`,
|
||||||
|
(id, type) => generateRawId(id, type, SPACE_ID)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with `multiple` namespace saved object', () => {
|
||||||
|
runTests(
|
||||||
|
SAVED_OBJECT_WITH_SECRET_AND_MULTIPLE_SPACES_TYPE,
|
||||||
|
() => `/s/${SPACE_ID}/api/saved_objects/`,
|
||||||
|
(id, type) => generateRawId(id, type, SPACE_ID)
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue