Skip to content

Commit

Permalink
[Alerting] fixes to allow pre-configured actions to be executed (#63432)
Browse files Browse the repository at this point in the history
resolves #63162

Most of the support for pre-configured actions has already been added
to Kibana, except for one small piece.  The ability for them to be
executed.  This PR adds that support.
  • Loading branch information
pmuellr authored and wayneseymour committed Apr 15, 2020
1 parent 5b04fa9 commit 86fac6d
Show file tree
Hide file tree
Showing 12 changed files with 445 additions and 18 deletions.
67 changes: 67 additions & 0 deletions x-pack/plugins/actions/server/create_execute_function.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('execute()', () => {
actionTypeRegistry: actionTypeRegistryMock.create(),
getScopedSavedObjectsClient: jest.fn().mockReturnValueOnce(savedObjectsClient),
isESOUsingEphemeralEncryptionKey: false,
preconfiguredActions: [],
});
savedObjectsClient.get.mockResolvedValueOnce({
id: '123',
Expand Down Expand Up @@ -68,6 +69,68 @@ describe('execute()', () => {
});
});

test('schedules the action with all given parameters with a preconfigured action', async () => {
const executeFn = createExecuteFunction({
getBasePath,
taskManager: mockTaskManager,
actionTypeRegistry: actionTypeRegistryMock.create(),
getScopedSavedObjectsClient: jest.fn().mockReturnValueOnce(savedObjectsClient),
isESOUsingEphemeralEncryptionKey: false,
preconfiguredActions: [
{
id: '123',
actionTypeId: 'mock-action-preconfigured',
config: {},
isPreconfigured: true,
name: 'x',
secrets: {},
},
],
});
savedObjectsClient.get.mockResolvedValueOnce({
id: '123',
type: 'action',
attributes: {
actionTypeId: 'mock-action',
},
references: [],
});
savedObjectsClient.create.mockResolvedValueOnce({
id: '234',
type: 'action_task_params',
attributes: {},
references: [],
});
await executeFn({
id: '123',
params: { baz: false },
spaceId: 'default',
apiKey: Buffer.from('123:abc').toString('base64'),
});
expect(mockTaskManager.schedule).toHaveBeenCalledTimes(1);
expect(mockTaskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"params": Object {
"actionTaskParamsId": "234",
"spaceId": "default",
},
"scope": Array [
"actions",
],
"state": Object {},
"taskType": "actions:mock-action-preconfigured",
},
]
`);
expect(savedObjectsClient.get).not.toHaveBeenCalled();
expect(savedObjectsClient.create).toHaveBeenCalledWith('action_task_params', {
actionId: '123',
params: { baz: false },
apiKey: Buffer.from('123:abc').toString('base64'),
});
});

test('uses API key when provided', async () => {
const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient);
const executeFn = createExecuteFunction({
Expand All @@ -76,6 +139,7 @@ describe('execute()', () => {
getScopedSavedObjectsClient,
isESOUsingEphemeralEncryptionKey: false,
actionTypeRegistry: actionTypeRegistryMock.create(),
preconfiguredActions: [],
});
savedObjectsClient.get.mockResolvedValueOnce({
id: '123',
Expand Down Expand Up @@ -125,6 +189,7 @@ describe('execute()', () => {
getScopedSavedObjectsClient,
isESOUsingEphemeralEncryptionKey: false,
actionTypeRegistry: actionTypeRegistryMock.create(),
preconfiguredActions: [],
});
savedObjectsClient.get.mockResolvedValueOnce({
id: '123',
Expand Down Expand Up @@ -171,6 +236,7 @@ describe('execute()', () => {
getScopedSavedObjectsClient,
isESOUsingEphemeralEncryptionKey: true,
actionTypeRegistry: actionTypeRegistryMock.create(),
preconfiguredActions: [],
});
await expect(
executeFn({
Expand All @@ -193,6 +259,7 @@ describe('execute()', () => {
getScopedSavedObjectsClient,
isESOUsingEphemeralEncryptionKey: false,
actionTypeRegistry: mockedActionTypeRegistry,
preconfiguredActions: [],
});
mockedActionTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => {
throw new Error('Fail');
Expand Down
25 changes: 21 additions & 4 deletions x-pack/plugins/actions/server/create_execute_function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@

import { SavedObjectsClientContract } from '../../../../src/core/server';
import { TaskManagerStartContract } from '../../task_manager/server';
import { GetBasePathFunction, RawAction, ActionTypeRegistryContract } from './types';
import {
GetBasePathFunction,
RawAction,
ActionTypeRegistryContract,
PreConfiguredAction,
} from './types';

interface CreateExecuteFunctionOptions {
taskManager: TaskManagerStartContract;
getScopedSavedObjectsClient: (request: any) => SavedObjectsClientContract;
getBasePath: GetBasePathFunction;
isESOUsingEphemeralEncryptionKey: boolean;
actionTypeRegistry: ActionTypeRegistryContract;
preconfiguredActions: PreConfiguredAction[];
}

export interface ExecuteOptions {
Expand All @@ -29,6 +35,7 @@ export function createExecuteFunction({
actionTypeRegistry,
getScopedSavedObjectsClient,
isESOUsingEphemeralEncryptionKey,
preconfiguredActions,
}: CreateExecuteFunctionOptions) {
return async function execute({ id, params, spaceId, apiKey }: ExecuteOptions) {
if (isESOUsingEphemeralEncryptionKey === true) {
Expand Down Expand Up @@ -61,9 +68,9 @@ export function createExecuteFunction({
};

const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest);
const actionSavedObject = await savedObjectsClient.get<RawAction>('action', id);
const actionTypeId = await getActionTypeId(id);

actionTypeRegistry.ensureActionTypeEnabled(actionSavedObject.attributes.actionTypeId);
actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);

const actionTaskParamsRecord = await savedObjectsClient.create('action_task_params', {
actionId: id,
Expand All @@ -72,13 +79,23 @@ export function createExecuteFunction({
});

await taskManager.schedule({
taskType: `actions:${actionSavedObject.attributes.actionTypeId}`,
taskType: `actions:${actionTypeId}`,
params: {
spaceId,
actionTaskParamsId: actionTaskParamsRecord.id,
},
state: {},
scope: ['actions'],
});

async function getActionTypeId(actionId: string): Promise<string> {
const pcAction = preconfiguredActions.find(action => action.id === actionId);
if (pcAction) {
return pcAction.actionTypeId;
}

const actionSO = await savedObjectsClient.get<RawAction>('action', actionId);
return actionSO.attributes.actionTypeId;
}
};
}
2 changes: 2 additions & 0 deletions x-pack/plugins/actions/server/lib/action_executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ actionExecutor.initialize({
actionTypeRegistry,
encryptedSavedObjectsPlugin,
eventLogger: eventLoggerMock.create(),
preconfiguredActions: [],
});

beforeEach(() => {
Expand Down Expand Up @@ -232,6 +233,7 @@ test('throws an error when passing isESOUsingEphemeralEncryptionKey with value o
actionTypeRegistry,
encryptedSavedObjectsPlugin,
eventLogger: eventLoggerMock.create(),
preconfiguredActions: [],
});
await expect(
customActionExecutor.execute(executeParams)
Expand Down
72 changes: 58 additions & 14 deletions x-pack/plugins/actions/server/lib/action_executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
ActionTypeRegistryContract,
GetServicesFunction,
RawAction,
PreConfiguredAction,
Services,
} from '../types';
import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server';
import { SpacesServiceSetup } from '../../../spaces/server';
Expand All @@ -24,6 +26,7 @@ export interface ActionExecutorContext {
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart;
actionTypeRegistry: ActionTypeRegistryContract;
eventLogger: IEventLogger;
preconfiguredActions: PreConfiguredAction[];
}

export interface ExecuteOptions {
Expand Down Expand Up @@ -72,28 +75,22 @@ export class ActionExecutor {
encryptedSavedObjectsPlugin,
actionTypeRegistry,
eventLogger,
preconfiguredActions,
} = this.actionExecutorContext!;

const services = getServices(request);
const spaceId = spaces && spaces.getSpaceId(request);
const namespace = spaceId && spaceId !== 'default' ? { namespace: spaceId } : {};

// Ensure user can read the action before processing
const {
attributes: { actionTypeId, config, name },
} = await services.savedObjectsClient.get<RawAction>('action', actionId);

actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);

// Only get encrypted attributes here, the remaining attributes can be fetched in
// the savedObjectsClient call
const {
attributes: { secrets },
} = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser<RawAction>(
'action',
const { actionTypeId, name, config, secrets } = await getActionInfo(
services,
encryptedSavedObjectsPlugin,
preconfiguredActions,
actionId,
namespace
namespace.namespace
);

actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
const actionType = actionTypeRegistry.get(actionTypeId);

let validatedParams: Record<string, any>;
Expand Down Expand Up @@ -173,3 +170,50 @@ function actionErrorToMessage(result: ActionTypeExecutorResult): string {

return message;
}

interface ActionInfo {
actionTypeId: string;
name: string;
config: any;
secrets: any;
}

async function getActionInfo(
services: Services,
encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart,
preconfiguredActions: PreConfiguredAction[],
actionId: string,
namespace: string | undefined
): Promise<ActionInfo> {
// check to see if it's a pre-configured action first
const pcAction = preconfiguredActions.find(
preconfiguredAction => preconfiguredAction.id === actionId
);
if (pcAction) {
return {
actionTypeId: pcAction.actionTypeId,
name: pcAction.name,
config: pcAction.config,
secrets: pcAction.secrets,
};
}

// if not pre-configured action, should be a saved object
// ensure user can read the action before processing
const {
attributes: { actionTypeId, config, name },
} = await services.savedObjectsClient.get<RawAction>('action', actionId);

const {
attributes: { secrets },
} = await encryptedSavedObjectsPlugin.getDecryptedAsInternalUser<RawAction>('action', actionId, {
namespace: namespace === 'default' ? undefined : namespace,
});

return {
actionTypeId,
name,
config,
secrets,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const actionExecutorInitializerParams = {
actionTypeRegistry,
encryptedSavedObjectsPlugin: mockedEncryptedSavedObjectsPlugin,
eventLogger: eventLoggerMock.create(),
preconfiguredActions: [],
};
const taskRunnerFactoryInitializerParams = {
spaceIdToNamespace,
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/actions/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
getServices: this.getServicesFactory(core.savedObjects),
encryptedSavedObjectsPlugin: plugins.encryptedSavedObjects,
actionTypeRegistry: actionTypeRegistry!,
preconfiguredActions,
});

taskRunnerFactory!.initialize({
Expand All @@ -265,6 +266,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi
getScopedSavedObjectsClient: core.savedObjects.getScopedClient,
getBasePath: this.getBasePath,
isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!,
preconfiguredActions,
}),
isActionTypeEnabled: id => {
return this.actionTypeRegistry!.isActionTypeEnabled(id);
Expand Down
21 changes: 21 additions & 0 deletions x-pack/test/alerting_api_integration/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,27 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
xyzSecret2: 'credential2',
},
},
{
id: 'preconfigured-es-index-action',
actionTypeId: '.index',
name: 'preconfigured_es_index_action',
config: {
index: 'functional-test-actions-index-preconfigured',
refresh: true,
executionTimeField: 'timestamp',
},
},
{
id: 'preconfigured.test.index-record',
actionTypeId: 'test.index-record',
name: 'Test:_Preconfigured_Index_Record',
config: {
unencrypted: 'ignored-but-required',
},
secrets: {
encrypted: 'this-is-also-ignored-and-also-required',
},
},
])}`,
...disabledPlugins.map(key => `--xpack.${key}.enabled=false`),
`--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`,
Expand Down
Loading

0 comments on commit 86fac6d

Please sign in to comment.