mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Migrate Encrypted Saved Objects plugin to the new platform. (#49890)
This commit is contained in:
parent
d10b7a1efb
commit
203ef5577c
46 changed files with 649 additions and 410 deletions
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
`;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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
|
||||
),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
);
|
8
x-pack/plugins/encrypted_saved_objects/kibana.json
Normal file
8
x-pack/plugins/encrypted_saved_objects/kibana.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"id": "encryptedSavedObjects",
|
||||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["xpack", "encryptedSavedObjects"],
|
||||
"server": true,
|
||||
"ui": false
|
||||
}
|
|
@ -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',
|
|
@ -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
|
|
@ -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>;
|
||||
},
|
||||
};
|
|
@ -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';
|
70
x-pack/plugins/encrypted_saved_objects/server/config.test.ts
Normal file
70
x-pack/plugins/encrypted_saved_objects/server/config.test.ts
Normal 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",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
46
x-pack/plugins/encrypted_saved_objects/server/config.ts
Normal file
46
x-pack/plugins/encrypted_saved_objects/server/config.ts
Normal 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 };
|
||||
})
|
||||
);
|
||||
}
|
|
@ -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
|
||||
);
|
||||
|
|
@ -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.`
|
|
@ -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;
|
||||
},
|
||||
};
|
|
@ -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';
|
16
x-pack/plugins/encrypted_saved_objects/server/index.ts
Normal file
16
x-pack/plugins/encrypted_saved_objects/server/index.ts
Normal 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);
|
26
x-pack/plugins/encrypted_saved_objects/server/mocks.ts
Normal file
26
x-pack/plugins/encrypted_saved_objects/server/mocks.ts
Normal 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,
|
||||
};
|
38
x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts
Normal file
38
x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts
Normal 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],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
113
x-pack/plugins/encrypted_saved_objects/server/plugin.ts
Normal file
113
x-pack/plugins/encrypted_saved_objects/server/plugin.ts
Normal 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');
|
||||
}
|
||||
}
|
|
@ -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' }
|
||||
);
|
|
@ -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;
|
|
@ -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
|
||||
),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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',
|
||||
],
|
||||
|
|
|
@ -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']),
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
|
|
2
x-pack/test/typings/hapi.d.ts
vendored
2
x-pack/test/typings/hapi.d.ts
vendored
|
@ -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;
|
||||
|
|
2
x-pack/typings/hapi.d.ts
vendored
2
x-pack/typings/hapi.d.ts
vendored
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue