Migrate Encrypted Saved Objects plugin to the new platform. (#49890)

This commit is contained in:
Aleh Zasypkin 2019-11-01 22:32:12 +01:00 committed by GitHub
parent d10b7a1efb
commit 203ef5577c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 649 additions and 410 deletions

3
.github/CODEOWNERS vendored
View file

@ -44,7 +44,7 @@
/x-pack/test/functional/services/ml.ts @elastic/ml-ui
# ML team owns the transform plugin, ES team added here for visibility
# because the plugin lives in Kibana's Elasticsearch management section.
/x-pack/legacy/plugins/transform/ @elastic/ml-ui @elastic/es-ui
/x-pack/legacy/plugins/transform/ @elastic/ml-ui @elastic/es-ui
# Operations
/renovate.json5 @elastic/kibana-operations
@ -70,6 +70,7 @@
/x-pack/legacy/plugins/spaces/ @elastic/kibana-security
/x-pack/plugins/spaces/ @elastic/kibana-security
/x-pack/legacy/plugins/encrypted_saved_objects/ @elastic/kibana-security
/x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security
/src/legacy/server/csp/ @elastic/kibana-security
/x-pack/plugins/security/ @elastic/kibana-security
/x-pack/test/api_integration/apis/security/ @elastic/kibana-security

View file

@ -22,10 +22,10 @@ export function actions(kibana: any) {
return new kibana.Plugin({
id: 'actions',
configPrefix: 'xpack.actions',
require: ['kibana', 'elasticsearch', 'task_manager', 'encrypted_saved_objects'],
require: ['kibana', 'elasticsearch', 'task_manager', 'encryptedSavedObjects'],
isEnabled(config: Legacy.KibanaConfig) {
return (
config.get('xpack.encrypted_saved_objects.enabled') === true &&
config.get('xpack.encryptedSavedObjects.enabled') === true &&
config.get('xpack.actions.enabled') === true &&
config.get('xpack.task_manager.enabled') === true
);

View file

@ -8,7 +8,7 @@ import Hapi from 'hapi';
import { schema } from '@kbn/config-schema';
import { ActionExecutor } from './action_executor';
import { actionTypeRegistryMock } from '../action_type_registry.mock';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock';
import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks';
import {
savedObjectsClientMock,
loggingServiceMock,
@ -24,7 +24,7 @@ function getServices() {
callCluster: jest.fn(),
};
}
const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.createStart();
const actionTypeRegistry = actionTypeRegistryMock.create();
const executeParams = {

View file

@ -5,7 +5,7 @@
*/
import Hapi from 'hapi';
import { EncryptedSavedObjectsStartContract } from '../shim';
import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../../plugins/encrypted_saved_objects/server';
import { LegacySpacesPlugin as SpacesPluginStartContract } from '../../../spaces';
import { Logger } from '../../../../../../src/core/server';
import { validateParams, validateConfig, validateSecrets } from './validate_with_schema';

View file

@ -11,7 +11,7 @@ import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager';
import { TaskRunnerFactory } from './task_runner_factory';
import { actionTypeRegistryMock } from '../action_type_registry.mock';
import { actionExecutorMock } from './action_executor.mock';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock';
import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks';
import {
savedObjectsClientMock,
loggingServiceMock,
@ -19,7 +19,7 @@ import {
const spaceIdToNamespace = jest.fn();
const actionTypeRegistry = actionTypeRegistryMock.create();
const mockedEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
const mockedEncryptedSavedObjectsPlugin = encryptedSavedObjectsMock.createStart();
const mockedActionExecutor = actionExecutorMock.create();
let fakeTimer: sinon.SinonFakeTimers;

View file

@ -7,7 +7,7 @@
import { ActionExecutorContract } from './action_executor';
import { ExecutorError } from './executor_error';
import { RunContext } from '../../../task_manager';
import { EncryptedSavedObjectsStartContract } from '../shim';
import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../../plugins/encrypted_saved_objects/server';
import { ActionTaskParams, GetBasePathFunction, SpaceIdToNamespaceFunction } from '../types';
export interface TaskRunnerContext {

View file

@ -97,12 +97,12 @@ export class Plugin {
// - `secrets` properties will be encrypted
// - `config` will be included in AAD
// - everything else excluded from AAD
plugins.encrypted_saved_objects.registerType({
plugins.encryptedSavedObjects.registerType({
type: 'action',
attributesToEncrypt: new Set(['secrets']),
attributesToExcludeFromAAD: new Set(['description']),
});
plugins.encrypted_saved_objects.registerType({
plugins.encryptedSavedObjects.registerType({
type: 'action_task_params',
attributesToEncrypt: new Set(['apiKey']),
});
@ -169,11 +169,11 @@ export class Plugin {
logger,
spaces: plugins.spaces,
getServices,
encryptedSavedObjectsPlugin: plugins.encrypted_saved_objects,
encryptedSavedObjectsPlugin: plugins.encryptedSavedObjects,
actionTypeRegistry: actionTypeRegistry!,
});
taskRunnerFactory!.initialize({
encryptedSavedObjectsPlugin: plugins.encrypted_saved_objects,
encryptedSavedObjectsPlugin: plugins.encryptedSavedObjects,
getBasePath,
spaceIdToNamespace,
});

View file

@ -8,7 +8,7 @@ import Hapi from 'hapi';
import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks';
import { actionsClientMock } from '../actions_client.mock';
import { actionTypeRegistryMock } from '../action_type_registry.mock';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock';
import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks';
const defaultConfig = {
'kibana.index': '.kibana',
@ -22,7 +22,8 @@ export function createMockServer(config: Record<string, any> = defaultConfig) {
const actionsClient = actionsClientMock.create();
const actionTypeRegistry = actionTypeRegistryMock.create();
const savedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.create();
const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
const encryptedSavedObjectsStart = encryptedSavedObjectsMock.createStart();
server.config = () => {
return {
@ -49,21 +50,16 @@ export function createMockServer(config: Record<string, any> = defaultConfig) {
},
});
server.register({
name: 'encrypted_saved_objects',
register(pluginServer: Hapi.Server) {
pluginServer.expose('isEncryptionError', encryptedSavedObjects.isEncryptionError);
pluginServer.expose('registerType', encryptedSavedObjects.registerType);
pluginServer.expose(
'getDecryptedAsInternalUser',
encryptedSavedObjects.getDecryptedAsInternalUser
);
},
});
server.decorate('request', 'getSavedObjectsClient', () => savedObjectsClient);
server.decorate('request', 'getActionsClient', () => actionsClient);
server.decorate('request', 'getBasePath', () => '/s/my-space');
return { server, savedObjectsClient, actionsClient, actionTypeRegistry, encryptedSavedObjects };
return {
server,
savedObjectsClient,
actionsClient,
actionTypeRegistry,
encryptedSavedObjectsSetup,
encryptedSavedObjectsStart,
};
}

View file

@ -12,7 +12,10 @@ import { TaskManager } from '../../task_manager';
import { XPackMainPlugin } from '../../xpack_main/xpack_main';
import KbnServer from '../../../../../src/legacy/server/kbn_server';
import { LegacySpacesPlugin as SpacesPluginStartContract } from '../../spaces';
import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects';
import {
PluginSetupContract as EncryptedSavedObjectsSetupContract,
PluginStartContract as EncryptedSavedObjectsStartContract,
} from '../../../../plugins/encrypted_saved_objects/server';
import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server';
import {
CoreSetup,
@ -24,7 +27,6 @@ import {
// due to being marked as dependencies
interface Plugins extends Hapi.PluginProperties {
task_manager: TaskManager;
encrypted_saved_objects: EncryptedSavedObjectsPlugin;
}
export interface Server extends Legacy.Server {
@ -42,15 +44,10 @@ export type TaskManagerStartContract = Pick<TaskManager, 'schedule' | 'fetch' |
export type XPackMainPluginSetupContract = Pick<XPackMainPlugin, 'registerFeature'>;
export type SecurityPluginSetupContract = Pick<SecurityPlugin, 'config' | 'registerLegacyAPI'>;
export type SecurityPluginStartContract = Pick<SecurityPlugin, 'authc'>;
export type EncryptedSavedObjectsSetupContract = Pick<EncryptedSavedObjectsPlugin, 'registerType'>;
export type TaskManagerSetupContract = Pick<
TaskManager,
'addMiddleware' | 'registerTaskDefinitions'
>;
export type EncryptedSavedObjectsStartContract = Pick<
EncryptedSavedObjectsPlugin,
'isEncryptionError' | 'getDecryptedAsInternalUser'
>;
/**
* New platform interfaces
@ -78,12 +75,12 @@ export interface ActionsPluginsSetup {
security?: SecurityPluginSetupContract;
task_manager: TaskManagerSetupContract;
xpack_main: XPackMainPluginSetupContract;
encrypted_saved_objects: EncryptedSavedObjectsSetupContract;
encryptedSavedObjects: EncryptedSavedObjectsSetupContract;
}
export interface ActionsPluginsStart {
security?: SecurityPluginStartContract;
spaces: () => SpacesPluginStartContract | undefined;
encrypted_saved_objects: EncryptedSavedObjectsStartContract;
encryptedSavedObjects: EncryptedSavedObjectsStartContract;
task_manager: TaskManagerStartContract;
}
@ -134,7 +131,8 @@ export function shim(
security: newPlatform.setup.plugins.security as SecurityPluginSetupContract | undefined,
task_manager: server.plugins.task_manager,
xpack_main: server.plugins.xpack_main,
encrypted_saved_objects: server.plugins.encrypted_saved_objects,
encryptedSavedObjects: newPlatform.setup.plugins
.encryptedSavedObjects as EncryptedSavedObjectsSetupContract,
};
const pluginsStart: ActionsPluginsStart = {
@ -142,7 +140,8 @@ export function shim(
// TODO: Currently a function because it's an optional dependency that
// initializes after this function is called
spaces: () => server.plugins.spaces,
encrypted_saved_objects: server.plugins.encrypted_saved_objects,
encryptedSavedObjects: newPlatform.start.plugins
.encryptedSavedObjects as EncryptedSavedObjectsStartContract,
task_manager: server.plugins.task_manager,
};

View file

@ -22,12 +22,12 @@ export function alerting(kibana: any) {
return new kibana.Plugin({
id: 'alerting',
configPrefix: 'xpack.alerting',
require: ['kibana', 'elasticsearch', 'actions', 'task_manager', 'encrypted_saved_objects'],
require: ['kibana', 'elasticsearch', 'actions', 'task_manager', 'encryptedSavedObjects'],
isEnabled(config: Legacy.KibanaConfig) {
return (
config.get('xpack.alerting.enabled') === true &&
config.get('xpack.actions.enabled') === true &&
config.get('xpack.encrypted_saved_objects.enabled') === true &&
config.get('xpack.encryptedSavedObjects.enabled') === true &&
config.get('xpack.task_manager.enabled') === true
);
},

View file

@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema';
import { AlertExecutorOptions } from '../types';
import { ConcreteTaskInstance } from '../../../task_manager';
import { TaskRunnerContext, TaskRunnerFactory } from './task_runner_factory';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/plugin.mock';
import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks';
import {
savedObjectsClientMock,
loggingServiceMock,
@ -52,7 +52,7 @@ beforeAll(() => {
afterAll(() => fakeTimer.restore());
const savedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.create();
const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.createStart();
const services = {
log: jest.fn(),
callCluster: jest.fn(),

View file

@ -11,7 +11,7 @@ import { createAlertInstanceFactory } from './create_alert_instance_factory';
import { AlertInstance } from './alert_instance';
import { getNextRunAt } from './get_next_run_at';
import { validateAlertTypeParams } from './validate_alert_type_params';
import { EncryptedSavedObjectsStartContract } from '../shim';
import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../../plugins/encrypted_saved_objects/server';
import { PluginStartContract as ActionsPluginStartContract } from '../../../actions';
import {
AlertType,

View file

@ -85,7 +85,7 @@ export class Plugin {
});
// Encrypted attributes
plugins.encrypted_saved_objects.registerType({
plugins.encryptedSavedObjects.registerType({
type: 'alert',
attributesToEncrypt: new Set(['apiKey']),
attributesToExcludeFromAAD: new Set([
@ -147,7 +147,7 @@ export class Plugin {
};
},
executeAction: plugins.actions.execute,
encryptedSavedObjectsPlugin: plugins.encrypted_saved_objects,
encryptedSavedObjectsPlugin: plugins.encryptedSavedObjects,
spaceIdToNamespace(spaceId?: string): string | undefined {
const spacesPlugin = plugins.spaces();
return spacesPlugin && spaceId ? spacesPlugin.spaceIdToNamespace(spaceId) : undefined;

View file

@ -10,7 +10,10 @@ import { LegacySpacesPlugin as SpacesPluginStartContract } from '../../spaces';
import { TaskManager } from '../../task_manager';
import { XPackMainPlugin } from '../../xpack_main/xpack_main';
import KbnServer from '../../../../../src/legacy/server/kbn_server';
import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects';
import {
PluginSetupContract as EncryptedSavedObjectsSetupContract,
PluginStartContract as EncryptedSavedObjectsStartContract,
} from '../../../../plugins/encrypted_saved_objects/server';
import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server';
import {
CoreSetup,
@ -28,7 +31,6 @@ import {
interface Plugins extends Hapi.PluginProperties {
actions: ActionsPlugin;
task_manager: TaskManager;
encrypted_saved_objects: EncryptedSavedObjectsPlugin;
}
export interface Server extends Legacy.Server {
@ -41,16 +43,11 @@ export interface Server extends Legacy.Server {
export type TaskManagerStartContract = Pick<TaskManager, 'schedule' | 'fetch' | 'remove'>;
export type SecurityPluginSetupContract = Pick<SecurityPlugin, 'config' | 'registerLegacyAPI'>;
export type SecurityPluginStartContract = Pick<SecurityPlugin, 'authc'>;
export type EncryptedSavedObjectsSetupContract = Pick<EncryptedSavedObjectsPlugin, 'registerType'>;
export type XPackMainPluginSetupContract = Pick<XPackMainPlugin, 'registerFeature'>;
export type TaskManagerSetupContract = Pick<
TaskManager,
'addMiddleware' | 'registerTaskDefinitions'
>;
export type EncryptedSavedObjectsStartContract = Pick<
EncryptedSavedObjectsPlugin,
'isEncryptionError' | 'getDecryptedAsInternalUser'
>;
/**
* New platform interfaces
@ -75,13 +72,13 @@ export interface AlertingPluginsSetup {
task_manager: TaskManagerSetupContract;
actions: ActionsPluginSetupContract;
xpack_main: XPackMainPluginSetupContract;
encrypted_saved_objects: EncryptedSavedObjectsSetupContract;
encryptedSavedObjects: EncryptedSavedObjectsSetupContract;
}
export interface AlertingPluginsStart {
actions: ActionsPluginStartContract;
security?: SecurityPluginStartContract;
spaces: () => SpacesPluginStartContract | undefined;
encrypted_saved_objects: EncryptedSavedObjectsStartContract;
encryptedSavedObjects: EncryptedSavedObjectsStartContract;
task_manager: TaskManagerStartContract;
}
@ -122,7 +119,8 @@ export function shim(
task_manager: server.plugins.task_manager,
actions: server.plugins.actions.setup,
xpack_main: server.plugins.xpack_main,
encrypted_saved_objects: server.plugins.encrypted_saved_objects,
encryptedSavedObjects: newPlatform.setup.plugins
.encryptedSavedObjects as EncryptedSavedObjectsSetupContract,
};
const pluginsStart: AlertingPluginsStart = {
@ -131,7 +129,8 @@ export function shim(
// TODO: Currently a function because it's an optional dependency that
// initializes after this function is called
spaces: () => server.plugins.spaces,
encrypted_saved_objects: server.plugins.encrypted_saved_objects,
encryptedSavedObjects: newPlatform.start.plugins
.encryptedSavedObjects as EncryptedSavedObjectsStartContract,
task_manager: server.plugins.task_manager,
};

View file

@ -1,14 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`config schema with context {"dist":false} produces correct config 1`] = `
Object {
"enabled": true,
"encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
}
`;
exports[`config schema with context {"dist":true} produces correct config 1`] = `
Object {
"enabled": true,
}
`;

View file

@ -1,17 +0,0 @@
/*
* 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 { encryptedSavedObjects } from './index';
import { getConfigSchema } from '../../../test_utils';
const describeWithContext = describe.each([[{ dist: false }], [{ dist: true }]]);
describeWithContext('config schema with context %j', context => {
it('produces correct config', async () => {
const schema = await getConfigSchema(encryptedSavedObjects);
await expect(schema.validate({}, { context })).resolves.toMatchSnapshot();
});
});

View file

@ -5,62 +5,41 @@
*/
import { Root } from 'joi';
import { Legacy, Server } from 'kibana';
import { Legacy } from 'kibana';
import { PluginSetupContract } from '../../../plugins/encrypted_saved_objects/server';
// @ts-ignore
import { AuditLogger } from '../../server/lib/audit_logger';
import { CONFIG_PREFIX, PLUGIN_ID, Plugin } from './server/plugin';
/**
* Public interface of the security plugin for the legacy plugin system.
*/
export type EncryptedSavedObjectsPlugin = ReturnType<Plugin['setup']>;
export const encryptedSavedObjects = (kibana: any) =>
export const encryptedSavedObjects = (kibana: {
Plugin: new (options: Legacy.PluginSpecOptions & { configPrefix?: string }) => unknown;
}) =>
new kibana.Plugin({
id: PLUGIN_ID,
configPrefix: CONFIG_PREFIX,
require: ['kibana', 'elasticsearch', 'xpack_main'],
id: 'encryptedSavedObjects',
configPrefix: 'xpack.encryptedSavedObjects',
require: ['xpack_main'],
config(Joi: Root) {
return Joi.object({
enabled: Joi.boolean().default(true),
encryptionKey: Joi.when(Joi.ref('$dist'), {
is: true,
then: Joi.string().min(32),
otherwise: Joi.string()
.min(32)
.default('a'.repeat(32)),
}),
}).default();
},
// Some legacy plugins still use `enabled` config key, so we keep it here, but the rest of the
// keys is handled by the New Platform plugin.
config: (Joi: Root) =>
Joi.object({ enabled: Joi.boolean().default(true) })
.unknown(true)
.default(),
async init(server: Legacy.Server) {
const loggerFacade = {
fatal: (errorOrMessage: string | Error) => server.log(['fatal', PLUGIN_ID], errorOrMessage),
trace: (message: string) => server.log(['debug', PLUGIN_ID], message),
error: (message: string) => server.log(['error', PLUGIN_ID], message),
warn: (message: string) => server.log(['warning', PLUGIN_ID], message),
debug: (message: string) => server.log(['debug', PLUGIN_ID], message),
info: (message: string) => server.log(['info', PLUGIN_ID], message),
} as Server.Logger;
const config = server.config();
const encryptedSavedObjectsSetup = new Plugin(loggerFacade).setup(
{
config: {
encryptionKey: config.get<string | undefined>(`${CONFIG_PREFIX}.encryptionKey`),
},
savedObjects: server.savedObjects,
elasticsearch: server.plugins.elasticsearch,
},
{ audit: new AuditLogger(server, PLUGIN_ID, config, server.plugins.xpack_main.info) }
);
// Re-expose plugin setup contract through legacy mechanism.
for (const [setupMethodName, setupMethod] of Object.entries(encryptedSavedObjectsSetup)) {
server.expose(setupMethodName, setupMethod);
init(server: Legacy.Server) {
const encryptedSavedObjectsPlugin = (server.newPlatform.setup.plugins
.encryptedSavedObjects as unknown) as PluginSetupContract;
if (!encryptedSavedObjectsPlugin) {
throw new Error('New Platform XPack EncryptedSavedObjects plugin is not available.');
}
encryptedSavedObjectsPlugin.__legacyCompat.registerLegacyAPI({
savedObjects: server.savedObjects,
auditLogger: new AuditLogger(
server,
'encryptedSavedObjects',
server.config(),
server.plugins.xpack_main.info
),
});
},
});

View file

@ -1,60 +0,0 @@
/*
* 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 {
EncryptedSavedObjectsService,
EncryptedSavedObjectTypeRegistration,
SavedObjectDescriptor,
} from './encrypted_saved_objects_service';
export function createEncryptedSavedObjectsServiceMock(
registrations: EncryptedSavedObjectTypeRegistration[] = []
) {
const mock: jest.Mocked<EncryptedSavedObjectsService> = new (jest.requireMock(
'./encrypted_saved_objects_service'
)).EncryptedSavedObjectsService();
function processAttributes<T extends Record<string, any>>(
descriptor: Pick<SavedObjectDescriptor, 'type'>,
attrs: T,
action: (attrs: T, attrName: string) => void
) {
const registration = registrations.find(r => r.type === descriptor.type);
if (!registration) {
return attrs;
}
const clonedAttrs = { ...attrs };
for (const attrName of registration.attributesToEncrypt) {
if (attrName in clonedAttrs) {
action(clonedAttrs, attrName);
}
}
return clonedAttrs;
}
mock.isRegistered.mockImplementation(type => registrations.findIndex(r => r.type === type) >= 0);
mock.encryptAttributes.mockImplementation(async (descriptor, attrs) =>
processAttributes(
descriptor,
attrs,
(clonedAttrs, attrName) => (clonedAttrs[attrName] = `*${clonedAttrs[attrName]}*`)
)
);
mock.decryptAttributes.mockImplementation(async (descriptor, attrs) =>
processAttributes(
descriptor,
attrs,
(clonedAttrs, attrName) =>
(clonedAttrs[attrName] = (clonedAttrs[attrName] as string).slice(1, -1))
)
);
mock.stripEncryptedAttributes.mockImplementation((type, attrs) =>
processAttributes({ type }, attrs, (clonedAttrs, attrName) => delete clonedAttrs[attrName])
);
return mock;
}

View file

@ -1,21 +0,0 @@
/*
* 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 { Plugin } from './plugin';
type EncryptedSavedObjectsPlugin = ReturnType<Plugin['setup']>;
const createEncryptedSavedObjectsMock = () => {
const mocked: jest.Mocked<EncryptedSavedObjectsPlugin> = {
isEncryptionError: jest.fn(),
registerType: jest.fn(),
getDecryptedAsInternalUser: jest.fn(),
};
return mocked;
};
export const encryptedSavedObjectsMock = {
create: createEncryptedSavedObjectsMock,
};

View file

@ -1,92 +0,0 @@
/*
* 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 crypto from 'crypto';
import { Legacy, Server } from 'kibana';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { SavedObjectsRepository } from 'src/core/server/saved_objects/service';
import { SavedObjectsBaseOptions, SavedObject, SavedObjectAttributes } from 'src/core/server';
import {
EncryptedSavedObjectsService,
EncryptedSavedObjectTypeRegistration,
EncryptionError,
EncryptedSavedObjectsAuditLogger,
EncryptedSavedObjectsClientWrapper,
} from './lib';
export const PLUGIN_ID = 'encrypted_saved_objects';
export const CONFIG_PREFIX = `xpack.${PLUGIN_ID}`;
interface CoreSetup {
config: { encryptionKey?: string };
elasticsearch: Legacy.Plugins.elasticsearch.Plugin;
savedObjects: Legacy.SavedObjectsService;
}
interface PluginsSetup {
audit: unknown;
}
export class Plugin {
constructor(private readonly log: Server.Logger) {}
public setup(core: CoreSetup, plugins: PluginsSetup) {
let encryptionKey = core.config.encryptionKey;
if (encryptionKey == null) {
this.log.warn(
`Generating a random key for ${CONFIG_PREFIX}.encryptionKey. To be able ` +
'to decrypt encrypted saved objects attributes after restart, please set ' +
`${CONFIG_PREFIX}.encryptionKey in kibana.yml`
);
encryptionKey = crypto.randomBytes(16).toString('hex');
}
const service = Object.freeze(
new EncryptedSavedObjectsService(
encryptionKey,
core.savedObjects.types,
this.log,
new EncryptedSavedObjectsAuditLogger(plugins.audit)
)
);
// Register custom saved object client that will encrypt, decrypt and strip saved object
// attributes where appropriate for any saved object repository request. We choose max possible
// priority for this wrapper to allow all other wrappers to set proper `namespace` for the Saved
// Object (e.g. wrapper registered by the Spaces plugin) before we encrypt attributes since
// `namespace` is included into AAD.
core.savedObjects.addScopedSavedObjectsClientWrapperFactory(
Number.MAX_SAFE_INTEGER,
'encrypted_saved_objects',
({ client: baseClient }) => new EncryptedSavedObjectsClientWrapper({ baseClient, service })
);
const internalRepository: SavedObjectsRepository = core.savedObjects.getSavedObjectsRepository(
core.elasticsearch.getCluster('admin').callWithInternalUser
);
return {
isEncryptionError: (error: Error) => error instanceof EncryptionError,
registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) =>
service.registerType(typeRegistration),
getDecryptedAsInternalUser: async <T extends SavedObjectAttributes = any>(
type: string,
id: string,
options?: SavedObjectsBaseOptions
): Promise<SavedObject<T>> => {
const savedObject = await internalRepository.get(type, id, options);
return {
...savedObject,
attributes: await service.decryptAttributes(
{ type, id, namespace: options && options.namespace },
savedObject.attributes
),
};
},
};
}
}

View file

@ -9,17 +9,16 @@ security and spaces filtering as well as performing audit logging.
## Usage
Follow these steps to use `encrypted_saved_objects` in your plugin:
Follow these steps to use `encryptedSavedObjects` in your plugin:
1. Declare `encrypted_saved_objects` as a dependency:
1. Declare `encryptedSavedObjects` as a dependency in `kibana.json`:
```typescript
...
new kibana.Plugin({
```json
{
...
require: ['encrypted_saved_objects'],
"requiredPlugins": ["encryptedSavedObjects"],
...
});
}
```
2. Add attributes to be encrypted in `mappings.json` file for the respective Saved Object type. These attributes should
@ -37,13 +36,17 @@ searchable or analyzed:
}
```
3. Register Saved Object type using the provided API:
3. Register Saved Object type using the provided API at the `setup` stage:
```typescript
server.plugins.encrypted_saved_objects.registerType({
type: 'my-saved-object-type',
attributesToEncrypt: new Set(['mySecret']),
});
...
public setup(core: CoreSetup, { encryptedSavedObjects }: PluginSetupDependencies) {
encryptedSavedObjects.registerType({
type: 'my-saved-object-type',
attributesToEncrypt: new Set(['mySecret']),
});
}
...
```
4. For any Saved Object operation that does not require retrieval of decrypted content, use standard REST or
@ -51,11 +54,17 @@ programmatic Saved Object API, e.g.:
```typescript
...
async handler(request: Request) {
return await server.savedObjects
.getScopedSavedObjectsClient(request)
.create('my-saved-object-type', { name: 'some name', mySecret: 'non encrypted secret' });
}
router.get(
{ path: '/some-path', validate: false },
async (context, req, res) => {
return res.ok({
body: await context.core.savedObjects.client.create(
'my-saved-object-type',
{ name: 'some name', mySecret: 'non encrypted secret' }
),
});
}
);
...
```
@ -63,12 +72,12 @@ async handler(request: Request) {
**Note:** As name suggests the method will retrieve the encrypted values and decrypt them on behalf of the internal Kibana
user to make it possible to use this method even when user request context is not available (e.g. in background tasks).
Hence this method should only be used wherever consumers would otherwise feel comfortable using `callWithInternalUser`
Hence this method should only be used wherever consumers would otherwise feel comfortable using `callAsInternalUser`
and preferably only as a part of the Kibana server routines that are outside of the lifecycle of a HTTP request that a
user has control over.
```typescript
const savedObjectWithDecryptedContent = await server.plugins.encrypted_saved_objects.getDecryptedAsInternalUser(
const savedObjectWithDecryptedContent = await encryptedSavedObjects.getDecryptedAsInternalUser(
'my-saved-object-type',
'saved-object-id'
);

View file

@ -0,0 +1,8 @@
{
"id": "encryptedSavedObjects",
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack", "encryptedSavedObjects"],
"server": true,
"ui": false
}

View file

@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EncryptedSavedObjectsAuditLogger } from './encrypted_saved_objects_audit_logger';
import { EncryptedSavedObjectsAuditLogger } from './audit_logger';
test('properly logs audit events', () => {
const mockInternalAuditLogger = { log: jest.fn() };
const audit = new EncryptedSavedObjectsAuditLogger(mockInternalAuditLogger);
const audit = new EncryptedSavedObjectsAuditLogger(() => mockInternalAuditLogger);
audit.encryptAttributesSuccess(['one', 'two'], {
type: 'known-type',

View file

@ -4,16 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SavedObjectDescriptor, descriptorToArray } from './encrypted_saved_objects_service';
import { SavedObjectDescriptor, descriptorToArray } from '../crypto';
import { LegacyAPI } from '../plugin';
/**
* Represents all audit events the plugin can log.
*/
export class EncryptedSavedObjectsAuditLogger {
constructor(private readonly auditLogger: any) {}
constructor(private readonly getAuditLogger: () => LegacyAPI['auditLogger']) {}
public encryptAttributeFailure(attributeName: string, descriptor: SavedObjectDescriptor) {
this.auditLogger.log(
this.getAuditLogger().log(
'encrypt_failure',
`Failed to encrypt attribute "${attributeName}" for saved object "[${descriptorToArray(
descriptor
@ -23,7 +24,7 @@ export class EncryptedSavedObjectsAuditLogger {
}
public decryptAttributeFailure(attributeName: string, descriptor: SavedObjectDescriptor) {
this.auditLogger.log(
this.getAuditLogger().log(
'decrypt_failure',
`Failed to decrypt attribute "${attributeName}" for saved object "[${descriptorToArray(
descriptor
@ -36,7 +37,7 @@ export class EncryptedSavedObjectsAuditLogger {
attributesNames: readonly string[],
descriptor: SavedObjectDescriptor
) {
this.auditLogger.log(
this.getAuditLogger().log(
'encrypt_success',
`Successfully encrypted attributes "[${attributesNames}]" for saved object "[${descriptorToArray(
descriptor
@ -49,7 +50,7 @@ export class EncryptedSavedObjectsAuditLogger {
attributesNames: readonly string[],
descriptor: SavedObjectDescriptor
) {
this.auditLogger.log(
this.getAuditLogger().log(
'decrypt_success',
`Successfully decrypted attributes "[${attributesNames}]" for saved object "[${descriptorToArray(
descriptor

View file

@ -0,0 +1,18 @@
/*
* 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 { EncryptedSavedObjectsAuditLogger } from './audit_logger';
export const encryptedSavedObjectsAuditLoggerMock = {
create() {
return ({
encryptAttributesSuccess: jest.fn(),
encryptAttributeFailure: jest.fn(),
decryptAttributesSuccess: jest.fn(),
decryptAttributeFailure: jest.fn(),
} as unknown) as jest.Mocked<EncryptedSavedObjectsAuditLogger>;
},
};

View file

@ -0,0 +1,7 @@
/*
* 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.
*/
export { EncryptedSavedObjectsAuditLogger } from './audit_logger';

View file

@ -0,0 +1,70 @@
/*
* 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.
*/
jest.mock('crypto', () => ({ randomBytes: jest.fn() }));
import { first } from 'rxjs/operators';
import { loggingServiceMock, coreMock } from 'src/core/server/mocks';
import { createConfig$, ConfigSchema } from './config';
describe('config schema', () => {
it('generates proper defaults', () => {
expect(ConfigSchema.validate({})).toMatchInlineSnapshot(`
Object {
"enabled": true,
"encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
}
`);
expect(ConfigSchema.validate({}, { dist: false })).toMatchInlineSnapshot(`
Object {
"enabled": true,
"encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
}
`);
expect(ConfigSchema.validate({}, { dist: true })).toMatchInlineSnapshot(`
Object {
"enabled": true,
}
`);
});
it('should throw error if xpack.encryptedSavedObjects.encryptionKey is less than 32 characters', () => {
expect(() =>
ConfigSchema.validate({ encryptionKey: 'foo' })
).toThrowErrorMatchingInlineSnapshot(
`"[encryptionKey]: value is [foo] but it must have a minimum length of [32]."`
);
expect(() =>
ConfigSchema.validate({ encryptionKey: 'foo' }, { dist: true })
).toThrowErrorMatchingInlineSnapshot(
`"[encryptionKey]: value is [foo] but it must have a minimum length of [32]."`
);
});
});
describe('createConfig$()', () => {
it('should log a warning and set xpack.encryptedSavedObjects.encryptionKey if not set', async () => {
const mockRandomBytes = jest.requireMock('crypto').randomBytes;
mockRandomBytes.mockReturnValue('ab'.repeat(16));
const contextMock = coreMock.createPluginInitializerContext({});
const config = await createConfig$(contextMock)
.pipe(first())
.toPromise();
expect(config).toEqual({ encryptionKey: 'ab'.repeat(16) });
expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"Generating a random key for xpack.encryptedSavedObjects.encryptionKey. To be able to decrypt encrypted saved objects attributes after restart, please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml",
],
]
`);
});
});

View file

@ -0,0 +1,46 @@
/*
* 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 crypto from 'crypto';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { schema, TypeOf } from '@kbn/config-schema';
import { PluginInitializerContext } from 'src/core/server';
export type ConfigType = ReturnType<typeof createConfig$> extends Observable<infer P>
? P
: ReturnType<typeof createConfig$>;
export const ConfigSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
encryptionKey: schema.conditional(
schema.contextRef('dist'),
true,
schema.maybe(schema.string({ minLength: 32 })),
schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) })
),
});
export function createConfig$(context: PluginInitializerContext) {
return context.config.create<TypeOf<typeof ConfigSchema>>().pipe(
map(config => {
const logger = context.logger.get('config');
let encryptionKey = config.encryptionKey;
if (encryptionKey === undefined) {
logger.warn(
'Generating a random key for xpack.encryptedSavedObjects.encryptionKey. ' +
'To be able to decrypt encrypted saved objects attributes after restart, ' +
'please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml'
);
encryptionKey = crypto.randomBytes(16).toString('hex');
}
return { ...config, encryptionKey };
})
);
}

View file

@ -6,19 +6,17 @@
jest.mock('@elastic/node-crypto', () => jest.fn());
import { EncryptedSavedObjectsAuditLogger } from './encrypted_saved_objects_audit_logger';
import { EncryptedSavedObjectsAuditLogger } from '../audit';
import { EncryptedSavedObjectsService } from './encrypted_saved_objects_service';
import { EncryptionError } from './encryption_error';
import { loggingServiceMock } from 'src/core/server/mocks';
import { encryptedSavedObjectsAuditLoggerMock } from '../audit/index.mock';
let service: EncryptedSavedObjectsService;
let mockAuditLogger: jest.Mocked<EncryptedSavedObjectsAuditLogger>;
beforeEach(() => {
mockAuditLogger = {
encryptAttributesSuccess: jest.fn(),
encryptAttributeFailure: jest.fn(),
decryptAttributesSuccess: jest.fn(),
decryptAttributeFailure: jest.fn(),
} as any;
mockAuditLogger = encryptedSavedObjectsAuditLoggerMock.create();
// Call actual `@elastic/node-crypto` by default, but allow to override implementation in tests.
jest
@ -27,8 +25,7 @@ beforeEach(() => {
service = new EncryptedSavedObjectsService(
'encryption-key-abc',
['known-type-1', 'known-type-2'],
{ debug: jest.fn(), error: jest.fn() } as any,
loggingServiceMock.create().get(),
mockAuditLogger
);
});
@ -54,12 +51,6 @@ describe('#registerType', () => {
service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attr']) })
).toThrowError('The "known-type-1" saved object type is already registered.');
});
it('throws if `type` references to the unknown type', () => {
expect(() =>
service.registerType({ type: 'unknown-type', attributesToEncrypt: new Set(['attr']) })
).toThrowError('The type "unknown-type" is not known saved object type.');
});
});
describe('#isRegistered', () => {
@ -137,8 +128,7 @@ describe('#encryptAttributes', () => {
service = new EncryptedSavedObjectsService(
'encryption-key-abc',
['known-type-1', 'known-type-2'],
{ debug: jest.fn(), error: jest.fn() } as any,
loggingServiceMock.create().get(),
mockAuditLogger
);
});
@ -773,8 +763,7 @@ describe('#decryptAttributes', () => {
it('fails if encrypted with another encryption key', async () => {
service = new EncryptedSavedObjectsService(
'encryption-key-abc*',
['known-type-1'],
{ debug: jest.fn(), error: jest.fn() } as any,
loggingServiceMock.create().get(),
mockAuditLogger
);

View file

@ -8,8 +8,8 @@
import nodeCrypto from '@elastic/node-crypto';
import stringify from 'json-stable-stringify';
import typeDetect from 'type-detect';
import { Server } from 'kibana';
import { EncryptedSavedObjectsAuditLogger } from './encrypted_saved_objects_audit_logger';
import { Logger } from 'src/core/server';
import { EncryptedSavedObjectsAuditLogger } from '../audit';
import { EncryptionError } from './encryption_error';
/**
@ -62,14 +62,12 @@ export class EncryptedSavedObjectsService {
/**
* @param encryptionKey The key used to encrypt and decrypt saved objects attributes.
* @param knownTypes The list of all known saved object types.
* @param log Ordinary logger instance.
* @param logger Ordinary logger instance.
* @param audit Audit logger instance.
*/
constructor(
encryptionKey: string,
private readonly knownTypes: readonly string[],
private readonly log: Server.Logger,
private readonly logger: Logger,
private readonly audit: EncryptedSavedObjectsAuditLogger
) {
this.crypto = nodeCrypto({ encryptionKey });
@ -91,10 +89,6 @@ export class EncryptedSavedObjectsService {
throw new Error(`The "${typeRegistration.type}" saved object type is already registered.`);
}
if (!this.knownTypes.includes(typeRegistration.type)) {
throw new Error(`The type "${typeRegistration.type}" is not known saved object type.`);
}
this.typeRegistrations.set(typeRegistration.type, typeRegistration);
}
@ -160,7 +154,9 @@ export class EncryptedSavedObjectsService {
encryptionAAD
);
} catch (err) {
this.log.error(`Failed to encrypt "${attributeName}" attribute: ${err.message || err}`);
this.logger.error(
`Failed to encrypt "${attributeName}" attribute: ${err.message || err}`
);
this.audit.encryptAttributeFailure(attributeName, descriptor);
throw new EncryptionError(
@ -176,7 +172,7 @@ export class EncryptedSavedObjectsService {
// not the case we should collect and log them to make troubleshooting easier.
const encryptedAttributesKeys = Object.keys(encryptedAttributes);
if (encryptedAttributesKeys.length !== typeRegistration.attributesToEncrypt.size) {
this.log.debug(
this.logger.debug(
`The following attributes of saved object "${descriptorToArray(
descriptor
)}" should have been encrypted: ${Array.from(
@ -238,7 +234,7 @@ export class EncryptedSavedObjectsService {
encryptionAAD
);
} catch (err) {
this.log.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);
throw new EncryptionError(
@ -253,7 +249,7 @@ export class EncryptedSavedObjectsService {
// not the case we should collect and log them to make troubleshooting easier.
const decryptedAttributesKeys = Object.keys(decryptedAttributes);
if (decryptedAttributesKeys.length !== typeRegistration.attributesToEncrypt.size) {
this.log.debug(
this.logger.debug(
`The following attributes of saved object "${descriptorToArray(
descriptor
)}" should have been decrypted: ${Array.from(
@ -298,8 +294,8 @@ export class EncryptedSavedObjectsService {
}
}
if (Object.keys(attributesAAD).length) {
this.log.debug(
if (Object.keys(attributesAAD).length === 0) {
this.logger.debug(
`The AAD for saved object "${descriptorToArray(
descriptor
)}" does not include any attributes.`

View file

@ -0,0 +1,62 @@
/*
* 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 {
EncryptedSavedObjectsService,
EncryptedSavedObjectTypeRegistration,
SavedObjectDescriptor,
} from '.';
export const encryptedSavedObjectsServiceMock = {
create(registrations: EncryptedSavedObjectTypeRegistration[] = []) {
const mock: jest.Mocked<EncryptedSavedObjectsService> = new (jest.requireMock(
'./encrypted_saved_objects_service'
)).EncryptedSavedObjectsService();
function processAttributes<T extends Record<string, any>>(
descriptor: Pick<SavedObjectDescriptor, 'type'>,
attrs: T,
action: (attrs: T, attrName: string) => void
) {
const registration = registrations.find(r => r.type === descriptor.type);
if (!registration) {
return attrs;
}
const clonedAttrs = { ...attrs };
for (const attrName of registration.attributesToEncrypt) {
if (attrName in clonedAttrs) {
action(clonedAttrs, attrName);
}
}
return clonedAttrs;
}
mock.isRegistered.mockImplementation(
type => registrations.findIndex(r => r.type === type) >= 0
);
mock.encryptAttributes.mockImplementation(async (descriptor, attrs) =>
processAttributes(
descriptor,
attrs,
(clonedAttrs, attrName) => (clonedAttrs[attrName] = `*${clonedAttrs[attrName]}*`)
)
);
mock.decryptAttributes.mockImplementation(async (descriptor, attrs) =>
processAttributes(
descriptor,
attrs,
(clonedAttrs, attrName) =>
(clonedAttrs[attrName] = (clonedAttrs[attrName] as string).slice(1, -1))
)
);
mock.stripEncryptedAttributes.mockImplementation((type, attrs) =>
processAttributes({ type }, attrs, (clonedAttrs, attrName) => delete clonedAttrs[attrName])
);
return mock;
},
};

View file

@ -7,7 +7,7 @@
export {
EncryptedSavedObjectsService,
EncryptedSavedObjectTypeRegistration,
descriptorToArray,
SavedObjectDescriptor,
} from './encrypted_saved_objects_service';
export { EncryptionError } from './encryption_error';
export { EncryptedSavedObjectsAuditLogger } from './encrypted_saved_objects_audit_logger';
export { EncryptedSavedObjectsClientWrapper } from './encrypted_saved_objects_client_wrapper';

View file

@ -0,0 +1,16 @@
/*
* 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 { PluginInitializerContext } from 'src/core/server';
import { ConfigSchema } from './config';
import { Plugin } from './plugin';
export { EncryptedSavedObjectTypeRegistration, EncryptionError } from './crypto';
export { PluginSetupContract, PluginStartContract } from './plugin';
export const config = { schema: ConfigSchema };
export const plugin = (initializerContext: PluginInitializerContext) =>
new Plugin(initializerContext);

View file

@ -0,0 +1,26 @@
/*
* 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 { PluginSetupContract, PluginStartContract } from './plugin';
function createEncryptedSavedObjectsSetupMock() {
return {
registerType: jest.fn(),
__legacyCompat: { registerLegacyAPI: jest.fn() },
} as jest.Mocked<PluginSetupContract>;
}
function createEncryptedSavedObjectsStartMock() {
return {
isEncryptionError: jest.fn(),
getDecryptedAsInternalUser: jest.fn(),
} as jest.Mocked<PluginStartContract>;
}
export const encryptedSavedObjectsMock = {
createSetup: createEncryptedSavedObjectsSetupMock,
createStart: createEncryptedSavedObjectsStartMock,
};

View file

@ -0,0 +1,38 @@
/*
* 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 { Plugin } from './plugin';
import { coreMock } from 'src/core/server/mocks';
describe('EncryptedSavedObjects Plugin', () => {
describe('setup()', () => {
it('exposes proper contract', async () => {
const plugin = new Plugin(coreMock.createPluginInitializerContext());
await expect(plugin.setup(coreMock.createSetup())).resolves.toMatchInlineSnapshot(`
Object {
"__legacyCompat": Object {
"registerLegacyAPI": [Function],
},
"registerType": [Function],
}
`);
});
});
describe('start()', () => {
it('exposes proper contract', async () => {
const plugin = new Plugin(coreMock.createPluginInitializerContext());
await plugin.setup(coreMock.createSetup());
await expect(plugin.start()).toMatchInlineSnapshot(`
Object {
"getDecryptedAsInternalUser": [Function],
"isEncryptionError": [Function],
}
`);
});
});
});

View file

@ -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 {
Logger,
SavedObjectsBaseOptions,
PluginInitializerContext,
CoreSetup,
SavedObjectsLegacyService,
KibanaRequest,
LegacyRequest,
} from 'src/core/server';
import { first } from 'rxjs/operators';
import { createConfig$ } from './config';
import {
EncryptedSavedObjectsService,
EncryptedSavedObjectTypeRegistration,
EncryptionError,
} from './crypto';
import { EncryptedSavedObjectsAuditLogger } from './audit';
import { SavedObjectsSetup, setupSavedObjects } from './saved_objects';
export interface PluginSetupContract {
registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => void;
__legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => void };
}
export interface PluginStartContract extends SavedObjectsSetup {
isEncryptionError: (error: Error) => boolean;
}
/**
* Describes a set of APIs that is available in the legacy platform only and required by this plugin
* to function properly.
*/
export interface LegacyAPI {
savedObjects: SavedObjectsLegacyService<KibanaRequest | LegacyRequest>;
auditLogger: {
log: (eventType: string, message: string, data?: Record<string, unknown>) => void;
};
}
/**
* Represents EncryptedSavedObjects Plugin instance that will be managed by the Kibana plugin system.
*/
export class Plugin {
private readonly logger: Logger;
private savedObjectsSetup?: ReturnType<typeof setupSavedObjects>;
private legacyAPI?: LegacyAPI;
private readonly getLegacyAPI = () => {
if (!this.legacyAPI) {
throw new Error('Legacy API is not registered!');
}
return this.legacyAPI;
};
constructor(private readonly initializerContext: PluginInitializerContext) {
this.logger = this.initializerContext.logger.get();
}
public async setup(core: CoreSetup): Promise<PluginSetupContract> {
const config = await createConfig$(this.initializerContext)
.pipe(first())
.toPromise();
const adminClusterClient = await core.elasticsearch.adminClient$.pipe(first()).toPromise();
const service = Object.freeze(
new EncryptedSavedObjectsService(
config.encryptionKey,
this.logger,
new EncryptedSavedObjectsAuditLogger(() => this.getLegacyAPI().auditLogger)
)
);
return {
registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) =>
service.registerType(typeRegistration),
__legacyCompat: {
registerLegacyAPI: (legacyAPI: LegacyAPI) => {
this.legacyAPI = legacyAPI;
this.savedObjectsSetup = setupSavedObjects({
adminClusterClient,
service,
savedObjects: legacyAPI.savedObjects,
});
},
},
};
}
public start() {
this.logger.debug('Starting plugin');
return {
isEncryptionError: (error: Error) => error instanceof EncryptionError,
getDecryptedAsInternalUser: (type: string, id: string, options?: SavedObjectsBaseOptions) => {
if (!this.savedObjectsSetup) {
throw new Error('Legacy SavedObjects API is not registered!');
}
return this.savedObjectsSetup.getDecryptedAsInternalUser(type, id, options);
},
};
}
public stop() {
this.logger.debug('Stopping plugin');
}
}

View file

@ -6,18 +6,19 @@
jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('uuid-v4-id') }));
import { EncryptedSavedObjectsClientWrapper } from './encrypted_saved_objects_client_wrapper';
import { EncryptedSavedObjectsService } from './encrypted_saved_objects_service';
import { createEncryptedSavedObjectsServiceMock } from './encrypted_saved_objects_service.mock';
import { savedObjectsClientMock } from 'src/core/server/saved_objects/service/saved_objects_client.mock';
import { SavedObjectsClientContract } from 'src/core/server';
import { EncryptedSavedObjectsService } from '../crypto';
import { EncryptedSavedObjectsClientWrapper } from './encrypted_saved_objects_client_wrapper';
import { savedObjectsClientMock } from 'src/core/server/mocks';
import { encryptedSavedObjectsServiceMock } from '../crypto/index.mock';
let wrapper: EncryptedSavedObjectsClientWrapper;
let mockBaseClient: jest.Mocked<SavedObjectsClientContract>;
let encryptedSavedObjectsServiceMock: jest.Mocked<EncryptedSavedObjectsService>;
let encryptedSavedObjectsServiceMockInstance: jest.Mocked<EncryptedSavedObjectsService>;
beforeEach(() => {
mockBaseClient = savedObjectsClientMock.create();
encryptedSavedObjectsServiceMock = createEncryptedSavedObjectsServiceMock([
encryptedSavedObjectsServiceMockInstance = encryptedSavedObjectsServiceMock.create([
{
type: 'known-type',
attributesToEncrypt: new Set(['attrSecret']),
@ -25,7 +26,7 @@ beforeEach(() => {
]);
wrapper = new EncryptedSavedObjectsClientWrapper({
service: encryptedSavedObjectsServiceMock,
service: encryptedSavedObjectsServiceMockInstance,
baseClient: mockBaseClient,
} as any);
});
@ -76,8 +77,8 @@ describe('#create', () => {
attributes: { attrOne: 'one', attrThree: 'three' },
});
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledWith(
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{ type: 'known-type', id: 'uuid-v4-id' },
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
);
@ -107,8 +108,8 @@ describe('#create', () => {
attributes: { attrOne: 'one', attrThree: 'three' },
});
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledWith(
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{ type: 'known-type', id: 'uuid-v4-id', namespace: 'some-namespace' },
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
);
@ -236,8 +237,8 @@ describe('#bulkCreate', () => {
],
});
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledWith(
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{ type: 'known-type', id: 'uuid-v4-id' },
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
);
@ -272,8 +273,8 @@ describe('#bulkCreate', () => {
],
});
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledWith(
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{ type: 'known-type', id: 'uuid-v4-id', namespace: 'some-namespace' },
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
);
@ -390,12 +391,12 @@ describe('#bulkUpdate', () => {
],
});
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledTimes(2);
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledWith(
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(2);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{ type: 'known-type', id: 'some-id' },
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
);
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledWith(
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{ type: 'known-type', id: 'some-id-2' },
{ attrOne: 'one 2', attrSecret: 'secret 2', attrThree: 'three 2' }
);
@ -459,8 +460,8 @@ describe('#bulkUpdate', () => {
],
});
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledWith(
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{ type: 'known-type', id: 'some-id', namespace: 'some-namespace' },
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
);
@ -822,8 +823,8 @@ describe('#update', () => {
attributes: { attrOne: 'one', attrThree: 'three' },
});
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledWith(
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{ type: 'known-type', id: 'some-id' },
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
);
@ -849,8 +850,8 @@ describe('#update', () => {
attributes: { attrOne: 'one', attrThree: 'three' },
});
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMock.encryptAttributes).toHaveBeenCalledWith(
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsServiceMockInstance.encryptAttributes).toHaveBeenCalledWith(
{ type: 'known-type', id: 'some-id', namespace: 'some-namespace' },
{ attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }
);

View file

@ -21,7 +21,7 @@ import {
SavedObjectsUpdateOptions,
SavedObjectsUpdateResponse,
} from 'src/core/server';
import { EncryptedSavedObjectsService } from './encrypted_saved_objects_service';
import { EncryptedSavedObjectsService } from '../crypto';
interface EncryptedSavedObjectsClientOptions {
baseClient: SavedObjectsClientContract;

View file

@ -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 {
IClusterClient,
SavedObject,
SavedObjectAttributes,
SavedObjectsBaseOptions,
} from 'src/core/server';
import { LegacyAPI } from '../plugin';
import { EncryptedSavedObjectsService } from '../crypto';
import { EncryptedSavedObjectsClientWrapper } from './encrypted_saved_objects_client_wrapper';
interface SetupSavedObjectsParams {
adminClusterClient: IClusterClient;
service: PublicMethodsOf<EncryptedSavedObjectsService>;
savedObjects: LegacyAPI['savedObjects'];
}
export interface SavedObjectsSetup {
getDecryptedAsInternalUser: <T extends SavedObjectAttributes = any>(
type: string,
id: string,
options?: SavedObjectsBaseOptions
) => Promise<SavedObject<T>>;
}
export function setupSavedObjects({
adminClusterClient,
service,
savedObjects,
}: SetupSavedObjectsParams): SavedObjectsSetup {
// Register custom saved object client that will encrypt, decrypt and strip saved object
// attributes where appropriate for any saved object repository request. We choose max possible
// priority for this wrapper to allow all other wrappers to set proper `namespace` for the Saved
// Object (e.g. wrapper registered by the Spaces plugin) before we encrypt attributes since
// `namespace` is included into AAD.
savedObjects.addScopedSavedObjectsClientWrapperFactory(
Number.MAX_SAFE_INTEGER,
'encryptedSavedObjects',
({ client: baseClient }) => new EncryptedSavedObjectsClientWrapper({ baseClient, service })
);
const internalRepository = savedObjects.getSavedObjectsRepository(
adminClusterClient.callAsInternalUser
);
return {
getDecryptedAsInternalUser: async <T extends SavedObjectAttributes = any>(
type: string,
id: string,
options?: SavedObjectsBaseOptions
): Promise<SavedObject<T>> => {
const savedObject = await internalRepository.get(type, id, options);
return {
...savedObject,
attributes: await service.decryptAttributes(
{ type, id, namespace: options && options.namespace },
savedObject.attributes
),
};
},
};
}

View file

@ -82,7 +82,7 @@ export default async function ({ readConfigFile }) {
'--xpack.reporting.csv.maxSizeBytes=2850', // small-ish limit for cutting off a 1999 byte report
'--stats.maximumWaitTimeForAllCollectorsInS=1',
'--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // server restarts should not invalidate active sessions
'--xpack.encrypted_saved_objects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"',
'--xpack.encryptedSavedObjects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"',
'--telemetry.banner=false',
'--timelion.ui.enabled=true',
],

View file

@ -7,6 +7,10 @@
import { Request } from 'hapi';
import { boomify, badRequest } from 'boom';
import { Legacy } from 'kibana';
import {
PluginSetupContract,
PluginStartContract,
} from '../../../../plugins/encrypted_saved_objects/server';
const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret';
@ -14,22 +18,24 @@ const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret';
export default function esoPlugin(kibana: any) {
return new kibana.Plugin({
id: 'eso',
require: ['encrypted_saved_objects'],
require: ['encryptedSavedObjects'],
uiExports: { mappings: require('./mappings.json') },
init(server: Legacy.Server) {
server.route({
method: 'GET',
path: '/api/saved_objects/get-decrypted-as-internal-user/{id}',
async handler(request: Request) {
const encryptedSavedObjectsStart = server.newPlatform.start.plugins
.encryptedSavedObjects as PluginStartContract;
const namespace = server.plugins.spaces && server.plugins.spaces.getSpaceId(request);
try {
return await (server.plugins as any).encrypted_saved_objects.getDecryptedAsInternalUser(
return await encryptedSavedObjectsStart.getDecryptedAsInternalUser(
SAVED_OBJECT_WITH_SECRET_TYPE,
request.params.id,
{ namespace: namespace === 'default' ? undefined : namespace }
);
} catch (err) {
if ((server.plugins as any).encrypted_saved_objects.isEncryptionError(err)) {
if (encryptedSavedObjectsStart.isEncryptionError(err)) {
return badRequest('Failed to encrypt attributes');
}
@ -38,7 +44,7 @@ export default function esoPlugin(kibana: any) {
},
});
(server.plugins as any).encrypted_saved_objects.registerType({
(server.newPlatform.setup.plugins.encryptedSavedObjects as PluginSetupContract).registerType({
type: SAVED_OBJECT_WITH_SECRET_TYPE,
attributesToEncrypt: new Set(['privateProperty']),
attributesToExcludeFromAAD: new Set(['publicPropertyExcludedFromAAD']),

View file

@ -7,7 +7,7 @@
import { FtrProviderContext } from '../../ftr_provider_context';
export default function({ loadTestFile }: FtrProviderContext) {
describe('encrypted_saved_objects', function encryptedSavedObjectsSuite() {
describe('encryptedSavedObjects', function encryptedSavedObjectsSuite() {
this.tags('ciGroup2');
loadTestFile(require.resolve('./encrypted_saved_objects_api'));
});

View file

@ -7,7 +7,6 @@
import 'hapi';
import { CloudPlugin } from '../../legacy/plugins/cloud';
import { EncryptedSavedObjectsPlugin } from '../../legacy/plugins/encrypted_saved_objects';
import { XPackMainPlugin } from '../../legacy/plugins/xpack_main/xpack_main';
import { SecurityPlugin } from '../../legacy/plugins/security';
import { ActionsPlugin, ActionsClient } from '../../legacy/plugins/actions';
@ -23,7 +22,6 @@ declare module 'hapi' {
cloud?: CloudPlugin;
xpack_main: XPackMainPlugin;
security?: SecurityPlugin;
encrypted_saved_objects?: EncryptedSavedObjectsPlugin;
actions?: ActionsPlugin;
alerting?: AlertingPlugin;
task_manager?: TaskManager;

View file

@ -7,7 +7,6 @@
import 'hapi';
import { CloudPlugin } from '../legacy/plugins/cloud';
import { EncryptedSavedObjectsPlugin } from '../legacy/plugins/encrypted_saved_objects';
import { XPackMainPlugin } from '../legacy/plugins/xpack_main/xpack_main';
import { SecurityPlugin } from '../legacy/plugins/security';
import { ActionsPlugin, ActionsClient } from '../legacy/plugins/actions';
@ -23,7 +22,6 @@ declare module 'hapi' {
cloud?: CloudPlugin;
xpack_main: XPackMainPlugin;
security?: SecurityPlugin;
encrypted_saved_objects?: EncryptedSavedObjectsPlugin;
actions?: ActionsPlugin;
alerting?: AlertingPlugin;
task_manager?: TaskManager;