[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, isSystemAction: false,
}, },
{ {
actionTypeId: '.cases', actionTypeId: 'test.system-action',
config: {}, config: {},
id: 'system-connector-.cases', id: 'system-connector-test.system-action',
name: 'System action: .cases', name: 'System action: test.system-action',
secrets: {}, secrets: {},
isPreconfigured: false, isPreconfigured: false,
isDeprecated: false, isDeprecated: false,
@ -393,7 +393,7 @@ describe('actionTypeRegistry', () => {
const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams);
actionTypeRegistry.register({ actionTypeRegistry.register({
id: '.cases', id: 'test.system-action',
name: 'Cases', name: 'Cases',
minimumLicenseRequired: 'platinum', minimumLicenseRequired: 'platinum',
supportedFeatureIds: ['alerting'], supportedFeatureIds: ['alerting'],
@ -410,7 +410,7 @@ describe('actionTypeRegistry', () => {
expect(actionTypes).toEqual([ expect(actionTypes).toEqual([
{ {
id: '.cases', id: 'test.system-action',
name: 'Cases', name: 'Cases',
enabled: true, enabled: true,
enabledInConfig: true, enabledInConfig: true,
@ -497,13 +497,16 @@ describe('actionTypeRegistry', () => {
expect(actionTypeRegistry.isActionExecutable('my-slack1', 'foo')).toEqual(true); 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); mockedActionsConfig.isActionTypeEnabled.mockReturnValue(false);
mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true });
expect( expect(
actionTypeRegistry.isActionExecutable('system-connector-.cases', 'system-action-type') actionTypeRegistry.isActionExecutable(
).toEqual(true); 'system-connector-test.system-action',
'system-action-type'
)
).toEqual(false);
}); });
test('should call isLicenseValidForActionType of the license state with notifyUsage false by default', async () => { 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); const registry = new ActionTypeRegistry(actionTypeRegistryParams);
registry.register({ registry.register({
id: '.cases', id: 'test.system-action',
name: 'Cases', name: 'Cases',
minimumLicenseRequired: 'platinum', minimumLicenseRequired: 'platinum',
supportedFeatureIds: ['alerting'], supportedFeatureIds: ['alerting'],
@ -675,7 +678,7 @@ describe('actionTypeRegistry', () => {
executor, executor,
}); });
const result = registry.isSystemActionType('.cases'); const result = registry.isSystemActionType('test.system-action');
expect(result).toBe(true); expect(result).toBe(true);
}); });
@ -720,7 +723,7 @@ describe('actionTypeRegistry', () => {
const registry = new ActionTypeRegistry(actionTypeRegistryParams); const registry = new ActionTypeRegistry(actionTypeRegistryParams);
registry.register({ registry.register({
id: '.cases', id: 'test.system-action',
name: 'Cases', name: 'Cases',
minimumLicenseRequired: 'platinum', minimumLicenseRequired: 'platinum',
supportedFeatureIds: ['alerting'], supportedFeatureIds: ['alerting'],
@ -734,7 +737,7 @@ describe('actionTypeRegistry', () => {
executor, executor,
}); });
const result = registry.getSystemActionKibanaPrivileges('.cases'); const result = registry.getSystemActionKibanaPrivileges('test.system-action');
expect(result).toEqual(['test/create']); expect(result).toEqual(['test/create']);
}); });
@ -742,7 +745,7 @@ describe('actionTypeRegistry', () => {
const registry = new ActionTypeRegistry(actionTypeRegistryParams); const registry = new ActionTypeRegistry(actionTypeRegistryParams);
registry.register({ registry.register({
id: '.cases', id: 'test.system-action',
name: 'Cases', name: 'Cases',
minimumLicenseRequired: 'platinum', minimumLicenseRequired: 'platinum',
supportedFeatureIds: ['alerting'], supportedFeatureIds: ['alerting'],
@ -755,7 +758,7 @@ describe('actionTypeRegistry', () => {
executor, executor,
}); });
const result = registry.getSystemActionKibanaPrivileges('.cases'); const result = registry.getSystemActionKibanaPrivileges('test.system-action');
expect(result).toEqual([]); expect(result).toEqual([]);
}); });
@ -784,7 +787,7 @@ describe('actionTypeRegistry', () => {
const getKibanaPrivileges = jest.fn().mockReturnValue(['test/create']); const getKibanaPrivileges = jest.fn().mockReturnValue(['test/create']);
registry.register({ registry.register({
id: '.cases', id: 'test.system-action',
name: 'Cases', name: 'Cases',
minimumLicenseRequired: 'platinum', minimumLicenseRequired: 'platinum',
supportedFeatureIds: ['alerting'], supportedFeatureIds: ['alerting'],
@ -798,7 +801,7 @@ describe('actionTypeRegistry', () => {
executor, executor,
}); });
registry.getSystemActionKibanaPrivileges('.cases', { foo: 'bar' }); registry.getSystemActionKibanaPrivileges('test.system-action', { foo: 'bar' });
expect(getKibanaPrivileges).toHaveBeenCalledWith({ params: { 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( public isActionExecutable(
actionId: string, actionId: string,
@ -87,12 +89,11 @@ export class ActionTypeRegistry {
options: { notifyUsage: boolean } = { notifyUsage: false } options: { notifyUsage: boolean } = { notifyUsage: false }
) { ) {
const actionTypeEnabled = this.isActionTypeEnabled(actionTypeId, options); const actionTypeEnabled = this.isActionTypeEnabled(actionTypeId, options);
return ( const inMemoryConnector = this.inMemoryConnectors.find(
actionTypeEnabled || (connector) => connector.id === actionId
(!actionTypeEnabled &&
this.inMemoryConnectors.find((inMemoryConnector) => inMemoryConnector.id === actionId) !==
undefined)
); );
return actionTypeEnabled || (!actionTypeEnabled && inMemoryConnector?.isPreconfigured === true);
} }
/** /**

View file

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

View file

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

View file

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

View file

@ -524,7 +524,7 @@ describe('Actions Plugin', () => {
}); });
describe('System actions', () => { describe('System actions', () => {
it('should handle system actions', async () => { it('should set system actions correctly', async () => {
setup(getConfig()); setup(getConfig());
// coreMock.createSetup doesn't support Plugin generics // coreMock.createSetup doesn't support Plugin generics
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -573,6 +573,45 @@ describe('Actions Plugin', () => {
]); ]);
expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.cases')).toBe(true); 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, isPreconfigured: true,
isSystemAction: false, isSystemAction: false,
}; };
this.inMemoryConnectors.push({ this.inMemoryConnectors.push({
...rawPreconfiguredConnector, ...rawPreconfiguredConnector,
isDeprecated: isConnectorDeprecated(rawPreconfiguredConnector), isDeprecated: isConnectorDeprecated(rawPreconfiguredConnector),
@ -396,6 +397,8 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
includedHiddenTypes, includedHiddenTypes,
}); });
this.throwIfSystemActionsInConfig();
/** /**
* Warning: this call mutates the inMemory collection * Warning: this call mutates the inMemory collection
* *
@ -616,6 +619,16 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
this.inMemoryConnectors = [...this.inMemoryConnectors, ...systemConnectors]; 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 = ( private createRouteHandlerContext = (
core: CoreSetup<ActionsPluginsStart> core: CoreSetup<ActionsPluginsStart>
): IContextProvider<ActionsRequestHandlerContext, 'actions'> => { ): IContextProvider<ActionsRequestHandlerContext, 'actions'> => {

View file

@ -104,17 +104,17 @@ describe('actions telemetry', () => {
expect(mockEsClient.search).toHaveBeenCalledTimes(1); expect(mockEsClient.search).toHaveBeenCalledTimes(1);
expect(telemetry).toMatchInlineSnapshot(` expect(telemetry).toMatchInlineSnapshot(`
Object { Object {
"countByType": Object { "countByType": Object {
"__index": 1, "__index": 1,
"__server-log": 1, "__server-log": 1,
"another.type__": 1, "another.type__": 1,
"some.type": 1, "some.type": 1,
}, },
"countTotal": 4, "countTotal": 4,
"hasErrors": false, "hasErrors": false,
} }
`); `);
}); });
test('getTotalCount should return empty results if query throws error', async () => { test('getTotalCount should return empty results if query throws error', async () => {
@ -128,13 +128,13 @@ Object {
`Error executing actions telemetry task: getTotalCount - {}` `Error executing actions telemetry task: getTotalCount - {}`
); );
expect(telemetry).toMatchInlineSnapshot(` expect(telemetry).toMatchInlineSnapshot(`
Object { Object {
"countByType": Object {}, "countByType": Object {},
"countTotal": 0, "countTotal": 0,
"errorMessage": "oh no", "errorMessage": "oh no",
"hasErrors": true, "hasErrors": true,
} }
`); `);
}); });
test('getInUseTotalCount', async () => { test('getInUseTotalCount', async () => {
@ -188,18 +188,18 @@ Object {
expect(mockEsClient.search).toHaveBeenCalledTimes(2); expect(mockEsClient.search).toHaveBeenCalledTimes(2);
expect(telemetry).toMatchInlineSnapshot(` expect(telemetry).toMatchInlineSnapshot(`
Object { Object {
"countByAlertHistoryConnectorType": 0, "countByAlertHistoryConnectorType": 0,
"countByType": Object { "countByType": Object {
"__server-log": 1, "__server-log": 1,
"__slack": 1, "__slack": 1,
}, },
"countEmailByService": Object {}, "countEmailByService": Object {},
"countNamespaces": 1, "countNamespaces": 1,
"countTotal": 2, "countTotal": 2,
"hasErrors": false, "hasErrors": false,
} }
`); `);
}); });
test('getInUseTotalCount should count preconfigured alert history connector usage', async () => { test('getInUseTotalCount should count preconfigured alert history connector usage', async () => {
@ -292,19 +292,19 @@ Object {
expect(mockEsClient.search).toHaveBeenCalledTimes(2); expect(mockEsClient.search).toHaveBeenCalledTimes(2);
expect(telemetry).toMatchInlineSnapshot(` expect(telemetry).toMatchInlineSnapshot(`
Object { Object {
"countByAlertHistoryConnectorType": 1, "countByAlertHistoryConnectorType": 1,
"countByType": Object { "countByType": Object {
"__index": 1, "__index": 1,
"__server-log": 1, "__server-log": 1,
"__slack": 1, "__slack": 1,
}, },
"countEmailByService": Object {}, "countEmailByService": Object {},
"countNamespaces": 1, "countNamespaces": 1,
"countTotal": 4, "countTotal": 4,
"hasErrors": false, "hasErrors": false,
} }
`); `);
}); });
test('getInUseTotalCount should return empty results if query throws error', async () => { test('getInUseTotalCount should return empty results if query throws error', async () => {
@ -318,16 +318,16 @@ Object {
`Error executing actions telemetry task: getInUseTotalCount - {}` `Error executing actions telemetry task: getInUseTotalCount - {}`
); );
expect(telemetry).toMatchInlineSnapshot(` expect(telemetry).toMatchInlineSnapshot(`
Object { Object {
"countByAlertHistoryConnectorType": 0, "countByAlertHistoryConnectorType": 0,
"countByType": Object {}, "countByType": Object {},
"countEmailByService": Object {}, "countEmailByService": Object {},
"countNamespaces": 0, "countNamespaces": 0,
"countTotal": 0, "countTotal": 0,
"errorMessage": "oh no", "errorMessage": "oh no",
"hasErrors": true, "hasErrors": true,
} }
`); `);
}); });
test('getTotalCount accounts for preconfigured connectors', async () => { test('getTotalCount accounts for preconfigured connectors', async () => {
@ -443,18 +443,61 @@ Object {
expect(mockEsClient.search).toHaveBeenCalledTimes(1); expect(mockEsClient.search).toHaveBeenCalledTimes(1);
expect(telemetry).toMatchInlineSnapshot(` expect(telemetry).toMatchInlineSnapshot(`
Object { Object {
"countByType": Object { "countByType": Object {
"__index": 1, "__index": 1,
"__server-log": 2, "__server-log": 2,
"__test": 1, "__test": 1,
"another.type__": 1, "another.type__": 1,
"some.type": 1, "some.type": 1,
}, },
"countTotal": 6, "countTotal": 6,
"hasErrors": false, "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 () => { test('getInUseTotalCount() accounts for preconfigured connectors', async () => {
@ -550,22 +593,105 @@ Object {
expect(mockEsClient.search).toHaveBeenCalledTimes(2); expect(mockEsClient.search).toHaveBeenCalledTimes(2);
expect(telemetry).toMatchInlineSnapshot(` expect(telemetry).toMatchInlineSnapshot(`
Object { Object {
"countByAlertHistoryConnectorType": 1, "countByAlertHistoryConnectorType": 1,
"countByType": Object { "countByType": Object {
"__email": 3, "__email": 3,
"__index": 1, "__index": 1,
"__server-log": 1, "__server-log": 1,
"__slack": 1, "__slack": 1,
}, },
"countEmailByService": Object { "countEmailByService": Object {
"other": 3, "other": 3,
}, },
"countNamespaces": 1, "countNamespaces": 1,
"countTotal": 6, "countTotal": 6,
"hasErrors": false, "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 () => { test('getInUseTotalCount() accounts for actions namespaces', async () => {
@ -650,22 +776,22 @@ Object {
expect(mockEsClient.search).toHaveBeenCalledTimes(2); expect(mockEsClient.search).toHaveBeenCalledTimes(2);
expect(telemetry).toMatchInlineSnapshot(` expect(telemetry).toMatchInlineSnapshot(`
Object { Object {
"countByAlertHistoryConnectorType": 1, "countByAlertHistoryConnectorType": 1,
"countByType": Object { "countByType": Object {
"__email": 3, "__email": 3,
"__index": 1, "__index": 1,
"__server-log": 1, "__server-log": 1,
"__slack": 1, "__slack": 1,
}, },
"countEmailByService": Object { "countEmailByService": Object {
"other": 1, "other": 1,
}, },
"countNamespaces": 3, "countNamespaces": 3,
"countTotal": 6, "countTotal": 6,
"hasErrors": false, "hasErrors": false,
} }
`); `);
}); });
test('getExecutionsTotalCount', async () => { test('getExecutionsTotalCount', async () => {
@ -818,17 +944,17 @@ Object {
`Error executing actions telemetry task: getExecutionsPerDayCount - {}` `Error executing actions telemetry task: getExecutionsPerDayCount - {}`
); );
expect(telemetry).toMatchInlineSnapshot(` expect(telemetry).toMatchInlineSnapshot(`
Object { Object {
"avgExecutionTime": 0, "avgExecutionTime": 0,
"avgExecutionTimeByType": Object {}, "avgExecutionTimeByType": Object {},
"countByType": Object {}, "countByType": Object {},
"countFailed": 0, "countFailed": 0,
"countFailedByType": Object {}, "countFailedByType": Object {},
"countRunOutcomeByConnectorType": Object {}, "countRunOutcomeByConnectorType": Object {},
"countTotal": 0, "countTotal": 0,
"errorMessage": "oh no", "errorMessage": "oh no",
"hasErrors": true, "hasErrors": true,
} }
`); `);
}); });
}); });

View file

@ -15,6 +15,11 @@ import {
import { AlertHistoryEsIndexConnectorId } from '../../common'; import { AlertHistoryEsIndexConnectorId } from '../../common';
import { ActionResult, InMemoryConnector } from '../types'; import { ActionResult, InMemoryConnector } from '../types';
interface InMemoryAggRes {
total: number;
actionRefs: Record<string, { actionRef: string; actionTypeId: string }>;
}
export async function getTotalCount( export async function getTotalCount(
esClient: ElasticsearchClient, esClient: ElasticsearchClient,
kibanaIndex: string, kibanaIndex: string,
@ -49,7 +54,10 @@ export async function getTotalCount(
}, },
}; };
try { try {
const searchResult = await esClient.search({ const searchResult = await esClient.search<
unknown,
{ byActionTypeId: { value: { types: Record<string, number> } } }
>({
index: kibanaIndex, index: kibanaIndex,
size: 0, size: 0,
body: { 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 aggs = searchResult.aggregations?.byActionTypeId.value?.types ?? {};
const countByType = Object.keys(aggs).reduce(
// ES DSL aggregations are returned as `any` by esClient.search const countByType = Object.keys(aggs).reduce<Record<string, number>>((obj, key) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any obj[replaceFirstAndLastDotSymbols(key)] = aggs[key];
(obj: any, key: string) => { return obj;
obj[replaceFirstAndLastDotSymbols(key)] = aggs[key]; }, {});
return obj;
},
{}
);
if (inMemoryConnectors && inMemoryConnectors.length) { if (inMemoryConnectors && inMemoryConnectors.length) {
for (const inMemoryConnector of inMemoryConnectors) { for (const inMemoryConnector of inMemoryConnectors) {
const actionTypeId = replaceFirstAndLastDotSymbols(inMemoryConnector.actionTypeId); const actionTypeId = replaceFirstAndLastDotSymbols(inMemoryConnector.actionTypeId);
@ -81,13 +86,14 @@ export async function getTotalCount(
countByType[actionTypeId]++; countByType[actionTypeId]++;
} }
} }
const totals =
Object.keys(aggs).reduce((total, key) => parseInt(aggs[key].toString(), 10) + total, 0) +
(inMemoryConnectors?.length ?? 0);
return { return {
hasErrors: false, hasErrors: false,
countTotal: countTotal: totals,
Object.keys(aggs).reduce(
(total: number, key: string) => parseInt(aggs[key], 10) + total,
0
) + (inMemoryConnectors?.length ?? 0),
countByType, countByType,
}; };
} catch (err) { } catch (err) {
@ -119,6 +125,44 @@ export async function getInUseTotalCount(
countEmailByService: Record<string, number>; countEmailByService: Record<string, number>;
countNamespaces: 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 = { const scriptedMetric = {
scripted_metric: { scripted_metric: {
init_script: 'state.connectorIds = new HashMap(); state.total = 0;', init_script: 'state.connectorIds = new HashMap(); state.total = 0;',
@ -154,43 +198,8 @@ export async function getInUseTotalCount(
}, },
}; };
const preconfiguredActionsScriptedMetric = { const preconfiguredActionsScriptedMetric = getInMemoryActionScriptedMetric('preconfigured:');
scripted_metric: { const systemActionsScriptedMetric = getInMemoryActionScriptedMetric('system_action:');
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 mustQuery = [ 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 { 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, index: kibanaIndex,
size: 0, size: 0,
body: { body: {
@ -285,15 +323,27 @@ export async function getInUseTotalCount(
preconfiguredActionRefIds: preconfiguredActionsScriptedMetric, 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 = 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<{ const { hits: actions } = await esClient.search<{
action: ActionResult; action: ActionResult;
@ -310,7 +360,7 @@ export async function getInUseTotalCount(
}, },
{ {
terms: { 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; let preconfiguredAlertHistoryConnectors = 0;
const preconfiguredActionsRefs: Array<{
actionTypeId: string; const inMemoryActionsRefs = [
actionRef: string; ...Object.values(preconfiguredActionsAggs?.actionRefs ?? {}),
}> = preconfiguredActionsAggs ? Object.values(preconfiguredActionsAggs?.actionRefs) : []; ...Object.values(systemActionsAggs?.actionRefs ?? {}),
for (const { actionRef, actionTypeId: rawActionTypeId } of preconfiguredActionsRefs) { ];
for (const { actionRef, actionTypeId: rawActionTypeId } of inMemoryActionsRefs) {
const actionTypeId = replaceFirstAndLastDotSymbols(rawActionTypeId); const actionTypeId = replaceFirstAndLastDotSymbols(rawActionTypeId);
countByActionTypeId[actionTypeId] = countByActionTypeId[actionTypeId] || 0; countByActionTypeId[actionTypeId] = countByActionTypeId[actionTypeId] || 0;
countByActionTypeId[actionTypeId]++; countByActionTypeId[actionTypeId]++;
if (actionRef === `preconfigured:${AlertHistoryEsIndexConnectorId}`) { if (actionRef === `preconfigured:${AlertHistoryEsIndexConnectorId}`) {
preconfiguredAlertHistoryConnectors++; preconfiguredAlertHistoryConnectors++;
} }
if (inMemoryConnectors && actionTypeId === '__email') { if (inMemoryConnectors && actionTypeId === '__email') {
const preconfiguredConnectorId = actionRef.split(':')[1]; const inMemoryConnectorId = actionRef.split(':')[1];
const service = (inMemoryConnectors.find( const service = (inMemoryConnectors.find(
(preconfConnector) => preconfConnector.id === preconfiguredConnectorId (connector) => connector.id === inMemoryConnectorId
)?.config?.service ?? 'other') as string; )?.config?.service ?? 'other') as string;
const currentCount = const currentCount =
countEmailByService[service] !== undefined ? countEmailByService[service] : 0; countEmailByService[service] !== undefined ? countEmailByService[service] : 0;
countEmailByService[service] = currentCount + 1; countEmailByService[service] = currentCount + 1;
} }
} }
return { return {
hasErrors: false, hasErrors: false,
countTotal: aggs.total + (preconfiguredActionsAggs?.total ?? 0), countTotal: (aggs?.total ?? 0) + totalInMemoryActions,
countByType: countByActionTypeId, countByType: countByActionTypeId,
countByAlertHistoryConnectorType: preconfiguredAlertHistoryConnectors, countByAlertHistoryConnectorType: preconfiguredAlertHistoryConnectors,
countEmailByService, countEmailByService,

View file

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

View file

@ -1168,6 +1168,265 @@ describe('create()', () => {
expect(actionsClient.isPreconfigured).toHaveBeenCalledTimes(3); 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 () => { test('creates a disabled alert', async () => {
const data = getMockData({ enabled: false }); const data = getMockData({ enabled: false });
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
@ -1345,7 +1604,7 @@ describe('create()', () => {
actionTypeId: 'test', actionTypeId: 'test',
group: 'default', group: 'default',
params: { foo: true }, params: { foo: true },
uuid: '112', uuid: '115',
}, },
], ],
alertTypeId: '123', alertTypeId: '123',
@ -1532,7 +1791,7 @@ describe('create()', () => {
actionTypeId: 'test', actionTypeId: 'test',
group: 'default', group: 'default',
params: { foo: true }, params: { foo: true },
uuid: '113', uuid: '116',
}, },
], ],
alertTypeId: '123', alertTypeId: '123',
@ -1707,7 +1966,7 @@ describe('create()', () => {
group: 'default', group: 'default',
actionTypeId: 'test', actionTypeId: 'test',
params: { foo: true }, params: { foo: true },
uuid: '115', uuid: '118',
}, },
], ],
alertTypeId: '123', alertTypeId: '123',
@ -1848,7 +2107,7 @@ describe('create()', () => {
group: 'default', group: 'default',
actionTypeId: 'test', actionTypeId: 'test',
params: { foo: true }, params: { foo: true },
uuid: '116', uuid: '119',
}, },
], ],
legacyId: null, legacyId: null,
@ -1989,7 +2248,7 @@ describe('create()', () => {
group: 'default', group: 'default',
actionTypeId: 'test', actionTypeId: 'test',
params: { foo: true }, params: { foo: true },
uuid: '117', uuid: '120',
}, },
], ],
legacyId: null, legacyId: null,
@ -2157,7 +2416,7 @@ describe('create()', () => {
}, },
actionRef: 'action_0', actionRef: 'action_0',
actionTypeId: 'test', actionTypeId: 'test',
uuid: '118', uuid: '121',
}, },
], ],
apiKeyOwner: null, apiKeyOwner: null,
@ -2538,7 +2797,7 @@ describe('create()', () => {
group: 'default', group: 'default',
actionTypeId: 'test', actionTypeId: 'test',
params: { foo: true }, params: { foo: true },
uuid: '126', uuid: '129',
}, },
], ],
alertTypeId: '123', alertTypeId: '123',
@ -2643,7 +2902,7 @@ describe('create()', () => {
group: 'default', group: 'default',
actionTypeId: 'test', actionTypeId: 'test',
params: { foo: true }, params: { foo: true },
uuid: '127', uuid: '130',
}, },
], ],
legacyId: null, legacyId: null,
@ -3375,7 +3634,7 @@ describe('create()', () => {
], ],
}); });
await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( 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(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
expect(taskManager.schedule).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled();
@ -3429,7 +3688,7 @@ describe('create()', () => {
], ],
}); });
await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( 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(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
expect(taskManager.schedule).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled();
@ -3500,7 +3759,7 @@ describe('create()', () => {
group: 'default', group: 'default',
actionTypeId: 'test', actionTypeId: 'test',
params: { foo: true }, params: { foo: true },
uuid: '151', uuid: '154',
}, },
], ],
alertTypeId: '123', 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 // NOTE: Changing this prefix will require a migration to update the prefix in all existing `rule` saved objects
export const preconfiguredConnectorActionRefPrefix = 'preconfigured:'; 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 = { export const alertingAuthorizationFilterOpts: AlertingAuthorizationFilterOpts = {
type: AlertingAuthorizationFilterType.KQL, type: AlertingAuthorizationFilterType.KQL,
fieldNames: { ruleTypeId: 'alert.attributes.alertTypeId', consumer: 'alert.attributes.consumer' }, fieldNames: { ruleTypeId: 'alert.attributes.alertTypeId', consumer: 'alert.attributes.consumer' },

View file

@ -14,6 +14,7 @@ import { RuleActionAttributes } from '../../data/rule/types';
import { import {
preconfiguredConnectorActionRefPrefix, preconfiguredConnectorActionRefPrefix,
extractedSavedObjectParamReferenceNamePrefix, extractedSavedObjectParamReferenceNamePrefix,
systemConnectorActionRefPrefix,
} from './constants'; } from './constants';
export function injectReferencesIntoActions( 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); const reference = references.find((ref) => ref.name === action.actionRef);
if (!reference) { if (!reference) {
throw new Error(`Action reference "${action.actionRef}" not found in alert id: ${alertId}`); 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 { SavedObjectReference } from '@kbn/core/server';
import { RawRule } from '../../types'; import { RawRule } from '../../types';
import { preconfiguredConnectorActionRefPrefix } from '../common/constants'; import {
preconfiguredConnectorActionRefPrefix,
systemConnectorActionRefPrefix,
} from '../common/constants';
import { NormalizedAlertActionWithGeneratedValues, RulesClientContext } from '../types'; import { NormalizedAlertActionWithGeneratedValues, RulesClientContext } from '../types';
export async function denormalizeActions( export async function denormalizeActions(
@ -19,7 +22,12 @@ export async function denormalizeActions(
if (alertActions.length) { if (alertActions.length) {
const actionsClient = await context.getActionsClient(); const actionsClient = await context.getActionsClient();
const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))]; 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))]; const actionTypeIds = [...new Set(actionResults.map((action) => action.actionTypeId))];
actionTypeIds.forEach((id) => { actionTypeIds.forEach((id) => {
// Notify action type usage via "isActionTypeEnabled" function // Notify action type usage via "isActionTypeEnabled" function
@ -34,6 +42,12 @@ export async function denormalizeActions(
actionRef: `${preconfiguredConnectorActionRefPrefix}${id}`, actionRef: `${preconfiguredConnectorActionRefPrefix}${id}`,
actionTypeId: actionResultValue.actionTypeId, actionTypeId: actionResultValue.actionTypeId,
}); });
} else if (actionsClient.isSystemAction(id)) {
actions.push({
...alertAction,
actionRef: `${systemConnectorActionRefPrefix}${id}`,
actionTypeId: actionResultValue.actionTypeId,
});
} else { } else {
const actionRef = `action_${i}`; const actionRef = `action_${i}`;
references.push({ references.push({

View file

@ -46,10 +46,14 @@ export async function validateActions(
// check for actions using connectors with missing secrets // check for actions using connectors with missing secrets
const actionsClient = await context.getActionsClient(); const actionsClient = await context.getActionsClient();
const actionIds = [...new Set(actions.map((action) => action.id))]; 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( const actionsUsingConnectorsWithMissingSecrets = actionResults.filter(
(result) => result.isMissingSecrets (result) => result.isMissingSecrets
); );
if (actionsUsingConnectorsWithMissingSecrets.length) { if (actionsUsingConnectorsWithMissingSecrets.length) {
if (allowMissingConnectorSecrets) { if (allowMissingConnectorSecrets) {
context.logger.error( 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 () => { test('calls mapSortField', async () => {
const rulesClient = new RulesClient(rulesClientParams); const rulesClient = new RulesClient(rulesClientParams);
await rulesClient.find({ options: { sortField: 'name' } }); 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 () => { test('should call useSavedObjectReferences.injectReferences if defined for rule type', async () => {
const injectReferencesFn = jest.fn().mockReturnValue({ const injectReferencesFn = jest.fn().mockReturnValue({
bar: true, bar: true,

View file

@ -720,6 +720,242 @@ describe('update()', () => {
expect(actionsClient.isPreconfigured).toHaveBeenCalledTimes(3); 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 () => { test('should call useSavedObjectReferences.extractReferences and useSavedObjectReferences.injectReferences if defined for rule type', async () => {
const ruleParams = { const ruleParams = {
bar: true, bar: true,
@ -832,7 +1068,7 @@ describe('update()', () => {
actionTypeId: 'test', actionTypeId: 'test',
group: 'default', group: 'default',
params: { foo: true }, params: { foo: true },
uuid: '106', uuid: '109',
}, },
], ],
alertTypeId: 'myType', alertTypeId: 'myType',
@ -1012,7 +1248,7 @@ describe('update()', () => {
"params": Object { "params": Object {
"foo": true, "foo": true,
}, },
"uuid": "107", "uuid": "110",
}, },
], ],
"alertTypeId": "myType", "alertTypeId": "myType",
@ -1165,7 +1401,7 @@ describe('update()', () => {
"params": Object { "params": Object {
"foo": true, "foo": true,
}, },
"uuid": "108", "uuid": "111",
}, },
], ],
"alertTypeId": "myType", "alertTypeId": "myType",
@ -2189,7 +2425,7 @@ describe('update()', () => {
params: { params: {
foo: true, foo: true,
}, },
uuid: '144', uuid: '147',
}, },
], ],
alertTypeId: 'myType', alertTypeId: 'myType',
@ -2740,7 +2976,7 @@ describe('update()', () => {
frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null }, frequency: { notifyWhen: 'onActiveAlert', summary: false, throttle: null },
group: 'default', group: 'default',
params: { foo: true }, params: { foo: true },
uuid: '151', uuid: '154',
}, },
], ],
alertTypeId: 'myType', alertTypeId: 'myType',
@ -2944,7 +3180,7 @@ describe('update()', () => {
"params": Object { "params": Object {
"foo": true, "foo": true,
}, },
"uuid": "152", "uuid": "155",
}, },
], ],
"alertTypeId": "myType", "alertTypeId": "myType",

View file

@ -151,7 +151,7 @@ const getActionConnectors = async (
ids: string[] ids: string[]
): Promise<ActionResult[]> => { ): Promise<ActionResult[]> => {
try { try {
return await actionsClient.getBulk(ids); return await actionsClient.getBulk({ ids });
} catch (error) { } catch (error) {
// silent error and log it // silent error and log it
logger.error(`Failed to retrieve action connectors in the get case connectors route: ${error}`); 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!', message: 'something important happened!',
}, },
}, },
{
id: 'system-connector-test.system-action',
group: 'default',
params: {},
},
], ],
}) })
); );
expect(response.status).to.eql(200); expect(response.status).to.eql(200);
objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting'); objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting');
expect(response.body).to.eql({ expect(response.body).to.eql({
id: response.body.id, id: response.body.id,
name: 'abc', name: 'abc',
@ -184,6 +191,13 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
}, },
uuid: response.body.actions[1].uuid, 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, enabled: true,
rule_type_id: 'test.noop', rule_type_id: 'test.noop',
@ -238,9 +252,17 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
}, },
uuid: rawActions[1].uuid, 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 ?? []; const references = esResponse.body._source?.references ?? [];
expect(references.length).to.eql(1); expect(references.length).to.eql(1);
expect(references[0]).to.eql({ expect(references[0]).to.eql({
id: createdAction.id, id: createdAction.id,