mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
971c6bd5eb
commit
f8a16009f9
20 changed files with 1262 additions and 295 deletions
|
@ -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' } });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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: [],
|
||||
},
|
||||
|
|
|
@ -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"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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'> => {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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' } });
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue