[Actions] Use references in action_task_params saved object (#108964) (#110056)

* Extracting saved object references before saving action_task_params saved object

* Injecting saved object ids from references when reading action_task_param

* Adding migration

* Adding unit test for migrations

* Not differentiating between preconfigured or not

* Adding functional test for migration

* Skip extracting action id if action is preconfigured

* Only migrating action task params for non preconfigured connectors

* Simplifying related saved objects

* Fixing functional test

* Fixing migration

* Javascript is sometimes magical

* Updating functional test

* PR feedback

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: ymao1 <ying.mao@elastic.co>
This commit is contained in:
Kibana Machine 2021-08-25 11:51:45 -04:00 committed by GitHub
parent 66ed66ae8b
commit a0fd8e2089
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 4154 additions and 41 deletions

View file

@ -76,7 +76,15 @@ describe('execute()', () => {
params: { baz: false },
apiKey: Buffer.from('123:abc').toString('base64'),
},
{}
{
references: [
{
id: '123',
name: 'actionRef',
type: 'action',
},
],
}
);
expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith('123', 'mock-action', {
notifyUsage: true,
@ -128,14 +136,27 @@ describe('execute()', () => {
apiKey: Buffer.from('123:abc').toString('base64'),
relatedSavedObjects: [
{
id: 'some-id',
id: 'related_some-type_0',
namespace: 'some-namespace',
type: 'some-type',
typeId: 'some-typeId',
},
],
},
{}
{
references: [
{
id: '123',
name: 'actionRef',
type: 'action',
},
{
id: 'some-id',
name: 'related_some-type_0',
type: 'some-type',
},
],
}
);
});
@ -214,6 +235,102 @@ describe('execute()', () => {
);
});
test('schedules the action with all given parameters with a preconfigured action and relatedSavedObjects', async () => {
const executeFn = createExecutionEnqueuerFunction({
taskManager: mockTaskManager,
actionTypeRegistry: actionTypeRegistryMock.create(),
isESOCanEncrypt: true,
preconfiguredActions: [
{
id: '123',
actionTypeId: 'mock-action-preconfigured',
config: {},
isPreconfigured: true,
name: 'x',
secrets: {},
},
],
});
const source = { type: 'alert', id: uuid.v4() };
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(savedObjectsClient, {
id: '123',
params: { baz: false },
spaceId: 'default',
apiKey: Buffer.from('123:abc').toString('base64'),
source: asSavedObjectExecutionSource(source),
relatedSavedObjects: [
{
id: 'some-id',
namespace: 'some-namespace',
type: 'some-type',
typeId: 'some-typeId',
},
],
});
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'),
relatedSavedObjects: [
{
id: 'related_some-type_0',
namespace: 'some-namespace',
type: 'some-type',
typeId: 'some-typeId',
},
],
},
{
references: [
{
id: source.id,
name: 'source',
type: source.type,
},
{
id: 'some-id',
name: 'related_some-type_0',
type: 'some-type',
},
],
}
);
});
test('throws when passing isESOCanEncrypt with false as a value', async () => {
const executeFn = createExecutionEnqueuerFunction({
taskManager: mockTaskManager,

View file

@ -15,7 +15,7 @@ import {
} from './types';
import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './constants/saved_objects';
import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor';
import { isSavedObjectExecutionSource } from './lib';
import { extractSavedObjectReferences, isSavedObjectExecutionSource } from './lib';
import { RelatedSavedObjects } from './lib/related_saved_objects';
interface CreateExecuteFunctionOptions {
@ -53,7 +53,11 @@ export function createExecutionEnqueuerFunction({
);
}
const action = await getAction(unsecuredSavedObjectsClient, preconfiguredActions, id);
const { action, isPreconfigured } = await getAction(
unsecuredSavedObjectsClient,
preconfiguredActions,
id
);
validateCanActionBeUsed(action);
const { actionTypeId } = action;
@ -61,15 +65,33 @@ export function createExecutionEnqueuerFunction({
actionTypeRegistry.ensureActionTypeEnabled(actionTypeId);
}
// Get saved object references from action ID and relatedSavedObjects
const { references, relatedSavedObjectWithRefs } = extractSavedObjectReferences(
id,
isPreconfigured,
relatedSavedObjects
);
const executionSourceReference = executionSourceAsSavedObjectReferences(source);
const taskReferences = [];
if (executionSourceReference.references) {
taskReferences.push(...executionSourceReference.references);
}
if (references) {
taskReferences.push(...references);
}
const actionTaskParamsRecord = await unsecuredSavedObjectsClient.create(
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
{
actionId: id,
params,
apiKey,
relatedSavedObjects,
relatedSavedObjects: relatedSavedObjectWithRefs,
},
executionSourceAsSavedObjectReferences(source)
{
references: taskReferences,
}
);
await taskManager.schedule({
@ -93,7 +115,7 @@ export function createEphemeralExecutionEnqueuerFunction({
unsecuredSavedObjectsClient: SavedObjectsClientContract,
{ id, params, spaceId, source, apiKey }: ExecuteOptions
): Promise<RunNowResult> {
const action = await getAction(unsecuredSavedObjectsClient, preconfiguredActions, id);
const { action } = await getAction(unsecuredSavedObjectsClient, preconfiguredActions, id);
validateCanActionBeUsed(action);
const { actionTypeId } = action;
@ -148,12 +170,12 @@ async function getAction(
unsecuredSavedObjectsClient: SavedObjectsClientContract,
preconfiguredActions: PreConfiguredAction[],
actionId: string
): Promise<PreConfiguredAction | RawAction> {
): Promise<{ action: PreConfiguredAction | RawAction; isPreconfigured: boolean }> {
const pcAction = preconfiguredActions.find((action) => action.id === actionId);
if (pcAction) {
return pcAction;
return { action: pcAction, isPreconfigured: true };
}
const { attributes } = await unsecuredSavedObjectsClient.get<RawAction>('action', actionId);
return attributes;
return { action: attributes, isPreconfigured: false };
}

View file

@ -0,0 +1,402 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
extractSavedObjectReferences,
injectSavedObjectReferences,
} from './action_task_params_utils';
describe('extractSavedObjectReferences()', () => {
test('correctly extracts action id into references array', () => {
expect(extractSavedObjectReferences('my-action-id', false)).toEqual({
references: [
{
id: 'my-action-id',
name: 'actionRef',
type: 'action',
},
],
});
});
test('correctly extracts related saved object into references array', () => {
const relatedSavedObjects = [
{
id: 'abc',
type: 'alert',
typeId: 'ruleTypeA',
},
{
id: 'def',
type: 'action',
typeId: 'connectorTypeB',
},
{
id: 'xyz',
type: 'alert',
typeId: 'ruleTypeB',
namespace: 'custom',
},
];
expect(extractSavedObjectReferences('my-action-id', false, relatedSavedObjects)).toEqual({
references: [
{
id: 'my-action-id',
name: 'actionRef',
type: 'action',
},
{
id: 'abc',
name: 'related_alert_0',
type: 'alert',
},
{
id: 'def',
name: 'related_action_1',
type: 'action',
},
{
id: 'xyz',
name: 'related_alert_2',
type: 'alert',
},
],
relatedSavedObjectWithRefs: [
{
id: 'related_alert_0',
type: 'alert',
typeId: 'ruleTypeA',
},
{
id: 'related_action_1',
type: 'action',
typeId: 'connectorTypeB',
},
{
id: 'related_alert_2',
type: 'alert',
typeId: 'ruleTypeB',
namespace: 'custom',
},
],
});
});
test('correctly skips extracting action id if action is preconfigured', () => {
expect(extractSavedObjectReferences('my-action-id', true)).toEqual({
references: [],
});
});
test('correctly extracts related saved object into references array if isPreconfigured is true', () => {
const relatedSavedObjects = [
{
id: 'abc',
type: 'alert',
typeId: 'ruleTypeA',
},
{
id: 'def',
type: 'action',
typeId: 'connectorTypeB',
},
{
id: 'xyz',
type: 'alert',
typeId: 'ruleTypeB',
namespace: 'custom',
},
];
expect(extractSavedObjectReferences('my-action-id', true, relatedSavedObjects)).toEqual({
references: [
{
id: 'abc',
name: 'related_alert_0',
type: 'alert',
},
{
id: 'def',
name: 'related_action_1',
type: 'action',
},
{
id: 'xyz',
name: 'related_alert_2',
type: 'alert',
},
],
relatedSavedObjectWithRefs: [
{
id: 'related_alert_0',
type: 'alert',
typeId: 'ruleTypeA',
},
{
id: 'related_action_1',
type: 'action',
typeId: 'connectorTypeB',
},
{
id: 'related_alert_2',
type: 'alert',
typeId: 'ruleTypeB',
namespace: 'custom',
},
],
});
});
});
describe('injectSavedObjectReferences()', () => {
test('correctly returns action id from references array', () => {
expect(
injectSavedObjectReferences([
{
id: 'my-action-id',
name: 'actionRef',
type: 'action',
},
])
).toEqual({ actionId: 'my-action-id' });
});
test('correctly returns undefined if no action id in references array', () => {
expect(injectSavedObjectReferences([])).toEqual({});
});
test('correctly injects related saved object ids from references array', () => {
expect(
injectSavedObjectReferences(
[
{
id: 'my-action-id',
name: 'actionRef',
type: 'action',
},
{
id: 'abc',
name: 'related_alert_0',
type: 'alert',
},
{
id: 'def',
name: 'related_action_1',
type: 'action',
},
{
id: 'xyz',
name: 'related_alert_2',
type: 'alert',
},
],
[
{
id: 'related_alert_0',
type: 'alert',
typeId: 'ruleTypeA',
},
{
id: 'related_action_1',
type: 'action',
typeId: 'connectorTypeB',
},
{
id: 'related_alert_2',
type: 'alert',
typeId: 'ruleTypeB',
namespace: 'custom',
},
]
)
).toEqual({
actionId: 'my-action-id',
relatedSavedObjects: [
{
id: 'abc',
type: 'alert',
typeId: 'ruleTypeA',
},
{
id: 'def',
type: 'action',
typeId: 'connectorTypeB',
},
{
id: 'xyz',
type: 'alert',
typeId: 'ruleTypeB',
namespace: 'custom',
},
],
});
});
test('correctly injects related saved object ids from references array if no actionRef', () => {
expect(
injectSavedObjectReferences(
[
{
id: 'abc',
name: 'related_alert_0',
type: 'alert',
},
{
id: 'def',
name: 'related_action_1',
type: 'action',
},
{
id: 'xyz',
name: 'related_alert_2',
type: 'alert',
},
],
[
{
id: 'related_alert_0',
type: 'alert',
typeId: 'ruleTypeA',
},
{
id: 'related_action_1',
type: 'action',
typeId: 'connectorTypeB',
},
{
id: 'related_alert_2',
type: 'alert',
typeId: 'ruleTypeB',
namespace: 'custom',
},
]
)
).toEqual({
relatedSavedObjects: [
{
id: 'abc',
type: 'alert',
typeId: 'ruleTypeA',
},
{
id: 'def',
type: 'action',
typeId: 'connectorTypeB',
},
{
id: 'xyz',
type: 'alert',
typeId: 'ruleTypeB',
namespace: 'custom',
},
],
});
});
test('correctly keeps related saved object ids if references array is empty', () => {
expect(
injectSavedObjectReferences(
[],
[
{
id: 'abc',
type: 'alert',
typeId: 'ruleTypeA',
},
{
id: 'def',
type: 'action',
typeId: 'connectorTypeB',
},
{
id: 'xyz',
type: 'alert',
typeId: 'ruleTypeB',
namespace: 'custom',
},
]
)
).toEqual({
relatedSavedObjects: [
{
id: 'abc',
type: 'alert',
typeId: 'ruleTypeA',
},
{
id: 'def',
type: 'action',
typeId: 'connectorTypeB',
},
{
id: 'xyz',
type: 'alert',
typeId: 'ruleTypeB',
namespace: 'custom',
},
],
});
});
test('correctly skips injecting missing related saved object ids in references array', () => {
expect(
injectSavedObjectReferences(
[
{
id: 'my-action-id',
name: 'actionRef',
type: 'action',
},
{
id: 'abc',
name: 'related_alert_0',
type: 'alert',
},
],
[
{
id: 'related_alert_0',
type: 'alert',
typeId: 'ruleTypeA',
},
{
id: 'def',
type: 'action',
typeId: 'connectorTypeB',
},
{
id: 'xyz',
type: 'alert',
typeId: 'ruleTypeB',
namespace: 'custom',
},
]
)
).toEqual({
actionId: 'my-action-id',
relatedSavedObjects: [
{
id: 'abc',
type: 'alert',
typeId: 'ruleTypeA',
},
{
id: 'def',
type: 'action',
typeId: 'connectorTypeB',
},
{
id: 'xyz',
type: 'alert',
typeId: 'ruleTypeB',
namespace: 'custom',
},
],
});
});
});

View file

@ -0,0 +1,80 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObjectAttribute, SavedObjectReference } from 'src/core/server/types';
import { RelatedSavedObjects } from './related_saved_objects';
export const ACTION_REF_NAME = `actionRef`;
export function extractSavedObjectReferences(
actionId: string,
isPreconfigured: boolean,
relatedSavedObjects?: RelatedSavedObjects
): {
references: SavedObjectReference[];
relatedSavedObjectWithRefs?: RelatedSavedObjects;
} {
const references: SavedObjectReference[] = [];
const relatedSavedObjectWithRefs: RelatedSavedObjects = [];
// Add action saved object to reference if it is not preconfigured
if (!isPreconfigured) {
references.push({
id: actionId,
name: ACTION_REF_NAME,
type: 'action',
});
}
// Add related saved objects, if any
(relatedSavedObjects ?? []).forEach((relatedSavedObject, index) => {
relatedSavedObjectWithRefs.push({
...relatedSavedObject,
id: `related_${relatedSavedObject.type}_${index}`,
});
references.push({
id: relatedSavedObject.id,
name: `related_${relatedSavedObject.type}_${index}`,
type: relatedSavedObject.type,
});
});
return {
references,
...(relatedSavedObjects ? { relatedSavedObjectWithRefs } : {}),
};
}
export function injectSavedObjectReferences(
references: SavedObjectReference[],
relatedSavedObjects?: RelatedSavedObjects
): { actionId?: string; relatedSavedObjects?: SavedObjectAttribute } {
references = references ?? [];
// Look for for the action id
const action = references.find((ref) => ref.name === ACTION_REF_NAME);
const injectedRelatedSavedObjects = (relatedSavedObjects ?? []).flatMap((relatedSavedObject) => {
const reference = references.find((ref) => ref.name === relatedSavedObject.id);
// relatedSavedObjects are used only in the event log document that is written during
// action execution. Because they are not critical to the actual execution of the action
// we will not throw an error if no reference is found matching this related saved object
return reference ? [{ ...relatedSavedObject, id: reference.id }] : [relatedSavedObject];
});
const result: { actionId?: string; relatedSavedObjects?: SavedObjectAttribute } = {};
if (action) {
result.actionId = action.id;
}
if (relatedSavedObjects) {
result.relatedSavedObjects = injectedRelatedSavedObjects;
}
return result;
}

View file

@ -13,6 +13,10 @@ export { ILicenseState, LicenseState } from './license_state';
export { verifyApiAccess } from './verify_api_access';
export { getActionTypeFeatureUsageName } from './get_action_type_feature_usage_name';
export { spaceIdToNamespace } from './space_id_to_namespace';
export {
extractSavedObjectReferences,
injectSavedObjectReferences,
} from './action_task_params_utils';
export {
ActionTypeDisabledError,
ActionTypeDisabledReason,

View file

@ -97,7 +97,7 @@ test(`throws an error if factory is already initialized`, () => {
).toThrowErrorMatchingInlineSnapshot(`"TaskRunnerFactory already initialized"`);
});
test('executes the task by calling the executor with proper parameters', async () => {
test('executes the task by calling the executor with proper parameters, using given actionId when no actionRef in references', async () => {
const taskRunner = taskRunnerFactory.create({
taskInstance: mockedTaskInstance,
});
@ -146,6 +146,61 @@ test('executes the task by calling the executor with proper parameters', async (
);
});
test('executes the task by calling the executor with proper parameters, using stored actionId when actionRef is in references', async () => {
const taskRunner = taskRunnerFactory.create({
taskInstance: mockedTaskInstance,
});
mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok', actionId: '2' });
spaceIdToNamespace.mockReturnValueOnce('namespace-test');
mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '3',
type: 'action_task_params',
attributes: {
actionId: '2',
params: { baz: true },
apiKey: Buffer.from('123:abc').toString('base64'),
},
references: [
{
id: '9',
name: 'actionRef',
type: 'action',
},
],
});
const runnerResult = await taskRunner.run();
expect(runnerResult).toBeUndefined();
expect(spaceIdToNamespace).toHaveBeenCalledWith('test');
expect(
mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser
).toHaveBeenCalledWith('action_task_params', '3', { namespace: 'namespace-test' });
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
actionId: '9',
isEphemeral: false,
params: { baz: true },
relatedSavedObjects: [],
request: expect.objectContaining({
headers: {
// base64 encoded "123:abc"
authorization: 'ApiKey MTIzOmFiYw==',
},
}),
taskInfo: {
scheduled: new Date(),
},
});
const [executeParams] = mockedActionExecutor.execute.mock.calls[0];
expect(taskRunnerFactoryInitializerParams.basePathService.set).toHaveBeenCalledWith(
executeParams.request,
'/s/test'
);
});
test('cleans up action_task_params object', async () => {
const taskRunner = taskRunnerFactory.create({
taskInstance: mockedTaskInstance,
@ -161,7 +216,13 @@ test('cleans up action_task_params object', async () => {
params: { baz: true },
apiKey: Buffer.from('123:abc').toString('base64'),
},
references: [],
references: [
{
id: '2',
name: 'actionRef',
type: 'action',
},
],
});
await taskRunner.run();
@ -184,7 +245,13 @@ test('runs successfully when cleanup fails and logs the error', async () => {
params: { baz: true },
apiKey: Buffer.from('123:abc').toString('base64'),
},
references: [],
references: [
{
id: '2',
name: 'actionRef',
type: 'action',
},
],
});
services.savedObjectsClient.delete.mockRejectedValueOnce(new Error('Fail'));
@ -209,7 +276,13 @@ test('throws an error with suggested retry logic when return status is error', a
params: { baz: true },
apiKey: Buffer.from('123:abc').toString('base64'),
},
references: [],
references: [
{
id: '2',
name: 'actionRef',
type: 'action',
},
],
});
mockedActionExecutor.execute.mockResolvedValueOnce({
status: 'error',
@ -244,7 +317,13 @@ test('uses API key when provided', async () => {
params: { baz: true },
apiKey: Buffer.from('123:abc').toString('base64'),
},
references: [],
references: [
{
id: '2',
name: 'actionRef',
type: 'action',
},
],
});
await taskRunner.run();
@ -272,7 +351,7 @@ test('uses API key when provided', async () => {
);
});
test('uses relatedSavedObjects when provided', async () => {
test('uses relatedSavedObjects merged with references when provided', async () => {
const taskRunner = taskRunnerFactory.create({
taskInstance: mockedTaskInstance,
});
@ -286,9 +365,20 @@ test('uses relatedSavedObjects when provided', async () => {
actionId: '2',
params: { baz: true },
apiKey: Buffer.from('123:abc').toString('base64'),
relatedSavedObjects: [{ id: 'some-id', type: 'some-type' }],
relatedSavedObjects: [{ id: 'related_some-type_0', type: 'some-type' }],
},
references: [],
references: [
{
id: '2',
name: 'actionRef',
type: 'action',
},
{
id: 'some-id',
name: 'related_some-type_0',
type: 'some-type',
},
],
});
await taskRunner.run();
@ -315,6 +405,56 @@ test('uses relatedSavedObjects when provided', async () => {
});
});
test('uses relatedSavedObjects as is when references are empty', async () => {
const taskRunner = taskRunnerFactory.create({
taskInstance: mockedTaskInstance,
});
mockedActionExecutor.execute.mockResolvedValueOnce({ status: 'ok', actionId: '2' });
spaceIdToNamespace.mockReturnValueOnce('namespace-test');
mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
id: '3',
type: 'action_task_params',
attributes: {
actionId: '2',
params: { baz: true },
apiKey: Buffer.from('123:abc').toString('base64'),
relatedSavedObjects: [{ id: 'abc', type: 'some-type', namespace: 'yo' }],
},
references: [
{
id: '2',
name: 'actionRef',
type: 'action',
},
],
});
await taskRunner.run();
expect(mockedActionExecutor.execute).toHaveBeenCalledWith({
actionId: '2',
isEphemeral: false,
params: { baz: true },
relatedSavedObjects: [
{
id: 'abc',
type: 'some-type',
namespace: 'yo',
},
],
request: expect.objectContaining({
headers: {
// base64 encoded "123:abc"
authorization: 'ApiKey MTIzOmFiYw==',
},
}),
taskInfo: {
scheduled: new Date(),
},
});
});
test('sanitizes invalid relatedSavedObjects when provided', async () => {
const taskRunner = taskRunnerFactory.create({
taskInstance: mockedTaskInstance,
@ -329,9 +469,20 @@ test('sanitizes invalid relatedSavedObjects when provided', async () => {
actionId: '2',
params: { baz: true },
apiKey: Buffer.from('123:abc').toString('base64'),
relatedSavedObjects: [{ Xid: 'some-id', type: 'some-type' }],
relatedSavedObjects: [{ Xid: 'related_some-type_0', type: 'some-type' }],
},
references: [],
references: [
{
id: '2',
name: 'actionRef',
type: 'action',
},
{
id: 'some-id',
name: 'related_some-type_0',
type: 'some-type',
},
],
});
await taskRunner.run();
@ -366,7 +517,13 @@ test(`doesn't use API key when not provided`, async () => {
actionId: '2',
params: { baz: true },
},
references: [],
references: [
{
id: '2',
name: 'actionRef',
type: 'action',
},
],
});
await taskRunner.run();
@ -404,7 +561,13 @@ test(`throws an error when license doesn't support the action type`, async () =>
params: { baz: true },
apiKey: Buffer.from('123:abc').toString('base64'),
},
references: [],
references: [
{
id: '2',
name: 'actionRef',
type: 'action',
},
],
});
mockedActionExecutor.execute.mockImplementation(() => {
throw new ActionTypeDisabledError('Fail', 'license_invalid');

View file

@ -33,7 +33,8 @@ import {
} from '../types';
import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../constants/saved_objects';
import { asSavedObjectExecutionSource } from './action_execution_source';
import { validatedRelatedSavedObjects } from './related_saved_objects';
import { RelatedSavedObjects, validatedRelatedSavedObjects } from './related_saved_objects';
import { injectSavedObjectReferences } from './action_task_params_utils';
export interface TaskRunnerContext {
logger: Logger;
@ -178,11 +179,30 @@ async function getActionTaskParams(
const { spaceId } = executorParams;
const namespace = spaceIdToNamespace(spaceId);
if (isPersistedActionTask(executorParams)) {
return encryptedSavedObjectsClient.getDecryptedAsInternalUser<ActionTaskParams>(
const actionTask = await encryptedSavedObjectsClient.getDecryptedAsInternalUser<ActionTaskParams>(
ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE,
executorParams.actionTaskParamsId,
{ namespace }
);
const {
attributes: { relatedSavedObjects },
references,
} = actionTask;
const {
actionId,
relatedSavedObjects: injectedRelatedSavedObjects,
} = injectSavedObjectReferences(references, relatedSavedObjects as RelatedSavedObjects);
return {
...actionTask,
attributes: {
...actionTask.attributes,
...(actionId ? { actionId } : {}),
...(relatedSavedObjects ? { relatedSavedObjects: injectedRelatedSavedObjects } : {}),
},
};
} else {
return { attributes: executorParams.taskParams, references: executorParams.references ?? [] };
}

View file

@ -229,7 +229,8 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
core.savedObjects,
plugins.encryptedSavedObjects,
this.actionTypeRegistry!,
plugins.taskManager.index
plugins.taskManager.index,
this.preconfiguredActions
);
registerBuiltInActionTypes({

View file

@ -0,0 +1,417 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import uuid from 'uuid';
import {
getActionTaskParamsMigrations,
isPreconfiguredAction,
} from './action_task_params_migrations';
import { ActionTaskParams } from '../types';
import { SavedObjectReference, SavedObjectUnsanitizedDoc } from 'kibana/server';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks';
import { migrationMocks } from 'src/core/server/mocks';
const context = migrationMocks.createContext();
const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
const preconfiguredActions = [
{
actionTypeId: 'foo',
config: {},
id: 'my-slack1',
name: 'Slack #xyz',
secrets: {},
isPreconfigured: true,
},
];
describe('successful migrations', () => {
beforeEach(() => {
jest.resetAllMocks();
encryptedSavedObjectsSetup.createMigration.mockImplementation(({ migration }) => migration);
});
describe('7.16.0', () => {
test('adds actionId to references array if actionId is not preconfigured', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const actionTaskParam = getMockData();
const migratedActionTaskParam = migration716(actionTaskParam, context);
expect(migratedActionTaskParam).toEqual({
...actionTaskParam,
references: [
{
id: actionTaskParam.attributes.actionId,
name: 'actionRef',
type: 'action',
},
],
});
});
test('does not add actionId to references array if actionId is preconfigured', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const actionTaskParam = getMockData({ actionId: 'my-slack1' });
const migratedActionTaskParam = migration716(actionTaskParam, context);
expect(migratedActionTaskParam).toEqual({
...actionTaskParam,
references: [],
});
});
test('handles empty relatedSavedObjects array', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const actionTaskParam = getMockData({ relatedSavedObjects: [] });
const migratedActionTaskParam = migration716(actionTaskParam, context);
expect(migratedActionTaskParam).toEqual({
...actionTaskParam,
attributes: {
...actionTaskParam.attributes,
relatedSavedObjects: [],
},
references: [
{
id: actionTaskParam.attributes.actionId,
name: 'actionRef',
type: 'action',
},
],
});
});
test('adds actionId and relatedSavedObjects to references array', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const actionTaskParam = getMockData({
relatedSavedObjects: [
{
id: 'some-id',
namespace: 'some-namespace',
type: 'some-type',
typeId: 'some-typeId',
},
],
});
const migratedActionTaskParam = migration716(actionTaskParam, context);
expect(migratedActionTaskParam).toEqual({
...actionTaskParam,
attributes: {
...actionTaskParam.attributes,
relatedSavedObjects: [
{
id: 'related_some-type_0',
namespace: 'some-namespace',
type: 'some-type',
typeId: 'some-typeId',
},
],
},
references: [
{
id: actionTaskParam.attributes.actionId,
name: 'actionRef',
type: 'action',
},
{
id: 'some-id',
name: 'related_some-type_0',
type: 'some-type',
},
],
});
});
test('only adds relatedSavedObjects to references array if action is preconfigured', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const actionTaskParam = getMockData({
actionId: 'my-slack1',
relatedSavedObjects: [
{
id: 'some-id',
namespace: 'some-namespace',
type: 'some-type',
typeId: 'some-typeId',
},
],
});
const migratedActionTaskParam = migration716(actionTaskParam, context);
expect(migratedActionTaskParam).toEqual({
...actionTaskParam,
attributes: {
...actionTaskParam.attributes,
relatedSavedObjects: [
{
id: 'related_some-type_0',
namespace: 'some-namespace',
type: 'some-type',
typeId: 'some-typeId',
},
],
},
references: [
{
id: 'some-id',
name: 'related_some-type_0',
type: 'some-type',
},
],
});
});
test('adds actionId and multiple relatedSavedObjects to references array', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const actionTaskParam = getMockData({
relatedSavedObjects: [
{
id: 'some-id',
namespace: 'some-namespace',
type: 'some-type',
typeId: 'some-typeId',
},
{
id: 'another-id',
type: 'another-type',
typeId: 'another-typeId',
},
],
});
const migratedActionTaskParam = migration716(actionTaskParam, context);
expect(migratedActionTaskParam).toEqual({
...actionTaskParam,
attributes: {
...actionTaskParam.attributes,
relatedSavedObjects: [
{
id: 'related_some-type_0',
namespace: 'some-namespace',
type: 'some-type',
typeId: 'some-typeId',
},
{
id: 'related_another-type_1',
type: 'another-type',
typeId: 'another-typeId',
},
],
},
references: [
{
id: actionTaskParam.attributes.actionId,
name: 'actionRef',
type: 'action',
},
{
id: 'some-id',
name: 'related_some-type_0',
type: 'some-type',
},
{
id: 'another-id',
name: 'related_another-type_1',
type: 'another-type',
},
],
});
});
test('does not overwrite existing references', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const actionTaskParam = getMockData(
{
relatedSavedObjects: [
{
id: 'some-id',
namespace: 'some-namespace',
type: 'some-type',
typeId: 'some-typeId',
},
],
},
[
{
id: 'existing-ref-id',
name: 'existingRef',
type: 'existing-ref-type',
},
]
);
const migratedActionTaskParam = migration716(actionTaskParam, context);
expect(migratedActionTaskParam).toEqual({
...actionTaskParam,
attributes: {
...actionTaskParam.attributes,
relatedSavedObjects: [
{
id: 'related_some-type_0',
namespace: 'some-namespace',
type: 'some-type',
typeId: 'some-typeId',
},
],
},
references: [
{
id: 'existing-ref-id',
name: 'existingRef',
type: 'existing-ref-type',
},
{
id: actionTaskParam.attributes.actionId,
name: 'actionRef',
type: 'action',
},
{
id: 'some-id',
name: 'related_some-type_0',
type: 'some-type',
},
],
});
});
test('does not overwrite existing references if relatedSavedObjects is undefined', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const actionTaskParam = getMockData({}, [
{
id: 'existing-ref-id',
name: 'existingRef',
type: 'existing-ref-type',
},
]);
const migratedActionTaskParam = migration716(actionTaskParam, context);
expect(migratedActionTaskParam).toEqual({
...actionTaskParam,
references: [
{
id: 'existing-ref-id',
name: 'existingRef',
type: 'existing-ref-type',
},
{
id: actionTaskParam.attributes.actionId,
name: 'actionRef',
type: 'action',
},
],
});
});
test('does not overwrite existing references if relatedSavedObjects is empty', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const actionTaskParam = getMockData({ relatedSavedObjects: [] }, [
{
id: 'existing-ref-id',
name: 'existingRef',
type: 'existing-ref-type',
},
]);
const migratedActionTaskParam = migration716(actionTaskParam, context);
expect(migratedActionTaskParam).toEqual({
...actionTaskParam,
attributes: {
...actionTaskParam.attributes,
relatedSavedObjects: [],
},
references: [
{
id: 'existing-ref-id',
name: 'existingRef',
type: 'existing-ref-type',
},
{
id: actionTaskParam.attributes.actionId,
name: 'actionRef',
type: 'action',
},
],
});
});
});
});
describe('handles errors during migrations', () => {
beforeEach(() => {
jest.resetAllMocks();
encryptedSavedObjectsSetup.createMigration.mockImplementation(() => () => {
throw new Error(`Can't migrate!`);
});
});
describe('7.16.0 throws if migration fails', () => {
test('should show the proper exception', () => {
const migration716 = getActionTaskParamsMigrations(
encryptedSavedObjectsSetup,
preconfiguredActions
)['7.16.0'];
const actionTaskParam = getMockData();
expect(() => {
migration716(actionTaskParam, context);
}).toThrowError(`Can't migrate!`);
expect(context.log.error).toHaveBeenCalledWith(
`encryptedSavedObject 7.16.0 migration failed for action task param ${actionTaskParam.id} with error: Can't migrate!`,
{
migrations: {
actionTaskParamDocument: actionTaskParam,
},
}
);
});
});
});
describe('isPreconfiguredAction()', () => {
test('returns true if actionId is preconfigured action', () => {
expect(
isPreconfiguredAction(getMockData({ actionId: 'my-slack1' }), preconfiguredActions)
).toEqual(true);
});
test('returns false if actionId is not preconfigured action', () => {
expect(isPreconfiguredAction(getMockData(), preconfiguredActions)).toEqual(false);
});
});
function getMockData(
overwrites: Record<string, unknown> = {},
referencesOverwrites: SavedObjectReference[] = []
): SavedObjectUnsanitizedDoc<ActionTaskParams> {
return {
attributes: {
actionId: uuid.v4(),
params: {},
...overwrites,
},
references: [...referencesOverwrites],
id: uuid.v4(),
type: 'action_task_param',
};
}

View file

@ -0,0 +1,139 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
LogMeta,
SavedObjectMigrationMap,
SavedObjectUnsanitizedDoc,
SavedObjectMigrationFn,
SavedObjectMigrationContext,
SavedObjectReference,
} from '../../../../../src/core/server';
import { ActionTaskParams, PreConfiguredAction } from '../types';
import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server';
import type { IsMigrationNeededPredicate } from '../../../encrypted_saved_objects/server';
import { RelatedSavedObjects } from '../lib/related_saved_objects';
interface ActionTaskParamsLogMeta extends LogMeta {
migrations: { actionTaskParamDocument: SavedObjectUnsanitizedDoc<ActionTaskParams> };
}
type ActionTaskParamMigration = (
doc: SavedObjectUnsanitizedDoc<ActionTaskParams>
) => SavedObjectUnsanitizedDoc<ActionTaskParams>;
function createEsoMigration(
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup,
isMigrationNeededPredicate: IsMigrationNeededPredicate<ActionTaskParams, ActionTaskParams>,
migrationFunc: ActionTaskParamMigration
) {
return encryptedSavedObjects.createMigration<ActionTaskParams, ActionTaskParams>({
isMigrationNeededPredicate,
migration: migrationFunc,
shouldMigrateIfDecryptionFails: true, // shouldMigrateIfDecryptionFails flag that applies the migration to undecrypted document if decryption fails
});
}
export function getActionTaskParamsMigrations(
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup,
preconfiguredActions: PreConfiguredAction[]
): SavedObjectMigrationMap {
const migrationActionTaskParamsSixteen = createEsoMigration(
encryptedSavedObjects,
(doc): doc is SavedObjectUnsanitizedDoc<ActionTaskParams> => true,
pipeMigrations(getUseSavedObjectReferencesFn(preconfiguredActions))
);
return {
'7.16.0': executeMigrationWithErrorHandling(migrationActionTaskParamsSixteen, '7.16.0'),
};
}
function executeMigrationWithErrorHandling(
migrationFunc: SavedObjectMigrationFn<ActionTaskParams, ActionTaskParams>,
version: string
) {
return (
doc: SavedObjectUnsanitizedDoc<ActionTaskParams>,
context: SavedObjectMigrationContext
) => {
try {
return migrationFunc(doc, context);
} catch (ex) {
context.log.error<ActionTaskParamsLogMeta>(
`encryptedSavedObject ${version} migration failed for action task param ${doc.id} with error: ${ex.message}`,
{
migrations: {
actionTaskParamDocument: doc,
},
}
);
throw ex;
}
};
}
export function isPreconfiguredAction(
doc: SavedObjectUnsanitizedDoc<ActionTaskParams>,
preconfiguredActions: PreConfiguredAction[]
): boolean {
return !!preconfiguredActions.find((action) => action.id === doc.attributes.actionId);
}
function getUseSavedObjectReferencesFn(preconfiguredActions: PreConfiguredAction[]) {
return (doc: SavedObjectUnsanitizedDoc<ActionTaskParams>) => {
return useSavedObjectReferences(doc, preconfiguredActions);
};
}
function useSavedObjectReferences(
doc: SavedObjectUnsanitizedDoc<ActionTaskParams>,
preconfiguredActions: PreConfiguredAction[]
): SavedObjectUnsanitizedDoc<ActionTaskParams> {
const {
attributes: { actionId, relatedSavedObjects },
references,
} = doc;
const newReferences: SavedObjectReference[] = [];
const relatedSavedObjectRefs: RelatedSavedObjects = [];
if (!isPreconfiguredAction(doc, preconfiguredActions)) {
newReferences.push({
id: actionId,
name: 'actionRef',
type: 'action',
});
}
// Add related saved objects, if any
((relatedSavedObjects as RelatedSavedObjects) ?? []).forEach((relatedSavedObject, index) => {
relatedSavedObjectRefs.push({
...relatedSavedObject,
id: `related_${relatedSavedObject.type}_${index}`,
});
newReferences.push({
id: relatedSavedObject.id,
name: `related_${relatedSavedObject.type}_${index}`,
type: relatedSavedObject.type,
});
});
return {
...doc,
attributes: {
...doc.attributes,
...(relatedSavedObjects ? { relatedSavedObjects: relatedSavedObjectRefs } : {}),
},
references: [...(references ?? []), ...(newReferences ?? [])],
};
}
function pipeMigrations(...migrations: ActionTaskParamMigration[]): ActionTaskParamMigration {
return (doc: SavedObjectUnsanitizedDoc<ActionTaskParams>) =>
migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc);
}

View file

@ -6,7 +6,7 @@
*/
import uuid from 'uuid';
import { getMigrations } from './migrations';
import { getActionsMigrations } from './actions_migrations';
import { RawAction } from '../types';
import { SavedObjectUnsanitizedDoc } from 'kibana/server';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks';
@ -23,7 +23,7 @@ describe('successful migrations', () => {
describe('7.10.0', () => {
test('add hasAuth config property for .email actions', () => {
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const migration710 = getActionsMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const action = getMockDataForEmail({});
const migratedAction = migration710(action, context);
expect(migratedAction.attributes.config).toEqual({
@ -41,7 +41,7 @@ describe('successful migrations', () => {
});
test('rename cases configuration object', () => {
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const migration710 = getActionsMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const action = getCasesMockData({});
const migratedAction = migration710(action, context);
expect(migratedAction.attributes.config).toEqual({
@ -61,7 +61,7 @@ describe('successful migrations', () => {
describe('7.11.0', () => {
test('add hasAuth = true for .webhook actions with user and password', () => {
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const migration711 = getActionsMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const action = getMockDataForWebhook({}, true);
expect(migration711(action, context)).toMatchObject({
...action,
@ -75,7 +75,7 @@ describe('successful migrations', () => {
});
test('add hasAuth = false for .webhook actions without user and password', () => {
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const migration711 = getActionsMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const action = getMockDataForWebhook({}, false);
expect(migration711(action, context)).toMatchObject({
...action,
@ -88,7 +88,7 @@ describe('successful migrations', () => {
});
});
test('remove cases mapping object', () => {
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const migration711 = getActionsMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const action = getMockData({
config: { incidentConfiguration: { mapping: [] }, isCaseOwned: true, another: 'value' },
});
@ -106,7 +106,7 @@ describe('successful migrations', () => {
describe('7.14.0', () => {
test('add isMissingSecrets property for actions', () => {
const migration714 = getMigrations(encryptedSavedObjectsSetup)['7.14.0'];
const migration714 = getActionsMigrations(encryptedSavedObjectsSetup)['7.14.0'];
const action = getMockData({ isMissingSecrets: undefined });
const migratedAction = migration714(action, context);
expect(migratedAction).toEqual({
@ -130,7 +130,7 @@ describe('handles errors during migrations', () => {
describe('7.10.0 throws if migration fails', () => {
test('should show the proper exception', () => {
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const migration710 = getActionsMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const action = getMockDataForEmail({});
expect(() => {
migration710(action, context);
@ -148,7 +148,7 @@ describe('handles errors during migrations', () => {
describe('7.11.0 throws if migration fails', () => {
test('should show the proper exception', () => {
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const migration711 = getActionsMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const action = getMockDataForEmail({});
expect(() => {
migration711(action, context);
@ -166,7 +166,7 @@ describe('handles errors during migrations', () => {
describe('7.14.0 throws if migration fails', () => {
test('should show the proper exception', () => {
const migration714 = getMigrations(encryptedSavedObjectsSetup)['7.14.0'];
const migration714 = getActionsMigrations(encryptedSavedObjectsSetup)['7.14.0'];
const action = getMockDataForEmail({});
expect(() => {
migration714(action, context);

View file

@ -36,7 +36,7 @@ function createEsoMigration(
});
}
export function getMigrations(
export function getActionsMigrations(
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
): SavedObjectMigrationMap {
const migrationActionsTen = createEsoMigration(

View file

@ -13,8 +13,9 @@ import type {
} from 'kibana/server';
import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server';
import mappings from './mappings.json';
import { getMigrations } from './migrations';
import { RawAction } from '../types';
import { getActionsMigrations } from './actions_migrations';
import { getActionTaskParamsMigrations } from './action_task_params_migrations';
import { PreConfiguredAction, RawAction } from '../types';
import { getImportWarnings } from './get_import_warnings';
import { transformConnectorsForExport } from './transform_connectors_for_export';
import { ActionTypeRegistry } from '../action_type_registry';
@ -28,14 +29,15 @@ export function setupSavedObjects(
savedObjects: SavedObjectsServiceSetup,
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup,
actionTypeRegistry: ActionTypeRegistry,
taskManagerIndex: string
taskManagerIndex: string,
preconfiguredActions: PreConfiguredAction[]
) {
savedObjects.registerType({
name: ACTION_SAVED_OBJECT_TYPE,
hidden: true,
namespaceType: 'single',
mappings: mappings.action as SavedObjectsTypeMappingDefinition,
migrations: getMigrations(encryptedSavedObjects),
migrations: getActionsMigrations(encryptedSavedObjects),
management: {
defaultSearchField: 'name',
importableAndExportable: true,
@ -71,6 +73,7 @@ export function setupSavedObjects(
hidden: true,
namespaceType: 'single',
mappings: mappings.action_task_params as SavedObjectsTypeMappingDefinition,
migrations: getActionTaskParamsMigrations(encryptedSavedObjects, preconfiguredActions),
excludeOnUpgrade: async ({ readonlyEsClient }) => {
const oldestIdleActionTask = await getOldestIdleActionTask(
readonlyEsClient,

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { buildUp, tearDown } from '..';
// eslint-disable-next-line import/no-default-export
export default function actionTaskParamsTests({ loadTestFile, getService }: FtrProviderContext) {
describe('Action Task Params', () => {
before(async () => buildUp(getService));
after(async () => tearDown(getService));
loadTestFile(require.resolve('./migrations'));
});
}

View file

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { SavedObject, SavedObjectReference } from 'src/core/server';
import { ActionTaskParams } from '../../../../../plugins/actions/server/types';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function createGetTests({ getService }: FtrProviderContext) {
const es = getService('es');
const esArchiver = getService('esArchiver');
describe('migrations', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/action_task_params');
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/action_task_params');
});
it('7.16.0 migrates action_task_params to use references array', async () => {
// Inspect migration of non-preconfigured connector ID
const response = await es.get<SavedObject<ActionTaskParams>>({
index: '.kibana',
id: 'action_task_params:b9af6280-0052-11ec-917b-f7aa317691ed',
});
expect(response.statusCode).to.eql(200);
const { actionId, relatedSavedObjects, references } = getActionIdAndRelatedSavedObjects(
response.body._source
);
expect(references.find((ref: SavedObjectReference) => ref.name === 'actionRef')).to.eql({
name: 'actionRef',
id: actionId,
type: 'action',
});
// Should have reference entry for each relatedSavedObject entry
(relatedSavedObjects ?? []).forEach((relatedSavedObject: any) => {
expect(
references.find((ref: SavedObjectReference) => ref.name === relatedSavedObject.id)
).not.to.be(undefined);
});
// Inspect migration of preconfigured connector ID
const preconfiguredConnectorResponse = await es.get<SavedObject<ActionTaskParams>>({
index: '.kibana',
id: 'action_task_params:0205a520-0054-11ec-917b-f7aa317691ed',
});
expect(preconfiguredConnectorResponse.statusCode).to.eql(200);
const {
relatedSavedObjects: preconfiguredRelatedSavedObjects,
references: preconfiguredReferences,
} = getActionIdAndRelatedSavedObjects(preconfiguredConnectorResponse.body._source);
expect(
preconfiguredReferences.find((ref: SavedObjectReference) => ref.name === 'actionRef')
).to.eql(undefined);
// Should have reference entry for each relatedSavedObject entry
(preconfiguredRelatedSavedObjects ?? []).forEach((relatedSavedObject: any) => {
expect(
preconfiguredReferences.find(
(ref: SavedObjectReference) => ref.name === relatedSavedObject.id
)
).not.to.be(undefined);
});
});
});
function getActionIdAndRelatedSavedObjects(responseSource: any) {
if (!responseSource) {
return {};
}
const actionTaskParams = (responseSource as any)?.action_task_params as ActionTaskParams;
const actionId = actionTaskParams.actionId;
const relatedSavedObjects = actionTaskParams.relatedSavedObjects as unknown[];
const references = responseSource?.references ?? [];
return { actionId, relatedSavedObjects, references };
}
}

View file

@ -15,6 +15,7 @@ export default function alertingApiIntegrationTests({ loadTestFile }: FtrProvide
loadTestFile(require.resolve('./actions'));
loadTestFile(require.resolve('./alerting'));
loadTestFile(require.resolve('./action_task_params'));
});
}

View file

@ -0,0 +1,63 @@
{
"type": "doc",
"value": {
"index": ".kibana_1",
"id": "action_task_params:b9af6280-0052-11ec-917b-f7aa317691ed",
"source": {
"type": "action_task_params",
"action_task_params" : {
"actionId" : "918da460-0052-11ec-917b-f7aa317691ed",
"params" : {
"level" : "info",
"message" : "yo yo"
},
"relatedSavedObjects" : [
{
"type" : "alert",
"typeId" : "example.always-firing",
"id" : "b6db0cd0-0052-11ec-917b-f7aa317691ed"
}
]
},
"references" : [
{
"name" : "source",
"id" : "b6db0cd0-0052-11ec-917b-f7aa317691ed",
"type" : "alert"
}
]
}
}
}
{
"type": "doc",
"value": {
"index": ".kibana_1",
"id": "action_task_params:0205a520-0054-11ec-917b-f7aa317691ed",
"source": {
"type": "action_task_params",
"action_task_params" : {
"actionId" : "my-slack1",
"params" : {
"level" : "info",
"message" : "hi hi"
},
"relatedSavedObjects" : [
{
"type" : "alert",
"typeId" : "example.always-firing",
"id" : "b50bfcb0-0053-11ec-917b-f7aa317691ed"
}
]
},
"references" : [
{
"name" : "source",
"id" : "b50bfcb0-0053-11ec-917b-f7aa317691ed",
"type" : "alert"
}
]
}
}
}

File diff suppressed because it is too large Load diff