diff --git a/x-pack/legacy/plugins/alerting/mappings.json b/x-pack/legacy/plugins/alerting/mappings.json index d9399ac767329..bc648e874cfa4 100644 --- a/x-pack/legacy/plugins/alerting/mappings.json +++ b/x-pack/legacy/plugins/alerting/mappings.json @@ -46,6 +46,12 @@ }, "throttle": { "type": "keyword" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" } } } diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.mock.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.mock.ts index 154014b93e899..c7d359491680f 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.mock.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.mock.ts @@ -18,6 +18,10 @@ const createAlertsClientMock = () => { enable: jest.fn(), disable: jest.fn(), updateApiKey: jest.fn(), + muteAll: jest.fn(), + unmuteAll: jest.fn(), + muteInstance: jest.fn(), + unmuteInstance: jest.fn(), }; return mocked; }; diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index 0cbb02b676a99..bba0051e31e08 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -127,25 +127,25 @@ describe('create()', () => { }); const result = await alertsClient.create({ data }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "alertTypeParams": Object { + "bar": true, + }, + "id": "1", + "interval": "10s", + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); expect(savedObjectsClient.create.mock.calls[0]).toHaveLength(3); expect(savedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); @@ -169,61 +169,63 @@ describe('create()', () => { "createdBy": "elastic", "enabled": true, "interval": "10s", + "muteAll": false, + "mutedInstanceIds": Array [], "throttle": null, "updatedBy": "elastic", } `); expect(savedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - } - `); + Object { + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + } + `); expect(taskManager.schedule).toHaveBeenCalledTimes(1); expect(taskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "params": Object { - "alertId": "1", - "spaceId": "default", - }, - "scope": Array [ - "alerting", - ], - "state": Object { - "alertInstances": Object {}, - "alertTypeState": Object {}, - "previousStartedAt": null, - }, - "taskType": "alerting:123", - }, - ] - `); + Array [ + Object { + "params": Object { + "alertId": "1", + "spaceId": "default", + }, + "scope": Array [ + "alerting", + ], + "state": Object { + "alertInstances": Object {}, + "alertTypeState": Object {}, + "previousStartedAt": null, + }, + "taskType": "alerting:123", + }, + ] + `); expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4); expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); expect(savedObjectsClient.update.mock.calls[0][1]).toEqual('1'); expect(savedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "scheduledTaskId": "task-123", - } - `); + Object { + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - } - `); + Object { + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + } + `); }); test('creates a disabled alert', async () => { @@ -265,25 +267,25 @@ describe('create()', () => { }); const result = await alertsClient.create({ data }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "enabled": false, - "id": "1", - "interval": 10000, - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "alertTypeParams": Object { + "bar": true, + }, + "enabled": false, + "id": "1", + "interval": 10000, + } + `); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); expect(taskManager.schedule).toHaveBeenCalledTimes(0); }); @@ -367,11 +369,11 @@ describe('create()', () => { ); expect(savedObjectsClient.delete).toHaveBeenCalledTimes(1); expect(savedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - "1", - ] - `); + Array [ + "alert", + "1", + ] + `); }); test('returns task manager error if cleanup fails, logs to console', async () => { @@ -417,14 +419,14 @@ describe('create()', () => { ); expect(alertsClientParams.log).toHaveBeenCalledTimes(1); expect(alertsClientParams.log.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Array [ - "alerting", - "error", - ], - "Failed to cleanup alert \\"1\\" after scheduling task failed. Error: Saved object delete error", - ] - `); + Array [ + Array [ + "alerting", + "error", + ], + "Failed to cleanup alert \\"1\\" after scheduling task failed. Error: Saved object delete error", + ] + `); }); test('throws an error if alert type not registerd', async () => { @@ -526,6 +528,8 @@ describe('create()', () => { enabled: true, interval: '10s', throttle: null, + muteAll: false, + mutedInstanceIds: [], }, { references: [ @@ -571,6 +575,8 @@ describe('enable()', () => { 'alert', '1', { + interval: '10s', + alertTypeId: '2', enabled: true, scheduledTaskId: 'task-123', updatedBy: 'elastic', @@ -648,6 +654,8 @@ describe('enable()', () => { 'alert', '1', { + interval: '10s', + alertTypeId: '2', enabled: true, scheduledTaskId: 'task-123', apiKey: Buffer.from('123:abc').toString('base64'), @@ -694,6 +702,8 @@ describe('disable()', () => { 'alert', '1', { + interval: '10s', + alertTypeId: '2', apiKey: null, apiKeyOwner: null, enabled: false, @@ -727,6 +737,216 @@ describe('disable()', () => { }); }); +describe('muteAll()', () => { + test('mutes an alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + muteAll: false, + }, + references: [], + }); + + await alertsClient.muteAll({ id: '1' }); + expect(savedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { muteAll: true, mutedInstanceIds: [], updatedBy: 'elastic' }, + { references: [] } + ); + }); + + test('skips muting when alert already muted', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + muteAll: true, + }, + references: [], + }); + + await alertsClient.muteAll({ id: '1' }); + expect(savedObjectsClient.update).not.toHaveBeenCalled(); + }); +}); + +describe('unmuteAll()', () => { + test('unmutes an alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + muteAll: true, + }, + references: [], + }); + + await alertsClient.unmuteAll({ id: '1' }); + expect(savedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { muteAll: false, mutedInstanceIds: [], updatedBy: 'elastic' }, + { references: [] } + ); + }); + + test(`skips unmuting when alert isn't unmuted`, async () => { + const alertsClient = new AlertsClient(alertsClientParams); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + muteAll: false, + }, + references: [], + }); + + await alertsClient.unmuteAll({ id: '1' }); + expect(savedObjectsClient.update).not.toHaveBeenCalled(); + }); +}); + +describe('muteInstance()', () => { + test('mutes an alert instance', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + interval: '10s', + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: [], + }, + references: [], + }); + + await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(savedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + mutedInstanceIds: ['2'], + updatedBy: 'elastic', + }, + { version: undefined, references: [] } + ); + }); + + test('skips muting when alert instance already muted', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + interval: '10s', + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: ['2'], + }, + references: [], + }); + + await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(savedObjectsClient.update).not.toHaveBeenCalled(); + }); + + test('skips muting when alert is muted', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + interval: '10s', + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: [], + muteAll: true, + }, + references: [], + }); + + await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(savedObjectsClient.update).not.toHaveBeenCalled(); + }); +}); + +describe('unmuteInstance()', () => { + test('unmutes an alert instance', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + interval: '10s', + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: ['2'], + }, + references: [], + }); + + await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(savedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + mutedInstanceIds: [], + updatedBy: 'elastic', + }, + { version: undefined, references: [] } + ); + }); + + test('skips unmuting when alert instance not muted', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + interval: '10s', + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: [], + }, + references: [], + }); + + await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(savedObjectsClient.update).not.toHaveBeenCalled(); + }); + + test('skips unmuting when alert is muted', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + interval: '10s', + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: [], + muteAll: true, + }, + references: [], + }); + + await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(savedObjectsClient.update).not.toHaveBeenCalled(); + }); +}); + describe('get()', () => { test('calls saved objects client with given params', async () => { const alertsClient = new AlertsClient(alertsClientParams); @@ -759,31 +979,31 @@ describe('get()', () => { }); const result = await alertsClient.get({ id: '1' }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "alertTypeParams": Object { + "bar": true, + }, + "id": "1", + "interval": "10s", + } + `); expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - "1", - ] - `); + Array [ + "alert", + "1", + ] + `); }); test(`throws an error when references aren't found`, async () => { @@ -854,39 +1074,39 @@ describe('find()', () => { }); const result = await alertsClient.find(); expect(result).toMatchInlineSnapshot(` - Object { - "data": Array [ - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "alertTypeParams": Object { - "bar": true, - }, - "id": "1", - "interval": "10s", - }, - ], - "page": 1, - "perPage": 10, - "total": 1, - } - `); + Object { + "data": Array [ + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "alertTypeParams": Object { + "bar": true, + }, + "id": "1", + "interval": "10s", + }, + ], + "page": 1, + "perPage": 10, + "total": 1, + } + `); expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); expect(savedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "type": "alert", - }, - ] - `); + Array [ + Object { + "type": "alert", + }, + ] + `); }); }); @@ -928,17 +1148,17 @@ describe('delete()', () => { expect(result).toEqual({ success: true }); expect(savedObjectsClient.delete).toHaveBeenCalledTimes(1); expect(savedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - "1", - ] - `); + Array [ + "alert", + "1", + ] + `); expect(taskManager.remove).toHaveBeenCalledTimes(1); expect(taskManager.remove.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "task-123", - ] - `); + Array [ + "task-123", + ] + `); }); }); @@ -960,6 +1180,7 @@ describe('update()', () => { scheduledTaskId: 'task-123', }, references: [], + version: '123', }); savedObjectsClient.update.mockResolvedValueOnce({ id: '1', @@ -1006,66 +1227,66 @@ describe('update()', () => { }, ], }, - options: { - version: '123', - }, }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeParams": Object { - "bar": true, - }, - "enabled": true, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeParams": Object { + "bar": true, + }, + "enabled": true, + "id": "1", + "interval": "10s", + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4); expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); expect(savedObjectsClient.update.mock.calls[0][1]).toEqual('1'); expect(savedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionRef": "action_0", - "group": "default", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeParams": Object { - "bar": true, - }, - "apiKey": null, - "apiKeyOwner": null, - "interval": "10s", - "updatedBy": "elastic", - } - `); + Object { + "actions": Array [ + Object { + "actionRef": "action_0", + "group": "default", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "alertTypeParams": Object { + "bar": true, + }, + "apiKey": null, + "apiKeyOwner": null, + "enabled": true, + "interval": "10s", + "scheduledTaskId": "task-123", + "updatedBy": "elastic", + } + `); expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - "version": "123", - } - `); + Object { + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + "version": "123", + } + `); }); it('calls the createApiKey function', async () => { @@ -1085,6 +1306,7 @@ describe('update()', () => { scheduledTaskId: 'task-123', }, references: [], + version: '123', }); alertsClientParams.createAPIKey.mockResolvedValueOnce({ created: true, @@ -1136,67 +1358,67 @@ describe('update()', () => { }, ], }, - options: { - version: '123', - }, }); expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeParams": Object { - "bar": true, - }, - "apiKey": "MTIzOmFiYw==", - "enabled": true, - "id": "1", - "interval": "10s", - "scheduledTaskId": "task-123", - } - `); + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeParams": Object { + "bar": true, + }, + "apiKey": "MTIzOmFiYw==", + "enabled": true, + "id": "1", + "interval": "10s", + "scheduledTaskId": "task-123", + } + `); expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(4); expect(savedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); expect(savedObjectsClient.update.mock.calls[0][1]).toEqual('1'); expect(savedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionRef": "action_0", - "group": "default", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeParams": Object { - "bar": true, - }, - "apiKey": "MTIzOmFiYw==", - "apiKeyOwner": "elastic", - "interval": "10s", - "updatedBy": "elastic", - } - `); + Object { + "actions": Array [ + Object { + "actionRef": "action_0", + "group": "default", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "alertTypeParams": Object { + "bar": true, + }, + "apiKey": "MTIzOmFiYw==", + "apiKeyOwner": "elastic", + "enabled": true, + "interval": "10s", + "scheduledTaskId": "task-123", + "updatedBy": "elastic", + } + `); expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - "version": "123", - } - `); + Object { + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + "version": "123", + } + `); }); it('should validate alertTypeParams', async () => { @@ -1238,9 +1460,6 @@ describe('update()', () => { }, ], }, - options: { - version: '123', - }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"alertTypeParams invalid: [param1]: expected value of type [string] but got [undefined]"` @@ -1271,6 +1490,9 @@ describe('updateApiKey()', () => { 'alert', '1', { + interval: '10s', + alertTypeId: '2', + enabled: true, apiKey: Buffer.from('123:abc').toString('base64'), apiKeyOwner: 'elastic', updatedBy: 'elastic', diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index 493b457abd914..10f87dbd6024a 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -56,7 +56,13 @@ interface FindResult { } interface CreateOptions { - data: Pick>; + data: Pick< + Alert, + Exclude< + keyof Alert, + 'createdBy' | 'updatedBy' | 'apiKey' | 'apiKeyOwner' | 'muteAll' | 'mutedInstanceIds' + > + >; options?: { migrationVersion?: Record; }; @@ -69,7 +75,6 @@ interface UpdateOptions { actions: AlertAction[]; alertTypeParams: Record; }; - options?: { version?: string }; } export class AlertsClient { @@ -117,6 +122,8 @@ export class AlertsClient { ? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64') : undefined, alertTypeParams: validatedAlertTypeParams, + muteAll: false, + mutedInstanceIds: [], }); const createdAlert = await this.savedObjectsClient.create('alert', rawAlert, { ...options, @@ -188,10 +195,9 @@ export class AlertsClient { return removeResult; } - public async update({ id, data, options = {} }: UpdateOptions) { - const existingObject = await this.savedObjectsClient.get('alert', id); - const { alertTypeId } = existingObject.attributes; - const alertType = this.alertTypeRegistry.get(alertTypeId); + public async update({ id, data }: UpdateOptions) { + const { attributes, version } = await this.savedObjectsClient.get('alert', id); + const alertType = this.alertTypeRegistry.get(attributes.alertTypeId); const apiKey = await this.createAPIKey(); // Validate @@ -204,6 +210,7 @@ export class AlertsClient { 'alert', id, { + ...attributes, ...data, alertTypeParams: validatedAlertTypeParams, actions, @@ -214,7 +221,7 @@ export class AlertsClient { : null, }, { - ...options, + version, references, } ); @@ -222,7 +229,7 @@ export class AlertsClient { } public async updateApiKey({ id }: { id: string }) { - const { references } = await this.savedObjectsClient.get('alert', id); + const { references, version, attributes } = await this.savedObjectsClient.get('alert', id); const apiKey = await this.createAPIKey(); const username = await this.getUserName(); @@ -230,6 +237,7 @@ export class AlertsClient { 'alert', id, { + ...attributes, updatedBy: username, apiKeyOwner: apiKey.created ? username : null, apiKey: apiKey.created @@ -237,25 +245,27 @@ export class AlertsClient { : null, }, { + version, references, } ); } public async enable({ id }: { id: string }) { - const existingObject = await this.savedObjectsClient.get('alert', id); - if (existingObject.attributes.enabled === false) { + const { attributes, version, references } = await this.savedObjectsClient.get('alert', id); + if (attributes.enabled === false) { const apiKey = await this.createAPIKey(); const scheduledTask = await this.scheduleAlert( id, - existingObject.attributes.alertTypeId, - existingObject.attributes.interval + attributes.alertTypeId, + attributes.interval ); const username = await this.getUserName(); await this.savedObjectsClient.update( 'alert', id, { + ...attributes, enabled: true, updatedBy: username, apiKeyOwner: apiKey.created ? username : null, @@ -264,27 +274,123 @@ export class AlertsClient { ? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64') : null, }, - { references: existingObject.references } + { + version, + references, + } ); } } public async disable({ id }: { id: string }) { - const existingObject = await this.savedObjectsClient.get('alert', id); - if (existingObject.attributes.enabled === true) { + const { attributes, version, references } = await this.savedObjectsClient.get('alert', id); + if (attributes.enabled === true) { await this.savedObjectsClient.update( 'alert', id, { + ...attributes, enabled: false, scheduledTaskId: null, apiKey: null, apiKeyOwner: null, updatedBy: await this.getUserName(), }, - { references: existingObject.references } + { + version, + references, + } + ); + await this.taskManager.remove(attributes.scheduledTaskId); + } + } + + public async muteAll({ id }: { id: string }) { + const { + references, + attributes: { muteAll }, + } = await this.savedObjectsClient.get('alert', id); + if (!muteAll) { + await this.savedObjectsClient.update( + 'alert', + id, + { + muteAll: true, + mutedInstanceIds: [], + updatedBy: await this.getUserName(), + }, + { references } + ); + } + } + + public async unmuteAll({ id }: { id: string }) { + const { + references, + attributes: { muteAll }, + } = await this.savedObjectsClient.get('alert', id); + if (muteAll) { + await this.savedObjectsClient.update( + 'alert', + id, + { + muteAll: false, + mutedInstanceIds: [], + updatedBy: await this.getUserName(), + }, + { references } + ); + } + } + + public async muteInstance({ + alertId, + alertInstanceId, + }: { + alertId: string; + alertInstanceId: string; + }) { + const { attributes, version, references } = await this.savedObjectsClient.get('alert', alertId); + const mutedInstanceIds = attributes.mutedInstanceIds || []; + if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) { + mutedInstanceIds.push(alertInstanceId); + await this.savedObjectsClient.update( + 'alert', + alertId, + { + mutedInstanceIds, + updatedBy: await this.getUserName(), + }, + { + version, + references, + } + ); + } + } + + public async unmuteInstance({ + alertId, + alertInstanceId, + }: { + alertId: string; + alertInstanceId: string; + }) { + const { attributes, version, references } = await this.savedObjectsClient.get('alert', alertId); + const mutedInstanceIds = attributes.mutedInstanceIds || []; + if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { + await this.savedObjectsClient.update( + 'alert', + alertId, + { + updatedBy: await this.getUserName(), + mutedInstanceIds: mutedInstanceIds.filter((id: string) => id !== alertInstanceId), + }, + { + version, + references, + } ); - await this.taskManager.remove(existingObject.attributes.scheduledTaskId); } } diff --git a/x-pack/legacy/plugins/alerting/server/init.ts b/x-pack/legacy/plugins/alerting/server/init.ts index e41275134a243..191d9b08c8ab7 100644 --- a/x-pack/legacy/plugins/alerting/server/init.ts +++ b/x-pack/legacy/plugins/alerting/server/init.ts @@ -28,6 +28,10 @@ import { enableAlertRoute, disableAlertRoute, updateApiKeyRoute, + muteAllAlertRoute, + unmuteAllAlertRoute, + muteAlertInstanceRoute, + unmuteAlertInstanceRoute, } from './routes'; // Extend PluginProperties to indicate which plugins are guaranteed to exist @@ -88,7 +92,12 @@ export function init(server: Server) { server.plugins.encrypted_saved_objects.registerType({ type: 'alert', attributesToEncrypt: new Set(['apiKey']), - attributesToExcludeFromAAD: new Set(['scheduledTaskId']), + attributesToExcludeFromAAD: new Set([ + 'scheduledTaskId', + 'muted', + 'mutedInstanceIds', + 'updatedBy', + ]), }); function getServices(request: any): Services { @@ -127,6 +136,10 @@ export function init(server: Server) { enableAlertRoute(server); disableAlertRoute(server); updateApiKeyRoute(server); + muteAllAlertRoute(server); + unmuteAllAlertRoute(server); + muteAlertInstanceRoute(server); + unmuteAlertInstanceRoute(server); // Expose functions server.decorate('request', 'getAlertsClient', function() { diff --git a/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.test.ts b/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.test.ts index b99e206c81034..2490a187ee458 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.test.ts @@ -68,6 +68,7 @@ const mockedAlertTypeSavedObject = { enabled: true, alertTypeId: '123', interval: '10s', + mutedInstanceIds: [], alertTypeParams: { bar: true, }, diff --git a/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.ts b/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.ts index 5e0225cff2a4b..8725017148d4a 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/get_create_task_runner_function.ts @@ -74,7 +74,7 @@ export function getCreateTaskRunnerFunction({ const services = getServices(fakeRequest); // Ensure API key is still valid and user has access const { - attributes: { alertTypeParams, actions, interval, throttle }, + attributes: { alertTypeParams, actions, interval, throttle, muteAll, mutedInstanceIds }, references, } = await services.savedObjectsClient.get('alert', alertId); @@ -128,6 +128,9 @@ export function getCreateTaskRunnerFunction({ Object.keys(alertInstances).map(alertInstanceId => { const alertInstance = alertInstances[alertInstanceId]; if (alertInstance.hasScheduledActions(throttle)) { + if (muteAll || mutedInstanceIds.includes(alertInstanceId)) { + return; + } const { actionGroup, context, state } = alertInstance.getScheduledActionOptions()!; alertInstance.updateLastScheduledActions(actionGroup); alertInstance.unscheduleActions(); diff --git a/x-pack/legacy/plugins/alerting/server/routes/index.ts b/x-pack/legacy/plugins/alerting/server/routes/index.ts index a8c0e28335973..9e568c21f2992 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/index.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/index.ts @@ -13,3 +13,7 @@ export { updateAlertRoute } from './update'; export { enableAlertRoute } from './enable'; export { disableAlertRoute } from './disable'; export { updateApiKeyRoute } from './update_api_key'; +export { muteAlertInstanceRoute } from './mute_instance'; +export { unmuteAlertInstanceRoute } from './unmute_instance'; +export { muteAllAlertRoute } from './mute_all'; +export { unmuteAllAlertRoute } from './unmute_all'; diff --git a/x-pack/legacy/plugins/alerting/server/routes/mute_all.test.ts b/x-pack/legacy/plugins/alerting/server/routes/mute_all.test.ts new file mode 100644 index 0000000000000..17114fcc62a42 --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/routes/mute_all.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { createMockServer } from './_mock_server'; +import { muteAllAlertRoute } from './mute_all'; + +const { server, alertsClient } = createMockServer(); +muteAllAlertRoute(server); + +test('mutes an alert', async () => { + const request = { + method: 'POST', + url: '/api/alert/1/_mute_all', + }; + + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(204); + expect(alertsClient.muteAll).toHaveBeenCalledWith({ id: '1' }); +}); diff --git a/x-pack/legacy/plugins/alerting/server/routes/mute_all.ts b/x-pack/legacy/plugins/alerting/server/routes/mute_all.ts new file mode 100644 index 0000000000000..5ad63c8ed97d4 --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/routes/mute_all.ts @@ -0,0 +1,31 @@ +/* + * 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 Hapi from 'hapi'; + +interface MuteAllRequest extends Hapi.Request { + params: { + id: string; + }; +} + +export function muteAllAlertRoute(server: Hapi.Server) { + server.route({ + method: 'POST', + path: '/api/alert/{id}/_mute_all', + options: { + tags: ['access:alerting-all'], + response: { + emptyStatusCode: 204, + }, + }, + async handler(request: MuteAllRequest, h: Hapi.ResponseToolkit) { + const alertsClient = request.getAlertsClient!(); + await alertsClient.muteAll(request.params); + return h.response(); + }, + }); +} diff --git a/x-pack/legacy/plugins/alerting/server/routes/mute_instance.test.ts b/x-pack/legacy/plugins/alerting/server/routes/mute_instance.test.ts new file mode 100644 index 0000000000000..9ab46562d12f2 --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/routes/mute_instance.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { createMockServer } from './_mock_server'; +import { muteAlertInstanceRoute } from './mute_instance'; + +const { server, alertsClient } = createMockServer(); +muteAlertInstanceRoute(server); + +test('mutes an alert instance', async () => { + const request = { + method: 'POST', + url: '/api/alert/1/alert_instance/2/_mute', + }; + + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(204); + expect(alertsClient.muteInstance).toHaveBeenCalledWith({ alertId: '1', alertInstanceId: '2' }); +}); diff --git a/x-pack/legacy/plugins/alerting/server/routes/mute_instance.ts b/x-pack/legacy/plugins/alerting/server/routes/mute_instance.ts new file mode 100644 index 0000000000000..90d3b116f6617 --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/routes/mute_instance.ts @@ -0,0 +1,32 @@ +/* + * 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 Hapi from 'hapi'; + +interface MuteInstanceRequest extends Hapi.Request { + params: { + alertId: string; + alertInstanceId: string; + }; +} + +export function muteAlertInstanceRoute(server: Hapi.Server) { + server.route({ + method: 'POST', + path: '/api/alert/{alertId}/alert_instance/{alertInstanceId}/_mute', + options: { + tags: ['access:alerting-all'], + response: { + emptyStatusCode: 204, + }, + }, + async handler(request: MuteInstanceRequest, h: Hapi.ResponseToolkit) { + const alertsClient = request.getAlertsClient!(); + await alertsClient.muteInstance(request.params); + return h.response(); + }, + }); +} diff --git a/x-pack/legacy/plugins/alerting/server/routes/unmute_all.test.ts b/x-pack/legacy/plugins/alerting/server/routes/unmute_all.test.ts new file mode 100644 index 0000000000000..62fce147b0105 --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/routes/unmute_all.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { createMockServer } from './_mock_server'; +import { unmuteAllAlertRoute } from './unmute_all'; + +const { server, alertsClient } = createMockServer(); +unmuteAllAlertRoute(server); + +test('unmutes an alert', async () => { + const request = { + method: 'POST', + url: '/api/alert/1/_unmute_all', + }; + + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(204); + expect(alertsClient.unmuteAll).toHaveBeenCalledWith({ id: '1' }); +}); diff --git a/x-pack/legacy/plugins/alerting/server/routes/unmute_all.ts b/x-pack/legacy/plugins/alerting/server/routes/unmute_all.ts new file mode 100644 index 0000000000000..8a72f1d9e08f9 --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/routes/unmute_all.ts @@ -0,0 +1,31 @@ +/* + * 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 Hapi from 'hapi'; + +interface UnmuteAllRequest extends Hapi.Request { + params: { + id: string; + }; +} + +export function unmuteAllAlertRoute(server: Hapi.Server) { + server.route({ + method: 'POST', + path: '/api/alert/{id}/_unmute_all', + options: { + tags: ['access:alerting-all'], + response: { + emptyStatusCode: 204, + }, + }, + async handler(request: UnmuteAllRequest, h: Hapi.ResponseToolkit) { + const alertsClient = request.getAlertsClient!(); + await alertsClient.unmuteAll(request.params); + return h.response(); + }, + }); +} diff --git a/x-pack/legacy/plugins/alerting/server/routes/unmute_instance.test.ts b/x-pack/legacy/plugins/alerting/server/routes/unmute_instance.test.ts new file mode 100644 index 0000000000000..002cfdd3d8051 --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/routes/unmute_instance.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { createMockServer } from './_mock_server'; +import { unmuteAlertInstanceRoute } from './unmute_instance'; + +const { server, alertsClient } = createMockServer(); +unmuteAlertInstanceRoute(server); + +test('unmutes an alert instance', async () => { + const request = { + method: 'POST', + url: '/api/alert/1/alert_instance/2/_unmute', + }; + + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(204); + expect(alertsClient.unmuteInstance).toHaveBeenCalledWith({ alertId: '1', alertInstanceId: '2' }); +}); diff --git a/x-pack/legacy/plugins/alerting/server/routes/unmute_instance.ts b/x-pack/legacy/plugins/alerting/server/routes/unmute_instance.ts new file mode 100644 index 0000000000000..2adc847d02629 --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/routes/unmute_instance.ts @@ -0,0 +1,32 @@ +/* + * 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 Hapi from 'hapi'; + +interface UnmuteInstanceRequest extends Hapi.Request { + params: { + alertId: string; + alertInstanceId: string; + }; +} + +export function unmuteAlertInstanceRoute(server: Hapi.Server) { + server.route({ + method: 'POST', + path: '/api/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute', + options: { + tags: ['access:alerting-all'], + response: { + emptyStatusCode: 204, + }, + }, + async handler(request: UnmuteInstanceRequest, h: Hapi.ResponseToolkit) { + const alertsClient = request.getAlertsClient!(); + await alertsClient.unmuteInstance(request.params); + return h.response(); + }, + }); +} diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts index c52cbf34156f3..723436c4f1c70 100644 --- a/x-pack/legacy/plugins/alerting/server/types.ts +++ b/x-pack/legacy/plugins/alerting/server/types.ts @@ -75,6 +75,8 @@ export interface Alert { apiKey?: string; apiKeyOwner?: string; throttle: string | null; + muteAll: boolean; + mutedInstanceIds: string[]; } export interface RawAlert extends SavedObjectAttributes { @@ -89,6 +91,8 @@ export interface RawAlert extends SavedObjectAttributes { apiKey?: string; apiKeyOwner?: string; throttle: string | null; + muteAll: boolean; + mutedInstanceIds: string[]; } export interface AlertingPlugin { diff --git a/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts new file mode 100644 index 0000000000000..83863ef11c4c9 --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/lib/alert_utils.ts @@ -0,0 +1,207 @@ +/* + * 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 { Space, User } from '../types'; +import { ObjectRemover } from './object_remover'; +import { getUrlPrefix } from './space_test_utils'; +import { ES_TEST_INDEX_NAME } from './es_test_index_tool'; + +export interface AlertUtilsOpts { + user?: User; + space: Space; + supertestWithoutAuth: any; + indexRecordActionId?: string; + objectRemover?: ObjectRemover; +} + +export interface CreateAlwaysFiringActionOpts { + indexRecordActionId?: string; + objectRemover?: ObjectRemover; + overwrites?: Record; + reference: string; +} + +export class AlertUtils { + private referenceCounter = 1; + private readonly user?: User; + private readonly space: Space; + private readonly supertestWithoutAuth: any; + private readonly indexRecordActionId?: string; + private readonly objectRemover?: ObjectRemover; + + constructor({ + indexRecordActionId, + objectRemover, + space, + supertestWithoutAuth, + user, + }: AlertUtilsOpts) { + this.user = user; + this.space = space; + this.objectRemover = objectRemover; + this.indexRecordActionId = indexRecordActionId; + this.supertestWithoutAuth = supertestWithoutAuth; + } + + public generateReference() { + return ['alert-utils-ref', this.referenceCounter++, this.user ? this.user.username : ''].join( + ':' + ); + } + + public getEnableRequest(alertId: string) { + const request = this.supertestWithoutAuth + .post(`${getUrlPrefix(this.space.id)}/api/alert/${alertId}/_enable`) + .set('kbn-xsrf', 'foo'); + if (this.user) { + return request.auth(this.user.username, this.user.password); + } + return request; + } + + public getDisableRequest(alertId: string) { + const request = this.supertestWithoutAuth + .post(`${getUrlPrefix(this.space.id)}/api/alert/${alertId}/_disable`) + .set('kbn-xsrf', 'foo'); + if (this.user) { + return request.auth(this.user.username, this.user.password); + } + return request; + } + + public getMuteAllRequest(alertId: string) { + const request = this.supertestWithoutAuth + .post(`${getUrlPrefix(this.space.id)}/api/alert/${alertId}/_mute_all`) + .set('kbn-xsrf', 'foo'); + if (this.user) { + return request.auth(this.user.username, this.user.password); + } + return request; + } + + public getUnmuteAllRequest(alertId: string) { + const request = this.supertestWithoutAuth + .post(`${getUrlPrefix(this.space.id)}/api/alert/${alertId}/_unmute_all`) + .set('kbn-xsrf', 'foo'); + if (this.user) { + return request.auth(this.user.username, this.user.password); + } + return request; + } + + public getMuteInstanceRequest(alertId: string, instanceId: string) { + const request = this.supertestWithoutAuth + .post( + `${getUrlPrefix(this.space.id)}/api/alert/${alertId}/alert_instance/${instanceId}/_mute` + ) + .set('kbn-xsrf', 'foo'); + if (this.user) { + return request.auth(this.user.username, this.user.password); + } + return request; + } + + public getUnmuteInstanceRequest(alertId: string, instanceId: string) { + const request = this.supertestWithoutAuth + .post( + `${getUrlPrefix(this.space.id)}/api/alert/${alertId}/alert_instance/${instanceId}/_unmute` + ) + .set('kbn-xsrf', 'foo'); + if (this.user) { + return request.auth(this.user.username, this.user.password); + } + return request; + } + + public getUpdateApiKeyRequest(alertId: string) { + const request = this.supertestWithoutAuth + .post(`${getUrlPrefix(this.space.id)}/api/alert/${alertId}/_update_api_key`) + .set('kbn-xsrf', 'foo'); + if (this.user) { + return request.auth(this.user.username, this.user.password); + } + return request; + } + + public async enable(alertId: string) { + await this.getEnableRequest(alertId).expect(204, ''); + } + + public async disable(alertId: string) { + await this.getDisableRequest(alertId).expect(204, ''); + } + + public async muteAll(alertId: string) { + await this.getMuteAllRequest(alertId).expect(204, ''); + } + + public async unmuteAll(alertId: string) { + await this.getUnmuteAllRequest(alertId).expect(204, ''); + } + + public async muteInstance(alertId: string, instanceId: string) { + await this.getMuteInstanceRequest(alertId, instanceId).expect(204, ''); + } + + public async unmuteInstance(alertId: string, instanceId: string) { + await this.getUnmuteInstanceRequest(alertId, instanceId).expect(204, ''); + } + + public async updateApiKey(alertId: string) { + await this.getUpdateApiKeyRequest(alertId).expect(204, ''); + } + + public async createAlwaysFiringAction({ + objectRemover, + overwrites = {}, + indexRecordActionId, + reference, + }: CreateAlwaysFiringActionOpts) { + const objRemover = objectRemover || this.objectRemover; + const actionId = indexRecordActionId || this.indexRecordActionId; + + if (!objRemover) { + throw new Error('objectRemover is required'); + } + if (!actionId) { + throw new Error('indexRecordActionId is required '); + } + + let request = this.supertestWithoutAuth + .post(`${getUrlPrefix(this.space.id)}/api/alert`) + .set('kbn-xsrf', 'foo'); + if (this.user) { + request = request.auth(this.user.username, this.user.password); + } + const response = await request.send({ + enabled: true, + interval: '1m', + throttle: '1m', + alertTypeId: 'test.always-firing', + alertTypeParams: { + index: ES_TEST_INDEX_NAME, + reference, + }, + actions: [ + { + group: 'default', + id: this.indexRecordActionId, + params: { + index: ES_TEST_INDEX_NAME, + reference, + message: + 'instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}', + }, + }, + ], + ...overwrites, + }); + if (response.statusCode === 200) { + objRemover.add(this.space.id, response.body.id, 'alert'); + } + return response; + } +} diff --git a/x-pack/test/alerting_api_integration/common/lib/index.ts b/x-pack/test/alerting_api_integration/common/lib/index.ts index 6c80cbd043f4f..444b767456bde 100644 --- a/x-pack/test/alerting_api_integration/common/lib/index.ts +++ b/x-pack/test/alerting_api_integration/common/lib/index.ts @@ -8,3 +8,4 @@ export { ObjectRemover } from './object_remover'; export { getUrlPrefix } from './space_test_utils'; export { ES_TEST_INDEX_NAME, ESTestIndexTool } from './es_test_index_tool'; export { getTestAlertData } from './get_test_alert_data'; +export { AlertUtils } from './alert_utils'; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index b29e7302189fa..7a984acb2c09f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -13,6 +13,7 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, + AlertUtils, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -38,57 +39,42 @@ export default function alertTests({ getService }: FtrProviderContext) { await es.indices.delete({ index: authorizationIndex }); }); - async function createIndexRecordAction(spaceId: string) { - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(spaceId)}/api/action`) - .set('kbn-xsrf', 'foo') - .send({ - description: 'My action', - actionTypeId: 'test.index-record', - config: { - unencrypted: `This value shouldn't get encrypted`, - }, - secrets: { - encrypted: 'This value should be encrypted', - }, - }) - .expect(200); - objectRemover.add(spaceId, createdAction.id, 'action'); - return createdAction; - } - for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; + describe(scenario.id, () => { - it('should schedule task, run alert and schedule actions when appropriate', async () => { - const reference = `create-test-1:${user.username}`; - const createdAction = await createIndexRecordAction(space.id); - const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) + let alertUtils: AlertUtils; + let indexRecordActionId: string; + + before(async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/action`) .set('kbn-xsrf', 'foo') - .auth(user.username, user.password) - .send( - getTestAlertData({ - interval: '1m', - alertTypeId: 'test.always-firing', - alertTypeParams: { - index: ES_TEST_INDEX_NAME, - reference, - }, - actions: [ - { - group: 'default', - id: createdAction.id, - params: { - index: ES_TEST_INDEX_NAME, - reference, - message: - 'instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}', - }, - }, - ], - }) - ); + .send({ + description: 'My action', + actionTypeId: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + indexRecordActionId = createdAction.id; + alertUtils = new AlertUtils({ + user, + space, + supertestWithoutAuth, + indexRecordActionId, + objectRemover, + }); + }); + after(() => objectRemover.add(space.id, indexRecordActionId, 'action')); + + it('should schedule task, run alert and schedule actions when appropriate', async () => { + const reference = alertUtils.generateReference(); + const response = await alertUtils.createAlwaysFiringAction({ reference }); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -104,7 +90,6 @@ export default function alertTests({ getService }: FtrProviderContext) { case 'superuser at space1': case 'space_1_all at space1': expect(response.statusCode).to.eql(200); - objectRemover.add(space.id, response.body.id, 'alert'); const alertTestRecord = (await esTestIndexTool.waitForDocs( 'alert:test.always-firing', reference @@ -160,7 +145,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .expect(200); objectRemover.add(space.id, createdAction.id, 'action'); - const reference = `create-test-2:${user.username}`; + const reference = alertUtils.generateReference(); const response = await supertestWithoutAuth .post(`${getUrlPrefix(space.id)}/api/alert`) .set('kbn-xsrf', 'foo') @@ -248,7 +233,7 @@ export default function alertTests({ getService }: FtrProviderContext) { it('should have proper callCluster and savedObjectsClient authorization for alert type executor when appropriate', async () => { let alertTestRecord: any; - const reference = `create-test-3:${user.username}`; + const reference = alertUtils.generateReference(); const response = await supertestWithoutAuth .post(`${getUrlPrefix(space.id)}/api/alert`) .set('kbn-xsrf', 'foo') @@ -326,7 +311,7 @@ export default function alertTests({ getService }: FtrProviderContext) { it('should have proper callCluster and savedObjectsClient authorization for action type executor when appropriate', async () => { let actionTestRecord: any; - const reference = `create-test-4:${user.username}`; + const reference = alertUtils.generateReference(); const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/action`) .set('kbn-xsrf', 'foo') @@ -422,34 +407,13 @@ export default function alertTests({ getService }: FtrProviderContext) { }); it('should throttle alerts when appropriate', async () => { - const reference = `create-test-5:${user.username}`; - const createdAction = await createIndexRecordAction(space.id); - const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) - .set('kbn-xsrf', 'foo') - .auth(user.username, user.password) - .send( - getTestAlertData({ - interval: '1s', - throttle: '1m', - alertTypeId: 'test.always-firing', - alertTypeParams: { - index: ES_TEST_INDEX_NAME, - reference, - }, - actions: [ - { - group: 'default', - id: createdAction.id, - params: { - index: ES_TEST_INDEX_NAME, - reference, - message: '', - }, - }, - ], - }) - ); + const reference = alertUtils.generateReference(); + const response = await alertUtils.createAlwaysFiringAction({ + reference, + overwrites: { + interval: '1s', + }, + }); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -478,44 +442,38 @@ export default function alertTests({ getService }: FtrProviderContext) { }); it('should not throttle when changing groups', async () => { - const reference = `create-test-6:${user.username}`; - const createdAction = await createIndexRecordAction(space.id); - const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) - .set('kbn-xsrf', 'foo') - .auth(user.username, user.password) - .send( - getTestAlertData({ - interval: '1s', - throttle: '1m', - alertTypeId: 'test.always-firing', - alertTypeParams: { - index: ES_TEST_INDEX_NAME, - reference, - groupsToScheduleActionsInSeries: ['default', 'other'], - }, - actions: [ - { - group: 'default', - id: createdAction.id, - params: { - index: ES_TEST_INDEX_NAME, - reference, - message: 'from:default', - }, + const reference = alertUtils.generateReference(); + const response = await alertUtils.createAlwaysFiringAction({ + reference, + overwrites: { + interval: '1s', + alertTypeParams: { + index: ES_TEST_INDEX_NAME, + reference, + groupsToScheduleActionsInSeries: ['default', 'other'], + }, + actions: [ + { + group: 'default', + id: indexRecordActionId, + params: { + index: ES_TEST_INDEX_NAME, + reference, + message: 'from:default', }, - { - group: 'other', - id: createdAction.id, - params: { - index: ES_TEST_INDEX_NAME, - reference, - message: 'from:other', - }, + }, + { + group: 'other', + id: indexRecordActionId, + params: { + index: ES_TEST_INDEX_NAME, + reference, + message: 'from:other', }, - ], - }) - ); + }, + ], + }, + }); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -548,35 +506,18 @@ export default function alertTests({ getService }: FtrProviderContext) { }); it('should reset throttle window when not firing', async () => { - const reference = `create-test-7:${user.username}`; - const createdAction = await createIndexRecordAction(space.id); - const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert`) - .set('kbn-xsrf', 'foo') - .auth(user.username, user.password) - .send( - getTestAlertData({ - interval: '1s', - throttle: '1m', - alertTypeId: 'test.always-firing', - alertTypeParams: { - index: ES_TEST_INDEX_NAME, - reference, - groupsToScheduleActionsInSeries: ['default', null, 'default'], - }, - actions: [ - { - group: 'default', - id: createdAction.id, - params: { - index: ES_TEST_INDEX_NAME, - reference, - message: 'from:default', - }, - }, - ], - }) - ); + const reference = alertUtils.generateReference(); + const response = await alertUtils.createAlwaysFiringAction({ + reference, + overwrites: { + interval: '1s', + alertTypeParams: { + index: ES_TEST_INDEX_NAME, + reference, + groupsToScheduleActionsInSeries: ['default', null, 'default'], + }, + }, + }); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -603,6 +544,122 @@ export default function alertTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it(`shouldn't schedule actions when alert is muted`, async () => { + const reference = alertUtils.generateReference(); + const response = await alertUtils.createAlwaysFiringAction({ + reference, + overwrites: { + enabled: false, + interval: '1s', + }, + }); + + 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(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'space_1_all at space1': + case 'superuser at space1': + await alertUtils.muteAll(response.body.id); + await alertUtils.enable(response.body.id); + // Wait until alerts schedule actions twice to ensure actions had a chance to skip execution once + await esTestIndexTool.waitForDocs('alert:test.always-firing', reference, 2); + const executedActionsResult = await esTestIndexTool.search( + 'action:test.index-record', + reference + ); + expect(executedActionsResult.hits.total.value).to.eql(0); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it(`shouldn't schedule actions when alert instance is muted`, async () => { + const reference = alertUtils.generateReference(); + const response = await alertUtils.createAlwaysFiringAction({ + reference, + overwrites: { + enabled: false, + interval: '1s', + }, + }); + + 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(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'space_1_all at space1': + case 'superuser at space1': + await alertUtils.muteInstance(response.body.id, '1'); + await alertUtils.enable(response.body.id); + // Wait until alerts scheduled actions twice to ensure actions had a chance to execute once + await esTestIndexTool.waitForDocs('alert:test.always-firing', reference, 2); + const executedActionsResult = await esTestIndexTool.search( + 'action:test.index-record', + reference + ); + expect(executedActionsResult.hits.total.value).to.eql(0); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it(`should unmute all instances when unmuting an alert`, async () => { + const reference = alertUtils.generateReference(); + const response = await alertUtils.createAlwaysFiringAction({ + reference, + overwrites: { + enabled: false, + interval: '1s', + }, + }); + + 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(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'space_1_all at space1': + case 'superuser at space1': + await alertUtils.muteInstance(response.body.id, '1'); + await alertUtils.muteAll(response.body.id); + await alertUtils.unmuteAll(response.body.id); + await alertUtils.enable(response.body.id); + // Wait until alerts scheduled actions twice to ensure actions had a chance to execute once + await esTestIndexTool.waitForDocs('alert:test.always-firing', reference, 2); + const executedActionsResult = await esTestIndexTool.search( + 'action:test.index-record', + reference + ); + expect(executedActionsResult.hits.total.value).to.eql(1); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts index f92ea085cdbfa..4d4bdfa1558b6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/create.ts @@ -64,6 +64,8 @@ export default function createAlertTests({ getService }: FtrProviderContext) { throttle: '1m', updatedBy: user.username, apiKeyOwner: user.username, + muteAll: false, + mutedInstanceIds: [], }); expect(typeof response.body.scheduledTaskId).to.be('string'); const { _source: taskRecord } = await getScheduledTask(response.body.scheduledTaskId); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts index 0bdbd60cfd885..8a9b7e3fc35c4 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/disable.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { AlertUtils, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -29,6 +29,8 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; + const alertUtils = new AlertUtils({ user, space, supertestWithoutAuth }); + describe(scenario.id, () => { it('should handle disable alert request appropriately', async () => { const { body: createdAlert } = await supertest @@ -38,10 +40,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte .expect(200); objectRemover.add(space.id, createdAlert.id, 'alert'); - const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/_disable`) - .set('kbn-xsrf', 'foo') - .auth(user.username, user.password); + const response = await alertUtils.getDisableRequest(createdAlert.id); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -74,23 +73,19 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte it(`shouldn't disable alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix('other')}/api/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: true })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert'); + objectRemover.add('other', createdAlert.id, 'alert'); - const response = await supertestWithoutAuth - .post(`${getUrlPrefix('other')}/api/alert/${createdAlert.id}/_disable`) - .set('kbn-xsrf', 'foo') - .auth(user.username, user.password); + const response = await alertUtils.getDisableRequest(createdAlert.id); expect(response.statusCode).to.eql(404); switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'global_read at space1': - case 'space_1_all at space1': expect(response.body).to.eql({ statusCode: 404, error: 'Not Found', @@ -98,6 +93,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte }); break; case 'superuser at space1': + case 'space_1_all at space1': expect(response.body).to.eql({ statusCode: 404, error: 'Not Found', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts index d025b35f3602e..543805fb83b18 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/enable.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { UserAtSpaceScenarios } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { AlertUtils, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -29,6 +29,8 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; + const alertUtils = new AlertUtils({ user, space, supertestWithoutAuth }); + describe(scenario.id, () => { it('should handle enable alert request appropriately', async () => { const { body: createdAlert } = await supertest @@ -38,10 +40,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex .expect(200); objectRemover.add(space.id, createdAlert.id, 'alert'); - const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/_enable`) - .set('kbn-xsrf', 'foo') - .auth(user.username, user.password); + const response = await alertUtils.getEnableRequest(createdAlert.id); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -79,23 +78,19 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex it(`shouldn't enable alert from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix('other')}/api/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert'); + objectRemover.add('other', createdAlert.id, 'alert'); - const response = await supertestWithoutAuth - .post(`${getUrlPrefix('other')}/api/alert/${createdAlert.id}/_enable`) - .set('kbn-xsrf', 'foo') - .auth(user.username, user.password); + const response = await alertUtils.getEnableRequest(createdAlert.id); expect(response.statusCode).to.eql(404); switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'global_read at space1': - case 'space_1_all at space1': expect(response.body).to.eql({ statusCode: 404, error: 'Not Found', @@ -103,6 +98,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex }); break; case 'superuser at space1': + case 'space_1_all at space1': expect(response.body).to.eql({ statusCode: 404, error: 'Not Found', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts index 429c43b3c44d6..e782f0780ea6c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts @@ -66,6 +66,8 @@ export default function createFindTests({ getService }: FtrProviderContext) { throttle: '1m', updatedBy: 'elastic', apiKeyOwner: 'elastic', + muteAll: false, + mutedInstanceIds: [], }); break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts index d189844dc4be8..a40fc0a9ac86e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts @@ -60,6 +60,8 @@ export default function createGetTests({ getService }: FtrProviderContext) { throttle: '1m', updatedBy: 'elastic', apiKeyOwner: 'elastic', + muteAll: false, + mutedInstanceIds: [], }); break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index 0774bdc17fd22..1aa084356cfa4 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -16,6 +16,10 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./find')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./list_alert_types')); + loadTestFile(require.resolve('./mute_all')); + loadTestFile(require.resolve('./mute_instance')); + loadTestFile(require.resolve('./unmute_all')); + loadTestFile(require.resolve('./unmute_instance')); loadTestFile(require.resolve('./update')); loadTestFile(require.resolve('./update_api_key')); loadTestFile(require.resolve('./alerts')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts new file mode 100644 index 0000000000000..9e479064e66c7 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts @@ -0,0 +1,66 @@ +/* + * 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 expect from '@kbn/expect'; +import { UserAtSpaceScenarios } from '../../scenarios'; +import { AlertUtils, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createMuteAlertTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('mute_all', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + for (const scenario of UserAtSpaceScenarios) { + const { user, space } = scenario; + const alertUtils = new AlertUtils({ user, space, supertestWithoutAuth }); + + describe(scenario.id, () => { + it('should handle mute alert request appropriately', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData({ enabled: false })) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert'); + + const response = await alertUtils.getMuteAllRequest(createdAlert.id); + + 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(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.muteAll).to.eql(true); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + }); + } + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts new file mode 100644 index 0000000000000..302b61074e87d --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_instance.ts @@ -0,0 +1,108 @@ +/* + * 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 expect from '@kbn/expect'; +import { UserAtSpaceScenarios } from '../../scenarios'; +import { AlertUtils, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createMuteAlertInstanceTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('mute_instance', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + for (const scenario of UserAtSpaceScenarios) { + const { user, space } = scenario; + const alertUtils = new AlertUtils({ user, space, supertestWithoutAuth }); + + describe(scenario.id, () => { + it('should handle mute alert instance request appropriately', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData({ enabled: false })) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert'); + + const response = await alertUtils.getMuteInstanceRequest(createdAlert.id, '1'); + + 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(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.mutedInstanceIds).to.eql(['1']); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle mute alert instance request appropriately and not duplicate mutedInstanceIds when muting an instance already muted', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData({ enabled: false })) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert'); + + await supertest + .post(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/alert_instance/1/_mute`) + .set('kbn-xsrf', 'foo') + .expect(204, ''); + + const response = await alertUtils.getMuteInstanceRequest(createdAlert.id, '1'); + + 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(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.mutedInstanceIds).to.eql(['1']); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + }); + } + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts new file mode 100644 index 0000000000000..5e9ad66cf40f3 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_all.ts @@ -0,0 +1,71 @@ +/* + * 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 expect from '@kbn/expect'; +import { UserAtSpaceScenarios } from '../../scenarios'; +import { AlertUtils, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createUnmuteAlertTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('unmute_all', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + for (const scenario of UserAtSpaceScenarios) { + const { user, space } = scenario; + const alertUtils = new AlertUtils({ user, space, supertestWithoutAuth }); + + describe(scenario.id, () => { + it('should handle unmute alert request appropriately', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData({ enabled: false })) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert'); + + await supertest + .post(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/_mute_all`) + .set('kbn-xsrf', 'foo') + .expect(204, ''); + + const response = await alertUtils.getUnmuteAllRequest(createdAlert.id); + + 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(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.muteAll).to.eql(false); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + }); + } + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts new file mode 100644 index 0000000000000..b466575841d0a --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/unmute_instance.ts @@ -0,0 +1,71 @@ +/* + * 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 expect from '@kbn/expect'; +import { UserAtSpaceScenarios } from '../../scenarios'; +import { AlertUtils, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createMuteAlertInstanceTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('unmute_instance', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + for (const scenario of UserAtSpaceScenarios) { + const { user, space } = scenario; + const alertUtils = new AlertUtils({ user, space, supertestWithoutAuth }); + + describe(scenario.id, () => { + it('should handle unmute alert instance request appropriately', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData({ enabled: false })) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert'); + + await supertest + .post(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/alert_instance/1/_mute`) + .set('kbn-xsrf', 'foo') + .expect(204, ''); + + const response = await alertUtils.getUnmuteInstanceRequest(createdAlert.id, '1'); + + 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(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.mutedInstanceIds).to.eql([]); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + }); + } + }); +} 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 c6fe9ca6da9b4..5736b82dffec5 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 @@ -61,8 +61,14 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(response.body).to.eql({ ...updatedData, id: createdAlert.id, + alertTypeId: 'test.noop', + createdBy: 'elastic', + enabled: true, updatedBy: user.username, apiKeyOwner: user.username, + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: createdAlert.scheduledTaskId, }); break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts index a3357d7ac46c3..4963d749c2935 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update_api_key.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { UserAtSpaceScenarios, Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { UserAtSpaceScenarios } from '../../scenarios'; +import { AlertUtils, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -16,16 +16,13 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte describe('update_api_key', () => { const objectRemover = new ObjectRemover(supertest); - const OtherSpace = Spaces.find(space => space.id === 'other'); - - if (!OtherSpace) { - throw new Error('Space "other" not defined in scenarios'); - } after(() => objectRemover.removeAll()); for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; + const alertUtils = new AlertUtils({ user, space, supertestWithoutAuth }); + describe(scenario.id, () => { it('should handle update alert api key request appropriately', async () => { const { body: createdAlert } = await supertest @@ -35,10 +32,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte .expect(200); objectRemover.add(space.id, createdAlert.id, 'alert'); - const response = await supertestWithoutAuth - .post(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/_update_api_key`) - .set('kbn-xsrf', 'foo') - .auth(user.username, user.password); + const response = await alertUtils.getUpdateApiKeyRequest(createdAlert.id); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -69,23 +63,19 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte it(`shouldn't update alert api key from another space`, async () => { const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(space.id)}/api/alert`) + .post(`${getUrlPrefix('other')}/api/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(space.id, createdAlert.id, 'alert'); + objectRemover.add('other', createdAlert.id, 'alert'); - const response = await supertestWithoutAuth - .post(`${getUrlPrefix(OtherSpace.id)}/api/alert/${createdAlert.id}/_update_api_key`) - .set('kbn-xsrf', 'foo') - .auth(user.username, user.password); + const response = await alertUtils.getUpdateApiKeyRequest(createdAlert.id); expect(response.statusCode).to.eql(404); switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'global_read at space1': - case 'space_1_all at space1': expect(response.body).to.eql({ statusCode: 404, error: 'Not Found', @@ -93,6 +83,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte }); break; case 'superuser at space1': + case 'space_1_all at space1': expect(response.body).to.eql({ statusCode: 404, error: 'Not Found', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts index 07f6fbfc395c3..02e0b3795fcc5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts @@ -13,65 +13,28 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, + AlertUtils, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export export default function alertTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const retry = getService('retry'); const esTestIndexTool = new ESTestIndexTool(es, retry); describe('alerts', () => { + let alertUtils: AlertUtils; + let indexRecordActionId: string; const authorizationIndex = '.kibana-test-authorization'; - const objectRemover = new ObjectRemover(supertest); + const objectRemover = new ObjectRemover(supertestWithoutAuth); before(async () => { await esTestIndexTool.destroy(); await esTestIndexTool.setup(); await es.indices.create({ index: authorizationIndex }); - }); - afterEach(() => objectRemover.removeAll()); - after(async () => { - await esTestIndexTool.destroy(); - await es.indices.delete({ index: authorizationIndex }); - }); - - async function searchTestIndexDocs(source: string, reference: string) { - return await es.search({ - index: ES_TEST_INDEX_NAME, - body: { - query: { - bool: { - must: [ - { - term: { - source, - }, - }, - { - term: { - reference, - }, - }, - ], - }, - }, - }, - }); - } - - async function waitForTestIndexDocs(source: string, reference: string, numDocs: number = 1) { - return await retry.try(async () => { - const searchResult = await searchTestIndexDocs(source, reference); - expect(searchResult.hits.total.value).to.eql(numDocs); - return searchResult.hits.hits; - }); - } - - async function createIndexRecordAction(spaceId: string) { - const { body: createdAction } = await supertest - .post(`${getUrlPrefix(spaceId)}/api/action`) + const { body: createdAction } = await supertestWithoutAuth + .post(`${getUrlPrefix(Spaces.space1.id)}/api/action`) .set('kbn-xsrf', 'foo') .send({ description: 'My action', @@ -84,43 +47,28 @@ export default function alertTests({ getService }: FtrProviderContext) { }, }) .expect(200); - objectRemover.add(spaceId, createdAction.id, 'action'); - return createdAction; - } + indexRecordActionId = createdAction.id; + alertUtils = new AlertUtils({ + space: Spaces.space1, + supertestWithoutAuth, + indexRecordActionId, + objectRemover, + }); + }); + afterEach(() => objectRemover.removeAll()); + after(async () => { + await esTestIndexTool.destroy(); + await es.indices.delete({ index: authorizationIndex }); + objectRemover.add(Spaces.space1.id, indexRecordActionId, 'action'); + await objectRemover.removeAll(); + }); it('should schedule task, run alert and schedule actions', async () => { - const reference = `create-test-1:${Spaces.space1.id}`; - const createdAction = await createIndexRecordAction(Spaces.space1.id); - - const response = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) - .set('kbn-xsrf', 'foo') - .send( - getTestAlertData({ - interval: '1m', - alertTypeId: 'test.always-firing', - alertTypeParams: { - index: ES_TEST_INDEX_NAME, - reference, - }, - actions: [ - { - group: 'default', - id: createdAction.id, - params: { - index: ES_TEST_INDEX_NAME, - reference, - message: - 'instanceContextValue: {{context.instanceContextValue}}, instanceStateValue: {{state.instanceStateValue}}', - }, - }, - ], - }) - ); + const reference = alertUtils.generateReference(); + const response = await alertUtils.createAlwaysFiringAction({ reference }); expect(response.statusCode).to.eql(200); - objectRemover.add(Spaces.space1.id, response.body.id, 'alert'); - const alertTestRecord = (await waitForTestIndexDocs( + const alertTestRecord = (await esTestIndexTool.waitForDocs( 'alert:test.always-firing', reference ))[0]; @@ -133,7 +81,7 @@ export default function alertTests({ getService }: FtrProviderContext) { reference, }, }); - const actionTestRecord = (await waitForTestIndexDocs( + const actionTestRecord = (await esTestIndexTool.waitForDocs( 'action:test.index-record', reference ))[0]; @@ -160,7 +108,7 @@ export default function alertTests({ getService }: FtrProviderContext) { // We have to provide the test.rate-limit the next runAt, for testing purposes const retryDate = new Date(Date.now() + 60000); - const { body: createdAction } = await supertest + const { body: createdAction } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/action`) .set('kbn-xsrf', 'foo') .send({ @@ -171,8 +119,8 @@ export default function alertTests({ getService }: FtrProviderContext) { .expect(200); objectRemover.add(Spaces.space1.id, createdAction.id, 'action'); - const reference = `create-test-2:${Spaces.space1.id}`; - const response = await supertest + const reference = alertUtils.generateReference(); + const response = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) .set('kbn-xsrf', 'foo') .send( @@ -240,8 +188,8 @@ export default function alertTests({ getService }: FtrProviderContext) { }); it('should have proper callCluster and savedObjectsClient authorization for alert type executor', async () => { - const reference = `create-test-3:${Spaces.space1.id}`; - const response = await supertest + const reference = alertUtils.generateReference(); + const response = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) .set('kbn-xsrf', 'foo') .send( @@ -259,7 +207,7 @@ export default function alertTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(200); objectRemover.add(Spaces.space1.id, response.body.id, 'alert'); - const alertTestRecord = (await waitForTestIndexDocs( + const alertTestRecord = (await esTestIndexTool.waitForDocs( 'alert:test.authorization', reference ))[0]; @@ -277,8 +225,8 @@ export default function alertTests({ getService }: FtrProviderContext) { }); it('should have proper callCluster and savedObjectsClient authorization for action type executor', async () => { - const reference = `create-test-4:${Spaces.space1.id}`; - const { body: createdAction } = await supertest + const reference = alertUtils.generateReference(); + const { body: createdAction } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/action`) .set('kbn-xsrf', 'foo') .send({ @@ -287,7 +235,7 @@ export default function alertTests({ getService }: FtrProviderContext) { }) .expect(200); objectRemover.add(Spaces.space1.id, createdAction.id, 'action'); - const response = await supertest + const response = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) .set('kbn-xsrf', 'foo') .send( @@ -315,7 +263,7 @@ export default function alertTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(200); objectRemover.add(Spaces.space1.id, response.body.id, 'alert'); - const actionTestRecord = (await waitForTestIndexDocs( + const actionTestRecord = (await esTestIndexTool.waitForDocs( 'action:test.authorization', reference ))[0]; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index ebd82a819c502..10335633f5d28 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -45,6 +45,8 @@ export default function createAlertTests({ getService }: FtrProviderContext) { scheduledTaskId: response.body.scheduledTaskId, updatedBy: null, throttle: '1m', + muteAll: false, + mutedInstanceIds: [], }); expect(typeof response.body.scheduledTaskId).to.be('string'); const { _source: taskRecord } = await getScheduledTask(response.body.scheduledTaskId); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts index 1b2e7a39f488e..664e74835d415 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/disable.ts @@ -6,16 +6,17 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { AlertUtils, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createDisableAlertTests({ getService }: FtrProviderContext) { const es = getService('es'); - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('disable', () => { - const objectRemover = new ObjectRemover(supertest); + const objectRemover = new ObjectRemover(supertestWithoutAuth); + const alertUtils = new AlertUtils({ space: Spaces.space1, supertestWithoutAuth }); after(() => objectRemover.removeAll()); @@ -27,17 +28,14 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte } it('should handle disable alert request appropriately', async () => { - const { body: createdAlert } = await supertest + const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: true })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); - await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/_disable`) - .set('kbn-xsrf', 'foo') - .expect(204, ''); + await alertUtils.disable(createdAlert.id); try { await getScheduledTask(createdAlert.scheduledTaskId); @@ -48,21 +46,18 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte }); it(`shouldn't disable alert from another space`, async () => { - const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + const { body: createdAlert } = await supertestWithoutAuth + .post(`${getUrlPrefix(Spaces.other.id)}/api/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: true })) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); + objectRemover.add(Spaces.other.id, createdAlert.id, 'alert'); - await supertest - .post(`${getUrlPrefix(Spaces.other.id)}/api/alert/${createdAlert.id}/_disable`) - .set('kbn-xsrf', 'foo') - .expect(404, { - statusCode: 404, - error: 'Not Found', - message: `Saved object [alert/${createdAlert.id}] not found`, - }); + await alertUtils.getDisableRequest(createdAlert.id).expect(404, { + statusCode: 404, + error: 'Not Found', + message: `Saved object [alert/${createdAlert.id}] not found`, + }); }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts index 3a43ccbd07ac5..2a8de1f6e31c3 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/enable.ts @@ -6,16 +6,17 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { AlertUtils, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createEnableAlertTests({ getService }: FtrProviderContext) { const es = getService('es'); - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('enable', () => { - const objectRemover = new ObjectRemover(supertest); + const objectRemover = new ObjectRemover(supertestWithoutAuth); + const alertUtils = new AlertUtils({ space: Spaces.space1, supertestWithoutAuth }); after(() => objectRemover.removeAll()); @@ -27,19 +28,16 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex } it('should handle enable alert request appropriately', async () => { - const { body: createdAlert } = await supertest + const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); - await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/_enable`) - .set('kbn-xsrf', 'foo') - .expect(204, ''); + await alertUtils.enable(createdAlert.id); - const { body: updatedAlert } = await supertest + const { body: updatedAlert } = await supertestWithoutAuth .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(200); @@ -54,21 +52,18 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex }); it(`shouldn't enable alert from another space`, async () => { - const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + const { body: createdAlert } = await supertestWithoutAuth + .post(`${getUrlPrefix(Spaces.other.id)}/api/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData({ enabled: false })) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); + objectRemover.add(Spaces.other.id, createdAlert.id, 'alert'); - await supertest - .post(`${getUrlPrefix(Spaces.other.id)}/api/alert/${createdAlert.id}/_enable`) - .set('kbn-xsrf', 'foo') - .expect(404, { - statusCode: 404, - error: 'Not Found', - message: `Saved object [alert/${createdAlert.id}] not found`, - }); + await alertUtils.getEnableRequest(createdAlert.id).expect(404, { + statusCode: 404, + error: 'Not Found', + message: `Saved object [alert/${createdAlert.id}] not found`, + }); }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts index e90475f8cf80b..521befeb49047 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts @@ -48,6 +48,8 @@ export default function createFindTests({ getService }: FtrProviderContext) { scheduledTaskId: match.scheduledTaskId, updatedBy: null, throttle: '1m', + muteAll: false, + mutedInstanceIds: [], }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts index 3848bac4c44ef..cd30b65cb180e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts @@ -42,6 +42,8 @@ export default function createGetTests({ getService }: FtrProviderContext) { scheduledTaskId: response.body.scheduledTaskId, updatedBy: null, throttle: '1m', + muteAll: false, + mutedInstanceIds: [], }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index 0774bdc17fd22..1aa084356cfa4 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -16,6 +16,10 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./find')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./list_alert_types')); + loadTestFile(require.resolve('./mute_all')); + loadTestFile(require.resolve('./mute_instance')); + loadTestFile(require.resolve('./unmute_all')); + loadTestFile(require.resolve('./unmute_instance')); loadTestFile(require.resolve('./update')); loadTestFile(require.resolve('./update_api_key')); loadTestFile(require.resolve('./alerts')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts new file mode 100644 index 0000000000000..dba3046aa4b7f --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_all.ts @@ -0,0 +1,39 @@ +/* + * 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 expect from '@kbn/expect'; +import { Spaces } from '../../scenarios'; +import { AlertUtils, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createMuteTests({ getService }: FtrProviderContext) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('mute_all', () => { + const objectRemover = new ObjectRemover(supertestWithoutAuth); + const alertUtils = new AlertUtils({ space: Spaces.space1, supertestWithoutAuth }); + + after(() => objectRemover.removeAll()); + + it('should handle mute alert request appropriately', async () => { + const { body: createdAlert } = await supertestWithoutAuth + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData({ enabled: false })) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); + + await alertUtils.muteAll(createdAlert.id); + + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .expect(200); + expect(updatedAlert.muteAll).to.eql(true); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts new file mode 100644 index 0000000000000..09ca359716026 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mute_instance.ts @@ -0,0 +1,39 @@ +/* + * 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 expect from '@kbn/expect'; +import { Spaces } from '../../scenarios'; +import { AlertUtils, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createMuteInstanceTests({ getService }: FtrProviderContext) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('mute_instance', () => { + const objectRemover = new ObjectRemover(supertestWithoutAuth); + const alertUtils = new AlertUtils({ space: Spaces.space1, supertestWithoutAuth }); + + after(() => objectRemover.removeAll()); + + it('should handle mute alert instance request appropriately', async () => { + const { body: createdAlert } = await supertestWithoutAuth + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData({ enabled: false })) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); + + await alertUtils.muteInstance(createdAlert.id, '1'); + + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .expect(200); + expect(updatedAlert.mutedInstanceIds).to.eql(['1']); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts new file mode 100644 index 0000000000000..70ee32a9e6fb9 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_all.ts @@ -0,0 +1,40 @@ +/* + * 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 expect from '@kbn/expect'; +import { Spaces } from '../../scenarios'; +import { AlertUtils, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createUnmuteTests({ getService }: FtrProviderContext) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('unmute_all', () => { + const objectRemover = new ObjectRemover(supertestWithoutAuth); + const alertUtils = new AlertUtils({ space: Spaces.space1, supertestWithoutAuth }); + + after(() => objectRemover.removeAll()); + + it('should handle unmute alert request appropriately', async () => { + const { body: createdAlert } = await supertestWithoutAuth + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData({ enabled: false })) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); + + await alertUtils.muteAll(createdAlert.id); + await alertUtils.unmuteAll(createdAlert.id); + + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .expect(200); + expect(updatedAlert.muteAll).to.eql(false); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts new file mode 100644 index 0000000000000..d0f1b2fdb3f9f --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/unmute_instance.ts @@ -0,0 +1,40 @@ +/* + * 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 expect from '@kbn/expect'; +import { Spaces } from '../../scenarios'; +import { AlertUtils, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createUnmuteInstanceTests({ getService }: FtrProviderContext) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('unmute_instance', () => { + const objectRemover = new ObjectRemover(supertestWithoutAuth); + const alertUtils = new AlertUtils({ space: Spaces.space1, supertestWithoutAuth }); + + after(() => objectRemover.removeAll()); + + it('should handle unmute alert instance request appropriately', async () => { + const { body: createdAlert } = await supertestWithoutAuth + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData({ enabled: false })) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); + + await alertUtils.muteInstance(createdAlert.id, '1'); + await alertUtils.unmuteInstance(createdAlert.id, '1'); + + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .expect(200); + expect(updatedAlert.mutedInstanceIds).to.eql([]); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts index 97311daac2099..d98f6b5cd830b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts @@ -40,8 +40,14 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .expect(200, { ...updatedData, id: createdAlert.id, + alertTypeId: 'test.noop', + createdBy: null, + enabled: true, updatedBy: null, apiKeyOwner: null, + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: createdAlert.scheduledTaskId, }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts index 71d20299f2377..2cd3634043740 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update_api_key.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { AlertUtils, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; /** @@ -15,27 +15,25 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function createUpdateApiKeyTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('update_api_key', () => { - const objectRemover = new ObjectRemover(supertest); + const objectRemover = new ObjectRemover(supertestWithoutAuth); + const alertUtils = new AlertUtils({ space: Spaces.space1, supertestWithoutAuth }); after(() => objectRemover.removeAll()); it('should handle update alert api key appropriately', async () => { - const { body: createdAlert } = await supertest + const { body: createdAlert } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); - await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/_update_api_key`) - .set('kbn-xsrf', 'foo') - .expect(204, ''); + await alertUtils.updateApiKey(createdAlert.id); - const { body: updatedAlert } = await supertest + const { body: updatedAlert } = await supertestWithoutAuth .get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`) .set('kbn-xsrf', 'foo') .expect(200); @@ -43,21 +41,18 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte }); it(`shouldn't update alert api key from another space`, async () => { - const { body: createdAlert } = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + const { body: createdAlert } = await supertestWithoutAuth + .post(`${getUrlPrefix(Spaces.other.id)}/api/alert`) .set('kbn-xsrf', 'foo') .send(getTestAlertData()) .expect(200); - objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); + objectRemover.add(Spaces.other.id, createdAlert.id, 'alert'); - await supertest - .post(`${getUrlPrefix(Spaces.other.id)}/api/alert/${createdAlert.id}/_update_api_key`) - .set('kbn-xsrf', 'foo') - .expect(404, { - statusCode: 404, - error: 'Not Found', - message: `Saved object [alert/${createdAlert.id}] not found`, - }); + await alertUtils.getUpdateApiKeyRequest(createdAlert.id).expect(404, { + statusCode: 404, + error: 'Not Found', + message: `Saved object [alert/${createdAlert.id}] not found`, + }); }); }); }