Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to get scoped call cluster from alerting and action executors #64432

Merged
merged 8 commits into from
Apr 27, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion x-pack/plugins/actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ This is the primary function for an action type. Whenever the action needs to ex
| actionId | The action saved object id that the action type is executing for. |
| config | The decrypted configuration given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. |
| params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. |
| services.callCluster(path, opts) | Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana.<br><br>**NOTE**: This currently authenticates as the Kibana internal user, but will change in a future PR. |
| services.callCluster(path, opts) | Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana but runs in the context of the user who is calling the action when security is enabled.|
| services.getScopedCallCluster | This function scopes an instance of CallCluster by returning a `callCluster(path, opts)` function that runs in the context of the user who is calling the action when security is enabled. This must only be called with instances of CallCluster provided by core.|
| services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.<br><br>The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). |
| services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ jest.mock('./lib/send_email', () => ({
}));

import { Logger } from '../../../../../src/core/server';
import { savedObjectsClientMock } from '../../../../../src/core/server/mocks';

import { ActionType, ActionTypeExecutorOptions } from '../types';
import { actionsConfigMock } from '../actions_config.mock';
import { validateConfig, validateSecrets, validateParams } from '../lib';
import { createActionTypeRegistry } from './index.test';
import { sendEmail } from './lib/send_email';
import { actionsMock } from '../mocks';
import {
ActionParamsType,
ActionTypeConfigType,
Expand All @@ -26,13 +26,8 @@ import {
const sendEmailMock = sendEmail as jest.Mock;

const ACTION_TYPE_ID = '.email';
const NO_OP_FN = () => {};

const services = {
log: NO_OP_FN,
callCluster: async (path: string, opts: unknown) => {},
savedObjectsClient: savedObjectsClientMock.create(),
};
const services = actionsMock.createServices();

let actionType: ActionType;
let mockedLogger: jest.Mocked<Logger>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,13 @@ jest.mock('./lib/send_email', () => ({

import { ActionType, ActionTypeExecutorOptions } from '../types';
import { validateConfig, validateParams } from '../lib';
import { savedObjectsClientMock } from '../../../../../src/core/server/mocks';
import { createActionTypeRegistry } from './index.test';
import { ActionParamsType, ActionTypeConfigType } from './es_index';
import { actionsMock } from '../mocks';

const ACTION_TYPE_ID = '.index';
const NO_OP_FN = () => {};

const services = {
log: NO_OP_FN,
callCluster: jest.fn(),
savedObjectsClient: savedObjectsClientMock.create(),
};
const services = actionsMock.createServices();

let actionType: ActionType;

Expand Down Expand Up @@ -196,9 +191,9 @@ describe('execute()', () => {
await actionType.executor(executorOptions);

const calls = services.callCluster.mock.calls;
const timeValue = calls[0][1].body[1].field_to_use_for_time;
const timeValue = calls[0][1]?.body[1].field_to_use_for_time;
expect(timeValue).toBeInstanceOf(Date);
delete calls[0][1].body[1].field_to_use_for_time;
delete calls[0][1]?.body[1].field_to_use_for_time;
expect(calls).toMatchInlineSnapshot(`
Array [
Array [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ async function executor(
bulkBody.push(document);
}

const bulkParams: unknown = {
const bulkParams = {
index,
body: bulkBody,
refresh: config.refresh,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,17 @@ jest.mock('./lib/post_pagerduty', () => ({
import { getActionType } from './pagerduty';
import { ActionType, Services, ActionTypeExecutorOptions } from '../types';
import { validateConfig, validateSecrets, validateParams } from '../lib';
import { savedObjectsClientMock } from '../../../../../src/core/server/mocks';
import { postPagerduty } from './lib/post_pagerduty';
import { createActionTypeRegistry } from './index.test';
import { Logger } from '../../../../../src/core/server';
import { actionsConfigMock } from '../actions_config.mock';
import { actionsMock } from '../mocks';

const postPagerdutyMock = postPagerduty as jest.Mock;

const ACTION_TYPE_ID = '.pagerduty';

const services: Services = {
callCluster: async (path: string, opts: unknown) => {},
savedObjectsClient: savedObjectsClientMock.create(),
};
const services: Services = actionsMock.createServices();

let actionType: ActionType;
let mockedLogger: jest.Mocked<Logger>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import { ActionType } from '../types';
import { validateParams } from '../lib';
import { Logger } from '../../../../../src/core/server';
import { savedObjectsClientMock } from '../../../../../src/core/server/mocks';
import { createActionTypeRegistry } from './index.test';
import { actionsMock } from '../mocks';

const ACTION_TYPE_ID = '.server-log';

Expand Down Expand Up @@ -90,10 +90,7 @@ describe('execute()', () => {
const actionId = 'some-id';
await actionType.executor({
actionId,
services: {
callCluster: async (path: string, opts: unknown) => {},
savedObjectsClient: savedObjectsClientMock.create(),
},
services: actionsMock.createServices(),
params: { message: 'message text here', level: 'info' },
config: {},
secrets: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import { getActionType } from '.';
import { ActionType, Services, ActionTypeExecutorOptions } from '../../types';
import { validateConfig, validateSecrets, validateParams } from '../../lib';
import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks';
import { createActionTypeRegistry } from '../index.test';
import { actionsConfigMock } from '../../actions_config.mock';
import { actionsMock } from '../../mocks';

import { ACTION_TYPE_ID } from './constants';
import * as i18n from './translations';
Expand All @@ -21,10 +21,7 @@ jest.mock('./action_handlers');

const handleIncidentMock = handleIncident as jest.Mock;

const services: Services = {
callCluster: async (path: string, opts: unknown) => {},
savedObjectsClient: savedObjectsClientMock.create(),
};
const services: Services = actionsMock.createServices();

let actionType: ActionType;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,14 @@ import {
ActionTypeExecutorOptions,
ActionTypeExecutorResult,
} from '../types';
import { savedObjectsClientMock } from '../../../../../src/core/server/mocks';
import { validateParams, validateSecrets } from '../lib';
import { getActionType } from './slack';
import { actionsConfigMock } from '../actions_config.mock';
import { actionsMock } from '../mocks';

const ACTION_TYPE_ID = '.slack';

const services: Services = {
callCluster: async (path: string, opts: unknown) => {},
savedObjectsClient: savedObjectsClientMock.create(),
};
const services: Services = actionsMock.createServices();

let actionType: ActionType;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,17 @@ jest.mock('axios', () => ({
import { getActionType } from './webhook';
import { ActionType, Services } from '../types';
import { validateConfig, validateSecrets, validateParams } from '../lib';
import { savedObjectsClientMock } from '../../../../../src/core/server/mocks';
import { actionsConfigMock } from '../actions_config.mock';
import { createActionTypeRegistry } from './index.test';
import { Logger } from '../../../../../src/core/server';
import { actionsMock } from '../mocks';
import axios from 'axios';

const axiosRequestMock = axios.request as jest.Mock;

const ACTION_TYPE_ID = '.webhook';

const services: Services = {
callCluster: async (path: string, opts: unknown) => {},
savedObjectsClient: savedObjectsClientMock.create(),
};
const services: Services = actionsMock.createServices();

let actionType: ActionType;
let mockedLogger: jest.Mocked<Logger>;
Expand Down
18 changes: 6 additions & 12 deletions x-pack/plugins/actions/server/lib/action_executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,15 @@ 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/mocks';
import { savedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks';
import { loggingServiceMock } from '../../../../../src/core/server/mocks';
import { eventLoggerMock } from '../../../event_log/server/mocks';
import { spacesServiceMock } from '../../../spaces/server/spaces_service/spaces_service.mock';
import { ActionType } from '../types';
import { actionsMock } from '../mocks';

const actionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false });
const savedObjectsClient = savedObjectsClientMock.create();

function getServices() {
return {
savedObjectsClient,
log: jest.fn(),
callCluster: jest.fn(),
};
}
const services = actionsMock.createServices();
const savedObjectsClient = services.savedObjectsClient;
const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.createStart();
const actionTypeRegistry = actionTypeRegistryMock.create();

Expand All @@ -39,7 +33,7 @@ const spacesMock = spacesServiceMock.createSetupContract();
actionExecutor.initialize({
logger: loggingServiceMock.create().get(),
spaces: spacesMock,
getServices,
getServices: () => services,
actionTypeRegistry,
encryptedSavedObjectsPlugin,
eventLogger: eventLoggerMock.create(),
Expand Down Expand Up @@ -229,7 +223,7 @@ test('throws an error when passing isESOUsingEphemeralEncryptionKey with value o
customActionExecutor.initialize({
logger: loggingServiceMock.create().get(),
spaces: spacesMock,
getServices,
getServices: () => services,
actionTypeRegistry,
encryptedSavedObjectsPlugin,
eventLogger: eventLoggerMock.create(),
Expand Down
17 changes: 17 additions & 0 deletions x-pack/plugins/actions/server/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

import { actionsClientMock } from './actions_client.mock';
import { PluginSetupContract, PluginStartContract } from './plugin';
import { Services } from './types';
import {
elasticsearchServiceMock,
savedObjectsClientMock,
} from '../../../../src/core/server/mocks';

export { actionsClientMock };

Expand All @@ -26,7 +31,19 @@ const createStartMock = () => {
return mock;
};

const createServicesMock = () => {
const mock: jest.Mocked<Services & {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this just be jest.Mocked< AlertServices> ? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The jest.Mocked<...> doesn't apply recursively. This was necessary to allow services.savedObjectsClient.get.mockResolvedValue(...).

savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>;
}> = {
callCluster: elasticsearchServiceMock.createScopedClusterClient().callAsCurrentUser,
getScopedCallCluster: jest.fn(),
savedObjectsClient: savedObjectsClientMock.create(),
};
return mock;
};

Comment on lines +35 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good clean up 👍

export const actionsMock = {
createServices: createServicesMock,
createSetup: createSetupMock,
createStart: createStartMock,
};
4 changes: 4 additions & 0 deletions x-pack/plugins/actions/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
IContextProvider,
SavedObjectsServiceStart,
ElasticsearchServiceStart,
IClusterClient,
} from '../../../../src/core/server';

import {
Expand Down Expand Up @@ -297,6 +298,9 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
return request => ({
callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser,
savedObjectsClient: savedObjects.getScopedClient(request),
getScopedCallCluster(clusterClient: IClusterClient) {
return clusterClient.asScoped(request).callAsCurrentUser;
},
});
}

Expand Down
15 changes: 9 additions & 6 deletions x-pack/plugins/actions/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/

import {
SavedObjectsClientContract,
SavedObjectAttributes,
KibanaRequest,
} from '../../../../src/core/server';
import { ActionTypeRegistry } from './action_type_registry';
import { PluginSetupContract, PluginStartContract } from './plugin';
import { ActionsClient } from './actions_client';
import { LicenseType } from '../../licensing/common/types';
import {
IClusterClient,
IScopedClusterClient,
KibanaRequest,
SavedObjectsClientContract,
SavedObjectAttributes,
} from '../../../../src/core/server';

export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
export type GetServicesFunction = (request: KibanaRequest) => Services;
Expand All @@ -21,8 +23,9 @@ export type GetBasePathFunction = (spaceId?: string) => string;
export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined;

export interface Services {
callCluster(path: string, opts: unknown): Promise<unknown>;
callCluster: IScopedClusterClient['callAsCurrentUser'];
savedObjectsClient: SavedObjectsClientContract;
getScopedCallCluster(clusterClient: IClusterClient): IScopedClusterClient['callAsCurrentUser'];
}

declare module 'src/core/server' {
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/alerting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ This is the primary function for an alert type. Whenever the alert needs to exec
|---|---|
|services.callCluster(path, opts)|Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana but in the context of the user who created the alert when security is enabled.|
|services.savedObjectsClient|This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.<br><br>The scope of the saved objects client is tied to the user who created the alert (only when security isenabled).|
|services.getScopedCallCluster|This function scopes an instance of CallCluster by returning a `callCluster(path, opts)` function that runs in the context of the user who created the alert when security is enabled. This must only be called with instances of CallCluster provided by core.|
|services.log(tags, [data], [timestamp])|Use this to create server logs. (This is the same function as server.log)|
|startedAt|The date and time the alert type started execution.|
|previousStartedAt|The previous date and time the alert type started a successful execution.|
Expand Down
8 changes: 6 additions & 2 deletions x-pack/plugins/alerting/server/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@

import { alertsClientMock } from './alerts_client.mock';
import { PluginSetupContract, PluginStartContract } from './plugin';
import { savedObjectsClientMock } from '../../../../src/core/server/mocks';
import { AlertInstance } from './alert_instance';
import {
elasticsearchServiceMock,
savedObjectsClientMock,
} from '../../../../src/core/server/mocks';

export { alertsClientMock };

Expand Down Expand Up @@ -55,7 +58,8 @@ const createAlertServicesMock = () => {
alertInstanceFactory: jest
.fn<jest.Mocked<AlertInstance>, [string]>()
.mockReturnValue(alertInstanceFactoryMock),
callCluster: jest.fn(),
callCluster: elasticsearchServiceMock.createScopedClusterClient().callAsCurrentUser,
getScopedCallCluster: jest.fn(),
savedObjectsClient: savedObjectsClientMock.create(),
};
};
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/alerting/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
RequestHandler,
SharedGlobalConfig,
ElasticsearchServiceStart,
IClusterClient,
} from '../../../../src/core/server';

import {
Expand Down Expand Up @@ -270,6 +271,9 @@ export class AlertingPlugin {
return request => ({
callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser,
savedObjectsClient: savedObjects.getScopedClient(request),
getScopedCallCluster(clusterClient: IClusterClient) {
return clusterClient.asScoped(request).callAsCurrentUser;
},
});
}

Expand Down
11 changes: 4 additions & 7 deletions x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import { ConcreteTaskInstance, TaskStatus } from '../../../../plugins/task_manag
import { TaskRunnerContext } from './task_runner_factory';
import { TaskRunner } from './task_runner';
import { encryptedSavedObjectsMock } from '../../../../plugins/encrypted_saved_objects/server/mocks';
import { savedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks';
import { loggingServiceMock } from '../../../../../src/core/server/mocks';
import { PluginStartContract as ActionsPluginStart } from '../../../actions/server';
import { actionsMock } from '../../../actions/server/mocks';
import { alertsMock } from '../mocks';
import { eventLoggerMock } from '../../../event_log/server/event_logger.mock';
import { IEventLogger } from '../../../event_log/server';
import { SavedObjectsErrorHelpers } from '../../../../../src/core/server';
Expand Down Expand Up @@ -52,13 +53,9 @@ describe('Task Runner', () => {

afterAll(() => fakeTimer.restore());

const savedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.createStart();
const services = {
log: jest.fn(),
callCluster: jest.fn(),
savedObjectsClient,
};
const services = alertsMock.createAlertServices();
const savedObjectsClient = services.savedObjectsClient;

const taskRunnerFactoryInitializerParams: jest.Mocked<TaskRunnerContext> & {
actionsPlugin: jest.Mocked<ActionsPluginStart>;
Expand Down
Loading