Skip to content

Commit

Permalink
[actions] add rule saved object reference to action execution event l…
Browse files Browse the repository at this point in the history
…og doc (elastic#101526)

resolves elastic#99225

Prior to this PR, when an alerting connection action was executed, the event 
log document generated did not contain a reference to the originating rule. 
This makes it difficult to diagnose problems with connector errors, since 
the error is often in the parameters specified in the actions in the alert.

In this PR, a reference to the alerting rule is added to the saved_objects 
field in the event document for these events.
  • Loading branch information
pmuellr committed Jun 22, 2021
1 parent be8aea6 commit 7824d32
Show file tree
Hide file tree
Showing 18 changed files with 442 additions and 17 deletions.
64 changes: 64 additions & 0 deletions x-pack/plugins/actions/server/actions_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1676,6 +1676,70 @@ describe('execute()', () => {
name: 'my name',
},
});

await expect(
actionsClient.execute({
actionId,
params: {
name: 'my name',
},
relatedSavedObjects: [
{
id: 'some-id',
typeId: 'some-type-id',
type: 'some-type',
},
],
})
).resolves.toMatchObject({ status: 'ok', actionId });

expect(actionExecutor.execute).toHaveBeenCalledWith({
actionId,
request,
params: {
name: 'my name',
},
relatedSavedObjects: [
{
id: 'some-id',
typeId: 'some-type-id',
type: 'some-type',
},
],
});

await expect(
actionsClient.execute({
actionId,
params: {
name: 'my name',
},
relatedSavedObjects: [
{
id: 'some-id',
typeId: 'some-type-id',
type: 'some-type',
namespace: 'some-namespace',
},
],
})
).resolves.toMatchObject({ status: 'ok', actionId });

expect(actionExecutor.execute).toHaveBeenCalledWith({
actionId,
request,
params: {
name: 'my name',
},
relatedSavedObjects: [
{
id: 'some-id',
typeId: 'some-type-id',
type: 'some-type',
namespace: 'some-namespace',
},
],
});
});
});

Expand Down
9 changes: 8 additions & 1 deletion x-pack/plugins/actions/server/actions_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,14 +469,21 @@ export class ActionsClient {
actionId,
params,
source,
relatedSavedObjects,
}: Omit<ExecuteOptions, 'request'>): Promise<ActionTypeExecutorResult<unknown>> {
if (
(await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) ===
AuthorizationMode.RBAC
) {
await this.authorization.ensureAuthorized('execute');
}
return this.actionExecutor.execute({ actionId, params, source, request: this.request });
return this.actionExecutor.execute({
actionId,
params,
source,
request: this.request,
relatedSavedObjects,
});
}

public async enqueueExecution(options: EnqueueExecutionOptions): Promise<void> {
Expand Down
56 changes: 56 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 @@ -83,6 +83,62 @@ describe('execute()', () => {
});
});

test('schedules the action with all given parameters and relatedSavedObjects', async () => {
const actionTypeRegistry = actionTypeRegistryMock.create();
const executeFn = createExecutionEnqueuerFunction({
taskManager: mockTaskManager,
actionTypeRegistry,
isESOCanEncrypt: true,
preconfiguredActions: [],
});
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: asHttpRequestExecutionSource(request),
relatedSavedObjects: [
{
id: 'some-id',
namespace: 'some-namespace',
type: 'some-type',
typeId: 'some-typeId',
},
],
});
expect(savedObjectsClient.create).toHaveBeenCalledWith(
'action_task_params',
{
actionId: '123',
params: { baz: false },
apiKey: Buffer.from('123:abc').toString('base64'),
relatedSavedObjects: [
{
id: 'some-id',
namespace: 'some-namespace',
type: 'some-type',
typeId: 'some-typeId',
},
],
},
{}
);
});

