From 331eb60a8b243dada0780e268c11bea0067439b3 Mon Sep 17 00:00:00 2001 From: Alexi Doak <109488926+doakalexi@users.noreply.github.com> Date: Tue, 11 Apr 2023 10:29:56 -0400 Subject: [PATCH] [ResponseOps] Allow users authenticated with an API keys to manage alerting rules (#154189) Resolves https://github.com/elastic/kibana/issues/152140 ## Summary Updates the following functions in the Rules Client to re-use the API key in context and avoid having the system invalidate them when no longer in use: - bulk_delete - bulk_edit - clone - create - delete - update - update_api_key Also adds a new field to the rule SO to help determine when whether an api key was created by a user or created by us. ### Checklist - [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 ### To verify - Follow these [instructions](https://www.elastic.co/guide/en/kibana/master/api-keys.html#create-api-key) to create an api key. Make sure to copy your api key - Run the following ``` curl -X POST "http://localhost:5601/api/alerting/rule/" -H 'Authorization: ApiKey ${API_KEY}' -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d' { "rule_type_id": "example.pattern", "name": "pattern", "schedule": { "interval": "5s" }, "actions": [ ], "consumer": "alerts", "tags": [], "notify_when": "onActionGroupChange", "params": { "patterns": { "instA": " a - - a " } } }' ``` - Verify that the request returns a rule with`"api_key_created_by_user":true` - Try this with the other rules clients functions listed above to verify that you can manage alerting rules when authenticated with an api key - Verify that `"api_key_created_by_user":false` when you remove the api key header and add `-u ${USERNAME}:${PASSWORD}` to authenticate --- .../group2/check_registered_types.test.ts | 2 +- x-pack/plugins/alerting/common/rule.ts | 1 + .../public/lib/common_transformations.ts | 3 + .../alerting/server/routes/clone_rule.ts | 2 + .../alerting/server/routes/create_rule.ts | 2 + .../alerting/server/routes/get_rule.ts | 2 + .../server/routes/lib/rewrite_rule.ts | 2 + .../alerting/server/routes/resolve_rule.ts | 2 + .../alerting/server/routes/update_rule.ts | 2 + .../api_key_as_alert_attributes.test.ts | 68 ++++++ .../common/api_key_as_alert_attributes.ts | 7 +- .../lib/create_new_api_key_set.test.ts | 169 ++++++++++++++ .../lib/create_new_api_key_set.ts | 28 ++- .../lib/create_rule_saved_object.ts | 2 +- .../rules_client/methods/bulk_delete.ts | 2 +- .../server/rules_client/methods/bulk_edit.ts | 48 ++-- .../rules_client/methods/bulk_enable.ts | 7 +- .../server/rules_client/methods/clone.ts | 21 +- .../server/rules_client/methods/create.ts | 18 +- .../server/rules_client/methods/delete.ts | 4 +- .../server/rules_client/methods/enable.ts | 8 +- .../server/rules_client/methods/update.ts | 28 +-- .../rules_client/methods/update_api_key.ts | 40 ++-- .../server/rules_client/rules_client.ts | 1 + .../rules_client/tests/aggregate.test.ts | 2 + .../rules_client/tests/bulk_delete.test.ts | 69 +++++- .../rules_client/tests/bulk_disable.test.ts | 2 + .../rules_client/tests/bulk_edit.test.ts | 144 ++++++++++++ .../rules_client/tests/bulk_enable.test.ts | 2 + .../tests/clear_expired_snoozes.test.ts | 2 + .../server/rules_client/tests/create.test.ts | 145 ++++++++++++ .../server/rules_client/tests/delete.test.ts | 16 ++ .../server/rules_client/tests/disable.test.ts | 2 + .../server/rules_client/tests/enable.test.ts | 4 +- .../server/rules_client/tests/find.test.ts | 2 + .../server/rules_client/tests/get.test.ts | 2 + .../tests/get_action_error_log.test.ts | 2 + .../tests/get_alert_state.test.ts | 2 + .../tests/get_alert_summary.test.ts | 2 + .../tests/get_execution_log.test.ts | 2 + .../tests/list_alert_types.test.ts | 2 + .../rules_client/tests/mute_all.test.ts | 2 + .../rules_client/tests/mute_instance.test.ts | 2 + .../server/rules_client/tests/resolve.test.ts | 2 + .../rules_client/tests/run_soon.test.ts | 2 + .../rules_client/tests/unmute_all.test.ts | 2 + .../tests/unmute_instance.test.ts | 2 + .../server/rules_client/tests/update.test.ts | 215 ++++++++++++++++++ .../rules_client/tests/update_api_key.test.ts | 108 +++++++++ .../alerting/server/rules_client/types.ts | 2 + .../rules_client_conflict_retries.test.ts | 2 + .../server/rules_client_factory.test.ts | 4 + .../alerting/server/rules_client_factory.ts | 30 ++- .../alerting/server/saved_objects/mappings.ts | 1 + .../transform_rule_for_export.test.ts | 3 + .../transform_rule_for_export.ts | 1 + x-pack/plugins/alerting/server/types.ts | 1 + .../alerts/license_expiration_rule.test.ts | 20 +- .../group1/tests/alerting/create.ts | 1 + .../group1/tests/alerting/get.ts | 1 + .../group2/tests/alerting/update.ts | 5 + .../group3/tests/alerting/bulk_delete.ts | 2 + .../group3/tests/alerting/bulk_disable.ts | 1 + .../group3/tests/alerting/bulk_enable.ts | 1 + .../group3/tests/alerting/clone.ts | 1 + .../tests/alerting/group1/create.ts | 3 + .../spaces_only/tests/alerting/group1/find.ts | 2 + .../spaces_only/tests/alerting/group1/get.ts | 2 + .../tests/alerting/group2/update.ts | 2 + .../synthetics/enable_default_alerting.ts | 1 + 70 files changed, 1191 insertions(+), 101 deletions(-) create mode 100644 x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.test.ts create mode 100644 x-pack/plugins/alerting/server/rules_client/lib/create_new_api_key_set.test.ts diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts index 2d1e44557b4f7..f193e1a88b97f 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts @@ -57,7 +57,7 @@ describe('checking migration metadata changes on all registered SO types', () => Object { "action": "6cfc277ed3211639e37546ac625f4a68f2494215", "action_task_params": "5f419caba96dd8c77d0f94013e71d43890e3d5d6", - "alert": "1e4cd6941f1eb39c729c646e91fbfb9700de84b9", + "alert": "7bec97d7775a025ecf36a33baf17386b9e7b4c3c", "api_key_pending_invalidation": "16e7bcf8e78764102d7f525542d5b616809a21ee", "apm-indices": "d19dd7fb51f2d2cbc1f8769481721e0953f9a6d2", "apm-server-schema": "1d42f17eff9ec6c16d3a9324d9539e2d123d0a9a", diff --git a/x-pack/plugins/alerting/common/rule.ts b/x-pack/plugins/alerting/common/rule.ts index e0616dbc142f8..e2b42e8ccf292 100644 --- a/x-pack/plugins/alerting/common/rule.ts +++ b/x-pack/plugins/alerting/common/rule.ts @@ -168,6 +168,7 @@ export interface Rule { updatedAt: Date; apiKey: string | null; apiKeyOwner: string | null; + apiKeyCreatedByUser?: boolean | null; throttle?: string | null; muteAll: boolean; notifyWhen?: RuleNotifyWhenType | null; diff --git a/x-pack/plugins/alerting/public/lib/common_transformations.ts b/x-pack/plugins/alerting/public/lib/common_transformations.ts index b123962bb4ea7..e79bed27ec3d8 100644 --- a/x-pack/plugins/alerting/public/lib/common_transformations.ts +++ b/x-pack/plugins/alerting/public/lib/common_transformations.ts @@ -109,6 +109,7 @@ export function transformRule(input: ApiRule): Rule { updated_at: updatedAt, api_key: apiKey, api_key_owner: apiKeyOwner, + api_key_created_by_user: apiKeyCreatedByUser, notify_when: notifyWhen, mute_all: muteAll, muted_alert_ids: mutedInstanceIds, @@ -140,6 +141,8 @@ export function transformRule(input: ApiRule): Rule { ...(nextRun ? { nextRun: new Date(nextRun) } : {}), ...(monitoring ? { monitoring: transformMonitoring(monitoring) } : {}), ...(lastRun ? { lastRun: transformLastRun(lastRun) } : {}), + ...(apiKeyCreatedByUser !== undefined ? { apiKeyCreatedByUser } : {}), + ...rest, }; } diff --git a/x-pack/plugins/alerting/server/routes/clone_rule.ts b/x-pack/plugins/alerting/server/routes/clone_rule.ts index 11819953cf9d6..fa775e626d903 100644 --- a/x-pack/plugins/alerting/server/routes/clone_rule.ts +++ b/x-pack/plugins/alerting/server/routes/clone_rule.ts @@ -36,6 +36,7 @@ const rewriteBodyRes: RewriteResponseCase> = ({ createdAt, updatedAt, apiKeyOwner, + apiKeyCreatedByUser, notifyWhen, muteAll, mutedInstanceIds, @@ -75,6 +76,7 @@ const rewriteBodyRes: RewriteResponseCase> = ({ : {}), ...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}), ...(nextRun ? { next_run: nextRun } : {}), + ...(apiKeyCreatedByUser !== undefined ? { api_key_created_by_user: apiKeyCreatedByUser } : {}), }); export const cloneRuleRoute = ( diff --git a/x-pack/plugins/alerting/server/routes/create_rule.ts b/x-pack/plugins/alerting/server/routes/create_rule.ts index 6ada08e161a50..01a0864013415 100644 --- a/x-pack/plugins/alerting/server/routes/create_rule.ts +++ b/x-pack/plugins/alerting/server/routes/create_rule.ts @@ -74,6 +74,7 @@ const rewriteBodyRes: RewriteResponseCase> = ({ createdAt, updatedAt, apiKeyOwner, + apiKeyCreatedByUser, notifyWhen, muteAll, mutedInstanceIds, @@ -103,6 +104,7 @@ const rewriteBodyRes: RewriteResponseCase> = ({ actions: rewriteActionsRes(actions), ...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}), ...(nextRun ? { next_run: nextRun } : {}), + ...(apiKeyCreatedByUser !== undefined ? { api_key_created_by_user: apiKeyCreatedByUser } : {}), }); export const createRuleRoute = ({ router, licenseState, usageCounter }: RouteOptions) => { diff --git a/x-pack/plugins/alerting/server/routes/get_rule.ts b/x-pack/plugins/alerting/server/routes/get_rule.ts index 0a7a08847399c..ab84f0c16a5e2 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule.ts @@ -29,6 +29,7 @@ const rewriteBodyRes: RewriteResponseCase> = ({ createdAt, updatedAt, apiKeyOwner, + apiKeyCreatedByUser, notifyWhen, muteAll, mutedInstanceIds, @@ -78,6 +79,7 @@ const rewriteBodyRes: RewriteResponseCase> = ({ ...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}), ...(nextRun ? { next_run: nextRun } : {}), ...(viewInAppRelativeUrl ? { view_in_app_relative_url: viewInAppRelativeUrl } : {}), + ...(apiKeyCreatedByUser !== undefined ? { api_key_created_by_user: apiKeyCreatedByUser } : {}), }); interface BuildGetRulesRouteParams { diff --git a/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.ts b/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.ts index b829df52cc417..953211a5ef4f7 100644 --- a/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.ts +++ b/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.ts @@ -25,6 +25,7 @@ export const rewriteRule = ({ createdAt, updatedAt, apiKeyOwner, + apiKeyCreatedByUser, notifyWhen, muteAll, mutedInstanceIds, @@ -76,4 +77,5 @@ export const rewriteRule = ({ })), ...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}), ...(nextRun ? { next_run: nextRun } : {}), + ...(apiKeyCreatedByUser !== undefined ? { api_key_created_by_user: apiKeyCreatedByUser } : {}), }); diff --git a/x-pack/plugins/alerting/server/routes/resolve_rule.ts b/x-pack/plugins/alerting/server/routes/resolve_rule.ts index 27128850d9e82..e42dd6795f14a 100644 --- a/x-pack/plugins/alerting/server/routes/resolve_rule.ts +++ b/x-pack/plugins/alerting/server/routes/resolve_rule.ts @@ -33,6 +33,7 @@ const rewriteBodyRes: RewriteResponseCase> createdAt, updatedAt, apiKeyOwner, + apiKeyCreatedByUser, notifyWhen, muteAll, mutedInstanceIds, @@ -62,6 +63,7 @@ const rewriteBodyRes: RewriteResponseCase> actions: rewriteActionsRes(actions), ...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}), ...(nextRun ? { next_run: nextRun } : {}), + ...(apiKeyCreatedByUser !== undefined ? { api_key_created_by_user: apiKeyCreatedByUser } : {}), }); export const resolveRuleRoute = ( diff --git a/x-pack/plugins/alerting/server/routes/update_rule.ts b/x-pack/plugins/alerting/server/routes/update_rule.ts index 475fc210dd2d5..d24af256de613 100644 --- a/x-pack/plugins/alerting/server/routes/update_rule.ts +++ b/x-pack/plugins/alerting/server/routes/update_rule.ts @@ -74,6 +74,7 @@ const rewriteBodyRes: RewriteResponseCase> = ({ createdAt, updatedAt, apiKeyOwner, + apiKeyCreatedByUser, notifyWhen, muteAll, mutedInstanceIds, @@ -113,6 +114,7 @@ const rewriteBodyRes: RewriteResponseCase> = ({ : {}), ...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}), ...(nextRun ? { next_run: nextRun } : {}), + ...(apiKeyCreatedByUser !== undefined ? { api_key_created_by_user: apiKeyCreatedByUser } : {}), }); export const updateRuleRoute = ( diff --git a/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.test.ts b/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.test.ts new file mode 100644 index 0000000000000..495dab4f81487 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { apiKeyAsAlertAttributes } from './api_key_as_alert_attributes'; + +describe('apiKeyAsAlertAttributes', () => { + test('return attributes', () => { + expect( + apiKeyAsAlertAttributes( + { + apiKeysEnabled: true, + result: { + id: '123', + name: '123', + api_key: 'abc', + }, + }, + 'test', + false + ) + ).toEqual({ + apiKey: 'MTIzOmFiYw==', + apiKeyOwner: 'test', + apiKeyCreatedByUser: false, + }); + }); + + test('returns null attributes when api keys are not enabled', () => { + expect( + apiKeyAsAlertAttributes( + { + apiKeysEnabled: false, + }, + 'test', + false + ) + ).toEqual({ + apiKey: null, + apiKeyOwner: null, + apiKeyCreatedByUser: null, + }); + }); + + test('returns apiKeyCreatedByUser as true when createdByUser is passed in', () => { + expect( + apiKeyAsAlertAttributes( + { + apiKeysEnabled: true, + result: { + id: '123', + name: '123', + api_key: 'abc', + }, + }, + 'test', + true + ) + ).toEqual({ + apiKey: 'MTIzOmFiYw==', + apiKeyOwner: 'test', + apiKeyCreatedByUser: true, + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.ts b/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.ts index e98fb875b74a7..841ffdd1703a7 100644 --- a/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.ts +++ b/x-pack/plugins/alerting/server/rules_client/common/api_key_as_alert_attributes.ts @@ -10,15 +10,18 @@ import { CreateAPIKeyResult } from '../types'; export function apiKeyAsAlertAttributes( apiKey: CreateAPIKeyResult | null, - username: string | null -): Pick { + username: string | null, + createdByUser: boolean +): Pick { return apiKey && apiKey.apiKeysEnabled ? { apiKeyOwner: username, apiKey: Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64'), + apiKeyCreatedByUser: createdByUser, } : { apiKeyOwner: null, apiKey: null, + apiKeyCreatedByUser: null, }; } diff --git a/x-pack/plugins/alerting/server/rules_client/lib/create_new_api_key_set.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/create_new_api_key_set.test.ts new file mode 100644 index 0000000000000..6f3e595a1ab46 --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/create_new_api_key_set.test.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks'; +import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; +import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; +import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; +import { ActionsAuthorization } from '@kbn/actions-plugin/server'; +import { RawRule } from '../../types'; +import { getBeforeSetup, mockedDateString } from '../tests/lib'; +import { createNewAPIKeySet } from './create_new_api_key_set'; +import { RulesClientContext } from '../types'; + +const taskManager = taskManagerMock.createStart(); +const ruleTypeRegistry = ruleTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertingAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v8.0.0'; +const rulesClientParams: jest.Mocked = { + taskManager, + ruleTypeRegistry, + unsecuredSavedObjectsClient, + authorization: authorization as unknown as AlertingAuthorization, + actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization, + spaceId: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, + minimumScheduleInterval: { value: '1m', enforce: false }, + minimumScheduleIntervalInMs: 1, + fieldsToExcludeFromPublicApi: [], + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), +}; + +const username = 'test'; +const attributes: RawRule = { + enabled: true, + name: 'rule-name', + tags: ['tag-1', 'tag-2'], + alertTypeId: '123', + consumer: 'rule-consumer', + legacyId: null, + schedule: { interval: '1s' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: mockedDateString, + updatedAt: mockedDateString, + apiKey: null, + apiKeyOwner: null, + throttle: null, + notifyWhen: null, + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'unknown', + lastExecutionDate: '2020-08-20T19:23:38Z', + error: null, + warning: null, + }, + revision: 0, +}; + +describe('createNewAPIKeySet', () => { + beforeEach(() => { + getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry); + }); + + test('create new api keys', async () => { + rulesClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, + }); + const apiKey = await createNewAPIKeySet(rulesClientParams, { + id: attributes.alertTypeId, + ruleName: attributes.name, + username, + shouldUpdateApiKey: true, + }); + expect(apiKey).toEqual({ + apiKey: 'MTIzOmFiYw==', + apiKeyCreatedByUser: undefined, + apiKeyOwner: 'test', + }); + expect(rulesClientParams.createAPIKey).toHaveBeenCalledTimes(1); + }); + + test('should get api key from the request if the user is authenticated using api keys', async () => { + rulesClientParams.getAuthenticationAPIKey.mockReturnValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, + }); + rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(true); + const apiKey = await createNewAPIKeySet(rulesClientParams, { + id: attributes.alertTypeId, + ruleName: attributes.name, + username, + shouldUpdateApiKey: true, + }); + expect(apiKey).toEqual({ + apiKey: 'MTIzOmFiYw==', + apiKeyCreatedByUser: true, + apiKeyOwner: 'test', + }); + expect(rulesClientParams.getAuthenticationAPIKey).toHaveBeenCalledTimes(1); + expect(rulesClientParams.isAuthenticationTypeAPIKey).toHaveBeenCalledTimes(1); + }); + + test('should throw an error if getting the api key fails', async () => { + rulesClientParams.createAPIKey.mockRejectedValueOnce(new Error('Test failure')); + await expect( + async () => + await createNewAPIKeySet(rulesClientParams, { + id: attributes.alertTypeId, + ruleName: attributes.name, + username, + shouldUpdateApiKey: true, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error creating API key for rule - Test failure"` + ); + }); + + test('should throw an error if getting the api key fails and an error message is passed in', async () => { + rulesClientParams.createAPIKey.mockRejectedValueOnce(new Error('Test failure')); + await expect( + async () => + await createNewAPIKeySet(rulesClientParams, { + id: attributes.alertTypeId, + ruleName: attributes.name, + username, + shouldUpdateApiKey: true, + errorMessage: 'Error updating rule: could not create API key', + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error updating rule: could not create API key - Test failure"` + ); + }); + + test('should return null if shouldUpdateApiKey: false', async () => { + const apiKey = await createNewAPIKeySet(rulesClientParams, { + id: attributes.alertTypeId, + ruleName: attributes.name, + username, + shouldUpdateApiKey: false, + }); + expect(apiKey).toEqual({ + apiKey: null, + apiKeyCreatedByUser: null, + apiKeyOwner: null, + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/rules_client/lib/create_new_api_key_set.ts b/x-pack/plugins/alerting/server/rules_client/lib/create_new_api_key_set.ts index 202ab4d23972d..a9d0a7ba9674a 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/create_new_api_key_set.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/create_new_api_key_set.ts @@ -13,21 +13,33 @@ import { RulesClientContext } from '../types'; export async function createNewAPIKeySet( context: RulesClientContext, { - attributes, + id, + ruleName, username, + shouldUpdateApiKey, + errorMessage, }: { - attributes: RawRule; + id: string; + ruleName: string; username: string | null; + shouldUpdateApiKey: boolean; + errorMessage?: string; } -): Promise> { +): Promise> { let createdAPIKey = null; + let isAuthTypeApiKey = false; try { - createdAPIKey = await context.createAPIKey( - generateAPIKeyName(attributes.alertTypeId, attributes.name) - ); + isAuthTypeApiKey = context.isAuthenticationTypeAPIKey(); + const name = generateAPIKeyName(id, ruleName); + createdAPIKey = shouldUpdateApiKey + ? isAuthTypeApiKey + ? context.getAuthenticationAPIKey(`${name}-user-created`) + : await context.createAPIKey(name) + : null; } catch (error) { - throw Boom.badRequest(`Error creating API key for rule: ${error.message}`); + const message = errorMessage ? errorMessage : 'Error creating API key for rule'; + throw Boom.badRequest(`${message} - ${error.message}`); } - return apiKeyAsAlertAttributes(createdAPIKey, username); + return apiKeyAsAlertAttributes(createdAPIKey, username, isAuthTypeApiKey); } diff --git a/x-pack/plugins/alerting/server/rules_client/lib/create_rule_saved_object.ts b/x-pack/plugins/alerting/server/rules_client/lib/create_rule_saved_object.ts index 87bc26b31e7fa..fbd812618aad6 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/create_rule_saved_object.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/create_rule_saved_object.ts @@ -54,7 +54,7 @@ export async function createRuleSavedObject { for await (const response of rulesFinder.find()) { for (const rule of response.saved_objects) { - if (rule.attributes.apiKey) { + if (rule.attributes.apiKey && !rule.attributes.apiKeyCreatedByUser) { apiKeyToRuleIdMapping[rule.id] = rule.attributes.apiKey; } if (rule.attributes.name) { diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts index f0f2891a954c5..a3d845af2c0af 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts @@ -41,8 +41,6 @@ import { applyBulkEditOperation, buildKueryNodeFilter, injectReferencesIntoActions, - generateAPIKeyName, - apiKeyAsAlertAttributes, getBulkSnoozeAttributes, getBulkUnsnoozeAttributes, verifySnoozeScheduleLimit, @@ -61,13 +59,13 @@ import { validateActions, updateMeta, addGeneratedActionValues, + createNewAPIKeySet, } from '../lib'; import { NormalizedAlertAction, BulkOperationError, RuleBulkOperationAggregation, RulesClientContext, - CreateAPIKeyResult, NormalizedAlertActionWithGeneratedValues, } from '../types'; @@ -121,9 +119,17 @@ export type BulkEditOperation = value?: undefined; }; -type ApiKeysMap = Map; +type ApiKeysMap = Map< + string, + { + oldApiKey?: string; + newApiKey?: string; + oldApiKeyCreatedByUser?: boolean | null; + newApiKeyCreatedByUser?: boolean | null; + } +>; -type ApiKeyAttributes = Pick; +type ApiKeyAttributes = Pick; type RuleType = ReturnType; @@ -433,7 +439,10 @@ async function updateRuleAttributesAndParamsInMemory { try { if (rule.attributes.apiKey) { - apiKeysMap.set(rule.id, { oldApiKey: rule.attributes.apiKey }); + apiKeysMap.set(rule.id, { + oldApiKey: rule.attributes.apiKey, + oldApiKeyCreatedByUser: rule.attributes.apiKeyCreatedByUser, + }); } const ruleType = context.ruleTypeRegistry.get(rule.attributes.alertTypeId); @@ -734,23 +743,20 @@ async function prepareApiKeys( hasUpdateApiKeyOperation: boolean, username: string | null ): Promise<{ apiKeyAttributes: ApiKeyAttributes }> { - const shouldUpdateApiKey = attributes.enabled || hasUpdateApiKeyOperation; - - let createdAPIKey: CreateAPIKeyResult | null = null; - try { - createdAPIKey = shouldUpdateApiKey - ? await context.createAPIKey(generateAPIKeyName(ruleType.id, attributes.name)) - : null; - } catch (error) { - throw Error(`Error updating rule: could not create API key - ${error.message}`); - } + const apiKeyAttributes = await createNewAPIKeySet(context, { + id: ruleType.id, + ruleName: attributes.name, + username, + shouldUpdateApiKey: attributes.enabled || hasUpdateApiKeyOperation, + errorMessage: 'Error updating rule: could not create API key', + }); - const apiKeyAttributes = apiKeyAsAlertAttributes(createdAPIKey, username); // collect generated API keys if (apiKeyAttributes.apiKey) { apiKeysMap.set(rule.id, { ...apiKeysMap.get(rule.id), newApiKey: apiKeyAttributes.apiKey, + newApiKeyCreatedByUser: apiKeyAttributes.apiKeyCreatedByUser, }); } @@ -812,7 +818,7 @@ async function saveBulkUpdatedRules( await bulkMarkApiKeysForInvalidation( { apiKeys: Array.from(apiKeysMap.values()) - .filter((value) => value.newApiKey) + .filter((value) => value.newApiKey && !value.newApiKeyCreatedByUser) .map((value) => value.newApiKey as string), }, context.logger, @@ -824,13 +830,15 @@ async function saveBulkUpdatedRules( result.saved_objects.map(({ id, error }) => { const oldApiKey = apiKeysMap.get(id)?.oldApiKey; + const oldApiKeyCreatedByUser = apiKeysMap.get(id)?.oldApiKeyCreatedByUser; const newApiKey = apiKeysMap.get(id)?.newApiKey; + const newApiKeyCreatedByUser = apiKeysMap.get(id)?.newApiKeyCreatedByUser; // if SO wasn't saved and has new API key it will be invalidated - if (error && newApiKey) { + if (error && newApiKey && !newApiKeyCreatedByUser) { apiKeysToInvalidate.push(newApiKey); // if SO saved and has old Api Key it will be invalidate - } else if (!error && oldApiKey) { + } else if (!error && oldApiKey && !oldApiKeyCreatedByUser) { apiKeysToInvalidate.push(oldApiKey); } }); diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts index 03643ccc02ac2..7273855379a1c 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_enable.ts @@ -146,7 +146,12 @@ const bulkEnableRulesWithOCC = async ( const updatedAttributes = updateMeta(context, { ...rule.attributes, ...(!rule.attributes.apiKey && - (await createNewAPIKeySet(context, { attributes: rule.attributes, username }))), + (await createNewAPIKeySet(context, { + id: rule.attributes.alertTypeId, + ruleName: rule.attributes.name, + username, + shouldUpdateApiKey: true, + }))), enabled: true, updatedBy: username, updatedAt: new Date().toISOString(), diff --git a/x-pack/plugins/alerting/server/rules_client/methods/clone.ts b/x-pack/plugins/alerting/server/rules_client/methods/clone.ts index ac58b213905be..8cb5aee60ca7f 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/clone.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/clone.ts @@ -17,8 +17,7 @@ import { parseDuration } from '../../../common/parse_duration'; import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; import { getRuleExecutionStatusPending } from '../../lib/rule_execution_status'; import { isDetectionEngineAADRuleType } from '../../saved_objects/migrations/utils'; -import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../common'; -import { createRuleSavedObject } from '../lib'; +import { createNewAPIKeySet, createRuleSavedObject } from '../lib'; import { RulesClientContext } from '../types'; export type CloneArguments = [string, { newId?: string }]; @@ -95,18 +94,18 @@ export async function clone( const createTime = Date.now(); const lastRunTimestamp = new Date(); const legacyId = Semver.lt(context.kibanaVersion, '8.0.0') ? id : null; - let createdAPIKey = null; - try { - createdAPIKey = ruleSavedObject.attributes.enabled - ? await context.createAPIKey(generateAPIKeyName(ruleType.id, ruleName)) - : null; - } catch (error) { - throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`); - } + const apiKeyAttributes = await createNewAPIKeySet(context, { + id: ruleType.id, + ruleName, + username, + shouldUpdateApiKey: ruleSavedObject.attributes.enabled, + errorMessage: 'Error creating rule: could not create API key', + }); + const rawRule: RawRule = { ...ruleSavedObject.attributes, name: ruleName, - ...apiKeyAsAlertAttributes(createdAPIKey, username), + ...apiKeyAttributes, legacyId, createdBy: username, updatedBy: username, diff --git a/x-pack/plugins/alerting/server/rules_client/methods/create.ts b/x-pack/plugins/alerting/server/rules_client/methods/create.ts index 4381392b7b091..4d2e586dcaade 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/create.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/create.ts @@ -40,6 +40,7 @@ export interface CreateOptions { | 'updatedAt' | 'apiKey' | 'apiKeyOwner' + | 'apiKeyCreatedByUser' | 'muteAll' | 'mutedInstanceIds' | 'actions' @@ -91,11 +92,20 @@ export async function create( const username = await context.getUserName(); let createdAPIKey = null; + let isAuthTypeApiKey = false; try { + isAuthTypeApiKey = context.isAuthenticationTypeAPIKey(); + const name = generateAPIKeyName(ruleType.id, data.name); createdAPIKey = data.enabled - ? await withSpan({ name: 'createAPIKey', type: 'rules' }, () => - context.createAPIKey(generateAPIKeyName(ruleType.id, data.name)) - ) + ? isAuthTypeApiKey + ? context.getAuthenticationAPIKey(`${name}-user-created`) + : await withSpan( + { + name: 'createAPIKey', + type: 'rules', + }, + () => context.createAPIKey(name) + ) : null; } catch (error) { throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`); @@ -144,7 +154,7 @@ export async function create( const rawRule: RawRule = { ...data, - ...apiKeyAsAlertAttributes(createdAPIKey, username), + ...apiKeyAsAlertAttributes(createdAPIKey, username, isAuthTypeApiKey), legacyId, actions, createdBy: username, diff --git a/x-pack/plugins/alerting/server/rules_client/methods/delete.ts b/x-pack/plugins/alerting/server/rules_client/methods/delete.ts index 406184e8f013e..ca6feefe3ff9b 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/delete.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/delete.ts @@ -23,6 +23,7 @@ export async function deleteRule(context: RulesClientContext, { id }: { id: stri async function deleteWithOCC(context: RulesClientContext, { id }: { id: string }) { let taskIdToRemove: string | undefined | null; let apiKeyToInvalidate: string | null = null; + let apiKeyCreatedByUser: boolean | undefined | null = false; let attributes: RawRule; try { @@ -31,6 +32,7 @@ async function deleteWithOCC(context: RulesClientContext, { id }: { id: string } namespace: context.namespace, }); apiKeyToInvalidate = decryptedAlert.attributes.apiKey; + apiKeyCreatedByUser = decryptedAlert.attributes.apiKeyCreatedByUser; taskIdToRemove = decryptedAlert.attributes.scheduledTaskId; attributes = decryptedAlert.attributes; } catch (e) { @@ -73,7 +75,7 @@ async function deleteWithOCC(context: RulesClientContext, { id }: { id: string } await Promise.all([ taskIdToRemove ? context.taskManager.removeIfExists(taskIdToRemove) : null, - apiKeyToInvalidate + apiKeyToInvalidate && !apiKeyCreatedByUser ? bulkMarkApiKeysForInvalidation( { apiKeys: [apiKeyToInvalidate] }, context.logger, diff --git a/x-pack/plugins/alerting/server/rules_client/methods/enable.ts b/x-pack/plugins/alerting/server/rules_client/methods/enable.ts index 6e93dee530665..a70b71b7b5116 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/enable.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/enable.ts @@ -83,7 +83,13 @@ async function enableWithOCC(context: RulesClientContext, { id }: { id: string } const updateAttributes = updateMeta(context, { ...attributes, - ...(!existingApiKey && (await createNewAPIKeySet(context, { attributes, username }))), + ...(!existingApiKey && + (await createNewAPIKeySet(context, { + id: attributes.alertTypeId, + ruleName: attributes.name, + username, + shouldUpdateApiKey: true, + }))), ...(attributes.monitoring && { monitoring: resetMonitoringLastRun(attributes.monitoring), }), diff --git a/x-pack/plugins/alerting/server/rules_client/methods/update.ts b/x-pack/plugins/alerting/server/rules_client/methods/update.ts index bbd99ae201501..d2b57d44fc528 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/update.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/update.ts @@ -32,8 +32,8 @@ import { getPartialRuleFromRaw, addGeneratedActionValues, incrementRevision, + createNewAPIKeySet, } from '../lib'; -import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../common'; export interface UpdateOptions { id: string; @@ -122,7 +122,7 @@ async function updateWithOCC( ); await Promise.all([ - alertSavedObject.attributes.apiKey + alertSavedObject.attributes.apiKey && !alertSavedObject.attributes.apiKeyCreatedByUser ? bulkMarkApiKeysForInvalidation( { apiKeys: [alertSavedObject.attributes.apiKey] }, context.logger, @@ -206,16 +206,13 @@ async function updateAlert( const username = await context.getUserName(); - let createdAPIKey = null; - try { - createdAPIKey = attributes.enabled - ? await context.createAPIKey(generateAPIKeyName(ruleType.id, data.name)) - : null; - } catch (error) { - throw Boom.badRequest(`Error updating rule: could not create API key - ${error.message}`); - } - - const apiKeyAttributes = apiKeyAsAlertAttributes(createdAPIKey, username); + const apiKeyAttributes = await createNewAPIKeySet(context, { + id: ruleType.id, + ruleName: data.name, + username, + shouldUpdateApiKey: attributes.enabled, + errorMessage: 'Error updating rule: could not create API key', + }); const notifyWhen = getRuleNotifyWhenType(data.notifyWhen ?? null, data.throttle ?? null); // Increment revision if applicable field has changed @@ -260,7 +257,12 @@ async function updateAlert( } catch (e) { // Avoid unused API key await bulkMarkApiKeysForInvalidation( - { apiKeys: createAttributes.apiKey ? [createAttributes.apiKey] : [] }, + { + apiKeys: + createAttributes.apiKey && !createAttributes.apiKeyCreatedByUser + ? [createAttributes.apiKey] + : [], + }, context.logger, context.unsecuredSavedObjectsClient ); diff --git a/x-pack/plugins/alerting/server/rules_client/methods/update_api_key.ts b/x-pack/plugins/alerting/server/rules_client/methods/update_api_key.ts index abb7d32404d57..4fefb5a8b367e 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/update_api_key.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/update_api_key.ts @@ -5,14 +5,12 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { RawRule } from '../../types'; import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization'; import { retryIfConflicts } from '../../lib/retry_if_conflicts'; import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; -import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../common'; -import { updateMeta } from '../lib'; +import { createNewAPIKeySet, updateMeta } from '../lib'; import { RulesClientContext } from '../types'; export async function updateApiKey( @@ -27,7 +25,8 @@ export async function updateApiKey( } async function updateApiKeyWithOCC(context: RulesClientContext, { id }: { id: string }) { - let apiKeyToInvalidate: string | null = null; + let oldApiKeyToInvalidate: string | null = null; + let oldApiKeyCreatedByUser: boolean | undefined | null = false; let attributes: RawRule; let version: string | undefined; @@ -36,7 +35,8 @@ async function updateApiKeyWithOCC(context: RulesClientContext, { id }: { id: st await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser('alert', id, { namespace: context.namespace, }); - apiKeyToInvalidate = decryptedAlert.attributes.apiKey; + oldApiKeyToInvalidate = decryptedAlert.attributes.apiKey; + oldApiKeyCreatedByUser = decryptedAlert.attributes.apiKeyCreatedByUser; attributes = decryptedAlert.attributes; version = decryptedAlert.version; } catch (e) { @@ -73,20 +73,17 @@ async function updateApiKeyWithOCC(context: RulesClientContext, { id }: { id: st const username = await context.getUserName(); - let createdAPIKey = null; - try { - createdAPIKey = await context.createAPIKey( - generateAPIKeyName(attributes.alertTypeId, attributes.name) - ); - } catch (error) { - throw Boom.badRequest( - `Error updating API key for rule: could not create API key - ${error.message}` - ); - } + const apiKeyAttributes = await createNewAPIKeySet(context, { + id: attributes.alertTypeId, + ruleName: attributes.name, + username, + shouldUpdateApiKey: true, + errorMessage: 'Error updating API key for rule: could not create API key', + }); const updateAttributes = updateMeta(context, { ...attributes, - ...apiKeyAsAlertAttributes(createdAPIKey, username), + ...apiKeyAttributes, updatedAt: new Date().toISOString(), updatedBy: username, }); @@ -106,16 +103,21 @@ async function updateApiKeyWithOCC(context: RulesClientContext, { id }: { id: st } catch (e) { // Avoid unused API key await bulkMarkApiKeysForInvalidation( - { apiKeys: updateAttributes.apiKey ? [updateAttributes.apiKey] : [] }, + { + apiKeys: + updateAttributes.apiKey && !updateAttributes.apiKeyCreatedByUser + ? [updateAttributes.apiKey] + : [], + }, context.logger, context.unsecuredSavedObjectsClient ); throw e; } - if (apiKeyToInvalidate) { + if (oldApiKeyToInvalidate && !oldApiKeyCreatedByUser) { await bulkMarkApiKeysForInvalidation( - { apiKeys: [apiKeyToInvalidate] }, + { apiKeys: [oldApiKeyToInvalidate] }, context.logger, context.unsecuredSavedObjectsClient ); diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 91f9910f4f274..fbf441705481d 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -71,6 +71,7 @@ export const fieldsToExcludeFromRevisionUpdates: ReadonlySet = { getActionsClient: jest.fn(), getEventLogClient: jest.fn(), kibanaVersion, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/bulk_delete.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/bulk_delete.test.ts index b61ef395d4e72..8e9f1a79b8de0 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/bulk_delete.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/bulk_delete.test.ts @@ -19,7 +19,13 @@ import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks'; import { getBeforeSetup, setGlobalDate } from './lib'; import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation'; import { loggerMock } from '@kbn/logging-mocks'; -import { enabledRule1, enabledRule2, returnedRule1, returnedRule2 } from './test_helpers'; +import { + defaultRule, + enabledRule1, + enabledRule2, + returnedRule1, + returnedRule2, +} from './test_helpers'; jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({ bulkMarkApiKeysForInvalidation: jest.fn(), @@ -53,6 +59,8 @@ const rulesClientParams: jest.Mocked = { kibanaVersion, auditLogger, minimumScheduleInterval: { value: '1m', enforce: false }, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; const getBulkOperationStatusErrorResponse = (statusCode: number) => ({ @@ -66,6 +74,36 @@ const getBulkOperationStatusErrorResponse = (statusCode: number) => ({ }, }); +export const enabledRule3 = { + ...defaultRule, + id: 'id3', + attributes: { + ...defaultRule.attributes, + enabled: true, + scheduledTaskId: 'id3', + apiKey: Buffer.from('789:ghi').toString('base64'), + apiKeyCreatedByUser: true, + }, +}; + +export const returnedRule3 = { + actions: [], + alertTypeId: 'fakeType', + apiKey: 'Nzg5OmdoaQ==', + apiKeyCreatedByUser: true, + consumer: 'fakeConsumer', + enabled: true, + id: 'id3', + name: 'fakeName', + notifyWhen: undefined, + params: undefined, + schedule: { + interval: '5m', + }, + scheduledTaskId: 'id3', + snoozeSchedule: [], +}; + beforeEach(() => { getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry); jest.clearAllMocks(); @@ -77,7 +115,7 @@ describe('bulkDelete', () => { let rulesClient: RulesClient; const mockCreatePointInTimeFinderAsInternalUser = ( - response = { saved_objects: [enabledRule1, enabledRule2] } + response = { saved_objects: [enabledRule1, enabledRule2, enabledRule3] } ) => { encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest .fn() @@ -126,11 +164,12 @@ describe('bulkDelete', () => { }); }); - test('should try to delete rules, one successful and one with 500 error', async () => { + test('should try to delete rules, two successful and one with 500 error', async () => { unsecuredSavedObjectsClient.bulkDelete.mockResolvedValue({ statuses: [ { id: 'id1', type: 'alert', success: true }, getBulkOperationStatusErrorResponse(500), + { id: 'id3', type: 'alert', success: true }, ], }); @@ -140,9 +179,10 @@ describe('bulkDelete', () => { expect(unsecuredSavedObjectsClient.bulkDelete).toHaveBeenCalledWith([ enabledRule1, enabledRule2, + enabledRule3, ]); expect(taskManager.bulkRemove).toHaveBeenCalledTimes(1); - expect(taskManager.bulkRemove).toHaveBeenCalledWith(['id1']); + expect(taskManager.bulkRemove).toHaveBeenCalledWith(['id1', 'id3']); expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1); expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith( { apiKeys: ['MTIzOmFiYw=='] }, @@ -150,7 +190,7 @@ describe('bulkDelete', () => { expect.anything() ); expect(result).toStrictEqual({ - rules: [returnedRule1], + rules: [returnedRule1, returnedRule3], errors: [{ message: 'UPS', rule: { id: 'id2', name: 'fakeName' }, status: 500 }], total: 2, taskIdsFailedToBeDeleted: [], @@ -313,6 +353,25 @@ describe('bulkDelete', () => { ); }); + test('should not mark API keys for invalidation if the user is authenticated using an api key', async () => { + unsecuredSavedObjectsClient.bulkDelete.mockResolvedValue({ + statuses: [ + { id: 'id3', type: 'alert', success: true }, + { id: 'id1', type: 'alert', success: true }, + { id: 'id2', type: 'alert', success: true }, + ], + }); + + await rulesClient.bulkDeleteRules({ filter: 'fake_filter' }); + + expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1); + expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith( + { apiKeys: ['MTIzOmFiYw==', 'MzIxOmFiYw=='] }, + expect.anything(), + expect.anything() + ); + }); + describe('taskManager', () => { test('should return task id if deleting task failed', async () => { unsecuredSavedObjectsClient.bulkDelete.mockResolvedValue({ diff --git a/x-pack/plugins/alerting/server/rules_client/tests/bulk_disable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/bulk_disable.test.ts index e69dd1b0ffb44..03c637adaae00 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/bulk_disable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/bulk_disable.test.ts @@ -73,6 +73,8 @@ const rulesClientParams: jest.Mocked = { auditLogger, eventLogger, minimumScheduleInterval: { value: '1m', enforce: false }, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts index 25cbbb7471b77..b91bb1cae5e3c 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts @@ -47,6 +47,9 @@ const auditLogger = auditLoggerMock.create(); const kibanaVersion = 'v8.2.0'; const createAPIKeyMock = jest.fn(); +const isAuthenticationTypeApiKeyMock = jest.fn(); +const getAuthenticationApiKeyMock = jest.fn(); + const rulesClientParams: jest.Mocked = { taskManager, ruleTypeRegistry, @@ -64,6 +67,8 @@ const rulesClientParams: jest.Mocked = { kibanaVersion, auditLogger, minimumScheduleInterval: { value: '1m', enforce: false }, + isAuthenticationTypeAPIKey: isAuthenticationTypeApiKeyMock, + getAuthenticationAPIKey: getAuthenticationApiKeyMock, }; const paramsModifier = jest.fn(); @@ -104,6 +109,7 @@ describe('bulkEdit()', () => { attributes: { ...existingRule.attributes, apiKey: MOCK_API_KEY, + apiKeyCreatedByUser: false, }, }; @@ -580,6 +586,7 @@ describe('bulkEdit()', () => { ], apiKey: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, meta: { versionApiKeyLastmodified: 'v8.2.0' }, name: 'my rule name', enabled: false, @@ -781,6 +788,7 @@ describe('bulkEdit()', () => { ], apiKey: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, meta: { versionApiKeyLastmodified: 'v8.2.0' }, name: 'my rule name', enabled: false, @@ -2070,6 +2078,142 @@ describe('bulkEdit()', () => { }); expect(rulesClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: myType/my rule name'); }); + + describe('set by the user when authenticated using api keys', () => { + beforeEach(() => { + isAuthenticationTypeApiKeyMock.mockReturnValue(true); + getAuthenticationApiKeyMock.mockReturnValue({ + apiKeysEnabled: true, + result: { api_key: '111' }, + }); + mockCreatePointInTimeFinderAsInternalUser({ + saved_objects: [ + { + ...existingDecryptedRule, + attributes: { + ...existingDecryptedRule.attributes, + enabled: true, + apiKeyCreatedByUser: true, + }, + }, + ], + }); + }); + + test('should not call bulkMarkApiKeysForInvalidation', async () => { + await rulesClient.bulkEdit({ + filter: 'alert.attributes.tags: "APM"', + operations: [ + { + field: 'tags', + operation: 'add', + value: ['test-1'], + }, + ], + }); + + expect(bulkMarkApiKeysForInvalidation).not.toHaveBeenCalled(); + }); + + test('should call bulkMarkApiKeysForInvalidation with empty array if bulkCreate failed', async () => { + unsecuredSavedObjectsClient.bulkCreate.mockImplementation(() => { + throw new Error('Fail'); + }); + + await expect( + rulesClient.bulkEdit({ + filter: 'alert.attributes.tags: "APM"', + operations: [ + { + field: 'tags', + operation: 'add', + value: ['test-1'], + }, + ], + }) + ).rejects.toThrow('Fail'); + + expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1); + expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith( + { apiKeys: [] }, + expect.any(Object), + expect.any(Object) + ); + }); + + test('should call bulkMarkApiKeysForInvalidation with empty array if SO update failed', async () => { + unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ + saved_objects: [ + { + id: '1', + type: 'alert', + attributes: { + enabled: true, + tags: ['foo'], + alertTypeId: 'myType', + schedule: { interval: '1m' }, + consumer: 'myApp', + scheduledTaskId: 'task-123', + params: { index: ['test-index-*'] }, + throttle: null, + notifyWhen: null, + actions: [], + }, + references: [], + version: '123', + error: { + error: 'test failure', + statusCode: 500, + message: 'test failure', + }, + }, + ], + }); + + await rulesClient.bulkEdit({ + filter: 'alert.attributes.tags: "APM"', + operations: [ + { + field: 'tags', + operation: 'add', + value: ['test-1'], + }, + ], + }); + + expect(bulkMarkApiKeysForInvalidation).not.toHaveBeenCalled(); + }); + + test('should not call get apiKey if rule is disabled', async () => { + await rulesClient.bulkEdit({ + filter: 'alert.attributes.tags: "APM"', + operations: [ + { + field: 'tags', + operation: 'add', + value: ['test-1'], + }, + ], + }); + expect(rulesClientParams.getAuthenticationAPIKey).not.toHaveBeenCalledWith(); + }); + + test('should return error in rule errors if key is not generated', async () => { + await rulesClient.bulkEdit({ + filter: 'alert.attributes.tags: "APM"', + operations: [ + { + field: 'tags', + operation: 'add', + value: ['test-1'], + }, + ], + }); + expect(rulesClientParams.getAuthenticationAPIKey).toHaveBeenCalledWith( + 'Alerting: myType/my rule name-user-created' + ); + }); + }); }); describe('params validation', () => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/bulk_enable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/bulk_enable.test.ts index 0aeca36a2b458..b82001e83886c 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/bulk_enable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/bulk_enable.test.ts @@ -64,6 +64,8 @@ const rulesClientParams: jest.Mocked = { kibanaVersion, auditLogger, minimumScheduleInterval: { value: '1m', enforce: false }, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/clear_expired_snoozes.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/clear_expired_snoozes.test.ts index 3952ed4c9003c..04b015986579e 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/clear_expired_snoozes.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/clear_expired_snoozes.test.ts @@ -60,6 +60,8 @@ const rulesClientParams: jest.Mocked = { kibanaVersion, auditLogger, eventLogger, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; describe('clearExpiredSnoozes()', () => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts index b59d1e36028e8..2be00b0fc5352 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts @@ -68,6 +68,8 @@ const rulesClientParams: jest.Mocked = { kibanaVersion, auditLogger, minimumScheduleInterval: { value: '1m', enforce: false }, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { @@ -415,6 +417,7 @@ describe('create()', () => { ], "alertTypeId": "123", "apiKey": null, + "apiKeyCreatedByUser": null, "apiKeyOwner": null, "consumer": "bar", "createdAt": "2019-02-12T21:01:22.479Z", @@ -636,6 +639,7 @@ describe('create()', () => { ], "alertTypeId": "123", "apiKey": null, + "apiKeyCreatedByUser": null, "apiKeyOwner": null, "consumer": "bar", "createdAt": "2019-02-12T21:01:22.479Z", @@ -1087,6 +1091,7 @@ describe('create()', () => { alertTypeId: '123', apiKey: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, consumer: 'bar', createdAt: '2019-02-12T21:01:22.479Z', createdBy: 'elastic', @@ -1297,6 +1302,7 @@ describe('create()', () => { alertTypeId: '123', apiKey: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, consumer: 'bar', createdAt: '2019-02-12T21:01:22.479Z', createdBy: 'elastic', @@ -1477,6 +1483,7 @@ describe('create()', () => { alertTypeId: '123', apiKey: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, legacyId: null, consumer: 'bar', createdAt: '2019-02-12T21:01:22.479Z', @@ -1650,6 +1657,7 @@ describe('create()', () => { params: { bar: true }, apiKey: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, createdBy: 'elastic', createdAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', @@ -1787,6 +1795,7 @@ describe('create()', () => { params: { bar: true }, apiKey: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, createdBy: 'elastic', createdAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', @@ -1924,6 +1933,7 @@ describe('create()', () => { params: { bar: true }, apiKey: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, createdBy: 'elastic', createdAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', @@ -2083,6 +2093,7 @@ describe('create()', () => { ], apiKeyOwner: null, apiKey: null, + apiKeyCreatedByUser: null, legacyId: null, createdBy: 'elastic', updatedBy: 'elastic', @@ -2562,6 +2573,7 @@ describe('create()', () => { params: { bar: true }, apiKey: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, createdBy: 'elastic', createdAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', @@ -3309,4 +3321,137 @@ describe('create()', () => { expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); }); + + test('calls the authentication API key function if the user is authenticated using an api key', async () => { + const data = getMockData(); + rulesClientParams.getAuthenticationAPIKey.mockReturnValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, + }); + rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(true); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '1m' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + scheduledTaskId: 'task-123', + }, + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + }); + await rulesClient.create({ data }); + + expect(rulesClientParams.isAuthenticationTypeAPIKey).toHaveBeenCalledTimes(1); + expect(rulesClientParams.getAuthenticationAPIKey).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( + 'alert', + { + actions: [ + { + actionRef: 'action_0', + group: 'default', + actionTypeId: 'test', + params: { foo: true }, + uuid: '151', + }, + ], + alertTypeId: '123', + consumer: 'bar', + name: 'abc', + legacyId: null, + params: { bar: true }, + apiKey: Buffer.from('123:abc').toString('base64'), + apiKeyOwner: 'elastic', + apiKeyCreatedByUser: true, + createdBy: 'elastic', + createdAt: '2019-02-12T21:01:22.479Z', + updatedBy: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', + enabled: true, + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + schedule: { interval: '1m' }, + throttle: null, + notifyWhen: null, + muteAll: false, + snoozeSchedule: [], + mutedInstanceIds: [], + tags: ['foo'], + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + error: null, + warning: null, + }, + monitoring: getDefaultMonitoring('2019-02-12T21:01:22.479Z'), + revision: 0, + running: false, + }, + { + id: 'mock-saved-object-id', + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + } + ); + }); + + test('throws error and does not add API key to invalidatePendingApiKey SO when create saved object fails if the user is authenticated using an api key', async () => { + const data = getMockData(); + rulesClientParams.getAuthenticationAPIKey.mockReturnValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, + }); + rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(true); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure')); + await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Test failure"` + ); + expect(taskManager.schedule).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1); + expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith( + { apiKeys: [] }, + expect.any(Object), + expect.any(Object) + ); + }); }); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/delete.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/delete.test.ts index 6b45c16bcd654..edf03a2d54e42 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/delete.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/delete.test.ts @@ -48,6 +48,8 @@ const rulesClientParams: jest.Mocked = { getEventLogClient: jest.fn(), kibanaVersion, auditLogger, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { @@ -161,6 +163,20 @@ describe('delete()', () => { expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); + test(`doesn't invalidate API key if set by the user when authenticated using api keys`, async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKeyCreatedByUser: true, + }, + }); + + await rulesClient.delete({ id: '1' }); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + expect(bulkMarkApiKeysForInvalidation).not.toHaveBeenCalled(); + }); + test('swallows error when invalidate API key throws', async () => { unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); await rulesClient.delete({ id: '1' }); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts index 57906bcae3b97..09dd8f54d01fd 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/disable.test.ts @@ -55,6 +55,8 @@ const rulesClientParams: jest.Mocked = { kibanaVersion, auditLogger, eventLogger, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts index 13826b720fa76..d15e96f573a84 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts @@ -48,6 +48,8 @@ const rulesClientParams: jest.Mocked = { getEventLogClient: jest.fn(), kibanaVersion, auditLogger, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; setGlobalDate(); @@ -375,7 +377,7 @@ describe('enable()', () => { }); await expect( async () => await rulesClient.enable({ id: '1' }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Error creating API key for rule: no"`); + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Error creating API key for rule - no"`); expect(taskManager.bulkEnable).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts index 200b4a291baa1..b8eb4d4d0c478 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts @@ -45,6 +45,8 @@ const rulesClientParams: jest.Mocked = { getActionsClient: jest.fn(), getEventLogClient: jest.fn(), kibanaVersion, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get.test.ts index 4465dbffcc48a..30abf4c661d19 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get.test.ts @@ -43,6 +43,8 @@ const rulesClientParams: jest.Mocked = { getActionsClient: jest.fn(), getEventLogClient: jest.fn(), kibanaVersion, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_action_error_log.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_action_error_log.test.ts index 90d68301f97aa..fdece7b756d42 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_action_error_log.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_action_error_log.test.ts @@ -50,6 +50,8 @@ const rulesClientParams: jest.Mocked = { getEventLogClient: jest.fn(), kibanaVersion, auditLogger, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_state.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_state.test.ts index 88f6e466be821..c35bcd6c56311 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_state.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_state.test.ts @@ -42,6 +42,8 @@ const rulesClientParams: jest.Mocked = { getActionsClient: jest.fn(), getEventLogClient: jest.fn(), kibanaVersion, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts index f9dcfaeb77a50..212977a8381c2 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts @@ -48,6 +48,8 @@ const rulesClientParams: jest.Mocked = { getActionsClient: jest.fn(), getEventLogClient: jest.fn(), kibanaVersion, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts index 3f8dfd92ee894..cebe9fa963971 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts @@ -51,6 +51,8 @@ const rulesClientParams: jest.Mocked = { getEventLogClient: jest.fn(), kibanaVersion, auditLogger, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/list_alert_types.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/list_alert_types.test.ts index 0a3f5981a33a0..4f6afce8d41ec 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/list_alert_types.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/list_alert_types.test.ts @@ -46,6 +46,8 @@ const rulesClientParams: jest.Mocked = { getActionsClient: jest.fn(), getEventLogClient: jest.fn(), kibanaVersion, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/mute_all.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/mute_all.test.ts index e2625be88482c..fde2534ec6e21 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/mute_all.test.ts @@ -42,6 +42,8 @@ const rulesClientParams: jest.Mocked = { getActionsClient: jest.fn(), getEventLogClient: jest.fn(), kibanaVersion, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/mute_instance.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/mute_instance.test.ts index d566ea8c18bd5..1582b84e59da8 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/mute_instance.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/mute_instance.test.ts @@ -42,6 +42,8 @@ const rulesClientParams: jest.Mocked = { getActionsClient: jest.fn(), getEventLogClient: jest.fn(), kibanaVersion, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts index 2caf948ea8753..6f52da62023e9 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts @@ -43,6 +43,8 @@ const rulesClientParams: jest.Mocked = { getActionsClient: jest.fn(), getEventLogClient: jest.fn(), kibanaVersion, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/run_soon.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/run_soon.test.ts index 138b167549c09..99579a7831b94 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/run_soon.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/run_soon.test.ts @@ -44,6 +44,8 @@ const rulesClientParams: jest.Mocked = { getEventLogClient: jest.fn(), kibanaVersion, auditLogger, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; setGlobalDate(); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/unmute_all.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/unmute_all.test.ts index f5d4cb372f867..737974eeba11e 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/unmute_all.test.ts @@ -42,6 +42,8 @@ const rulesClientParams: jest.Mocked = { getActionsClient: jest.fn(), getEventLogClient: jest.fn(), kibanaVersion, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/unmute_instance.test.ts index 6f52b6291474f..75f47210f11d1 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/unmute_instance.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/unmute_instance.test.ts @@ -42,6 +42,8 @@ const rulesClientParams: jest.Mocked = { getActionsClient: jest.fn(), getEventLogClient: jest.fn(), kibanaVersion, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts index 074bbfa3552b1..6fcc0cb915a36 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts @@ -69,6 +69,8 @@ const rulesClientParams: jest.Mocked = { kibanaVersion, auditLogger, minimumScheduleInterval: { value: '1m', enforce: false }, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { @@ -381,6 +383,7 @@ describe('update()', () => { ], "alertTypeId": "myType", "apiKey": null, + "apiKeyCreatedByUser": null, "apiKeyOwner": null, "consumer": "myApp", "enabled": true, @@ -622,6 +625,7 @@ describe('update()', () => { alertTypeId: 'myType', apiKey: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, consumer: 'myApp', enabled: true, meta: { versionApiKeyLastmodified: 'v7.10.0' }, @@ -809,6 +813,7 @@ describe('update()', () => { alertTypeId: 'myType', apiKey: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, consumer: 'myApp', enabled: true, meta: { versionApiKeyLastmodified: 'v7.10.0' }, @@ -987,6 +992,7 @@ describe('update()', () => { ], "alertTypeId": "myType", "apiKey": "MTIzOmFiYw==", + "apiKeyCreatedByUser": undefined, "apiKeyOwner": "elastic", "consumer": "myApp", "enabled": true, @@ -1139,6 +1145,7 @@ describe('update()', () => { ], "alertTypeId": "myType", "apiKey": null, + "apiKeyCreatedByUser": null, "apiKeyOwner": null, "consumer": "myApp", "enabled": false, @@ -2155,6 +2162,7 @@ describe('update()', () => { alertTypeId: 'myType', apiKey: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, consumer: 'myApp', enabled: true, meta: { versionApiKeyLastmodified: 'v7.10.0' }, @@ -2702,6 +2710,7 @@ describe('update()', () => { alertTypeId: 'myType', apiKey: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, consumer: 'myApp', enabled: true, mapped_params: { risk_score: 40, severity: '20-low' }, @@ -2724,4 +2733,210 @@ describe('update()', () => { } ); }); + + it('calls the authentication API key function if the user is authenticated using an api key', async () => { + rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(true); + rulesClientParams.getAuthenticationAPIKey.mockReturnValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: true, + schedule: { interval: '1m' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + notifyWhen: 'onThrottleInterval', + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + apiKey: Buffer.from('123:abc').toString('base64'), + apiKeyCreatedByUser: true, + revision: 1, + scheduledTaskId: 'task-123', + }, + updated_at: new Date().toISOString(), + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingDecryptedAlert, + attributes: { + ...existingDecryptedAlert.attributes, + apiKeyCreatedByUser: true, + }, + }); + + const result = await rulesClient.update({ + id: '1', + data: { + schedule: { interval: '1m' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: '5m', + notifyWhen: 'onThrottleInterval', + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "apiKey": "MTIzOmFiYw==", + "apiKeyCreatedByUser": true, + "createdAt": 2019-02-12T21:01:22.479Z, + "enabled": true, + "id": "1", + "notifyWhen": "onThrottleInterval", + "params": Object { + "bar": true, + }, + "revision": 1, + "schedule": Object { + "interval": "1m", + }, + "scheduledTaskId": "task-123", + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(bulkMarkApiKeysForInvalidationMock).not.toHaveBeenCalled(); + + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionRef": "action_0", + "actionTypeId": "test", + "group": "default", + "params": Object { + "foo": true, + }, + "uuid": "152", + }, + ], + "alertTypeId": "myType", + "apiKey": "MTIzOmFiYw==", + "apiKeyCreatedByUser": true, + "apiKeyOwner": "elastic", + "consumer": "myApp", + "enabled": true, + "meta": Object { + "versionApiKeyLastmodified": "v7.10.0", + }, + "name": "abc", + "notifyWhen": "onThrottleInterval", + "params": Object { + "bar": true, + }, + "revision": 1, + "schedule": Object { + "interval": "1m", + }, + "scheduledTaskId": "task-123", + "tags": Array [ + "foo", + ], + "throttle": "5m", + "updatedAt": "2019-02-12T21:01:22.479Z", + "updatedBy": "elastic", + } + `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "overwrite": true, + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + "version": "123", + } + `); + }); + + test('throws when unsecuredSavedObjectsClient update fails and does not invalidate newly created API key if the user is authenticated using an api key', async () => { + rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(true); + rulesClientParams.getAuthenticationAPIKey.mockReturnValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, + }); + unsecuredSavedObjectsClient.create.mockRejectedValue(new Error('Fail')); + await expect( + rulesClient.update({ + id: '1', + data: { + schedule: { interval: '1m' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + frequency: { + summary: false, + notifyWhen: 'onActionGroupChange', + throttle: null, + }, + }, + ], + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); + expect(bulkMarkApiKeysForInvalidationMock).toHaveBeenCalledTimes(1); + expect(bulkMarkApiKeysForInvalidationMock).toHaveBeenCalledWith( + { + apiKeys: [], + }, + expect.any(Object), + expect.any(Object) + ); + }); }); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update_api_key.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/update_api_key.test.ts index 3051a39f238c1..8689e0e7de2ca 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/update_api_key.test.ts @@ -49,6 +49,8 @@ const rulesClientParams: jest.Mocked = { getEventLogClient: jest.fn(), kibanaVersion, auditLogger, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; beforeEach(() => { @@ -99,6 +101,7 @@ describe('updateApiKey()', () => { }); test('updates the API key for the alert', async () => { + rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(false); rulesClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, result: { id: '234', name: '123', api_key: 'abc' }, @@ -118,6 +121,7 @@ describe('updateApiKey()', () => { enabled: true, apiKey: Buffer.from('234:abc').toString('base64'), apiKeyOwner: 'elastic', + apiKeyCreatedByUser: false, revision: 0, updatedBy: 'elastic', updatedAt: '2019-02-12T21:01:22.479Z', @@ -146,6 +150,110 @@ describe('updateApiKey()', () => { ); }); + test('updates the API key for the alert and does not invalidate the old api key if created by a user authenticated using an api key', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingEncryptedAlert, + attributes: { + ...existingEncryptedAlert.attributes, + apiKeyCreatedByUser: true, + }, + }); + rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(false); + rulesClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '234', name: '123', api_key: 'abc' }, + }); + await rulesClient.updateApiKey({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + apiKey: Buffer.from('234:abc').toString('base64'), + apiKeyOwner: 'elastic', + apiKeyCreatedByUser: false, + revision: 0, + updatedBy: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + }, + { version: '123' } + ); + expect(bulkMarkApiKeysForInvalidation).not.toHaveBeenCalled(); + }); + + test('calls the authentication API key function if the user is authenticated using an api key', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingEncryptedAlert, + attributes: { + ...existingEncryptedAlert.attributes, + apiKeyCreatedByUser: true, + }, + }); + rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(true); + rulesClientParams.getAuthenticationAPIKey.mockReturnValueOnce({ + apiKeysEnabled: true, + result: { id: '234', name: '123', api_key: 'abc' }, + }); + await rulesClient.updateApiKey({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + apiKey: Buffer.from('234:abc').toString('base64'), + apiKeyOwner: 'elastic', + apiKeyCreatedByUser: true, + revision: 0, + updatedBy: 'elastic', + updatedAt: '2019-02-12T21:01:22.479Z', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + }, + { version: '123' } + ); + expect(bulkMarkApiKeysForInvalidation).not.toHaveBeenCalled(); + }); + test('throws an error if API key creation throws', async () => { rulesClientParams.createAPIKey.mockImplementation(() => { throw new Error('no'); diff --git a/x-pack/plugins/alerting/server/rules_client/types.ts b/x-pack/plugins/alerting/server/rules_client/types.ts index 42faafa3db7dd..cb68900e49083 100644 --- a/x-pack/plugins/alerting/server/rules_client/types.ts +++ b/x-pack/plugins/alerting/server/rules_client/types.ts @@ -69,6 +69,8 @@ export interface RulesClientContext { readonly auditLogger?: AuditLogger; readonly eventLogger?: IEventLogger; readonly fieldsToExcludeFromPublicApi: Array; + readonly isAuthenticationTypeAPIKey: () => boolean; + readonly getAuthenticationAPIKey: (name: string) => CreateAPIKeyResult; } export type NormalizedAlertAction = Omit; diff --git a/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts b/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts index bafb7b78293be..7f1a26b0e4940 100644 --- a/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts +++ b/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts @@ -53,6 +53,8 @@ const rulesClientParams: jest.Mocked = { getEventLogClient: jest.fn(), kibanaVersion, minimumScheduleInterval: { value: '1m', enforce: false }, + isAuthenticationTypeAPIKey: jest.fn(), + getAuthenticationAPIKey: jest.fn(), }; // this suite consists of two suites running tests against mutable RulesClient APIs: diff --git a/x-pack/plugins/alerting/server/rules_client_factory.test.ts b/x-pack/plugins/alerting/server/rules_client_factory.test.ts index 14363fcabf440..18364256ce4a9 100644 --- a/x-pack/plugins/alerting/server/rules_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/rules_client_factory.test.ts @@ -122,6 +122,8 @@ test('creates a rules client with proper constructor arguments when security is encryptedSavedObjectsClient: rulesClientFactoryParams.encryptedSavedObjectsClient, kibanaVersion: '7.10.0', minimumScheduleInterval: { value: '1m', enforce: false }, + isAuthenticationTypeAPIKey: expect.any(Function), + getAuthenticationAPIKey: expect.any(Function), }); }); @@ -160,6 +162,8 @@ test('creates a rules client with proper constructor arguments', async () => { getEventLogClient: expect.any(Function), kibanaVersion: '7.10.0', minimumScheduleInterval: { value: '1m', enforce: false }, + isAuthenticationTypeAPIKey: expect.any(Function), + getAuthenticationAPIKey: expect.any(Function), }); }); diff --git a/x-pack/plugins/alerting/server/rules_client_factory.ts b/x-pack/plugins/alerting/server/rules_client_factory.ts index e6d77b618f0b0..d99dc12b13e28 100644 --- a/x-pack/plugins/alerting/server/rules_client_factory.ts +++ b/x-pack/plugins/alerting/server/rules_client_factory.ts @@ -12,7 +12,11 @@ import { PluginInitializerContext, } from '@kbn/core/server'; import { PluginStartContract as ActionsPluginStartContract } from '@kbn/actions-plugin/server'; -import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; +import { + HTTPAuthorizationHeader, + SecurityPluginSetup, + SecurityPluginStart, +} from '@kbn/security-plugin/server'; import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server'; import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; import { IEventLogClientService, IEventLogger } from '@kbn/event-log-plugin/server'; @@ -133,6 +137,30 @@ export class RulesClientFactory { return eventLog.getClient(request); }, eventLogger: this.eventLogger, + isAuthenticationTypeAPIKey() { + if (!securityPluginStart) { + return false; + } + const user = securityPluginStart.authc.getCurrentUser(request); + return user && user.authentication_type ? user.authentication_type === 'api_key' : false; + }, + getAuthenticationAPIKey(name: string) { + const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request); + if (authorizationHeader && authorizationHeader.credentials) { + const apiKey = Buffer.from(authorizationHeader.credentials, 'base64') + .toString() + .split(':'); + return { + apiKeysEnabled: true, + result: { + name, + id: apiKey[0], + api_key: apiKey[1], + }, + }; + } + return { apiKeysEnabled: false }; + }, }); } } diff --git a/x-pack/plugins/alerting/server/saved_objects/mappings.ts b/x-pack/plugins/alerting/server/saved_objects/mappings.ts index 9e3bc06e7ca39..80260733eafed 100644 --- a/x-pack/plugins/alerting/server/saved_objects/mappings.ts +++ b/x-pack/plugins/alerting/server/saved_objects/mappings.ts @@ -8,6 +8,7 @@ import { SavedObjectsTypeMappingDefinition } from '@kbn/core/server'; export const alertMappings: SavedObjectsTypeMappingDefinition = { + dynamic: false, properties: { enabled: { type: 'boolean', diff --git a/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.test.ts b/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.test.ts index a5befc94a340a..eea12e78bdc69 100644 --- a/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.test.ts @@ -34,6 +34,7 @@ describe('transform rule for export', () => { updatedAt: date, apiKey: '4tndskbuhewotw4klrhgjewrt9u', apiKeyOwner: 'me', + apiKeyCreatedByUser: true, throttle: null, legacyId: '1', notifyWhen: 'onActionGroupChange', @@ -66,6 +67,7 @@ describe('transform rule for export', () => { updatedAt: date, apiKey: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, throttle: null, legacyId: '2', notifyWhen: 'onActionGroupChange', @@ -91,6 +93,7 @@ describe('transform rule for export', () => { enabled: false, apiKey: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, scheduledTaskId: null, legacyId: null, executionStatus: { diff --git a/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.ts b/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.ts index 4c1aafa9e5cb7..fe4f1023d8103 100644 --- a/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.ts +++ b/x-pack/plugins/alerting/server/saved_objects/transform_rule_for_export.ts @@ -26,6 +26,7 @@ function transformRuleForExport( enabled: false, apiKey: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, scheduledTaskId: null, executionStatus: getRuleExecutionStatusPending(exportDate), }, diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 804c2739c74e7..d1be241612135 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -357,6 +357,7 @@ export interface RawRule extends SavedObjectAttributes { updatedAt: string; apiKey: string | null; apiKeyOwner: string | null; + apiKeyCreatedByUser?: boolean | null; throttle?: string | null; notifyWhen?: RuleNotifyWhenType | null; muteAll: boolean; diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts index 565bbd6100702..7943e2dfca973 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts @@ -20,13 +20,6 @@ jest.mock('../lib/alerts/fetch_licenses', () => ({ jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), })); -jest.mock('moment', () => { - const actual = jest.requireActual('moment'); - return { - ...actual, - duration: () => ({ humanize: () => 'HUMANIZED_DURATION' }), - }; -}); jest.mock('../static_globals', () => ({ Globals: { @@ -120,7 +113,12 @@ describe('LicenseExpirationRule', () => { getState.mockReset(); }); + afterAll(() => { + jest.useRealTimers(); + }); + it('should fire actions', async () => { + jest.useFakeTimers().setSystemTime(new Date('2023-03-30T00:00:00.000Z')); const alert = new LicenseExpirationRule(); const type = alert.getRuleType(); await type.executor({ @@ -169,7 +167,7 @@ describe('LicenseExpirationRule', () => { ], }, severity: 'danger', - triggeredMS: 1, + triggeredMS: 1680134400000, lastCheckedMS: 0, }, }, @@ -179,11 +177,11 @@ describe('LicenseExpirationRule', () => { action: '[Please update your license.](elasticsearch/nodes)', actionPlain: 'Please update your license.', internalFullMessage: - 'License expiration alert is firing for testCluster. Your license expires in HUMANIZED_DURATION. [Please update your license.](elasticsearch/nodes)', + 'License expiration alert is firing for testCluster. Your license expires in 53 years. [Please update your license.](elasticsearch/nodes)', internalShortMessage: - 'License expiration alert is firing for testCluster. Your license expires in HUMANIZED_DURATION. Please update your license.', + 'License expiration alert is firing for testCluster. Your license expires in 53 years. Please update your license.', clusterName, - expiredDate: 'HUMANIZED_DURATION', + expiredDate: '53 years', state: 'firing', }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts index 5ff6a1cf55175..5f2b892a416a3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/create.ts @@ -123,6 +123,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { notify_when: 'onThrottleInterval', updated_by: user.username, api_key_owner: user.username, + api_key_created_by_user: false, mute_all: false, muted_alert_ids: [], execution_status: response.body.execution_status, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts index d0d93c0154745..d3117ed4ce209 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get.ts @@ -79,6 +79,7 @@ const getTestUtils = ( notify_when: 'onThrottleInterval', updated_by: 'elastic', api_key_owner: 'elastic', + api_key_created_by_user: false, mute_all: false, muted_alert_ids: [], execution_status: response.body.execution_status, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts index ba3963a00ddd1..b6972730cc780 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/alerting/update.ts @@ -119,6 +119,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { enabled: true, updated_by: user.username, api_key_owner: user.username, + api_key_created_by_user: false, mute_all: false, muted_alert_ids: [], actions: [ @@ -219,6 +220,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { enabled: true, updated_by: user.username, api_key_owner: user.username, + api_key_created_by_user: false, mute_all: false, muted_alert_ids: [], scheduled_task_id: createdAlert.scheduled_task_id, @@ -321,6 +323,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { enabled: true, updated_by: user.username, api_key_owner: user.username, + api_key_created_by_user: false, mute_all: false, muted_alert_ids: [], scheduled_task_id: createdAlert.scheduled_task_id, @@ -423,6 +426,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { enabled: true, updated_by: user.username, api_key_owner: user.username, + api_key_created_by_user: false, mute_all: false, muted_alert_ids: [], scheduled_task_id: createdAlert.scheduled_task_id, @@ -523,6 +527,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { enabled: true, updated_by: user.username, api_key_owner: user.username, + api_key_created_by_user: false, mute_all: false, muted_alert_ids: [], scheduled_task_id: createdAlert.scheduled_task_id, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_delete.ts index 3073fc077b5e8..4febd9b866f15 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_delete.ts @@ -13,6 +13,7 @@ import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common const getDefaultRules = (response: any) => ({ id: response.body.rules[0].id, apiKey: response.body.rules[0].apiKey, + apiKeyCreatedByUser: false, notifyWhen: 'onThrottleInterval', enabled: true, name: 'abc', @@ -46,6 +47,7 @@ const getThreeRules = (response: any) => { rules.push({ id: response.body.rules[i].id, apiKey: response.body.rules[i].apiKey, + apiKeyCreatedByUser: false, notifyWhen: 'onThrottleInterval', enabled: true, name: 'abc', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_disable.ts index 86f468534318e..13a89de8381ca 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_disable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_disable.ts @@ -19,6 +19,7 @@ const getDefaultRules = (response: any) => ({ consumer: 'alertsFixture', throttle: '1m', alertTypeId: 'test.noop', + apiKeyCreatedByUser: false, apiKeyOwner: response.body.rules[0].apiKeyOwner, createdBy: 'elastic', updatedBy: response.body.rules[0].updatedBy, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts index 7469773cc5705..3f1239558c5eb 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/bulk_enable.ts @@ -83,6 +83,7 @@ export default ({ getService }: FtrProviderContext) => { consumer: 'alertsFixture', throttle: '1m', alertTypeId: 'test.noop', + apiKeyCreatedByUser: false, apiKeyOwner: response.body.rules[0].apiKeyOwner, createdBy: 'elastic', updatedBy: response.body.rules[0].updatedBy, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts index d9fc773d5d12a..46aef808c0877 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/alerting/clone.ts @@ -161,6 +161,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { throttle: '1m', notify_when: 'onThrottleInterval', updated_by: user.username, + api_key_created_by_user: false, api_key_owner: user.username, mute_all: false, muted_alert_ids: [], diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts index fe71eca901745..3f540927a9be7 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/create.ts @@ -90,6 +90,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { scheduled_task_id: response.body.scheduled_task_id, updated_by: null, api_key_owner: null, + api_key_created_by_user: null, throttle: '1m', notify_when: 'onThrottleInterval', mute_all: false, @@ -194,6 +195,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { scheduled_task_id: response.body.scheduled_task_id, updated_by: null, api_key_owner: null, + api_key_created_by_user: null, throttle: '1m', notify_when: 'onThrottleInterval', mute_all: false, @@ -498,6 +500,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { scheduledTaskId: response.body.scheduledTaskId, updatedBy: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, throttle: '1m', notifyWhen: 'onThrottleInterval', muteAll: false, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find.ts index 2d85f6249db5e..295c8acebbf1a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/find.ts @@ -109,6 +109,7 @@ const findTestUtils = ( params: {}, created_by: null, api_key_owner: null, + api_key_created_by_user: null, scheduled_task_id: match.scheduled_task_id, updated_by: null, throttle: null, @@ -361,6 +362,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { params: {}, createdBy: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, scheduledTaskId: match.scheduledTaskId, updatedBy: null, throttle: '1m', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get.ts index 43f93cccfe470..51f246e2c5609 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get.ts @@ -49,6 +49,7 @@ const getTestUtils = ( scheduled_task_id: response.body.scheduled_task_id, updated_by: null, api_key_owner: null, + api_key_created_by_user: null, throttle: '1m', notify_when: 'onThrottleInterval', mute_all: false, @@ -149,6 +150,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { scheduledTaskId: response.body.scheduledTaskId, updatedBy: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, throttle: '1m', notifyWhen: 'onThrottleInterval', muteAll: false, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update.ts index d3ef53d5d4bd8..c7dc4e1484580 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/update.ts @@ -57,6 +57,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { enabled: true, updated_by: null, api_key_owner: null, + api_key_created_by_user: null, mute_all: false, muted_alert_ids: [], notify_when: 'onThrottleInterval', @@ -163,6 +164,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { enabled: true, updatedBy: null, apiKeyOwner: null, + apiKeyCreatedByUser: null, muteAll: false, mutedInstanceIds: [], notifyWhen: 'onThrottleInterval', diff --git a/x-pack/test/api_integration/apis/synthetics/enable_default_alerting.ts b/x-pack/test/api_integration/apis/synthetics/enable_default_alerting.ts index b1e587eedb5ea..219473ae19b7d 100644 --- a/x-pack/test/api_integration/apis/synthetics/enable_default_alerting.ts +++ b/x-pack/test/api_integration/apis/synthetics/enable_default_alerting.ts @@ -63,6 +63,7 @@ export default function ({ getService }: FtrProviderContext) { notifyWhen: 'onActionGroupChange', consumer: 'uptime', alertTypeId: 'xpack.synthetics.alerts.monitorStatus', + apiKeyCreatedByUser: false, tags: ['SYNTHETICS_DEFAULT_ALERT'], name: 'Synthetics internal alert', enabled: true,