diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts index 14bddceb1c03d..e97b37f16faf0 100644 --- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts @@ -31,7 +31,6 @@ import { } from '../types'; import { validateAlertTypeParams, alertExecutionStatusFromRaw } from '../lib'; import { - InvalidateAPIKeyParams, GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, } from '../../../security/server'; @@ -48,6 +47,7 @@ import { IEvent } from '../../../event_log/server'; import { parseDuration } from '../../common/parse_duration'; import { retryIfConflicts } from '../lib/retry_if_conflicts'; import { partiallyUpdateAlert } from '../saved_objects'; +import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_api_key_for_invalidation'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; @@ -72,7 +72,6 @@ export interface ConstructorOptions { namespace?: string; getUserName: () => Promise; createAPIKey: (name: string) => Promise; - invalidateAPIKey: (params: InvalidateAPIKeyParams) => Promise; getActionsClient: () => Promise; getEventLogClient: () => Promise; kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; @@ -172,9 +171,6 @@ export class AlertsClient { private readonly authorization: AlertsAuthorization; private readonly alertTypeRegistry: AlertTypeRegistry; private readonly createAPIKey: (name: string) => Promise; - private readonly invalidateAPIKey: ( - params: InvalidateAPIKeyParams - ) => Promise; private readonly getActionsClient: () => Promise; private readonly actionsAuthorization: ActionsAuthorization; private readonly getEventLogClient: () => Promise; @@ -191,7 +187,6 @@ export class AlertsClient { namespace, getUserName, createAPIKey, - invalidateAPIKey, encryptedSavedObjectsClient, getActionsClient, actionsAuthorization, @@ -207,7 +202,6 @@ export class AlertsClient { this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; this.authorization = authorization; this.createAPIKey = createAPIKey; - this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; this.getActionsClient = getActionsClient; this.actionsAuthorization = actionsAuthorization; @@ -263,7 +257,11 @@ export class AlertsClient { ); } catch (e) { // Avoid unused API key - this.invalidateApiKey({ apiKey: rawAlert.apiKey }); + markApiKeyForInvalidation( + { apiKey: rawAlert.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ); throw e; } if (data.enabled) { @@ -487,7 +485,13 @@ export class AlertsClient { await Promise.all([ taskIdToRemove ? deleteTaskIfItExists(this.taskManager, taskIdToRemove) : null, - apiKeyToInvalidate ? this.invalidateApiKey({ apiKey: apiKeyToInvalidate }) : null, + apiKeyToInvalidate + ? markApiKeyForInvalidation( + { apiKey: apiKeyToInvalidate }, + this.logger, + this.unsecuredSavedObjectsClient + ) + : null, ]); return removeResult; @@ -526,7 +530,11 @@ export class AlertsClient { await Promise.all([ alertSavedObject.attributes.apiKey - ? this.invalidateApiKey({ apiKey: alertSavedObject.attributes.apiKey }) + ? markApiKeyForInvalidation( + { apiKey: alertSavedObject.attributes.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ) : null, (async () => { if ( @@ -591,7 +599,11 @@ export class AlertsClient { ); } catch (e) { // Avoid unused API key - this.invalidateApiKey({ apiKey: createAttributes.apiKey }); + markApiKeyForInvalidation( + { apiKey: createAttributes.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ); throw e; } @@ -671,28 +683,20 @@ export class AlertsClient { await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); } catch (e) { // Avoid unused API key - this.invalidateApiKey({ apiKey: updateAttributes.apiKey }); + markApiKeyForInvalidation( + { apiKey: updateAttributes.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ); throw e; } if (apiKeyToInvalidate) { - await this.invalidateApiKey({ apiKey: apiKeyToInvalidate }); - } - } - - private async invalidateApiKey({ apiKey }: { apiKey: string | null }): Promise { - if (!apiKey) { - return; - } - - try { - const apiKeyId = Buffer.from(apiKey, 'base64').toString().split(':')[0]; - const response = await this.invalidateAPIKey({ id: apiKeyId }); - if (response.apiKeysEnabled === true && response.result.error_count > 0) { - this.logger.error(`Failed to invalidate API Key [id="${apiKeyId}"]`); - } - } catch (e) { - this.logger.error(`Failed to invalidate API Key: ${e.message}`); + await markApiKeyForInvalidation( + { apiKey: apiKeyToInvalidate }, + this.logger, + this.unsecuredSavedObjectsClient + ); } } @@ -752,7 +756,11 @@ export class AlertsClient { await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version }); } catch (e) { // Avoid unused API key - this.invalidateApiKey({ apiKey: updateAttributes.apiKey }); + markApiKeyForInvalidation( + { apiKey: updateAttributes.apiKey }, + this.logger, + this.unsecuredSavedObjectsClient + ); throw e; } const scheduledTask = await this.scheduleAlert( @@ -764,7 +772,11 @@ export class AlertsClient { scheduledTaskId: scheduledTask.id, }); if (apiKeyToInvalidate) { - await this.invalidateApiKey({ apiKey: apiKeyToInvalidate }); + await markApiKeyForInvalidation( + { apiKey: apiKeyToInvalidate }, + this.logger, + this.unsecuredSavedObjectsClient + ); } } } @@ -825,7 +837,13 @@ export class AlertsClient { attributes.scheduledTaskId ? deleteTaskIfItExists(this.taskManager, attributes.scheduledTaskId) : null, - apiKeyToInvalidate ? this.invalidateApiKey({ apiKey: apiKeyToInvalidate }) : null, + apiKeyToInvalidate + ? await markApiKeyForInvalidation( + { apiKey: apiKeyToInvalidate }, + this.logger, + this.unsecuredSavedObjectsClient + ) + : null, ]); } } diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts index 0f89fc6c9c25c..cc5d10c3346e8 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts @@ -34,7 +34,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts index 965ea1949bf3a..ee407b1a6d50c 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts @@ -34,7 +34,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -711,7 +710,7 @@ describe('create()', () => { expect(taskManager.schedule).not.toHaveBeenCalled(); }); - test('throws error and invalidates API key when create saved object fails', async () => { + test('throws error and add API key to invalidatePendingApiKey SO when create saved object fails', async () => { const data = getMockData(); alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, @@ -731,11 +730,25 @@ describe('create()', () => { ], }); unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure')); + const createdAt = new Date().toISOString(); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt, + }, + references: [], + }); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Test failure"` ); expect(taskManager.schedule).not.toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(2); + expect(unsecuredSavedObjectsClient.create.mock.calls[1][1]).toStrictEqual({ + apiKeyId: '123', + createdAt, + }); }); test('attempts to remove saved object if scheduling failed', async () => { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts index d9b253c3a56e8..e7b975aec8eb0 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts @@ -32,7 +32,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -94,11 +93,22 @@ describe('delete()', () => { }); test('successfully removes an alert', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); const result = await alertsClient.delete({ id: '1' }); expect(result).toEqual({ success: true }); expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe( + 'api_key_pending_invalidation' + ); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); @@ -107,12 +117,21 @@ describe('delete()', () => { test('falls back to SOC.get when getDecryptedAsInternalUser throws an error', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); const result = await alertsClient.delete({ id: '1' }); expect(result).toEqual({ success: true }); expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'delete(): Failed to load API key to invalidate on alert 1: Fail' @@ -133,6 +152,15 @@ describe('delete()', () => { }); test(`doesn't invalidate API key when apiKey is null`, async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ ...existingAlert, attributes: { @@ -142,24 +170,34 @@ describe('delete()', () => { }); await alertsClient.delete({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); await alertsClient.delete({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe( + 'api_key_pending_invalidation' + ); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' + 'Failed to mark for API key [id="MTIzOmFiYw=="] for invalidation: Fail' ); }); test('swallows error when getDecryptedAsInternalUser throws an error', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); await alertsClient.delete({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'delete(): Failed to load API key to invalidate on alert 1: Fail' ); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts index d0557df622028..11ce0027f82d8 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts @@ -13,6 +13,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { getBeforeSetup } from './lib'; +import { InvalidatePendingApiKey } from '../../types'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -33,7 +34,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -108,6 +108,15 @@ describe('disable()', () => { }); test('disables an alert', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); await alertsClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { @@ -145,11 +154,22 @@ describe('disable()', () => { } ); expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect( + (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId + ).toBe('123'); }); test('falls back when getDecryptedAsInternalUser throws an error', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); await alertsClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); @@ -188,7 +208,7 @@ describe('disable()', () => { } ); expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test(`doesn't disable already disabled alerts`, async () => { @@ -201,26 +221,54 @@ describe('disable()', () => { }, }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); + await alertsClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); expect(taskManager.remove).not.toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test(`doesn't invalidate when no API key is used`, async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce(existingAlert); await alertsClient.disable({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('swallows error when failing to load decrypted saved object', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); await alertsClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); expect(taskManager.remove).toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'disable(): Failed to load API key to invalidate on alert 1: Fail' ); @@ -235,11 +283,10 @@ describe('disable()', () => { }); test('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); await alertsClient.disable({ id: '1' }); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' + 'Failed to mark for API key [id="MTIzOmFiYw=="] for invalidation: Fail' ); }); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts index 215493c71aec7..16e83c42d8930 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts @@ -14,6 +14,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { TaskStatus } from '../../../../task_manager/server'; import { getBeforeSetup } from './lib'; +import { InvalidatePendingApiKey } from '../../types'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -34,7 +35,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -147,6 +147,7 @@ describe('enable()', () => { }); test('enables an alert', async () => { + const createdAt = new Date().toISOString(); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ ...existingAlert, attributes: { @@ -157,13 +158,22 @@ describe('enable()', () => { updatedBy: 'elastic', }, }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt, + }, + references: [], + }); await alertsClient.enable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toBeCalledWith('api_key_pending_invalidation'); expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', @@ -217,6 +227,7 @@ describe('enable()', () => { }); test('invalidates API key if ever one existed prior to updating', async () => { + const createdAt = new Date().toISOString(); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ ...existingAlert, attributes: { @@ -224,13 +235,24 @@ describe('enable()', () => { apiKey: Buffer.from('123:abc').toString('base64'), }, }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt, + }, + references: [], + }); await alertsClient.enable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect( + (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId + ).toBe('123'); }); test(`doesn't enable already enabled alerts`, async () => { @@ -312,19 +334,31 @@ describe('enable()', () => { }); test('throws error when failing to update the first time', async () => { + const createdAt = new Date().toISOString(); alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, result: { id: '123', name: '123', api_key: 'abc' }, }); unsecuredSavedObjectsClient.update.mockReset(); unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt, + }, + references: [], + }); await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Fail to update"` ); expect(alertsClientParams.getUserName).toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect( + (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId + ).toBe('123'); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); expect(taskManager.schedule).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts index c1adaddc80d9e..1b3a776bd23e0 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts @@ -35,7 +35,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts index 004230403de2e..5c0d80f159b31 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts @@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts index 9cb2a33222d23..269b2eb2ab7a7 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts @@ -39,7 +39,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts index 8b32f05f6d5a1..79a064beba166 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts @@ -34,7 +34,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts b/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts index 5ebb4e90d4b50..028a7c6737474 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts @@ -46,14 +46,6 @@ export function getBeforeSetup( ) { jest.resetAllMocks(); alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false }); - alertsClientParams.invalidateAPIKey.mockResolvedValue({ - apiKeysEnabled: true, - result: { - invalidated_api_keys: [], - previously_invalidated_api_keys: [], - error_count: 0, - }, - }); alertsClientParams.getUserName.mockResolvedValue('elastic'); taskManager.runNow.mockResolvedValue({ id: '' }); const actionsClient = actionsClientMock.create(); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts index b2f5c5498f848..8cbe47655ef68 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts @@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts index 88199dfd1f7b9..868fa3d8c6aa2 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts @@ -32,7 +32,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts index cd7112b3551b3..05ca741f480ca 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts @@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts index 07666c1cc6261..5ef1af9b6f0ee 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts @@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts index 97711b8c14579..88692239ac2fe 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts @@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts index 1dcde6addb9bf..ad58e36ade722 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts @@ -10,7 +10,7 @@ import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; -import { IntervalSchedule } from '../../types'; +import { IntervalSchedule, InvalidatePendingApiKey } from '../../types'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; @@ -38,7 +38,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -161,6 +160,15 @@ describe('update()', () => { }, ], }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); const result = await alertsClient.update({ id: '1', data: { @@ -241,7 +249,7 @@ describe('update()', () => { namespace: 'default', }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(2); 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(` @@ -376,6 +384,24 @@ describe('update()', () => { }, ], }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); const result = await alertsClient.update({ id: '1', data: { @@ -423,7 +449,7 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(2); 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(` @@ -530,6 +556,15 @@ describe('update()', () => { }, ], }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); const result = await alertsClient.update({ id: '1', data: { @@ -578,7 +613,7 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(2); 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(` @@ -732,7 +767,6 @@ describe('update()', () => { }); it('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { @@ -775,6 +809,7 @@ describe('update()', () => { }, ], }); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); // add ApiKey to invalidate await alertsClient.update({ id: '1', data: { @@ -797,7 +832,7 @@ describe('update()', () => { }, }); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' + 'Failed to mark for API key [id="MTIzOmFiYw=="] for invalidation: Fail' ); }); @@ -965,8 +1000,9 @@ describe('update()', () => { }, }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalledWith({ id: '123' }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '234' }); + expect( + (unsecuredSavedObjectsClient.create.mock.calls[1][1] as InvalidatePendingApiKey).apiKeyId + ).toBe('234'); }); describe('updating an alert schedule', () => { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts index 1f3b567b2c031..af178a1fac5f5 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts @@ -13,6 +13,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { ActionsAuthorization } from '../../../../actions/server'; import { getBeforeSetup } from './lib'; +import { InvalidatePendingApiKey } from '../../types'; const taskManager = taskManagerMock.createStart(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -32,7 +33,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger: loggingSystemMock.create().get(), encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -80,6 +80,15 @@ describe('updateApiKey()', () => { beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingEncryptedAlert); alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, @@ -121,11 +130,22 @@ describe('updateApiKey()', () => { }, { version: '123' } ); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe( + 'api_key_pending_invalidation' + ); }); test('falls back to SOC when getDecryptedAsInternalUser throws an error', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); await alertsClient.updateApiKey({ id: '1' }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); @@ -160,28 +180,37 @@ describe('updateApiKey()', () => { }, { version: '123' } ); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValue(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); await alertsClient.updateApiKey({ id: '1' }); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' - ); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe( + 'api_key_pending_invalidation' + ); }); test('swallows error when getting decrypted object throws', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); await alertsClient.updateApiKey({ id: '1' }); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'updateApiKey(): Failed to load API key to invalidate on alert 1: Fail' ); expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('throws when unsecuredSavedObjectsClient update fails and invalidates newly created API key', async () => { @@ -190,12 +219,22 @@ describe('updateApiKey()', () => { result: { id: '234', name: '234', api_key: 'abc' }, }); unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail')); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '234', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Fail"` ); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalledWith({ id: '123' }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '234' }); + expect( + (unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId + ).toBe('234'); }); describe('authorization', () => { diff --git a/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts b/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts index b1ac5ac4c6783..ca9389ece310c 100644 --- a/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_conflict_retries.test.ts @@ -45,7 +45,6 @@ const alertsClientParams: jest.Mocked = { namespace: 'default', getUserName: jest.fn(), createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), logger, encryptedSavedObjectsClient: encryptedSavedObjects, getActionsClient: jest.fn(), @@ -115,7 +114,7 @@ async function update(success: boolean) { ); return expectConflict(success, err, 'create'); } - expectSuccess(success, 2, 'create'); + expectSuccess(success, 3, 'create'); // only checking the debug messages in this test expect(logger.debug).nthCalledWith(1, `alertsClient.update('alert-id') conflict, retrying ...`); @@ -306,14 +305,6 @@ beforeEach(() => { jest.resetAllMocks(); alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false }); - alertsClientParams.invalidateAPIKey.mockResolvedValue({ - apiKeysEnabled: true, - result: { - invalidated_api_keys: [], - previously_invalidated_api_keys: [], - error_count: 0, - }, - }); alertsClientParams.getUserName.mockResolvedValue('elastic'); taskManager.runNow.mockResolvedValue({ id: '' }); diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 3cf6666e90eb0..bdbfc726dab8f 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -92,7 +92,7 @@ test('creates an alerts client with proper constructor arguments when security i expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, { excludedWrappers: ['security'], - includedHiddenTypes: ['alert'], + includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }); const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); @@ -125,7 +125,6 @@ test('creates an alerts client with proper constructor arguments when security i getActionsClient: expect.any(Function), getEventLogClient: expect.any(Function), createAPIKey: expect.any(Function), - invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, kibanaVersion: '7.10.0', }); @@ -142,7 +141,7 @@ test('creates an alerts client with proper constructor arguments', async () => { expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, { excludedWrappers: ['security'], - includedHiddenTypes: ['alert'], + includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }); const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); @@ -167,7 +166,6 @@ test('creates an alerts client with proper constructor arguments', async () => { namespace: 'default', getUserName: expect.any(Function), createAPIKey: expect.any(Function), - invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, getActionsClient: expect.any(Function), getEventLogClient: expect.any(Function), diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index eccd810391307..069703be72f8a 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -14,7 +14,7 @@ import { PluginStartContract as ActionsPluginStartContract } from '../../actions import { AlertsClient } from './alerts_client'; import { ALERTS_FEATURE_ID } from '../common'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; -import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup } from '../../security/server'; import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../task_manager/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; @@ -94,7 +94,7 @@ export class AlertsClientFactory { alertTypeRegistry: this.alertTypeRegistry, unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, { excludedWrappers: ['security'], - includedHiddenTypes: ['alert'], + includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }), authorization, actionsAuthorization: actions.getActionsAuthorizationWithRequest(request), @@ -129,22 +129,6 @@ export class AlertsClientFactory { result: createAPIKeyResult, }; }, - async invalidateAPIKey(params: InvalidateAPIKeyParams) { - if (!securityPluginSetup) { - return { apiKeysEnabled: false }; - } - const invalidateAPIKeyResult = await securityPluginSetup.authc.invalidateAPIKeyAsInternalUser( - params - ); - // Null when Elasticsearch security is disabled - if (!invalidateAPIKeyResult) { - return { apiKeysEnabled: false }; - } - return { - apiKeysEnabled: true, - result: invalidateAPIKeyResult, - }; - }, async getActionsClient() { return actions.getActionsClientWithRequest(request); }, diff --git a/x-pack/plugins/alerts/server/config.test.ts b/x-pack/plugins/alerts/server/config.test.ts index 93aa3c38a0460..bf3b30b5d2378 100644 --- a/x-pack/plugins/alerts/server/config.test.ts +++ b/x-pack/plugins/alerts/server/config.test.ts @@ -13,6 +13,10 @@ describe('config validation', () => { "healthCheck": Object { "interval": "60m", }, + "invalidateApiKeysTask": Object { + "interval": "5m", + "removalDelay": "5m", + }, } `); }); diff --git a/x-pack/plugins/alerts/server/config.ts b/x-pack/plugins/alerts/server/config.ts index a6d2196a407b5..41340c7dfe5fc 100644 --- a/x-pack/plugins/alerts/server/config.ts +++ b/x-pack/plugins/alerts/server/config.ts @@ -11,6 +11,10 @@ export const configSchema = schema.object({ healthCheck: schema.object({ interval: schema.string({ validate: validateDurationSchema, defaultValue: '60m' }), }), + invalidateApiKeysTask: schema.object({ + interval: schema.string({ validate: validateDurationSchema, defaultValue: '5m' }), + removalDelay: schema.string({ validate: validateDurationSchema, defaultValue: '5m' }), + }), }); export type AlertsConfig = TypeOf; diff --git a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.test.ts b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.test.ts new file mode 100644 index 0000000000000..7b30c22c47f8a --- /dev/null +++ b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { loggingSystemMock, savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { markApiKeyForInvalidation } from './mark_api_key_for_invalidation'; + +describe('markApiKeyForInvalidation', () => { + test('should call savedObjectsClient create with the proper params', async () => { + const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'api_key_pending_invalidation', + attributes: { + apiKeyId: '123', + createdAt: '2019-02-12T21:01:22.479Z', + }, + references: [], + }); + await markApiKeyForInvalidation( + { apiKey: Buffer.from('123:abc').toString('base64') }, + loggingSystemMock.create().get(), + unsecuredSavedObjectsClient + ); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(2); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual( + 'api_key_pending_invalidation' + ); + }); + + test('should log the proper error when savedObjectsClient create failed', async () => { + const logger = loggingSystemMock.create().get(); + const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); + await markApiKeyForInvalidation( + { apiKey: Buffer.from('123').toString('base64') }, + logger, + unsecuredSavedObjectsClient + ); + expect(logger.error).toHaveBeenCalledWith( + 'Failed to mark for API key [id="MTIz"] for invalidation: Fail' + ); + }); +}); diff --git a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.ts b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.ts new file mode 100644 index 0000000000000..db25f5b3e19eb --- /dev/null +++ b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/mark_api_key_for_invalidation.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Logger, SavedObjectsClientContract } from 'src/core/server'; + +export const markApiKeyForInvalidation = async ( + { apiKey }: { apiKey: string | null }, + logger: Logger, + savedObjectsClient: SavedObjectsClientContract +): Promise => { + if (!apiKey) { + return; + } + try { + const apiKeyId = Buffer.from(apiKey, 'base64').toString().split(':')[0]; + await savedObjectsClient.create('api_key_pending_invalidation', { + apiKeyId, + createdAt: new Date().toISOString(), + }); + } catch (e) { + logger.error(`Failed to mark for API key [id="${apiKey}"] for invalidation: ${e.message}`); + } +}; diff --git a/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts new file mode 100644 index 0000000000000..77cbb9f4a4a85 --- /dev/null +++ b/x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts @@ -0,0 +1,226 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Logger, + CoreStart, + SavedObjectsFindResponse, + KibanaRequest, + SavedObjectsClientContract, +} from 'kibana/server'; +import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; +import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../../security/server'; +import { + RunContext, + TaskManagerSetupContract, + TaskManagerStartContract, +} from '../../../task_manager/server'; +import { InvalidateAPIKeyResult } from '../alerts_client'; +import { AlertsConfig } from '../config'; +import { timePeriodBeforeDate } from '../lib/get_cadence'; +import { AlertingPluginsStart } from '../plugin'; +import { InvalidatePendingApiKey } from '../types'; + +const TASK_TYPE = 'alerts_invalidate_api_keys'; +export const TASK_ID = `Alerts-${TASK_TYPE}`; + +const invalidateAPIKey = async ( + params: InvalidateAPIKeyParams, + securityPluginSetup?: SecurityPluginSetup +): Promise => { + if (!securityPluginSetup) { + return { apiKeysEnabled: false }; + } + const invalidateAPIKeyResult = await securityPluginSetup.authc.invalidateAPIKeyAsInternalUser( + params + ); + // Null when Elasticsearch security is disabled + if (!invalidateAPIKeyResult) { + return { apiKeysEnabled: false }; + } + return { + apiKeysEnabled: true, + result: invalidateAPIKeyResult, + }; +}; + +export function initializeApiKeyInvalidator( + logger: Logger, + coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>, + taskManager: TaskManagerSetupContract, + config: Promise, + securityPluginSetup?: SecurityPluginSetup +) { + registerApiKeyInvalitorTaskDefinition( + logger, + coreStartServices, + taskManager, + config, + securityPluginSetup + ); +} + +export async function scheduleApiKeyInvalidatorTask( + logger: Logger, + config: Promise, + taskManager: TaskManagerStartContract +) { + const interval = (await config).invalidateApiKeysTask.interval; + try { + await taskManager.ensureScheduled({ + id: TASK_ID, + taskType: TASK_TYPE, + schedule: { + interval, + }, + state: {}, + params: {}, + }); + } catch (e) { + logger.debug(`Error scheduling task, received ${e.message}`); + } +} + +function registerApiKeyInvalitorTaskDefinition( + logger: Logger, + coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>, + taskManager: TaskManagerSetupContract, + config: Promise, + securityPluginSetup?: SecurityPluginSetup +) { + taskManager.registerTaskDefinitions({ + [TASK_TYPE]: { + title: 'Invalidate alert API Keys', + createTaskRunner: taskRunner(logger, coreStartServices, config, securityPluginSetup), + }, + }); +} + +function getFakeKibanaRequest(basePath: string) { + const requestHeaders: Record = {}; + return ({ + headers: requestHeaders, + getBasePath: () => basePath, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + } as unknown) as KibanaRequest; +} + +function taskRunner( + logger: Logger, + coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>, + config: Promise, + securityPluginSetup?: SecurityPluginSetup +) { + return ({ taskInstance }: RunContext) => { + const { state } = taskInstance; + return { + async run() { + let totalInvalidated = 0; + const configResult = await config; + try { + const [{ savedObjects, http }, { encryptedSavedObjects }] = await coreStartServices; + const savedObjectsClient = savedObjects.getScopedClient( + getFakeKibanaRequest(http.basePath.serverBasePath), + { + includedHiddenTypes: ['api_key_pending_invalidation'], + excludedWrappers: ['security'], + } + ); + const encryptedSavedObjectsClient = encryptedSavedObjects.getClient({ + includedHiddenTypes: ['api_key_pending_invalidation'], + }); + const configuredDelay = configResult.invalidateApiKeysTask.removalDelay; + const delay = timePeriodBeforeDate(new Date(), configuredDelay).toISOString(); + + let hasApiKeysPendingInvalidation = true; + const PAGE_SIZE = 100; + do { + const apiKeysToInvalidate = await savedObjectsClient.find({ + type: 'api_key_pending_invalidation', + filter: `api_key_pending_invalidation.attributes.createdAt <= "${delay}"`, + page: 1, + sortField: 'createdAt', + sortOrder: 'asc', + perPage: PAGE_SIZE, + }); + totalInvalidated += await invalidateApiKeys( + logger, + savedObjectsClient, + apiKeysToInvalidate, + encryptedSavedObjectsClient, + securityPluginSetup + ); + + hasApiKeysPendingInvalidation = apiKeysToInvalidate.total > PAGE_SIZE; + } while (hasApiKeysPendingInvalidation); + + return { + state: { + runs: (state.runs || 0) + 1, + total_invalidated: totalInvalidated, + }, + schedule: { + interval: configResult.invalidateApiKeysTask.interval, + }, + }; + } catch (e) { + logger.warn(`Error executing alerting apiKey invalidation task: ${e.message}`); + return { + state: { + runs: (state.runs || 0) + 1, + total_invalidated: totalInvalidated, + }, + schedule: { + interval: configResult.invalidateApiKeysTask.interval, + }, + }; + } + }, + }; + }; +} + +async function invalidateApiKeys( + logger: Logger, + savedObjectsClient: SavedObjectsClientContract, + apiKeysToInvalidate: SavedObjectsFindResponse, + encryptedSavedObjectsClient: EncryptedSavedObjectsClient, + securityPluginSetup?: SecurityPluginSetup +) { + let totalInvalidated = 0; + await Promise.all( + apiKeysToInvalidate.saved_objects.map(async (apiKeyObj) => { + const decryptedApiKey = await encryptedSavedObjectsClient.getDecryptedAsInternalUser< + InvalidatePendingApiKey + >('api_key_pending_invalidation', apiKeyObj.id); + const apiKeyId = decryptedApiKey.attributes.apiKeyId; + const response = await invalidateAPIKey({ id: apiKeyId }, securityPluginSetup); + if (response.apiKeysEnabled === true && response.result.error_count > 0) { + logger.error(`Failed to invalidate API Key [id="${apiKeyObj.attributes.apiKeyId}"]`); + } else { + try { + await savedObjectsClient.delete('api_key_pending_invalidation', apiKeyObj.id); + totalInvalidated++; + } catch (err) { + logger.error( + `Failed to cleanup api key "${apiKeyObj.attributes.apiKeyId}". Error: ${err.message}` + ); + } + } + }) + ); + logger.debug(`Total invalidated api keys "${totalInvalidated}"`); + return totalInvalidated; +} diff --git a/x-pack/plugins/alerts/server/lib/get_cadence.ts b/x-pack/plugins/alerts/server/lib/get_cadence.ts new file mode 100644 index 0000000000000..d09ed0c2122cd --- /dev/null +++ b/x-pack/plugins/alerts/server/lib/get_cadence.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { memoize } from 'lodash'; + +export enum TimeUnit { + Minute = 'm', + Second = 's', + Hour = 'h', + Day = 'd', +} +const VALID_CADENCE = new Set(Object.values(TimeUnit)); +const CADENCE_IN_MS: Record = { + [TimeUnit.Second]: 1000, + [TimeUnit.Minute]: 60 * 1000, + [TimeUnit.Hour]: 60 * 60 * 1000, + [TimeUnit.Day]: 24 * 60 * 60 * 1000, +}; + +const isNumeric = (numAsStr: string) => /^\d+$/.test(numAsStr); + +export const parseIntervalAsMillisecond = memoize((value: string): number => { + const numericAsStr: string = value.slice(0, -1); + const numeric: number = parseInt(numericAsStr, 10); + const cadence: TimeUnit | string = value.slice(-1); + if ( + !VALID_CADENCE.has(cadence as TimeUnit) || + isNaN(numeric) || + numeric <= 0 || + !isNumeric(numericAsStr) + ) { + throw new Error( + `Invalid time value "${value}". Time must be of the form {number}m. Example: 5m.` + ); + } + return numeric * CADENCE_IN_MS[cadence as TimeUnit]; +}); + +/** + * Returns a date that is the specified interval from given date. + * + * @param {Date} date - The date to add interval to + * @param {string} interval - THe time of the form `Nm` such as `5m` + */ +export function timePeriodBeforeDate(date: Date, timePeriod: string): Date { + const result = new Date(date.valueOf()); + const milisecFromTime = parseIntervalAsMillisecond(timePeriod); + result.setMilliseconds(result.getMilliseconds() - milisecFromTime); + return result; +} diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index 715fbc6aeed45..62f4b7d5a3fc4 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -22,6 +22,10 @@ describe('Alerting Plugin', () => { healthCheck: { interval: '5m', }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '5m', + }, }); const plugin = new AlertingPlugin(context); @@ -67,6 +71,10 @@ describe('Alerting Plugin', () => { healthCheck: { interval: '5m', }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '5m', + }, }); const plugin = new AlertingPlugin(context); @@ -114,6 +122,10 @@ describe('Alerting Plugin', () => { healthCheck: { interval: '5m', }, + invalidateApiKeysTask: { + interval: '5m', + removalDelay: '5m', + }, }); const plugin = new AlertingPlugin(context); diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 1fa89606a76fc..0c91e93938346 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -65,6 +65,10 @@ import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/ import { IEventLogger, IEventLogService, IEventLogClientService } from '../../event_log/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; import { setupSavedObjects } from './saved_objects'; +import { + initializeApiKeyInvalidator, + scheduleApiKeyInvalidatorTask, +} from './invalidate_pending_api_keys/task'; import { getHealthStatusStream, scheduleAlertingHealthCheck, @@ -200,6 +204,14 @@ export class AlertingPlugin { }); } + initializeApiKeyInvalidator( + this.logger, + core.getStartServices(), + plugins.taskManager, + this.config, + this.security + ); + core.getStartServices().then(async ([, startPlugins]) => { core.status.set( combineLatest([ @@ -308,7 +320,9 @@ export class AlertingPlugin { }); scheduleAlertingTelemetry(this.telemetryLogger, plugins.taskManager); + scheduleAlertingHealthCheck(this.logger, this.config, plugins.taskManager); + scheduleApiKeyInvalidatorTask(this.telemetryLogger, this.config, plugins.taskManager); return { listTypes: alertTypeRegistry!.list.bind(this.alertTypeRegistry!), diff --git a/x-pack/plugins/alerts/server/saved_objects/index.ts b/x-pack/plugins/alerts/server/saved_objects/index.ts index 9aa1f86676eaa..da30273e93c6b 100644 --- a/x-pack/plugins/alerts/server/saved_objects/index.ts +++ b/x-pack/plugins/alerts/server/saved_objects/index.ts @@ -42,10 +42,32 @@ export function setupSavedObjects( mappings: mappings.alert, }); + savedObjects.registerType({ + name: 'api_key_pending_invalidation', + hidden: true, + namespaceType: 'agnostic', + mappings: { + properties: { + apiKeyId: { + type: 'keyword', + }, + createdAt: { + type: 'date', + }, + }, + }, + }); + // Encrypted attributes encryptedSavedObjects.registerType({ type: 'alert', attributesToEncrypt: new Set(['apiKey']), attributesToExcludeFromAAD: new Set(AlertAttributesExcludedFromAAD), }); + + // Encrypted attributes + encryptedSavedObjects.registerType({ + type: 'api_key_pending_invalidation', + attributesToEncrypt: new Set(['apiKeyId']), + }); } diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts index 9226461f6e30a..dde1628156658 100644 --- a/x-pack/plugins/alerts/server/types.ts +++ b/x-pack/plugins/alerts/server/types.ts @@ -180,4 +180,16 @@ export interface AlertsConfigType { }; } +export interface AlertsConfigType { + invalidateApiKeysTask: { + interval: string; + removalDelay: string; + }; +} + +export interface InvalidatePendingApiKey { + apiKeyId: string; + createdAt: string; +} + export type AlertTypeRegistry = PublicMethodsOf; diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index f9fdfaed1c79b..cb78e76bdd697 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -92,6 +92,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', + '--xpack.alerts.invalidateApiKeysTask.interval="15s"', `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, ...actionsProxyUrl, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts index 7ed864afac4cc..998ec6ab2ed0e 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/alert_types.ts @@ -437,6 +437,21 @@ export function defineAlertTypes( throw new Error('this alert is intended to fail'); }, }; + const longRunningAlertType: AlertType = { + id: 'test.longRunning', + name: 'Test: Long Running', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + producer: 'alertsFixture', + defaultActionGroupId: 'default', + async executor() { + await new Promise((resolve) => setTimeout(resolve, 5000)); + }, + }; alerts.registerType(getAlwaysFiringAlertType()); alerts.registerType(getCumulativeFiringAlertType()); @@ -449,4 +464,5 @@ export function defineAlertTypes( alerts.registerType(onlyStateVariablesAlertType); alerts.registerType(getPatternFiringAlertType()); alerts.registerType(throwAlertType); + alerts.registerType(longRunningAlertType); } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index fbf3b798500d3..d832902fe066d 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -50,6 +50,7 @@ export class FixturePlugin implements Plugin, + res: KibanaResponseFactory + ): Promise> => { + try { + const [{ savedObjects }] = await core.getStartServices(); + const savedObjectsWithTasksAndAlerts = await savedObjects.getScopedClient(req, { + includedHiddenTypes: ['api_key_pending_invalidation'], + }); + const findResult = await savedObjectsWithTasksAndAlerts.find({ + type: 'api_key_pending_invalidation', + }); + return res.ok({ + body: { apiKeysToInvalidate: findResult.saved_objects }, + }); + } catch (err) { + return res.badRequest({ body: err }); + } + } + ); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index 8836bc2e4db2f..9c3d2801c0886 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -836,6 +836,80 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { } }); + it('should handle updates for a long running alert type without failing the underlying tasks due to invalidated ApiKey', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send({ + enabled: true, + name: 'abc', + tags: ['foo'], + alertTypeId: 'test.longRunning', + consumer: 'alertsFixture', + schedule: { interval: '1s' }, + throttle: '1m', + actions: [], + params: {}, + }) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + const updatedData = { + name: 'bcd', + tags: ['bar'], + params: { + foo: true, + }, + schedule: { interval: '1m' }, + actions: [], + throttle: '1m', + }; + const response = await supertestWithoutAuth + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send(updatedData); + + const statusUpdates: string[] = []; + await retry.try(async () => { + const alertTask = (await getAlertingTaskById(createdAlert.scheduledTaskId)).docs[0]; + statusUpdates.push(alertTask.status); + expect(alertTask.status).to.eql('idle'); + }); + + expect(statusUpdates.find((status) => status === 'failed')).to.be(undefined); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'update', + 'test.longRunning', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + await retry.try(async () => { + const alertTask = (await getAlertingTaskById(createdAlert.scheduledTaskId)).docs[0]; + expect(alertTask.status).to.eql('idle'); + // ensure the alert is rescheduled to a minute from now + ensureDatetimeIsWithinRange(Date.parse(alertTask.runAt), 60 * 1000); + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it('should handle updates to an alert schedule by setting the new schedule for the underlying task', async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`)