test('schedules the action with all given parameters with a preconfigured action', async () => {
const executeFn = createExecutionEnqueuerFunction({
taskManager: mockTaskManager,
Expand Down
5 changes: 4 additions & 1 deletion x-pack/plugins/actions/server/create_execute_function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { RawAction, ActionTypeRegistryContract, PreConfiguredAction } from './ty
import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './constants/saved_objects';
import { ExecuteOptions as ActionExecutorOptions } from './lib/action_executor';
import { isSavedObjectExecutionSource } from './lib';
import { RelatedSavedObjects } from './lib/related_saved_objects';

interface CreateExecuteFunctionOptions {
taskManager: TaskManagerStartContract;
Expand All @@ -23,6 +24,7 @@ export interface ExecuteOptions extends Pick<ActionExecutorOptions, 'params' | '
id: string;
spaceId: string;
apiKey: string | null;
relatedSavedObjects?: RelatedSavedObjects;
}

export type ExecutionEnqueuer = (
Expand All @@ -38,7 +40,7 @@ export function createExecutionEnqueuerFunction({
}: CreateExecuteFunctionOptions) {
return async function execute(
unsecuredSavedObjectsClient: SavedObjectsClientContract,
{ id, params, spaceId, source, apiKey }: ExecuteOptions
{ id, params, spaceId, source, apiKey, relatedSavedObjects }: ExecuteOptions
) {
if (!isESOCanEncrypt) {
throw new Error(
Expand Down Expand Up @@ -68,6 +70,7 @@ export function createExecutionEnqueuerFunction({
actionId: id,
params,
apiKey,
relatedSavedObjects,
},
executionSourceAsSavedObjectReferences(source)
);
Expand Down
13 changes: 13 additions & 0 deletions x-pack/plugins/actions/server/lib/action_executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { EVENT_LOG_ACTIONS } from '../constants/event_log';
import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
import { ActionsClient } from '../actions_client';
import { ActionExecutionSource } from './action_execution_source';
import { RelatedSavedObjects } from './related_saved_objects';

export interface ActionExecutorContext {
logger: Logger;
Expand All @@ -42,6 +43,7 @@ export interface ExecuteOptions<Source = unknown> {
request: KibanaRequest;
params: Record<string, unknown>;
source?: ActionExecutionSource<Source>;
relatedSavedObjects?: RelatedSavedObjects;
}

export type ActionExecutorContract = PublicMethodsOf<ActionExecutor>;
Expand All @@ -68,6 +70,7 @@ export class ActionExecutor {
params,
request,
source,
relatedSavedObjects,
}: ExecuteOptions): Promise<ActionTypeExecutorResult<unknown>> {
if (!this.isInitialized) {
throw new Error('ActionExecutor not initialized');
Expand Down Expand Up @@ -154,6 +157,16 @@ export class ActionExecutor {
},
};

for (const relatedSavedObject of relatedSavedObjects || []) {
event.kibana?.saved_objects?.push({
rel: SAVED_OBJECT_REL_PRIMARY,
type: relatedSavedObject.type,
id: relatedSavedObject.id,
type_id: relatedSavedObject.typeId,
namespace: relatedSavedObject.namespace,
});
}

eventLogger.startTiming(event);
let rawResult: ActionTypeExecutorResult<unknown>;
try {
Expand Down
86 changes: 86 additions & 0 deletions x-pack/plugins/actions/server/lib/related_saved_objects.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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 { validatedRelatedSavedObjects } from './related_saved_objects';
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
import { Logger } from '../../../../../src/core/server';

const loggerMock = loggingSystemMock.createLogger();

describe('related_saved_objects', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('validates valid objects', () => {
ensureValid(loggerMock, undefined);
ensureValid(loggerMock, []);
ensureValid(loggerMock, [
{
id: 'some-id',
type: 'some-type',
},
]);
ensureValid(loggerMock, [
{
id: 'some-id',
type: 'some-type',
typeId: 'some-type-id',
},
]);
ensureValid(loggerMock, [
{
id: 'some-id',
type: 'some-type',
namespace: 'some-namespace',
},
]);
ensureValid(loggerMock, [
{
id: 'some-id',
type: 'some-type',
typeId: 'some-type-id',
namespace: 'some-namespace',
},
]);
ensureValid(loggerMock, [
{
id: 'some-id',
type: 'some-type',
},
{
id: 'some-id-2',
type: 'some-type-2',
},
]);
});
});

it('handles invalid objects', () => {
ensureInvalid(loggerMock, 42);
ensureInvalid(loggerMock, {});
ensureInvalid(loggerMock, [{}]);
ensureInvalid(loggerMock, [{ id: 'some-id' }]);
ensureInvalid(loggerMock, [{ id: 42 }]);
ensureInvalid(loggerMock, [{ id: 'some-id', type: 'some-type', x: 42 }]);
});

function ensureValid(logger: Logger, savedObjects: unknown) {
const result = validatedRelatedSavedObjects(logger, savedObjects);
expect(result).toEqual(savedObjects === undefined ? [] : savedObjects);
expect(loggerMock.warn).not.toHaveBeenCalled();
}

function ensureInvalid(logger: Logger, savedObjects: unknown) {
const result = validatedRelatedSavedObjects(logger, savedObjects);
expect(result).toEqual([]);

const message = loggerMock.warn.mock.calls[0][0];
expect(message).toMatch(
/ignoring invalid related saved objects: expected value of type \[array\] but got/
);
}
31 changes: 31 additions & 0 deletions x-pack/plugins/actions/server/lib/related_saved_objects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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 { schema, TypeOf } from '@kbn/config-schema';
import { Logger } from '../../../../../src/core/server';

export type RelatedSavedObjects = TypeOf<typeof RelatedSavedObjectsSchema>;

const RelatedSavedObjectsSchema = schema.arrayOf(
schema.object({
namespace: schema.maybe(schema.string({ minLength: 1 })),
id: schema.string({ minLength: 1 }),
type: schema.string({ minLength: 1 }),
// optional; for SO types like action/alert that have type id's
typeId: schema.maybe(schema.string({ minLength: 1 })),
}),
{ defaultValue: [] }
);

export function validatedRelatedSavedObjects(logger: Logger, data: unknown): RelatedSavedObjects {
try {
return RelatedSavedObjectsSchema.validate(data);
} catch (err) {
logger.warn(`ignoring invalid related saved objects: ${err.message}`);
return [];
}
}
Loading

0 comments on commit 7824d32

Please sign in to comment.