[Actions] System actions enhancements (#161340)

## Summary

This PR:
- Handles the references for system actions in the rule
- Forbids the creation of system actions through the `kibana.yml`
- Adds telemetry for system actions
- Allow system action types to be disabled from the `kibana.yml`

Depends on: https://github.com/elastic/kibana/pull/160983,
https://github.com/elastic/kibana/pull/161341

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Christos Nasikas 2023-07-28 15:53:34 +03:00 committed by GitHub
parent 971c6bd5eb
commit f8a16009f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1262 additions and 295 deletions

View file

@ -49,10 +49,10 @@ describe('actionTypeRegistry', () => {
isSystemAction: false,
},
{
actionTypeId: '.cases',
actionTypeId: 'test.system-action',
config: {},
id: 'system-connector-.cases',
name: 'System action: .cases',
id: 'system-connector-test.system-action',
name: 'System action: test.system-action',
secrets: {},
isPreconfigured: false,
isDeprecated: false,
@ -393,7 +393,7 @@ describe('actionTypeRegistry', () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register({
id: '.cases',
id: 'test.system-action',
name: 'Cases',
minimumLicenseRequired: 'platinum',
supportedFeatureIds: ['alerting'],
@ -410,7 +410,7 @@ describe('actionTypeRegistry', () => {
expect(actionTypes).toEqual([
{
id: '.cases',
id: 'test.system-action',
name: 'Cases',
enabled: true,
enabledInConfig: true,
@ -497,13 +497,16 @@ describe('actionTypeRegistry', () => {
expect(actionTypeRegistry.isActionExecutable('my-slack1', 'foo')).toEqual(true);
});
test('should return true when isActionTypeEnabled is false and isLicenseValidForActionType is true and it has system connectors', async () => {
test('should return false when isActionTypeEnabled is false and isLicenseValidForActionType is true and it has system connectors', async () => {
mockedActionsConfig.isActionTypeEnabled.mockReturnValue(false);
mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true });
expect(
actionTypeRegistry.isActionExecutable('system-connector-.cases', 'system-action-type')
).toEqual(true);
actionTypeRegistry.isActionExecutable(
'system-connector-test.system-action',
'system-action-type'
)
).toEqual(false);
});
test('should call isLicenseValidForActionType of the license state with notifyUsage false by default', async () => {
@ -662,7 +665,7 @@ describe('actionTypeRegistry', () => {
const registry = new ActionTypeRegistry(actionTypeRegistryParams);
registry.register({
id: '.cases',
id: 'test.system-action',
name: 'Cases',
minimumLicenseRequired: 'platinum',
supportedFeatureIds: ['alerting'],
@ -675,7 +678,7 @@ describe('actionTypeRegistry', () => {
executor,
});
const result = registry.isSystemActionType('.cases');
const result = registry.isSystemActionType('test.system-action');
expect(result).toBe(true);
});
@ -720,7 +723,7 @@ describe('actionTypeRegistry', () => {
const registry = new ActionTypeRegistry(actionTypeRegistryParams);
registry.register({
id: '.cases',
id: 'test.system-action',
name: 'Cases',
minimumLicenseRequired: 'platinum',
supportedFeatureIds: ['alerting'],
@ -734,7 +737,7 @@ describe('actionTypeRegistry', () => {
executor,
});
const result = registry.getSystemActionKibanaPrivileges('.cases');
const result = registry.getSystemActionKibanaPrivileges('test.system-action');
expect(result).toEqual(['test/create']);
});
@ -742,7 +745,7 @@ describe('actionTypeRegistry', () => {
const registry = new ActionTypeRegistry(actionTypeRegistryParams);
registry.register({
id: '.cases',
id: 'test.system-action',
name: 'Cases',
minimumLicenseRequired: 'platinum',
supportedFeatureIds: ['alerting'],
@ -755,7 +758,7 @@ describe('actionTypeRegistry', () => {
executor,
});
const result = registry.getSystemActionKibanaPrivileges('.cases');
const result = registry.getSystemActionKibanaPrivileges('test.system-action');
expect(result).toEqual([]);
});
@ -784,7 +787,7 @@ describe('actionTypeRegistry', () => {
const getKibanaPrivileges = jest.fn().mockReturnValue(['test/create']);
registry.register({
id: '.cases',
id: 'test.system-action',
name: 'Cases',
minimumLicenseRequired: 'platinum',
supportedFeatureIds: ['alerting'],
@ -798,7 +801,7 @@ describe('actionTypeRegistry', () => {
executor,
});
registry.getSystemActionKibanaPrivileges('.cases', { foo: 'bar' });
registry.getSystemActionKibanaPrivileges('test.system-action', { foo: 'bar' });
expect(getKibanaPrivileges).toHaveBeenCalledWith({ params: { foo: 'bar' } });
});
});

View file

@ -79,7 +79,9 @@ export class ActionTypeRegistry {
}
/**
* Returns true if action type is enabled or it is an in memory action type.
* Returns true if action type is enabled or preconfigured.
* An action type can be disabled but used with a preconfigured action.
* This does not apply to system actions as those can be disabled.
*/
public isActionExecutable(
actionId: string,
@ -87,12 +89,11 @@ export class ActionTypeRegistry {
options: { notifyUsage: boolean } = { notifyUsage: false }
) {
const actionTypeEnabled = this.isActionTypeEnabled(actionTypeId, options);
return (
actionTypeEnabled ||
(!actionTypeEnabled &&
this.inMemoryConnectors.find((inMemoryConnector) => inMemoryConnector.id === actionId) !==
undefined)
const inMemoryConnector = this.inMemoryConnectors.find(
(connector) => connector.id === actionId
);
return actionTypeEnabled || (!actionTypeEnabled && inMemoryConnector?.isPreconfigured === true);
}
/**

View file

@ -1659,7 +1659,7 @@ describe('getBulk()', () => {
connectorTokenClient: connectorTokenClientMock.create(),
getEventLogClient,
});
return actionsClient.getBulk(['1', 'testPreconfigured']);
return actionsClient.getBulk({ ids: ['1', 'testPreconfigured'] });
}
test('ensures user is authorised to get the type of action', async () => {
@ -1709,7 +1709,7 @@ describe('getBulk()', () => {
}
);
await actionsClient.getBulk(['1']);
await actionsClient.getBulk({ ids: ['1'] });
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
@ -1725,7 +1725,7 @@ describe('getBulk()', () => {
test('logs audit event when not authorised to bulk get connectors', async () => {
authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized'));
await expect(actionsClient.getBulk(['1'])).rejects.toThrow();
await expect(actionsClient.getBulk({ ids: ['1'] })).rejects.toThrow();
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
@ -1810,7 +1810,7 @@ describe('getBulk()', () => {
getEventLogClient,
});
const result = await actionsClient.getBulk(['1', 'testPreconfigured']);
const result = await actionsClient.getBulk({ ids: ['1', 'testPreconfigured'] });
expect(result).toEqual([
{
@ -1907,7 +1907,7 @@ describe('getBulk()', () => {
});
await expect(
actionsClient.getBulk(['1', 'testPreconfigured', 'system-connector-.cases'])
actionsClient.getBulk({ ids: ['1', 'testPreconfigured', 'system-connector-.cases'] })
).rejects.toThrowErrorMatchingInlineSnapshot(`"Connector system-connector-.cases not found"`);
});
@ -1982,7 +1982,10 @@ describe('getBulk()', () => {
});
expect(
await actionsClient.getBulk(['1', 'testPreconfigured', 'system-connector-.cases'], false)
await actionsClient.getBulk({
ids: ['1', 'testPreconfigured', 'system-connector-.cases'],
throwIfSystemAction: false,
})
).toEqual([
{
actionTypeId: '.slack',

View file

@ -534,10 +534,13 @@ export class ActionsClient {
/**
* Get bulk actions with in-memory list
*/
public async getBulk(
ids: string[],
throwIfSystemAction: boolean = true
): Promise<ActionResult[]> {
public async getBulk({
ids,
throwIfSystemAction = true,
}: {
ids: string[];
throwIfSystemAction?: boolean;
}): Promise<ActionResult[]> {
try {
await this.authorization.ensureAuthorized({ operation: 'get' });
} catch (error) {

View file

@ -329,10 +329,10 @@ describe('execute()', () => {
isESOCanEncrypt: true,
inMemoryConnectors: [
{
actionTypeId: '.cases',
actionTypeId: 'test.system-action',
config: {},
id: 'system-connector-.cases',
name: 'System action: .cases',
id: 'system-connector-test.system-action',
name: 'System action: test.system-action',
secrets: {},
isPreconfigured: false,
isDeprecated: false,
@ -346,7 +346,7 @@ describe('execute()', () => {
id: '123',
type: 'action',
attributes: {
actionTypeId: '.cases',
actionTypeId: 'test.system-action',
},
references: [],
});
@ -359,11 +359,11 @@ describe('execute()', () => {
});
await executeFn(savedObjectsClient, {
id: 'system-connector-.cases',
id: 'system-connector-test.system-action',
params: { baz: false },
spaceId: 'default',
executionId: 'system-connector-.casesabc',
apiKey: Buffer.from('system-connector-.cases:abc').toString('base64'),
apiKey: Buffer.from('system-connector-test.system-action:abc').toString('base64'),
source: asSavedObjectExecutionSource(source),
});
@ -379,7 +379,7 @@ describe('execute()', () => {
"actions",
],
"state": Object {},
"taskType": "actions:.cases",
"taskType": "actions:test.system-action",
},
]
`);
@ -387,11 +387,11 @@ describe('execute()', () => {
expect(savedObjectsClient.create).toHaveBeenCalledWith(
'action_task_params',
{
actionId: 'system-connector-.cases',
actionId: 'system-connector-test.system-action',
params: { baz: false },
executionId: 'system-connector-.casesabc',
source: 'SAVED_OBJECT',
apiKey: Buffer.from('system-connector-.cases:abc').toString('base64'),
apiKey: Buffer.from('system-connector-test.system-action:abc').toString('base64'),
},
{
references: [
@ -513,10 +513,10 @@ describe('execute()', () => {
isESOCanEncrypt: true,
inMemoryConnectors: [
{
actionTypeId: '.cases',
actionTypeId: 'test.system-action',
config: {},
id: 'system-connector-.cases',
name: 'System action: .cases',
id: 'system-connector-test.system-action',
name: 'System action: test.system-action',
secrets: {},
isPreconfigured: false,
isDeprecated: false,
@ -530,7 +530,7 @@ describe('execute()', () => {
id: '123',
type: 'action',
attributes: {
actionTypeId: '.cases',
actionTypeId: 'test.system-action',
},
references: [],
});
@ -541,10 +541,10 @@ describe('execute()', () => {
references: [],
});
await executeFn(savedObjectsClient, {
id: 'system-connector-.cases',
id: 'system-connector-test.system-action',
params: { baz: false },
spaceId: 'default',
apiKey: Buffer.from('system-connector-.cases:abc').toString('base64'),
apiKey: Buffer.from('system-connector-test.system-action:abc').toString('base64'),
source: asSavedObjectExecutionSource(source),
executionId: 'system-connector-.casesabc',
relatedSavedObjects: [
@ -568,7 +568,7 @@ describe('execute()', () => {
"actions",
],
"state": Object {},
"taskType": "actions:.cases",
"taskType": "actions:test.system-action",
},
]
`);
@ -576,9 +576,9 @@ describe('execute()', () => {
expect(savedObjectsClient.create).toHaveBeenCalledWith(
'action_task_params',
{
actionId: 'system-connector-.cases',
actionId: 'system-connector-test.system-action',
params: { baz: false },
apiKey: Buffer.from('system-connector-.cases:abc').toString('base64'),
apiKey: Buffer.from('system-connector-test.system-action:abc').toString('base64'),
executionId: 'system-connector-.casesabc',
source: 'SAVED_OBJECT',
relatedSavedObjects: [
@ -738,7 +738,7 @@ describe('execute()', () => {
expect(mockedActionTypeRegistry.ensureActionTypeEnabled).not.toHaveBeenCalled();
});
test('should skip ensure action type if action type is system action and license is valid', async () => {
test('should ensure if a system action type is enabled', async () => {
const mockedActionTypeRegistry = actionTypeRegistryMock.create();
const executeFn = createExecutionEnqueuerFunction({
taskManager: mockTaskManager,
@ -746,10 +746,10 @@ describe('execute()', () => {
actionTypeRegistry: mockedActionTypeRegistry,
inMemoryConnectors: [
{
actionTypeId: '.cases',
actionTypeId: 'test.system-action',
config: {},
id: 'system-connector-.cases',
name: 'System action: .cases',
id: 'system-connector-test.system-action',
name: 'System action: test.system-action',
secrets: {},
isPreconfigured: false,
isDeprecated: false,
@ -757,15 +757,20 @@ describe('execute()', () => {
},
],
});
mockedActionTypeRegistry.isActionExecutable.mockImplementation(() => true);
mockedActionTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => {
throw new Error('Fail');
});
savedObjectsClient.get.mockResolvedValueOnce({
id: '123',
type: 'action',
attributes: {
actionTypeId: '.cases',
actionTypeId: 'test.system-action',
},
references: [],
});
savedObjectsClient.create.mockResolvedValueOnce({
id: '234',
type: 'action_task_params',
@ -773,16 +778,20 @@ describe('execute()', () => {
references: [],
});
await executeFn(savedObjectsClient, {
id: 'system-connector-.case',
params: { baz: false },
spaceId: 'default',
executionId: 'system-connector-.caseabc',
apiKey: null,
source: asHttpRequestExecutionSource(request),
});
await expect(
executeFn(savedObjectsClient, {
id: 'system-connector-test.system-action',
params: { baz: false },
spaceId: 'default',
executionId: 'system-connector-.test.system-action-abc',
apiKey: null,
source: asHttpRequestExecutionSource(request),
})
).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`);
expect(mockedActionTypeRegistry.ensureActionTypeEnabled).not.toHaveBeenCalled();
expect(mockedActionTypeRegistry.ensureActionTypeEnabled).toHaveBeenCalledWith(
'test.system-action'
);
});
});
@ -1155,10 +1164,10 @@ describe('bulkExecute()', () => {
isESOCanEncrypt: true,
inMemoryConnectors: [
{
actionTypeId: '.cases',
actionTypeId: 'test.system-action',
config: {},
id: 'system-connector-.cases',
name: 'System action: .cases',
id: 'system-connector-test.system-action',
name: 'System action: test.system-action',
secrets: {},
isPreconfigured: false,
isDeprecated: false,
@ -1174,7 +1183,7 @@ describe('bulkExecute()', () => {
id: '123',
type: 'action',
attributes: {
actionTypeId: '.cases',
actionTypeId: 'test.system-action',
},
references: [],
},
@ -1186,7 +1195,7 @@ describe('bulkExecute()', () => {
id: '234',
type: 'action_task_params',
attributes: {
actionId: 'system-connector-.cases',
actionId: 'system-connector-test.system-action',
},
references: [],
},
@ -1194,11 +1203,11 @@ describe('bulkExecute()', () => {
});
await executeFn(savedObjectsClient, [
{
id: 'system-connector-.cases',
id: 'system-connector-test.system-action',
params: { baz: false },
spaceId: 'default',
executionId: 'system-connector-.casesabc',
apiKey: Buffer.from('system-connector-.cases:abc').toString('base64'),
apiKey: Buffer.from('system-connector-test.system-action:abc').toString('base64'),
source: asSavedObjectExecutionSource(source),
},
]);
@ -1215,7 +1224,7 @@ describe('bulkExecute()', () => {
"actions",
],
"state": Object {},
"taskType": "actions:.cases",
"taskType": "actions:test.system-action",
},
],
]
@ -1226,11 +1235,11 @@ describe('bulkExecute()', () => {
{
type: 'action_task_params',
attributes: {
actionId: 'system-connector-.cases',
actionId: 'system-connector-test.system-action',
params: { baz: false },
executionId: 'system-connector-.casesabc',
source: 'SAVED_OBJECT',
apiKey: Buffer.from('system-connector-.cases:abc').toString('base64'),
apiKey: Buffer.from('system-connector-test.system-action:abc').toString('base64'),
},
references: [
{
@ -1370,10 +1379,10 @@ describe('bulkExecute()', () => {
isESOCanEncrypt: true,
inMemoryConnectors: [
{
actionTypeId: '.cases',
actionTypeId: 'test.system-action',
config: {},
id: 'system-connector-.cases',
name: 'System action: .cases',
id: 'system-connector-test.system-action',
name: 'System action: test.system-action',
secrets: {},
isPreconfigured: false,
isDeprecated: false,
@ -1389,7 +1398,7 @@ describe('bulkExecute()', () => {
id: '123',
type: 'action',
attributes: {
actionTypeId: '.cases',
actionTypeId: 'test.system-action',
},
references: [],
},
@ -1401,7 +1410,7 @@ describe('bulkExecute()', () => {
id: '234',
type: 'action_task_params',
attributes: {
actionId: 'system-connector-.cases',
actionId: 'system-connector-test.system-action',
},
references: [],
},
@ -1409,10 +1418,10 @@ describe('bulkExecute()', () => {
});
await executeFn(savedObjectsClient, [
{
id: 'system-connector-.cases',
id: 'system-connector-test.system-action',
params: { baz: false },
spaceId: 'default',
apiKey: Buffer.from('system-connector-.cases:abc').toString('base64'),
apiKey: Buffer.from('system-connector-test.system-action:abc').toString('base64'),
source: asSavedObjectExecutionSource(source),
executionId: 'system-connector-.casesabc',
relatedSavedObjects: [
@ -1438,7 +1447,7 @@ describe('bulkExecute()', () => {
"actions",
],
"state": Object {},
"taskType": "actions:.cases",
"taskType": "actions:test.system-action",
},
],
]
@ -1449,9 +1458,9 @@ describe('bulkExecute()', () => {
{
type: 'action_task_params',
attributes: {
actionId: 'system-connector-.cases',
actionId: 'system-connector-test.system-action',
params: { baz: false },
apiKey: Buffer.from('system-connector-.cases:abc').toString('base64'),
apiKey: Buffer.from('system-connector-test.system-action:abc').toString('base64'),
executionId: 'system-connector-.casesabc',
source: 'SAVED_OBJECT',
relatedSavedObjects: [
@ -1646,10 +1655,10 @@ describe('bulkExecute()', () => {
actionTypeRegistry: mockedActionTypeRegistry,
inMemoryConnectors: [
{
actionTypeId: '.cases',
actionTypeId: 'test.system-action',
config: {},
id: 'system-connector-.cases',
name: 'System action: .cases',
id: 'system-connector-test.system-action',
name: 'System action: test.system-action',
secrets: {},
isPreconfigured: false,
isDeprecated: false,
@ -1664,7 +1673,7 @@ describe('bulkExecute()', () => {
id: '123',
type: 'action',
attributes: {
actionTypeId: '.cases',
actionTypeId: 'test.system-action',
},
references: [],
},

View file

@ -524,7 +524,7 @@ describe('Actions Plugin', () => {
});
describe('System actions', () => {
it('should handle system actions', async () => {
it('should set system actions correctly', async () => {
setup(getConfig());
// coreMock.createSetup doesn't support Plugin generics
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -573,6 +573,45 @@ describe('Actions Plugin', () => {
]);
expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.cases')).toBe(true);
});
it('should throw if a system action type is set in preconfigured connectors', async () => {
setup(
getConfig({
preconfigured: {
preconfiguredServerLog: {
actionTypeId: 'test.system-action',
name: 'preconfigured-system-action',
config: {},
secrets: {},
},
},
})
);
// coreMock.createSetup doesn't support Plugin generics
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup);
pluginSetup.registerType({
id: 'test.system-action',
name: 'Test',
minimumLicenseRequired: 'platinum',
supportedFeatureIds: ['alerting'],
validate: {
config: { schema: schema.object({}) },
secrets: { schema: schema.object({}) },
params: { schema: schema.object({}) },
},
isSystemActionType: true,
executor,
});
await expect(async () =>
plugin.start(coreStart, pluginsStart)
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Setting system action types in preconfigured connectors are not allowed"`
);
});
});
});

View file

@ -256,6 +256,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
isPreconfigured: true,
isSystemAction: false,
};
this.inMemoryConnectors.push({
...rawPreconfiguredConnector,
isDeprecated: isConnectorDeprecated(rawPreconfiguredConnector),
@ -396,6 +397,8 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
includedHiddenTypes,
});
this.throwIfSystemActionsInConfig();
/**
* Warning: this call mutates the inMemory collection
*
@ -616,6 +619,16 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
this.inMemoryConnectors = [...this.inMemoryConnectors, ...systemConnectors];
};
private throwIfSystemActionsInConfig = () => {
const hasSystemActionAsPreconfiguredInConfig = this.inMemoryConnectors
.filter((connector) => connector.isPreconfigured)
.some((connector) => this.actionTypeRegistry!.isSystemActionType(connector.actionTypeId));
if (hasSystemActionAsPreconfiguredInConfig) {
throw new Error('Setting system action types in preconfigured connectors are not allowed');
}
};
private createRouteHandlerContext = (
core: CoreSetup<ActionsPluginsStart>
): IContextProvider<ActionsRequestHandlerContext, 'actions'> => {

View file

@ -104,17 +104,17 @@ describe('actions telemetry', () => {
expect(mockEsClient.search).toHaveBeenCalledTimes(1);
expect(telemetry).toMatchInlineSnapshot(`
Object {
"countByType": Object {
"__index": 1,
"__server-log": 1,
"another.type__": 1,
"some.type": 1,
},
"countTotal": 4,
"hasErrors": false,
}
`);
Object {
"countByType": Object {
"__index": 1,
"__server-log": 1,
"another.type__": 1,
"some.type": 1,
},
"countTotal": 4,
"hasErrors": false,
}
`);
});
test('getTotalCount should return empty results if query throws error', async () => {
@ -128,13 +128,13 @@ Object {
`Error executing actions telemetry task: getTotalCount - {}`
);
expect(telemetry).toMatchInlineSnapshot(`
Object {
"countByType": Object {},
"countTotal": 0,
"errorMessage": "oh no",
"hasErrors": true,
}
`);
Object {
"countByType": Object {},
"countTotal": 0,
"errorMessage": "oh no",
"hasErrors": true,
}
`);
});
test('getInUseTotalCount', async () => {
@ -188,18 +188,18 @@ Object {
expect(mockEsClient.search).toHaveBeenCalledTimes(2);
expect(telemetry).toMatchInlineSnapshot(`
Object {
"countByAlertHistoryConnectorType": 0,
"countByType": Object {
"__server-log": 1,
"__slack": 1,
},
"countEmailByService": Object {},
"countNamespaces": 1,
"countTotal": 2,
"hasErrors": false,
}
`);
Object {
"countByAlertHistoryConnectorType": 0,
"countByType": Object {
"__server-log": 1,
"__slack": 1,
},
"countEmailByService": Object {},
"countNamespaces": 1,
"countTotal": 2,
"hasErrors": false,
}
`);
});
test('getInUseTotalCount should count preconfigured alert history connector usage', async () => {
@ -292,19 +292,19 @@ Object {
expect(mockEsClient.search).toHaveBeenCalledTimes(2);
expect(telemetry).toMatchInlineSnapshot(`
Object {
"countByAlertHistoryConnectorType": 1,
"countByType": Object {
"__index": 1,
"__server-log": 1,
"__slack": 1,
},
"countEmailByService": Object {},
"countNamespaces": 1,
"countTotal": 4,
"hasErrors": false,
}
`);
Object {
"countByAlertHistoryConnectorType": 1,
"countByType": Object {
"__index": 1,
"__server-log": 1,
"__slack": 1,
},
"countEmailByService": Object {},
"countNamespaces": 1,
"countTotal": 4,
"hasErrors": false,
}
`);
});
test('getInUseTotalCount should return empty results if query throws error', async () => {
@ -318,16 +318,16 @@ Object {
`Error executing actions telemetry task: getInUseTotalCount - {}`
);
expect(telemetry).toMatchInlineSnapshot(`
Object {
"countByAlertHistoryConnectorType": 0,
"countByType": Object {},
"countEmailByService": Object {},
"countNamespaces": 0,
"countTotal": 0,
"errorMessage": "oh no",
"hasErrors": true,
}
`);
Object {
"countByAlertHistoryConnectorType": 0,
"countByType": Object {},
"countEmailByService": Object {},
"countNamespaces": 0,
"countTotal": 0,
"errorMessage": "oh no",
"hasErrors": true,
}
`);
});
test('getTotalCount accounts for preconfigured connectors', async () => {
@ -443,18 +443,61 @@ Object {
expect(mockEsClient.search).toHaveBeenCalledTimes(1);
expect(telemetry).toMatchInlineSnapshot(`
Object {
"countByType": Object {
"__index": 1,
"__server-log": 2,
"__test": 1,
"another.type__": 1,
"some.type": 1,
},
"countTotal": 6,
"hasErrors": false,
}
`);
Object {
"countByType": Object {
"__index": 1,
"__server-log": 2,
"__test": 1,
"another.type__": 1,
"some.type": 1,
},
"countTotal": 6,
"hasErrors": false,
}
`);
});
test('getTotalCount accounts for system connectors', async () => {
const mockEsClient = elasticsearchClientMock.createClusterClient().asScoped().asInternalUser;
mockEsClient.search.mockResponse(
// @ts-expect-error not full search response
{
aggregations: {
byActionTypeId: {
value: {
types: {},
},
},
},
hits: {
hits: [],
},
}
);
const telemetry = await getTotalCount(mockEsClient, 'test', mockLogger, [
{
id: 'system_action:system-connector-test.system-action',
actionTypeId: 'test.system-action',
name: 'System connector',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: true,
secrets: {},
config: {},
},
]);
expect(mockEsClient.search).toHaveBeenCalledTimes(1);
expect(telemetry).toMatchInlineSnapshot(`
Object {
"countByType": Object {
"test.system-action": 1,
},
"countTotal": 1,
"hasErrors": false,
}
`);
});
test('getInUseTotalCount() accounts for preconfigured connectors', async () => {
@ -550,22 +593,105 @@ Object {
expect(mockEsClient.search).toHaveBeenCalledTimes(2);
expect(telemetry).toMatchInlineSnapshot(`
Object {
"countByAlertHistoryConnectorType": 1,
"countByType": Object {
"__email": 3,
"__index": 1,
"__server-log": 1,
"__slack": 1,
},
"countEmailByService": Object {
"other": 3,
},
"countNamespaces": 1,
"countTotal": 6,
"hasErrors": false,
}
`);
Object {
"countByAlertHistoryConnectorType": 1,
"countByType": Object {
"__email": 3,
"__index": 1,
"__server-log": 1,
"__slack": 1,
},
"countEmailByService": Object {
"other": 3,
},
"countNamespaces": 1,
"countTotal": 6,
"hasErrors": false,
}
`);
});
test('getInUseTotalCount() accounts for system connectors', async () => {
const mockEsClient = elasticsearchClientMock.createClusterClient().asScoped().asInternalUser;
mockEsClient.search.mockResponseOnce(
// @ts-expect-error not full search response
{
aggregations: {
refs: {
actionRefIds: {
value: {
connectorIds: {
'1': 'action-0',
'2': 'action-1',
},
total: 3,
},
},
},
system_actions: {
systemActionRefIds: {
value: {
total: 2,
actionRefs: {
'system_action:system-connector-test.system-action': {
actionRef: 'system_action:system-connector-test.system-action',
actionTypeId: 'test.system-action',
},
'system_action:system-connector-test.system-action-2': {
actionRef: 'system_action:system-connector-test.system-action-2',
actionTypeId: 'test.system-action-2',
},
},
},
},
},
},
}
);
mockEsClient.search.mockResponseOnce({
hits: {
hits: [
// @ts-expect-error not full search response
{
_source: {
action: {
id: '1',
actionTypeId: '.index',
},
namespaces: ['default'],
},
},
// @ts-expect-error not full search response
{
_source: {
action: {
id: '2',
actionTypeId: '.index',
},
namespaces: ['default'],
},
},
],
},
});
const telemetry = await getInUseTotalCount(mockEsClient, 'test', mockLogger, undefined, []);
expect(mockEsClient.search).toHaveBeenCalledTimes(2);
expect(telemetry).toMatchInlineSnapshot(`
Object {
"countByAlertHistoryConnectorType": 0,
"countByType": Object {
"__index": 2,
"test.system-action": 1,
"test.system-action-2": 1,
},
"countEmailByService": Object {},
"countNamespaces": 1,
"countTotal": 5,
"hasErrors": false,
}
`);
});
test('getInUseTotalCount() accounts for actions namespaces', async () => {
@ -650,22 +776,22 @@ Object {
expect(mockEsClient.search).toHaveBeenCalledTimes(2);
expect(telemetry).toMatchInlineSnapshot(`
Object {
"countByAlertHistoryConnectorType": 1,
"countByType": Object {
"__email": 3,
"__index": 1,
"__server-log": 1,
"__slack": 1,
},
"countEmailByService": Object {
"other": 1,
},
"countNamespaces": 3,
"countTotal": 6,
"hasErrors": false,
}
`);
Object {
"countByAlertHistoryConnectorType": 1,
"countByType": Object {
"__email": 3,
"__index": 1,
"__server-log": 1,
"__slack": 1,
},
"countEmailByService": Object {
"other": 1,
},
"countNamespaces": 3,
"countTotal": 6,
"hasErrors": false,
}
`);
});
test('getExecutionsTotalCount', async () => {
@ -818,17 +944,17 @@ Object {
`Error executing actions telemetry task: getExecutionsPerDayCount - {}`
);
expect(telemetry).toMatchInlineSnapshot(`
Object {
"avgExecutionTime": 0,
"avgExecutionTimeByType": Object {},
"countByType": Object {},
"countFailed": 0,
"countFailedByType": Object {},
"countRunOutcomeByConnectorType": Object {},
"countTotal": 0,
"errorMessage": "oh no",
"hasErrors": true,
}
`);
Object {
"avgExecutionTime": 0,
"avgExecutionTimeByType": Object {},
"countByType": Object {},
"countFailed": 0,
"countFailedByType": Object {},
"countRunOutcomeByConnectorType": Object {},
"countTotal": 0,
"errorMessage": "oh no",
"hasErrors": true,
}
`);
});
});

View file

@ -15,6 +15,11 @@ import {
import { AlertHistoryEsIndexConnectorId } from '../../common';
import { ActionResult, InMemoryConnector } from '../types';
interface InMemoryAggRes {
total: number;
actionRefs: Record<string, { actionRef: string; actionTypeId: string }>;
}
export async function getTotalCount(
esClient: ElasticsearchClient,
kibanaIndex: string,
@ -49,7 +54,10 @@ export async function getTotalCount(
},
};
try {
const searchResult = await esClient.search({
const searchResult = await esClient.search<
unknown,
{ byActionTypeId: { value: { types: Record<string, number> } } }
>({
index: kibanaIndex,
size: 0,
body: {
@ -63,17 +71,14 @@ export async function getTotalCount(
},
},
});
// @ts-expect-error aggegation type is not specified
const aggs = searchResult.aggregations?.byActionTypeId.value?.types;
const countByType = Object.keys(aggs).reduce(
// ES DSL aggregations are returned as `any` by esClient.search
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(obj: any, key: string) => {
obj[replaceFirstAndLastDotSymbols(key)] = aggs[key];
return obj;
},
{}
);
const aggs = searchResult.aggregations?.byActionTypeId.value?.types ?? {};
const countByType = Object.keys(aggs).reduce<Record<string, number>>((obj, key) => {
obj[replaceFirstAndLastDotSymbols(key)] = aggs[key];
return obj;
}, {});
if (inMemoryConnectors && inMemoryConnectors.length) {
for (const inMemoryConnector of inMemoryConnectors) {
const actionTypeId = replaceFirstAndLastDotSymbols(inMemoryConnector.actionTypeId);
@ -81,13 +86,14 @@ export async function getTotalCount(
countByType[actionTypeId]++;
}
}
const totals =
Object.keys(aggs).reduce((total, key) => parseInt(aggs[key].toString(), 10) + total, 0) +
(inMemoryConnectors?.length ?? 0);
return {
hasErrors: false,
countTotal:
Object.keys(aggs).reduce(
(total: number, key: string) => parseInt(aggs[key], 10) + total,
0
) + (inMemoryConnectors?.length ?? 0),
countTotal: totals,
countByType,
};
} catch (err) {
@ -119,6 +125,44 @@ export async function getInUseTotalCount(
countEmailByService: Record<string, number>;
countNamespaces: number;
}> {
const getInMemoryActionScriptedMetric = (actionRefPrefix: string) => ({
scripted_metric: {
init_script: 'state.actionRefs = new HashMap(); state.total = 0;',
map_script: `
String actionRef = doc['alert.actions.actionRef'].value;
String actionTypeId = doc['alert.actions.actionTypeId'].value;
if (actionRef.startsWith('${actionRefPrefix}') && state.actionRefs[actionRef] === null) {
HashMap map = new HashMap();
map.actionRef = actionRef;
map.actionTypeId = actionTypeId;
state.actionRefs[actionRef] = map;
state.total++;
}
`,
// Combine script is executed per cluster, but we already have a key-value pair per cluster.
// Despite docs that say this is optional, this script can't be blank.
combine_script: 'return state',
// Reduce script is executed across all clusters, so we need to add up all the total from each cluster
// This also needs to account for having no data
reduce_script: `
Map actionRefs = [:];
long total = 0;
for (state in states) {
if (state !== null) {
total += state.total;
for (String k : state.actionRefs.keySet()) {
actionRefs.put(k, state.actionRefs.get(k));
}
}
}
Map result = new HashMap();
result.total = total;
result.actionRefs = actionRefs;
return result;
`,
},
});
const scriptedMetric = {
scripted_metric: {
init_script: 'state.connectorIds = new HashMap(); state.total = 0;',
@ -154,43 +198,8 @@ export async function getInUseTotalCount(
},
};
const preconfiguredActionsScriptedMetric = {
scripted_metric: {
init_script: 'state.actionRefs = new HashMap(); state.total = 0;',
map_script: `
String actionRef = doc['alert.actions.actionRef'].value;
String actionTypeId = doc['alert.actions.actionTypeId'].value;
if (actionRef.startsWith('preconfigured:') && state.actionRefs[actionRef] === null) {
HashMap map = new HashMap();
map.actionRef = actionRef;
map.actionTypeId = actionTypeId;
state.actionRefs[actionRef] = map;
state.total++;
}
`,
// Combine script is executed per cluster, but we already have a key-value pair per cluster.
// Despite docs that say this is optional, this script can't be blank.
combine_script: 'return state',
// Reduce script is executed across all clusters, so we need to add up all the total from each cluster
// This also needs to account for having no data
reduce_script: `
Map actionRefs = [:];
long total = 0;
for (state in states) {
if (state !== null) {
total += state.total;
for (String k : state.actionRefs.keySet()) {
actionRefs.put(k, state.actionRefs.get(k));
}
}
}
Map result = new HashMap();
result.total = total;
result.actionRefs = actionRefs;
return result;
`,
},
};
const preconfiguredActionsScriptedMetric = getInMemoryActionScriptedMetric('preconfigured:');
const systemActionsScriptedMetric = getInMemoryActionScriptedMetric('system_action:');
const mustQuery = [
{
@ -238,6 +247,28 @@ export async function getInUseTotalCount(
},
},
},
{
nested: {
path: 'alert.actions',
query: {
bool: {
filter: {
bool: {
must: [
{
prefix: {
'alert.actions.actionRef': {
value: 'system_action:',
},
},
},
],
},
},
},
},
},
},
],
},
},
@ -250,7 +281,14 @@ export async function getInUseTotalCount(
}
try {
const actionResults = await esClient.search({
const actionResults = await esClient.search<
unknown,
{
refs: { actionRefIds: { value: { total: number; connectorIds: Record<string, string> } } };
preconfigured_actions: { preconfiguredActionRefIds: { value: InMemoryAggRes } };
system_actions: { systemActionRefIds: { value: InMemoryAggRes } };
}
>({
index: kibanaIndex,
size: 0,
body: {
@ -285,15 +323,27 @@ export async function getInUseTotalCount(
preconfiguredActionRefIds: preconfiguredActionsScriptedMetric,
},
},
system_actions: {
nested: {
path: 'alert.actions',
},
aggs: {
systemActionRefIds: systemActionsScriptedMetric,
},
},
},
},
});
// @ts-expect-error aggegation type is not specified
const aggs = actionResults.aggregations.refs.actionRefIds.value;
const aggs = actionResults.aggregations?.refs.actionRefIds.value;
const preconfiguredActionsAggs =
// @ts-expect-error aggegation type is not specified
actionResults.aggregations.preconfigured_actions?.preconfiguredActionRefIds.value;
actionResults.aggregations?.preconfigured_actions?.preconfiguredActionRefIds.value;
const systemActionsAggs = actionResults.aggregations?.system_actions?.systemActionRefIds.value;
const totalInMemoryActions =
(preconfiguredActionsAggs?.total ?? 0) + (systemActionsAggs?.total ?? 0);
const { hits: actions } = await esClient.search<{
action: ActionResult;
@ -310,7 +360,7 @@ export async function getInUseTotalCount(
},
{
terms: {
_id: Object.entries(aggs.connectorIds).map(([key]) => `action:${key}`),
_id: Object.entries(aggs?.connectorIds ?? {}).map(([key]) => `action:${key}`),
},
},
],
@ -352,31 +402,37 @@ export async function getInUseTotalCount(
}, {});
let preconfiguredAlertHistoryConnectors = 0;
const preconfiguredActionsRefs: Array<{
actionTypeId: string;
actionRef: string;
}> = preconfiguredActionsAggs ? Object.values(preconfiguredActionsAggs?.actionRefs) : [];
for (const { actionRef, actionTypeId: rawActionTypeId } of preconfiguredActionsRefs) {
const inMemoryActionsRefs = [
...Object.values(preconfiguredActionsAggs?.actionRefs ?? {}),
...Object.values(systemActionsAggs?.actionRefs ?? {}),
];
for (const { actionRef, actionTypeId: rawActionTypeId } of inMemoryActionsRefs) {
const actionTypeId = replaceFirstAndLastDotSymbols(rawActionTypeId);
countByActionTypeId[actionTypeId] = countByActionTypeId[actionTypeId] || 0;
countByActionTypeId[actionTypeId]++;
if (actionRef === `preconfigured:${AlertHistoryEsIndexConnectorId}`) {
preconfiguredAlertHistoryConnectors++;
}
if (inMemoryConnectors && actionTypeId === '__email') {
const preconfiguredConnectorId = actionRef.split(':')[1];
const inMemoryConnectorId = actionRef.split(':')[1];
const service = (inMemoryConnectors.find(
(preconfConnector) => preconfConnector.id === preconfiguredConnectorId
(connector) => connector.id === inMemoryConnectorId
)?.config?.service ?? 'other') as string;
const currentCount =
countEmailByService[service] !== undefined ? countEmailByService[service] : 0;
countEmailByService[service] = currentCount + 1;
}
}
return {
hasErrors: false,
countTotal: aggs.total + (preconfiguredActionsAggs?.total ?? 0),
countTotal: (aggs?.total ?? 0) + totalInMemoryActions,
countByType: countByActionTypeId,
countByAlertHistoryConnectorType: preconfiguredAlertHistoryConnectors,
countEmailByService,

View file

@ -72,13 +72,7 @@ export function telemetryTaskRunner(
getInMemoryConnectors: () => InMemoryConnector[],
eventLogIndex: string
) {
/**
* Filter out system actions from the
* inMemoryConnectors list.
*/
const inMemoryConnectors = getInMemoryConnectors().filter(
(inMemoryConnector) => inMemoryConnector.isPreconfigured
);
const inMemoryConnectors = getInMemoryConnectors();
return ({ taskInstance }: RunContext) => {
const state = taskInstance.state as LatestTaskStateSchema;

View file

@ -1168,6 +1168,265 @@ describe('create()', () => {
expect(actionsClient.isPreconfigured).toHaveBeenCalledTimes(3);
});
test('creates a rule with some actions using system connectors', async () => {
const data = getMockData({
actions: [
{
group: 'default',
id: '1',
params: {
foo: true,
},
},
{
group: 'default',
id: 'system_action-id',
params: {},
},
{
group: 'default',
id: '2',
params: {
foo: true,
},
},
],
});
actionsClient.getBulk.mockReset();
actionsClient.getBulk.mockResolvedValue([
{
id: '1',
actionTypeId: 'test',
config: {
from: 'me@me.com',
hasAuth: false,
host: 'hello',
port: 22,
secure: null,
service: null,
},
isMissingSecrets: false,
name: 'email connector',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
},
{
id: '2',
actionTypeId: 'test2',
config: {
from: 'me@me.com',
hasAuth: false,
host: 'hello',
port: 22,
secure: null,
service: null,
},
isMissingSecrets: false,
name: 'another email connector',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
},
{
id: 'system_action-id',
actionTypeId: 'test',
config: {},
isMissingSecrets: false,
name: 'system action connector',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: true,
},
]);
actionsClient.isSystemAction.mockReset();
actionsClient.isSystemAction.mockReturnValueOnce(false);
actionsClient.isSystemAction.mockReturnValueOnce(true);
actionsClient.isSystemAction.mockReturnValueOnce(false);
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
alertTypeId: '123',
schedule: { interval: '1m' },
params: {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
notifyWhen: null,
actions: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
params: {
foo: true,
},
},
{
group: 'default',
actionRef: 'system_action:system_action-id',
actionTypeId: 'test',
params: {},
},
{
group: 'default',
actionRef: 'action_2',
actionTypeId: 'test2',
params: {
foo: true,
},
},
],
running: false,
},
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
{
name: 'action_2',
type: 'action',
id: '2',
},
],
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
actions: [],
scheduledTaskId: 'task-123',
},
references: [],
});
const result = await rulesClient.create({ data });
expect(result).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {
"actionTypeId": "test",
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
Object {
"actionTypeId": "test",
"group": "default",
"id": "system_action-id",
"params": Object {},
},
Object {
"actionTypeId": "test2",
"group": "default",
"id": "2",
"params": Object {
"foo": true,
},
},
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"executionStatus": Object {
"lastExecutionDate": 2019-02-12T21:01:22.000Z,
"status": "pending",
},
"id": "1",
"notifyWhen": null,
"params": Object {
"bar": true,
},
"running": false,
"schedule": Object {
"interval": "1m",
},
"scheduledTaskId": "task-123",
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
'alert',
{
actions: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
params: {
foo: true,
},
uuid: '111',
},
{
group: 'default',
actionRef: 'system_action:system_action-id',
actionTypeId: 'test',
params: {},
uuid: '112',
},
{
group: 'default',
actionRef: 'action_2',
actionTypeId: 'test2',
params: {
foo: true,
},
uuid: '113',
},
],
alertTypeId: '123',
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
consumer: 'bar',
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
enabled: true,
legacyId: null,
executionStatus: {
lastExecutionDate: '2019-02-12T21:01:22.479Z',
status: 'pending',
},
monitoring: getDefaultMonitoring('2019-02-12T21:01:22.479Z'),
meta: { versionApiKeyLastmodified: kibanaVersion },
muteAll: false,
snoozeSchedule: [],
mutedInstanceIds: [],
name: 'abc',
notifyWhen: null,
params: { bar: true },
revision: 0,
running: false,
schedule: { interval: '1m' },
tags: ['foo'],
throttle: null,
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
},
{
id: 'mock-saved-object-id',
references: [
{ id: '1', name: 'action_0', type: 'action' },
{ id: '2', name: 'action_2', type: 'action' },
],
}
);
expect(actionsClient.isSystemAction).toHaveBeenCalledTimes(3);
});
test('creates a disabled alert', async () => {
const data = getMockData({ enabled: false });
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
@ -1345,7 +1604,7 @@ describe('create()', () => {
actionTypeId: 'test',
group: 'default',
params: { foo: true },
uuid: '112',
uuid: '115',
},
],
alertTypeId: '123',
@ -1532,7 +1791,7 @@ describe('create()', () => {
actionTypeId: 'test',
group: 'default',
params: { foo: true },
uuid: '113',
uuid: '116',
},
],
alertTypeId: '123',
@ -1707,7 +1966,7 @@ describe('create()', () => {
group: 'default',
actionTypeId: 'test',
params: { foo: true },
uuid: '115',
uuid: '118',
},
],
alertTypeId: '123',
@ -1848,7 +2107,7 @@ describe('create()', () => {
group: 'default',
actionTypeId: 'test',
params: { foo: true },
uuid: '116',
uuid: '119',
},
],
legacyId: null,
@ -1989,7 +2248,7 @@ describe('create()', () => {
group: 'default',
actionTypeId: 'test',
params: { foo: true },
uuid: '117',
uuid: '120',
},
],
legacyId: null,
@ -2157,7 +2416,7 @@ describe('create()', () => {
},
actionRef: 'action_0',
actionTypeId: 'test',
uuid: '118',
uuid: '121',
},
],
apiKeyOwner: null,
@ -2538,7 +2797,7 @@ describe('create()', () => {
group: 'default',
actionTypeId: 'test',
params: { foo: true },
uuid: '126',
uuid: '129',
},
],
alertTypeId: '123',
@ -2643,7 +2902,7 @@ describe('create()', () => {
group: 'default',
actionTypeId: 'test',
params: { foo: true },
uuid: '127',
uuid: '130',
},
],
legacyId: null,
@ -3375,7 +3634,7 @@ describe('create()', () => {
],
});
await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Failed to validate actions due to the following error: Action's alertsFilter must have either \\"query\\" or \\"timeframe\\" : 149"`
`"Failed to validate actions due to the following error: Action's alertsFilter must have either \\"query\\" or \\"timeframe\\" : 152"`
);
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
expect(taskManager.schedule).not.toHaveBeenCalled();
@ -3429,7 +3688,7 @@ describe('create()', () => {
],
});
await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Failed to validate actions due to the following error: This ruleType (Test) can't have an action with Alerts Filter. Actions: [150]"`
`"Failed to validate actions due to the following error: This ruleType (Test) can't have an action with Alerts Filter. Actions: [153]"`
);
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
expect(taskManager.schedule).not.toHaveBeenCalled();
@ -3500,7 +3759,7 @@ describe('create()', () => {
group: 'default',
actionTypeId: 'test',
params: { foo: true },
uuid: '151',
uuid: '154',
},
],
alertTypeId: '123',

View file

@ -16,6 +16,9 @@ export const extractedSavedObjectParamReferenceNamePrefix = 'param:';
// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects
export const preconfiguredConnectorActionRefPrefix = 'preconfigured:';
// NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects
export const systemConnectorActionRefPrefix = 'system_action:';
export const alertingAuthorizationFilterOpts: AlertingAuthorizationFilterOpts = {
type: AlertingAuthorizationFilterType.KQL,
fieldNames: { ruleTypeId: 'alert.attributes.alertTypeId', consumer: 'alert.attributes.consumer' },

View file

@ -14,6 +14,7 @@ import { RuleActionAttributes } from '../../data/rule/types';
import {
preconfiguredConnectorActionRefPrefix,
extractedSavedObjectParamReferenceNamePrefix,
systemConnectorActionRefPrefix,
} from './constants';
export function injectReferencesIntoActions(
@ -29,6 +30,13 @@ export function injectReferencesIntoActions(
};
}
if (action.actionRef.startsWith(systemConnectorActionRefPrefix)) {
return {
...omit(action, 'actionRef'),
id: action.actionRef.replace(systemConnectorActionRefPrefix, ''),
};
}
const reference = references.find((ref) => ref.name === action.actionRef);
if (!reference) {
throw new Error(`Action reference "${action.actionRef}" not found in alert id: ${alertId}`);

View file

@ -7,7 +7,10 @@
import { SavedObjectReference } from '@kbn/core/server';
import { RawRule } from '../../types';
import { preconfiguredConnectorActionRefPrefix } from '../common/constants';
import {
preconfiguredConnectorActionRefPrefix,
systemConnectorActionRefPrefix,
} from '../common/constants';
import { NormalizedAlertActionWithGeneratedValues, RulesClientContext } from '../types';
export async function denormalizeActions(
@ -19,7 +22,12 @@ export async function denormalizeActions(
if (alertActions.length) {
const actionsClient = await context.getActionsClient();
const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))];
const actionResults = await actionsClient.getBulk(actionIds);
const actionResults = await actionsClient.getBulk({
ids: actionIds,
throwIfSystemAction: false,
});
const actionTypeIds = [...new Set(actionResults.map((action) => action.actionTypeId))];
actionTypeIds.forEach((id) => {
// Notify action type usage via "isActionTypeEnabled" function
@ -34,6 +42,12 @@ export async function denormalizeActions(
actionRef: `${preconfiguredConnectorActionRefPrefix}${id}`,
actionTypeId: actionResultValue.actionTypeId,
});
} else if (actionsClient.isSystemAction(id)) {
actions.push({
...alertAction,
actionRef: `${systemConnectorActionRefPrefix}${id}`,
actionTypeId: actionResultValue.actionTypeId,
});
} else {
const actionRef = `action_${i}`;
references.push({

View file

@ -46,10 +46,14 @@ export async function validateActions(
// check for actions using connectors with missing secrets
const actionsClient = await context.getActionsClient();
const actionIds = [...new Set(actions.map((action) => action.id))];
const actionResults = (await actionsClient.getBulk(actionIds)) || [];
const actionResults =
(await actionsClient.getBulk({ ids: actionIds, throwIfSystemAction: false })) || [];
const actionsUsingConnectorsWithMissingSecrets = actionResults.filter(
(result) => result.isMissingSecrets
);
if (actionsUsingConnectorsWithMissingSecrets.length) {
if (allowMissingConnectorSecrets) {
context.logger.error(

View file

@ -296,6 +296,103 @@ describe('find()', () => {
`);
});
test('finds rules with actions using system connectors', async () => {
unsecuredSavedObjectsClient.find.mockReset();
unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
total: 1,
per_page: 10,
page: 1,
saved_objects: [
{
id: '1',
type: 'alert',
attributes: {
alertTypeId: 'myType',
schedule: { interval: '10s' },
params: {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
notifyWhen: 'onActiveAlert',
actions: [
{
group: 'default',
actionRef: 'action_0',
params: {
foo: true,
},
},
{
group: 'default',
actionRef: 'system_action:system_action-id',
params: {},
},
],
},
score: 1,
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
],
},
],
});
const rulesClient = new RulesClient(rulesClientParams);
const result = await rulesClient.find({ options: {} });
expect(result).toMatchInlineSnapshot(`
Object {
"data": Array [
Object {
"actions": Array [
Object {
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
Object {
"group": "default",
"id": "system_action-id",
"params": Object {},
},
],
"alertTypeId": "myType",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "1",
"notifyWhen": "onActiveAlert",
"params": Object {
"bar": true,
},
"schedule": Object {
"interval": "10s",
},
"snoozeSchedule": Array [],
"updatedAt": 2019-02-12T21:01:22.479Z,
},
],
"page": 1,
"perPage": 10,
"total": 1,
}
`);
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1);
expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"fields": undefined,
"filter": null,
"sortField": undefined,
"type": "alert",
},
]
`);
});
test('calls mapSortField', async () => {
const rulesClient = new RulesClient(rulesClientParams);
await rulesClient.find({ options: { sortField: 'name' } });

View file

@ -211,6 +211,83 @@ describe('get()', () => {
`);
});
test('gets rule with actions using system connectors', async () => {
const rulesClient = new RulesClient(rulesClientParams);
unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
alertTypeId: '123',
schedule: { interval: '10s' },
params: {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
actions: [
{
group: 'default',
actionRef: 'action_0',
params: {
foo: true,
},
},
{
group: 'default',
actionRef: 'system_action:system_action-id',
params: {},
},
],
notifyWhen: 'onActiveAlert',
},
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
],
});
const result = await rulesClient.get({ id: '1' });
expect(result).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
Object {
"group": "default",
"id": "system_action-id",
"params": Object {},
},
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"id": "1",
"notifyWhen": "onActiveAlert",
"params": Object {
"bar": true,
},
"schedule": Object {
"interval": "10s",
},
"snoozeSchedule": Array [],
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1);
expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"alert",
"1",
]
`);
});
test('should call useSavedObjectReferences.injectReferences if defined for rule type', async () => {
const injectReferencesFn = jest.fn().mockReturnValue({
bar: true,

View file

@ -720,6 +720,242 @@ describe('update()', () => {
expect(actionsClient.isPreconfigured).toHaveBeenCalledTimes(3);
});
test('should update a rule with some system actions', async () => {
actionsClient.getBulk.mockReset();
actionsClient.getBulk.mockResolvedValue([
{
id: '1',
actionTypeId: 'test',
config: {
from: 'me@me.com',
hasAuth: false,
host: 'hello',
port: 22,
secure: null,
service: null,
},
isMissingSecrets: false,
name: 'email connector',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
},
{
id: '2',
actionTypeId: 'test2',
config: {
from: 'me@me.com',
hasAuth: false,
host: 'hello',
port: 22,
secure: null,
service: null,
},
isMissingSecrets: false,
name: 'another email connector',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: false,
},
{
id: 'system_action-id',
actionTypeId: 'test',
config: {},
isMissingSecrets: false,
name: 'system action connector',
isPreconfigured: false,
isDeprecated: false,
isSystemAction: true,
},
]);
actionsClient.isSystemAction.mockReset();
actionsClient.isSystemAction.mockReturnValueOnce(false);
actionsClient.isSystemAction.mockReturnValueOnce(true);
actionsClient.isSystemAction.mockReturnValueOnce(true);
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
enabled: true,
schedule: { interval: '1m' },
params: {
bar: true,
},
actions: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
params: {
foo: true,
},
},
{
group: 'default',
actionRef: 'system_action:system_action-id',
actionTypeId: 'test',
params: {},
},
{
group: 'custom',
actionRef: 'system_action:system_action-id',
actionTypeId: 'test',
params: {},
},
],
notifyWhen: 'onActiveAlert',
revision: 1,
scheduledTaskId: 'task-123',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
{
name: 'param:soRef_0',
type: 'someSavedObjectType',
id: '9',
},
],
});
const result = await rulesClient.update({
id: '1',
data: {
schedule: { interval: '1m' },
name: 'abc',
tags: ['foo'],
params: {
bar: true,
},
throttle: null,
notifyWhen: 'onActiveAlert',
actions: [
{
group: 'default',
id: '1',
params: {
foo: true,
},
},
{
group: 'default',
id: 'system_action-id',
params: {},
},
{
group: 'custom',
id: 'system_action-id',
params: {},
},
],
},
});
expect(unsecuredSavedObjectsClient.create).toHaveBeenNthCalledWith(
1,
'alert',
{
actions: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
params: {
foo: true,
},
uuid: '106',
},
{
group: 'default',
actionRef: 'system_action:system_action-id',
actionTypeId: 'test',
params: {},
uuid: '107',
},
{
group: 'custom',
actionRef: 'system_action:system_action-id',
actionTypeId: 'test',
params: {},
uuid: '108',
},
],
alertTypeId: 'myType',
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
consumer: 'myApp',
enabled: true,
meta: { versionApiKeyLastmodified: 'v7.10.0' },
name: 'abc',
notifyWhen: 'onActiveAlert',
params: { bar: true },
revision: 1,
schedule: { interval: '1m' },
scheduledTaskId: 'task-123',
tags: ['foo'],
throttle: null,
updatedAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
},
{
id: '1',
overwrite: true,
references: [{ id: '1', name: 'action_0', type: 'action' }],
version: '123',
}
);
expect(result).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {
"actionTypeId": "test",
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
Object {
"actionTypeId": "test",
"group": "default",
"id": "system_action-id",
"params": Object {},
},
Object {
"actionTypeId": "test",
"group": "custom",
"id": "system_action-id",
"params": Object {},
},
],
"createdAt": 2019-02-12T21:01:22.479Z,
"enabled": true,
"id": "1",
"notifyWhen": "onActiveAlert",
"params": Object {
"bar": true,
},
"revision": 1,
"schedule": Object {
"interval": "1m",
},
"scheduledTaskId": "task-123",
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
namespace: 'default',
});
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
expect(actionsClient.isSystemAction).toHaveBeenCalledTimes(3);
});
test('should call useSavedObjectReferences.extractReferences and useSavedObjectReferences.injectReferences if defined for rule type', async () => {
const ruleParams = {
bar: true,
@ -832,7 +1068,7 @@ describe('update()', () => {
actionTypeId: 'test',
group: 'default',
params: { foo: true },
uuid: '106',
uuid: '109',
},
],
alertTypeId: 'myType',
@ -1012,7 +1248,7 @@ describe('update()', () => {
"params": Object {
"foo": true,
},
"uuid": "107",
"uuid": "110",
},
],
"alertTypeId": "myType",
@ -1165,7 +1401,7 @@ describe('update()', () => {
"params": Object {
"foo": true,
},
"uuid": "108",
"uuid": "111",
},
],
"alertTypeId": "myType",
@ -2189,7 +2425,7 @@ describe('update()', () => {
params: {
foo: true,
},
uuid: '144',
uuid: '147',
},
],
alertTypeId: 'myType',
@ -2740,7 +2976,7 @@ describe('update()', () => {
frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null },
group: 'default',
params: { foo: true },
uuid: '151',
uuid: '154',
},
],
alertTypeId: 'myType',
@ -2944,7 +3180,7 @@ describe('update()', () => {
"params": Object {
"foo": true,
},
"uuid": "152",
"uuid": "155",
},
],
"alertTypeId": "myType",

View file

@ -151,7 +151,7 @@ const getActionConnectors = async (
ids: string[]
): Promise<ActionResult[]> => {
try {
return await actionsClient.getBulk(ids);
return await actionsClient.getBulk({ ids });
} catch (error) {
// silent error and log it
logger.error(`Failed to retrieve action connectors in the get case connectors route: ${error}`);

View file

@ -157,12 +157,19 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
message: 'something important happened!',
},
},
{
id: 'system-connector-test.system-action',
group: 'default',
params: {},
},
],
})
);
expect(response.status).to.eql(200);
objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting');
expect(response.body).to.eql({
id: response.body.id,
name: 'abc',
@ -184,6 +191,13 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
},
uuid: response.body.actions[1].uuid,
},
{
id: 'system-connector-test.system-action',
group: 'default',
connector_type_id: 'test.system-action',
params: {},
uuid: response.body.actions[2].uuid,
},
],
enabled: true,
rule_type_id: 'test.noop',
@ -238,9 +252,17 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
},
uuid: rawActions[1].uuid,
},
{
actionRef: 'system_action:system-connector-test.system-action',
actionTypeId: 'test.system-action',
group: 'default',
params: {},
uuid: rawActions[2].uuid,
},
]);
const references = esResponse.body._source?.references ?? [];
expect(references.length).to.eql(1);
expect(references[0]).to.eql({
id: createdAction.id,