From c62a8fc79ddda500a32e86b71b090510b577c436 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 21 May 2020 09:50:32 +0100 Subject: [PATCH 001/126] made SO client unsecure in alerting --- .../plugins/alerting/server/alerts_client.ts | 85 +++++++++++++------ .../alerting/server/alerts_client_factory.ts | 13 +-- x-pack/plugins/alerting/server/plugin.ts | 14 +-- x-pack/plugins/security/server/plugin.ts | 6 +- 4 files changed, 77 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client.ts index 01687f33f631d..0e273f05ffd17 100644 --- a/x-pack/plugins/alerting/server/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client.ts @@ -12,6 +12,7 @@ import { SavedObjectsClientContract, SavedObjectReference, SavedObject, + KibanaRequest, } from 'src/core/server'; import { PreConfiguredAction } from '../../actions/server'; import { @@ -31,6 +32,7 @@ import { InvalidateAPIKeyParams, GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, + SecurityPluginSetup, } from '../../../plugins/security/server'; import { EncryptedSavedObjectsClient } from '../../../plugins/encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../plugins/task_manager/server'; @@ -48,7 +50,9 @@ export type InvalidateAPIKeyResult = interface ConstructorOptions { logger: Logger; taskManager: TaskManagerStartContract; - savedObjectsClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; + authorization?: SecurityPluginSetup['authz']; + request: KibanaRequest; alertTypeRegistry: AlertTypeRegistry; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; spaceId?: string; @@ -121,7 +125,9 @@ export class AlertsClient { private readonly spaceId?: string; private readonly namespace?: string; private readonly taskManager: TaskManagerStartContract; - private readonly savedObjectsClient: SavedObjectsClientContract; + private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; + private readonly request: KibanaRequest; + private readonly authorization?: SecurityPluginSetup['authz']; private readonly alertTypeRegistry: AlertTypeRegistry; private readonly createAPIKey: () => Promise; private readonly invalidateAPIKey: ( @@ -132,7 +138,9 @@ export class AlertsClient { constructor({ alertTypeRegistry, - savedObjectsClient, + unsecuredSavedObjectsClient, + request, + authorization, taskManager, logger, spaceId, @@ -149,7 +157,9 @@ export class AlertsClient { this.namespace = namespace; this.taskManager = taskManager; this.alertTypeRegistry = alertTypeRegistry; - this.savedObjectsClient = savedObjectsClient; + this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; + this.request = request; + this.authorization = authorization; this.createAPIKey = createAPIKey; this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; @@ -177,7 +187,7 @@ export class AlertsClient { muteAll: false, mutedInstanceIds: [], }; - const createdAlert = await this.savedObjectsClient.create('alert', rawAlert, { + const createdAlert = await this.unsecuredSavedObjectsClient.create('alert', rawAlert, { ...options, references, }); @@ -188,7 +198,7 @@ export class AlertsClient { } catch (e) { // Cleanup data, something went wrong scheduling the task try { - await this.savedObjectsClient.delete('alert', createdAlert.id); + await this.unsecuredSavedObjectsClient.delete('alert', createdAlert.id); } catch (err) { // Skip the cleanup error and throw the task manager error to avoid confusion this.logger.error( @@ -197,7 +207,7 @@ export class AlertsClient { } throw e; } - await this.savedObjectsClient.update('alert', createdAlert.id, { + await this.unsecuredSavedObjectsClient.update('alert', createdAlert.id, { scheduledTaskId: scheduledTask.id, }); createdAlert.attributes.scheduledTaskId = scheduledTask.id; @@ -211,7 +221,7 @@ export class AlertsClient { } public async get({ id }: { id: string }): Promise { - const result = await this.savedObjectsClient.get('alert', id); + const result = await this.unsecuredSavedObjectsClient.get('alert', id); return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references); } @@ -232,7 +242,7 @@ export class AlertsClient { per_page: perPage, total, saved_objects: data, - } = await this.savedObjectsClient.find({ + } = await this.unsecuredSavedObjectsClient.find({ ...options, type: 'alert', }); @@ -263,11 +273,11 @@ export class AlertsClient { `delete(): Failed to load API key to invalidate on alert ${id}: ${e.message}` ); // Still attempt to load the scheduledTaskId using SOC - const alert = await this.savedObjectsClient.get('alert', id); + const alert = await this.unsecuredSavedObjectsClient.get('alert', id); taskIdToRemove = alert.attributes.scheduledTaskId; } - const removeResult = await this.savedObjectsClient.delete('alert', id); + const removeResult = await this.unsecuredSavedObjectsClient.delete('alert', id); await Promise.all([ taskIdToRemove ? deleteTaskIfItExists(this.taskManager, taskIdToRemove) : null, @@ -290,7 +300,7 @@ export class AlertsClient { `update(): Failed to load API key to invalidate on alert ${id}: ${e.message}` ); // Still attempt to load the object using SOC - alertSavedObject = await this.savedObjectsClient.get('alert', id); + alertSavedObject = await this.unsecuredSavedObjectsClient.get('alert', id); } const updateResult = await this.updateAlert({ id, data }, alertSavedObject); @@ -331,7 +341,7 @@ export class AlertsClient { const createdAPIKey = attributes.enabled ? await this.createAPIKey() : null; const apiKeyAttributes = this.apiKeyAsAlertAttributes(createdAPIKey, username); - const updatedObject = await this.savedObjectsClient.update( + const updatedObject = await this.unsecuredSavedObjectsClient.update( 'alert', id, { @@ -389,13 +399,13 @@ export class AlertsClient { `updateApiKey(): Failed to load API key to invalidate on alert ${id}: ${e.message}` ); // Still attempt to load the attributes and version using SOC - const alert = await this.savedObjectsClient.get('alert', id); + const alert = await this.unsecuredSavedObjectsClient.get('alert', id); attributes = alert.attributes; version = alert.version; } const username = await this.getUserName(); - await this.savedObjectsClient.update( + await this.unsecuredSavedObjectsClient.update( 'alert', id, { @@ -447,14 +457,14 @@ export class AlertsClient { `enable(): Failed to load API key to invalidate on alert ${id}: ${e.message}` ); // Still attempt to load the attributes and version using SOC - const alert = await this.savedObjectsClient.get('alert', id); + const alert = await this.unsecuredSavedObjectsClient.get('alert', id); attributes = alert.attributes; version = alert.version; } if (attributes.enabled === false) { const username = await this.getUserName(); - await this.savedObjectsClient.update( + await this.unsecuredSavedObjectsClient.update( 'alert', id, { @@ -466,7 +476,9 @@ export class AlertsClient { { version } ); const scheduledTask = await this.scheduleAlert(id, attributes.alertTypeId); - await this.savedObjectsClient.update('alert', id, { scheduledTaskId: scheduledTask.id }); + await this.unsecuredSavedObjectsClient.update('alert', id, { + scheduledTaskId: scheduledTask.id, + }); if (apiKeyToInvalidate) { await this.invalidateApiKey({ apiKey: apiKeyToInvalidate }); } @@ -491,13 +503,13 @@ export class AlertsClient { `disable(): Failed to load API key to invalidate on alert ${id}: ${e.message}` ); // Still attempt to load the attributes and version using SOC - const alert = await this.savedObjectsClient.get('alert', id); + const alert = await this.unsecuredSavedObjectsClient.get('alert', id); attributes = alert.attributes; version = alert.version; } if (attributes.enabled === true) { - await this.savedObjectsClient.update( + await this.unsecuredSavedObjectsClient.update( 'alert', id, { @@ -521,7 +533,7 @@ export class AlertsClient { } public async muteAll({ id }: { id: string }) { - await this.savedObjectsClient.update('alert', id, { + await this.unsecuredSavedObjectsClient.update('alert', id, { muteAll: true, mutedInstanceIds: [], updatedBy: await this.getUserName(), @@ -529,7 +541,7 @@ export class AlertsClient { } public async unmuteAll({ id }: { id: string }) { - await this.savedObjectsClient.update('alert', id, { + await this.unsecuredSavedObjectsClient.update('alert', id, { muteAll: false, mutedInstanceIds: [], updatedBy: await this.getUserName(), @@ -543,11 +555,14 @@ export class AlertsClient { alertId: string; alertInstanceId: string; }) { - const { attributes, version } = await this.savedObjectsClient.get('alert', alertId); + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + 'alert', + alertId + ); const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) { mutedInstanceIds.push(alertInstanceId); - await this.savedObjectsClient.update( + await this.unsecuredSavedObjectsClient.update( 'alert', alertId, { @@ -566,10 +581,13 @@ export class AlertsClient { alertId: string; alertInstanceId: string; }) { - const { attributes, version } = await this.savedObjectsClient.get('alert', alertId); + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + 'alert', + alertId + ); const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { - await this.savedObjectsClient.update( + await this.unsecuredSavedObjectsClient.update( 'alert', alertId, { @@ -582,6 +600,19 @@ export class AlertsClient { } } + private async ensureAuthorized(alertTypeId: string, operation: string) { + if (this.authorization == null) { + return; + } + const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest(this.request); + const { hasAllRequested } = await checkPrivileges( + this.authorization.actions.savedObject.get(alertTypeId, operation) + ); + if (!hasAllRequested) { + throw Boom.forbidden(`Unable to ${operation} ${alertTypeId}`); + } + } + private async scheduleAlert(id: string, alertTypeId: string) { return await this.taskManager.schedule({ taskType: `alerting:${alertTypeId}`, @@ -689,7 +720,7 @@ export class AlertsClient { ]; if (actionIds.length > 0) { const bulkGetOpts = actionIds.map(id => ({ id, type: 'action' })); - const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts); + const bulkGetResult = await this.unsecuredSavedObjectsClient.bulkGet(bulkGetOpts); for (const action of bulkGetResult.saved_objects) { if (action.error) { diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.ts b/x-pack/plugins/alerting/server/alerts_client_factory.ts index 913b4e2e81fe1..e2d5c406c26e3 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.ts @@ -7,7 +7,7 @@ import { PreConfiguredAction } from '../../actions/server'; import { AlertsClient } from './alerts_client'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; -import { KibanaRequest, Logger, SavedObjectsClientContract } from '../../../../src/core/server'; +import { KibanaRequest, Logger, SavedObjectsServiceStart } from '../../../../src/core/server'; import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../../plugins/security/server'; import { EncryptedSavedObjectsClient } from '../../../plugins/encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../plugins/task_manager/server'; @@ -49,10 +49,7 @@ export class AlertsClientFactory { this.preconfiguredActions = options.preconfiguredActions; } - public create( - request: KibanaRequest, - savedObjectsClient: SavedObjectsClientContract - ): AlertsClient { + public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): AlertsClient { const { securityPluginSetup } = this; const spaceId = this.getSpaceId(request); return new AlertsClient({ @@ -60,7 +57,11 @@ export class AlertsClientFactory { logger: this.logger, taskManager: this.taskManager, alertTypeRegistry: this.alertTypeRegistry, - savedObjectsClient, + unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, { + excludedWrappers: ['security'], + }), + authorization: this.securityPluginSetup?.authz, + request, namespace: this.spaceIdToNamespace(spaceId), encryptedSavedObjectsClient: this.encryptedSavedObjectsClient, async getUserName() { diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index 0835365635990..97619bff98b9f 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -164,7 +164,7 @@ export class AlertingPlugin { }); } - core.http.registerRouteHandlerContext('alerting', this.createRouteHandlerContext()); + core.http.registerRouteHandlerContext('alerting', this.createRouteHandlerContext(core)); // Routes const router = core.http.createRouter(); @@ -237,20 +237,20 @@ export class AlertingPlugin { `Unable to create alerts client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` ); } - return alertsClientFactory!.create(request, core.savedObjects.getScopedClient(request)); + return alertsClientFactory!.create(request, core.savedObjects); }, }; } - private createRouteHandlerContext = (): IContextProvider< - RequestHandler, - 'alerting' - > => { + private createRouteHandlerContext = ( + core: CoreSetup + ): IContextProvider, 'alerting'> => { const { alertTypeRegistry, alertsClientFactory } = this; return async function alertsRouteHandlerContext(context, request) { + const [{ savedObjects }] = await core.getStartServices(); return { getAlertsClient: () => { - return alertsClientFactory!.create(request, context.core.savedObjects.client); + return alertsClientFactory!.create(request, savedObjects); }, listTypes: alertTypeRegistry!.list.bind(alertTypeRegistry!), }; diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 77a2d716e6d87..219a82d323efe 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -58,7 +58,10 @@ export interface SecurityPluginSetup { | 'grantAPIKeyAsInternalUser' | 'invalidateAPIKeyAsInternalUser' >; - authz: Pick; + authz: Pick< + Authorization, + 'actions' | 'checkPrivilegesWithRequest' | 'checkPrivilegesDynamicallyWithRequest' | 'mode' + >; license: SecurityLicense; /** @@ -191,6 +194,7 @@ export class Plugin { authz: { actions: authz.actions, checkPrivilegesWithRequest: authz.checkPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: authz.checkPrivilegesDynamicallyWithRequest, mode: authz.mode, }, From 764f5150a115b2a3f428de51e9115efe7eb0a74f Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 21 May 2020 11:57:29 +0100 Subject: [PATCH 002/126] fixed typing, commented unused authz --- .../alerting/server/alerts_client.test.ts | 294 +++++++++--------- .../plugins/alerting/server/alerts_client.ts | 36 +-- .../server/alerts_client_factory.test.ts | 63 +++- x-pack/plugins/security/server/mocks.ts | 1 + 4 files changed, 223 insertions(+), 171 deletions(-) diff --git a/x-pack/plugins/alerting/server/alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client.test.ts index 93b98f6a0fe03..fcaeb275c47d7 100644 --- a/x-pack/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client.test.ts @@ -13,16 +13,18 @@ import { TaskStatus } from '../../../plugins/task_manager/server'; import { IntervalSchedule } from './types'; import { resolvable } from './test_utils'; import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; +import { KibanaRequest } from 'kibana/server'; const taskManager = taskManagerMock.start(); const alertTypeRegistry = alertTypeRegistryMock.create(); -const savedObjectsClient = savedObjectsClientMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const alertsClientParams = { taskManager, alertTypeRegistry, - savedObjectsClient, + unsecuredSavedObjectsClient, + request: {} as KibanaRequest, spaceId: 'default', namespace: 'default', getUserName: jest.fn(), @@ -97,7 +99,7 @@ describe('create()', () => { test('creates an alert', async () => { const data = getMockData(); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -109,7 +111,7 @@ describe('create()', () => { }, ], }); - savedObjectsClient.create.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -151,7 +153,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -191,10 +193,10 @@ describe('create()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.create.mock.calls[0]).toHaveLength(3); - expect(savedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); - expect(savedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -229,7 +231,7 @@ describe('create()', () => { "updatedBy": "elastic", } `); - expect(savedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` Object { "references": Array [ Object { @@ -260,11 +262,11 @@ describe('create()', () => { }, ] `); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(3); - 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(` + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` Object { "scheduledTaskId": "task-123", } @@ -297,7 +299,7 @@ describe('create()', () => { }, ], }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -317,7 +319,7 @@ describe('create()', () => { }, ], }); - savedObjectsClient.create.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -385,7 +387,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -435,7 +437,7 @@ describe('create()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith([ + expect(unsecuredSavedObjectsClient.bulkGet).toHaveBeenCalledWith([ { id: '1', type: 'action', @@ -449,7 +451,7 @@ describe('create()', () => { test('creates a disabled alert', async () => { const data = getMockData({ enabled: false }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -461,7 +463,7 @@ describe('create()', () => { }, ], }); - savedObjectsClient.create.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -517,7 +519,7 @@ describe('create()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); expect(taskManager.schedule).toHaveBeenCalledTimes(0); }); @@ -549,17 +551,17 @@ describe('create()', () => { test('throws error if loading actions fails', async () => { const data = getMockData(); - savedObjectsClient.bulkGet.mockRejectedValueOnce(new Error('Test Error')); + unsecuredSavedObjectsClient.bulkGet.mockRejectedValueOnce(new Error('Test Error')); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Test Error"` ); - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); }); test('throws error if create saved object fails', async () => { const data = getMockData(); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -571,7 +573,7 @@ describe('create()', () => { }, ], }); - savedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure')); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure')); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Test failure"` ); @@ -580,7 +582,7 @@ describe('create()', () => { test('attempts to remove saved object if scheduling failed', async () => { const data = getMockData(); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -592,7 +594,7 @@ describe('create()', () => { }, ], }); - savedObjectsClient.create.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -621,12 +623,12 @@ describe('create()', () => { ], }); taskManager.schedule.mockRejectedValueOnce(new Error('Test failure')); - savedObjectsClient.delete.mockResolvedValueOnce({}); + unsecuredSavedObjectsClient.delete.mockResolvedValueOnce({}); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Test failure"` ); - expect(savedObjectsClient.delete).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alert", "1", @@ -636,7 +638,7 @@ describe('create()', () => { test('returns task manager error if cleanup fails, logs to console', async () => { const data = getMockData(); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -648,7 +650,7 @@ describe('create()', () => { }, ], }); - savedObjectsClient.create.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -677,7 +679,9 @@ describe('create()', () => { ], }); taskManager.schedule.mockRejectedValueOnce(new Error('Task manager error')); - savedObjectsClient.delete.mockRejectedValueOnce(new Error('Saved object delete error')); + unsecuredSavedObjectsClient.delete.mockRejectedValueOnce( + new Error('Saved object delete error') + ); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Task manager error"` ); @@ -702,7 +706,7 @@ describe('create()', () => { apiKeysEnabled: true, result: { id: '123', api_key: 'abc' }, }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -714,7 +718,7 @@ describe('create()', () => { }, ], }); - savedObjectsClient.create.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -755,7 +759,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -772,7 +776,7 @@ describe('create()', () => { await alertsClient.create({ data }); expect(alertsClientParams.createAPIKey).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.create).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( 'alert', { actions: [ @@ -813,7 +817,7 @@ describe('create()', () => { test(`doesn't create API key for disabled alerts`, async () => { const data = getMockData({ enabled: false }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -825,7 +829,7 @@ describe('create()', () => { }, ], }); - savedObjectsClient.create.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -866,7 +870,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -883,7 +887,7 @@ describe('create()', () => { await alertsClient.create({ data }); expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(savedObjectsClient.create).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( 'alert', { actions: [ @@ -940,7 +944,7 @@ describe('enable()', () => { beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); - savedObjectsClient.get.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false, }); @@ -961,13 +965,13 @@ describe('enable()', () => { test('enables an alert', async () => { await alertsClient.enable({ id: '1' }); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(savedObjectsClient.update).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { @@ -995,7 +999,7 @@ describe('enable()', () => { }, scope: ['alerting'], }); - expect(savedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { scheduledTaskId: 'task-123', }); }); @@ -1010,7 +1014,7 @@ describe('enable()', () => { }); await alertsClient.enable({ id: '1' }); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); @@ -1029,7 +1033,7 @@ describe('enable()', () => { await alertsClient.enable({ id: '1' }); expect(alertsClientParams.getUserName).not.toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(savedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); }); @@ -1040,7 +1044,7 @@ describe('enable()', () => { }); await alertsClient.enable({ id: '1' }); - expect(savedObjectsClient.update).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { @@ -1061,7 +1065,7 @@ describe('enable()', () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); await alertsClient.enable({ id: '1' }); - expect(savedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'enable(): Failed to load API key to invalidate on alert 1: Fail' ); @@ -1069,45 +1073,47 @@ describe('enable()', () => { test('throws error when failing to load the saved object using SOC', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - savedObjectsClient.get.mockRejectedValueOnce(new Error('Fail to get')); + unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('Fail to get')); await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Fail to get"` ); expect(alertsClientParams.getUserName).not.toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(savedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); }); test('throws error when failing to update the first time', async () => { - savedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update')); + unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update')); await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Fail to update"` ); expect(alertsClientParams.getUserName).toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); expect(taskManager.schedule).not.toHaveBeenCalled(); }); test('throws error when failing to update the second time', async () => { - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ ...existingAlert, attributes: { ...existingAlert.attributes, enabled: true, }, }); - savedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update second time')); + unsecuredSavedObjectsClient.update.mockRejectedValueOnce( + new Error('Fail to update second time') + ); await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Fail to update second time"` ); expect(alertsClientParams.getUserName).toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(2); expect(taskManager.schedule).toHaveBeenCalled(); }); @@ -1119,7 +1125,7 @@ describe('enable()', () => { ); expect(alertsClientParams.getUserName).toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); }); }); @@ -1147,17 +1153,17 @@ describe('disable()', () => { beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); }); test('disables an alert', async () => { await alertsClient.disable({ id: '1' }); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(savedObjectsClient.update).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { @@ -1181,11 +1187,11 @@ describe('disable()', () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); await alertsClient.disable({ id: '1' }); - expect(savedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(savedObjectsClient.update).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { @@ -1215,7 +1221,7 @@ describe('disable()', () => { }); await alertsClient.disable({ id: '1' }); - expect(savedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); expect(taskManager.remove).not.toHaveBeenCalled(); expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); }); @@ -1231,7 +1237,7 @@ describe('disable()', () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); await alertsClient.disable({ id: '1' }); - expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); expect(taskManager.remove).toHaveBeenCalled(); expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( @@ -1239,8 +1245,8 @@ describe('disable()', () => { ); }); - test('throws when savedObjectsClient update fails', async () => { - savedObjectsClient.update.mockRejectedValueOnce(new Error('Failed to update')); + test('throws when unsecuredSavedObjectsClient update fails', async () => { + unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Failed to update')); await expect(alertsClient.disable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Failed to update"` @@ -1268,7 +1274,7 @@ describe('disable()', () => { describe('muteAll()', () => { test('mutes an alert', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1278,7 +1284,7 @@ describe('muteAll()', () => { }); await alertsClient.muteAll({ id: '1' }); - expect(savedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { muteAll: true, mutedInstanceIds: [], updatedBy: 'elastic', @@ -1289,7 +1295,7 @@ describe('muteAll()', () => { describe('unmuteAll()', () => { test('unmutes an alert', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1299,7 +1305,7 @@ describe('unmuteAll()', () => { }); await alertsClient.unmuteAll({ id: '1' }); - expect(savedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { muteAll: false, mutedInstanceIds: [], updatedBy: 'elastic', @@ -1310,7 +1316,7 @@ describe('unmuteAll()', () => { describe('muteInstance()', () => { test('mutes an alert instance', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1325,7 +1331,7 @@ describe('muteInstance()', () => { }); await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(savedObjectsClient.update).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { @@ -1338,7 +1344,7 @@ describe('muteInstance()', () => { test('skips muting when alert instance already muted', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1352,12 +1358,12 @@ describe('muteInstance()', () => { }); await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(savedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); }); test('skips muting when alert is muted', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1372,14 +1378,14 @@ describe('muteInstance()', () => { }); await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(savedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); }); }); describe('unmuteInstance()', () => { test('unmutes an alert instance', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1394,7 +1400,7 @@ describe('unmuteInstance()', () => { }); await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(savedObjectsClient.update).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { @@ -1407,7 +1413,7 @@ describe('unmuteInstance()', () => { test('skips unmuting when alert instance not muted', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1421,12 +1427,12 @@ describe('unmuteInstance()', () => { }); await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(savedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); }); test('skips unmuting when alert is muted', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1441,14 +1447,14 @@ describe('unmuteInstance()', () => { }); await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(savedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); }); }); describe('get()', () => { test('calls saved objects client with given params', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1499,8 +1505,8 @@ describe('get()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alert", "1", @@ -1510,7 +1516,7 @@ describe('get()', () => { test(`throws an error when references aren't found`, async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1540,7 +1546,7 @@ describe('get()', () => { describe('getAlertState()', () => { test('calls saved objects client with given params', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1583,8 +1589,8 @@ describe('getAlertState()', () => { }); await alertsClient.getAlertState({ id: '1' }); - expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alert", "1", @@ -1597,7 +1603,7 @@ describe('getAlertState()', () => { const scheduledTaskId = 'task-123'; - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1654,7 +1660,7 @@ describe('getAlertState()', () => { describe('find()', () => { test('calls saved objects client with given params', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.find.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ total: 1, per_page: 10, page: 1, @@ -1719,8 +1725,8 @@ describe('find()', () => { "total": 1, } `); - expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "type": "alert", @@ -1770,8 +1776,8 @@ describe('delete()', () => { beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValue(existingAlert); - savedObjectsClient.delete.mockResolvedValue({ + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.delete.mockResolvedValue({ success: true, }); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); @@ -1780,13 +1786,13 @@ describe('delete()', () => { test('successfully removes an alert', async () => { const result = await alertsClient.delete({ id: '1' }); expect(result).toEqual({ success: true }); - expect(savedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); expect(taskManager.remove).toHaveBeenCalledWith('task-123'); expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); }); test('falls back to SOC.get when getDecryptedAsInternalUser throws an error', async () => { @@ -1794,10 +1800,10 @@ describe('delete()', () => { const result = await alertsClient.delete({ id: '1' }); expect(result).toEqual({ success: true }); - expect(savedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); expect(taskManager.remove).toHaveBeenCalledWith('task-123'); expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - expect(savedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'delete(): Failed to load API key to invalidate on alert 1: Fail' ); @@ -1849,9 +1855,9 @@ describe('delete()', () => { ); }); - test('throws error when savedObjectsClient.get throws an error', async () => { + test('throws error when unsecuredSavedObjectsClient.get throws an error', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - savedObjectsClient.get.mockRejectedValue(new Error('SOC Fail')); + unsecuredSavedObjectsClient.get.mockRejectedValue(new Error('SOC Fail')); await expect(alertsClient.delete({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"SOC Fail"` @@ -1890,7 +1896,7 @@ describe('update()', () => { beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); alertTypeRegistry.get.mockReturnValue({ id: '123', @@ -1903,7 +1909,7 @@ describe('update()', () => { }); test('updates given parameters', async () => { - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -1923,7 +1929,7 @@ describe('update()', () => { }, ], }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -2059,12 +2065,12 @@ describe('update()', () => { expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); - 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(` + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -2111,7 +2117,7 @@ describe('update()', () => { "updatedBy": "elastic", } `); - expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` Object { "references": Array [ Object { @@ -2136,7 +2142,7 @@ describe('update()', () => { }); it('calls the createApiKey function', async () => { - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -2152,7 +2158,7 @@ describe('update()', () => { apiKeysEnabled: true, result: { id: '123', api_key: 'abc' }, }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -2231,11 +2237,11 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - 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(` + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -2266,7 +2272,7 @@ describe('update()', () => { "updatedBy": "elastic", } `); - expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` Object { "references": Array [ Object { @@ -2288,7 +2294,7 @@ describe('update()', () => { enabled: false, }, }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -2300,7 +2306,7 @@ describe('update()', () => { }, ], }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -2380,11 +2386,11 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - 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(` + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -2415,7 +2421,7 @@ describe('update()', () => { "updatedBy": "elastic", } `); - expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` Object { "references": Array [ Object { @@ -2472,7 +2478,7 @@ describe('update()', () => { it('swallows error when invalidate API key throws', async () => { alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -2484,7 +2490,7 @@ describe('update()', () => { }, ], }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -2541,7 +2547,7 @@ describe('update()', () => { it('swallows error when getDecryptedAsInternalUser throws', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -2561,7 +2567,7 @@ describe('update()', () => { }, ], }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -2653,7 +2659,7 @@ describe('update()', () => { ], }, }); - expect(savedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'update(): Failed to load API key to invalidate on alert 1: Fail' ); @@ -2675,7 +2681,7 @@ describe('update()', () => { async executor() {}, producer: 'alerting', }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -2713,7 +2719,7 @@ describe('update()', () => { params: {}, ownerId: null, }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: alertId, type: 'alert', attributes: { @@ -2907,7 +2913,7 @@ describe('updateApiKey()', () => { beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingEncryptedAlert); alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, @@ -2917,11 +2923,11 @@ describe('updateApiKey()', () => { test('updates the API key for the alert', async () => { await alertsClient.updateApiKey({ id: '1' }); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(savedObjectsClient.update).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { @@ -2941,11 +2947,11 @@ describe('updateApiKey()', () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); await alertsClient.updateApiKey({ id: '1' }); - expect(savedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(savedObjectsClient.update).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { @@ -2968,7 +2974,7 @@ describe('updateApiKey()', () => { expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'Failed to invalidate API Key: Fail' ); - expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); }); test('swallows error when getting decrypted object throws', async () => { @@ -2978,12 +2984,12 @@ describe('updateApiKey()', () => { expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'updateApiKey(): Failed to load API key to invalidate on alert 1: Fail' ); - expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); }); - test('throws when savedObjectsClient update fails', async () => { - savedObjectsClient.update.mockRejectedValueOnce(new Error('Fail')); + test('throws when unsecuredSavedObjectsClient update fails', async () => { + unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail')); await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Fail"` diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client.ts index 0e273f05ffd17..3149f11bedabf 100644 --- a/x-pack/plugins/alerting/server/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client.ts @@ -126,8 +126,8 @@ export class AlertsClient { private readonly namespace?: string; private readonly taskManager: TaskManagerStartContract; private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; - private readonly request: KibanaRequest; - private readonly authorization?: SecurityPluginSetup['authz']; + // private readonly request: KibanaRequest; + // private readonly authorization?: SecurityPluginSetup['authz']; private readonly alertTypeRegistry: AlertTypeRegistry; private readonly createAPIKey: () => Promise; private readonly invalidateAPIKey: ( @@ -139,8 +139,8 @@ export class AlertsClient { constructor({ alertTypeRegistry, unsecuredSavedObjectsClient, - request, - authorization, + // request, + // authorization, taskManager, logger, spaceId, @@ -158,8 +158,8 @@ export class AlertsClient { this.taskManager = taskManager; this.alertTypeRegistry = alertTypeRegistry; this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; - this.request = request; - this.authorization = authorization; + // this.request = request; + // this.authorization = authorization; this.createAPIKey = createAPIKey; this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; @@ -600,18 +600,18 @@ export class AlertsClient { } } - private async ensureAuthorized(alertTypeId: string, operation: string) { - if (this.authorization == null) { - return; - } - const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested } = await checkPrivileges( - this.authorization.actions.savedObject.get(alertTypeId, operation) - ); - if (!hasAllRequested) { - throw Boom.forbidden(`Unable to ${operation} ${alertTypeId}`); - } - } + // private async ensureAuthorized(alertTypeId: string, operation: string) { + // if (this.authorization == null) { + // return; + // } + // const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest(this.request); + // const { hasAllRequested } = await checkPrivileges( + // this.authorization.actions.savedObject.get(alertTypeId, operation) + // ); + // if (!hasAllRequested) { + // throw Boom.forbidden(`Unable to ${operation} ${alertTypeId}`); + // } + // } private async scheduleAlert(id: string, alertTypeId: string) { return await this.taskManager.schedule({ diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts index cc792d11c890d..ea3e0188725eb 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts @@ -9,7 +9,11 @@ import { AlertsClientFactory, AlertsClientFactoryOpts } from './alerts_client_fa import { alertTypeRegistryMock } from './alert_type_registry.mock'; import { taskManagerMock } from '../../../plugins/task_manager/server/task_manager.mock'; import { KibanaRequest } from '../../../../src/core/server'; -import { loggingServiceMock, savedObjectsClientMock } from '../../../../src/core/server/mocks'; +import { + loggingServiceMock, + savedObjectsClientMock, + savedObjectsServiceMock, +} from '../../../../src/core/server/mocks'; import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; import { AuthenticatedUser } from '../../../plugins/security/public'; import { securityMock } from '../../../plugins/security/server/mocks'; @@ -17,6 +21,8 @@ import { securityMock } from '../../../plugins/security/server/mocks'; jest.mock('./alerts_client'); const savedObjectsClient = savedObjectsClientMock.create(); +const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(); + const securityPluginSetup = securityMock.createSetup(); const alertsClientFactoryParams: jest.Mocked = { logger: loggingServiceMock.create().get(), @@ -49,13 +55,52 @@ beforeEach(() => { alertsClientFactoryParams.spaceIdToNamespace.mockReturnValue('default'); }); +test('creates an alerts client with proper constructor arguments when security is enabled', async () => { + const factory = new AlertsClientFactory(); + factory.initialize({ securityPluginSetup, ...alertsClientFactoryParams }); + const request = KibanaRequest.from(fakeRequest); + + savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient); + + factory.create(request, savedObjectsService); + + expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, { + excludedWrappers: ['security'], + }); + + expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ + unsecuredSavedObjectsClient: savedObjectsClient, + request, + authorization: securityPluginSetup.authz, + logger: alertsClientFactoryParams.logger, + taskManager: alertsClientFactoryParams.taskManager, + alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, + spaceId: 'default', + namespace: 'default', + getUserName: expect.any(Function), + createAPIKey: expect.any(Function), + invalidateAPIKey: expect.any(Function), + encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, + preconfiguredActions: [], + }); +}); + test('creates an alerts client with proper constructor arguments', async () => { const factory = new AlertsClientFactory(); factory.initialize(alertsClientFactoryParams); - factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + const request = KibanaRequest.from(fakeRequest); + + savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient); + + factory.create(request, savedObjectsService); + + expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, { + excludedWrappers: ['security'], + }); expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ - savedObjectsClient, + unsecuredSavedObjectsClient: savedObjectsClient, + request, logger: alertsClientFactoryParams.logger, taskManager: alertsClientFactoryParams.taskManager, alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, @@ -72,7 +117,7 @@ test('creates an alerts client with proper constructor arguments', async () => { test('getUserName() returns null when security is disabled', async () => { const factory = new AlertsClientFactory(); factory.initialize(alertsClientFactoryParams); - factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; const userNameResult = await constructorCall.getUserName(); @@ -85,7 +130,7 @@ test('getUserName() returns a name when security is enabled', async () => { ...alertsClientFactoryParams, securityPluginSetup, }); - factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; securityPluginSetup.authc.getCurrentUser.mockReturnValueOnce(({ @@ -98,7 +143,7 @@ test('getUserName() returns a name when security is enabled', async () => { test('createAPIKey() returns { apiKeysEnabled: false } when security is disabled', async () => { const factory = new AlertsClientFactory(); factory.initialize(alertsClientFactoryParams); - factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; const createAPIKeyResult = await constructorCall.createAPIKey(); @@ -108,7 +153,7 @@ test('createAPIKey() returns { apiKeysEnabled: false } when security is disabled test('createAPIKey() returns { apiKeysEnabled: false } when security is enabled but ES security is disabled', async () => { const factory = new AlertsClientFactory(); factory.initialize(alertsClientFactoryParams); - factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce(null); @@ -122,7 +167,7 @@ test('createAPIKey() returns an API key when security is enabled', async () => { ...alertsClientFactoryParams, securityPluginSetup, }); - factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce({ @@ -143,7 +188,7 @@ test('createAPIKey() throws when security plugin createAPIKey throws an error', ...alertsClientFactoryParams, securityPluginSetup, }); - factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockRejectedValueOnce( diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index a6407366bbd3b..adf532389d88f 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -17,6 +17,7 @@ function createSetupMock() { authz: { actions: mockAuthz.actions, checkPrivilegesWithRequest: mockAuthz.checkPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockAuthz.checkPrivilegesDynamicallyWithRequest, mode: mockAuthz.mode, }, registerSpacesService: jest.fn(), From 52a153ec219fc935febd2c36fcf70082459fe486 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 21 May 2020 16:51:50 +0100 Subject: [PATCH 003/126] fixed unit test --- x-pack/plugins/security/server/plugin.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index d58c999ddccdf..74132c3c67961 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -83,6 +83,7 @@ describe('Security Plugin', () => { "version": "version:version", "versionNumber": "version", }, + "checkPrivilegesDynamicallyWithRequest": [Function], "checkPrivilegesWithRequest": [Function], "mode": Object { "useRbacForRequest": [Function], From 4b95c81e6be42ce54da531626f8a42da8bcab5b6 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 3 Jun 2020 19:56:53 +0100 Subject: [PATCH 004/126] added rbac in alerting --- .../server/alert_type_registry.test.ts | 6 +- .../alerting/server/alert_type_registry.ts | 33 +- .../alerting/server/alerts_client.mock.ts | 1 + .../alerting/server/alerts_client.test.ts | 1852 ++++++++++++++++- .../plugins/alerting/server/alerts_client.ts | 154 +- .../alerting/server/routes/create.test.ts | 7 - .../plugins/alerting/server/routes/create.ts | 3 - .../alerting/server/routes/delete.test.ts | 7 - .../plugins/alerting/server/routes/delete.ts | 3 - .../alerting/server/routes/disable.test.ts | 7 - .../plugins/alerting/server/routes/disable.ts | 3 - .../alerting/server/routes/enable.test.ts | 7 - .../plugins/alerting/server/routes/enable.ts | 3 - .../alerting/server/routes/find.test.ts | 7 - x-pack/plugins/alerting/server/routes/find.ts | 3 - .../alerting/server/routes/get.test.ts | 7 - x-pack/plugins/alerting/server/routes/get.ts | 3 - .../server/routes/get_alert_state.test.ts | 7 - .../alerting/server/routes/get_alert_state.ts | 3 - .../server/routes/list_alert_types.test.ts | 7 - .../server/routes/list_alert_types.ts | 5 +- .../alerting/server/routes/mute_all.test.ts | 7 - .../alerting/server/routes/mute_all.ts | 3 - .../server/routes/mute_instance.test.ts | 7 - .../alerting/server/routes/mute_instance.ts | 3 - .../alerting/server/routes/unmute_all.test.ts | 7 - .../alerting/server/routes/unmute_all.ts | 3 - .../server/routes/unmute_instance.test.ts | 7 - .../alerting/server/routes/unmute_instance.ts | 3 - .../alerting/server/routes/update.test.ts | 7 - .../plugins/alerting/server/routes/update.ts | 3 - .../server/routes/update_api_key.test.ts | 7 - .../alerting/server/routes/update_api_key.ts | 3 - x-pack/plugins/apm/server/feature.ts | 24 +- .../common/feature_kibana_privileges.ts | 53 + .../plugins/features/server/feature_schema.ts | 8 + x-pack/plugins/infra/server/features.ts | 25 +- .../__snapshots__/alerting.test.ts.snap | 35 + .../authorization/actions/actions.mock.ts | 35 + .../server/authorization/actions/actions.ts | 3 + .../authorization/actions/alerting.test.ts | 52 + .../server/authorization/actions/alerting.ts | 31 + .../server/authorization/index.mock.ts | 4 +- .../security/server/authorization/index.ts | 1 + .../alerting.test.ts | 311 +++ .../feature_privilege_builder/alerting.ts | 48 + .../feature_privilege_builder/index.ts | 2 + .../authorization/privileges/privileges.ts | 1 - x-pack/plugins/security/server/index.ts | 1 + x-pack/plugins/siem/server/plugin.ts | 12 +- x-pack/plugins/uptime/server/kibana.index.ts | 17 +- .../fixtures/plugins/alerts/server/plugin.ts | 52 +- .../common/lib/alert_utils.ts | 12 +- .../common/lib/get_test_alert_data.ts | 2 +- .../common/lib/index.ts | 2 +- .../security_and_spaces/scenarios.ts | 2 + .../tests/alerting/alerts.ts | 133 +- .../tests/alerting/create.ts | 69 +- .../tests/alerting/delete.ts | 29 +- .../tests/alerting/disable.ts | 23 +- .../tests/alerting/enable.ts | 23 +- .../tests/alerting/find.ts | 43 +- .../security_and_spaces/tests/alerting/get.ts | 30 +- .../tests/alerting/get_alert_state.ts | 28 +- .../tests/alerting/list_alert_types.ts | 13 +- .../tests/alerting/mute_all.ts | 9 +- .../tests/alerting/mute_instance.ts | 17 +- .../tests/alerting/unmute_all.ts | 9 +- .../tests/alerting/unmute_instance.ts | 13 +- .../tests/alerting/update.ts | 64 +- .../tests/alerting/update_api_key.ts | 24 +- .../spaces_only/tests/alerting/create.ts | 2 +- .../spaces_only/tests/alerting/find.ts | 2 +- .../spaces_only/tests/alerting/get.ts | 2 +- .../tests/alerting/get_alert_state.ts | 2 +- .../tests/alerting/list_alert_types.ts | 2 +- .../spaces_only/tests/alerting/update.ts | 2 +- 77 files changed, 2923 insertions(+), 537 deletions(-) create mode 100644 x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap create mode 100644 x-pack/plugins/security/server/authorization/actions/actions.mock.ts create mode 100644 x-pack/plugins/security/server/authorization/actions/alerting.test.ts create mode 100644 x-pack/plugins/security/server/authorization/actions/alerting.ts create mode 100644 x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts create mode 100644 x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts diff --git a/x-pack/plugins/alerting/server/alert_type_registry.test.ts b/x-pack/plugins/alerting/server/alert_type_registry.test.ts index e78e5ab7932c2..2e4f71abdce84 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/alert_type_registry.test.ts @@ -177,7 +177,7 @@ describe('list()', () => { test('should return empty when nothing is registered', () => { const registry = new AlertTypeRegistry(alertTypeRegistryParams); const result = registry.list(); - expect(result).toMatchInlineSnapshot(`Array []`); + expect(result).toMatchInlineSnapshot(`Set {}`); }); test('should return registered types', () => { @@ -197,7 +197,7 @@ describe('list()', () => { }); const result = registry.list(); expect(result).toMatchInlineSnapshot(` - Array [ + Set { Object { "actionGroups": Array [ Object { @@ -214,7 +214,7 @@ describe('list()', () => { "name": "Test", "producer": "alerting", }, - ] + } `); }); diff --git a/x-pack/plugins/alerting/server/alert_type_registry.ts b/x-pack/plugins/alerting/server/alert_type_registry.ts index 0163cb71166e8..6f3b2d0a32a22 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.ts +++ b/x-pack/plugins/alerting/server/alert_type_registry.ts @@ -15,6 +15,14 @@ interface ConstructorOptions { taskRunnerFactory: TaskRunnerFactory; } +export interface RegistryAlertType + extends Pick< + AlertType, + 'name' | 'actionGroups' | 'defaultActionGroupId' | 'actionVariables' | 'producer' + > { + id: string; +} + export class AlertTypeRegistry { private readonly taskManager: TaskManagerSetupContract; private readonly alertTypes: Map = new Map(); @@ -66,15 +74,22 @@ export class AlertTypeRegistry { return this.alertTypes.get(id)!; } - public list() { - return Array.from(this.alertTypes).map(([alertTypeId, alertType]) => ({ - id: alertTypeId, - name: alertType.name, - actionGroups: alertType.actionGroups, - defaultActionGroupId: alertType.defaultActionGroupId, - actionVariables: alertType.actionVariables, - producer: alertType.producer, - })); + public list(): Set { + return new Set( + Array.from(this.alertTypes).map( + ([id, { name, actionGroups, defaultActionGroupId, actionVariables, producer }]: [ + string, + AlertType + ]) => ({ + id, + name, + actionGroups, + defaultActionGroupId, + actionVariables, + producer, + }) + ) + ); } } diff --git a/x-pack/plugins/alerting/server/alerts_client.mock.ts b/x-pack/plugins/alerting/server/alerts_client.mock.ts index 1848b3432ae5a..be70e441b6fc5 100644 --- a/x-pack/plugins/alerting/server/alerts_client.mock.ts +++ b/x-pack/plugins/alerting/server/alerts_client.mock.ts @@ -24,6 +24,7 @@ const createAlertsClientMock = () => { unmuteAll: jest.fn(), muteInstance: jest.fn(), unmuteInstance: jest.fn(), + listAlertTypes: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/alerting/server/alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client.test.ts index fcaeb275c47d7..f9fac8795bd61 100644 --- a/x-pack/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client.test.ts @@ -5,15 +5,17 @@ */ import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; -import { AlertsClient, CreateOptions } from './alerts_client'; +import { AlertsClient, CreateOptions, UpdateOptions, FindOptions } from './alerts_client'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../plugins/task_manager/server/task_manager.mock'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; import { TaskStatus } from '../../../plugins/task_manager/server'; -import { IntervalSchedule } from './types'; +import { IntervalSchedule, PartialAlert } from './types'; import { resolvable } from './test_utils'; import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; import { KibanaRequest } from 'kibana/server'; +import { SecurityPluginSetup } from '../../../plugins/security/server'; +import { securityMock } from '../../../plugins/security/server/mocks'; const taskManager = taskManagerMock.start(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -35,6 +37,15 @@ const alertsClientParams = { preconfiguredActions: [], }; +function mockAuthorization() { + const authorization = securityMock.createSetup().authz; + // typescript is havingtrouble inferring jest's automocking + (authorization.actions.alerting.get as jest.MockedFunction< + typeof authorization.actions.alerting.get + >).mockImplementation((type, app, operation) => `${type}${app ? `/${app}` : ``}/${operation}`); + return authorization; +} + beforeEach(() => { jest.resetAllMocks(); alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false }); @@ -97,6 +108,185 @@ describe('create()', () => { }); }); + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + }); + + function tryToExecuteOperation(options: CreateOptions): Promise { + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: '2019-02-12T21:01:22.479Z', + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + scheduledTaskId: 'task-123', + }, + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + }); + + return alertsClientWithAuthorization.create(options); + } + + test('create when user is authorised to create this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/create', + authorized: false, + }, + { + privilege: 'myType/myApp/create', + authorized: true, + }, + ], + }); + + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myApp', + }); + + await tryToExecuteOperation({ data }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'create' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); + }); + + test('create when user is authorised to create this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/create', + authorized: true, + }, + { + privilege: 'myType/myApp/create', + authorized: false, + }, + ], + }); + + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myApp', + }); + + await tryToExecuteOperation({ data }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'create' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); + }); + + test('throws when user is not authorised to create this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/create', + authorized: false, + }, + { + privilege: 'myType/myApp/create', + authorized: false, + }, + ], + }); + + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myApp', + }); + + await expect(tryToExecuteOperation({ data })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to create a "myType" alert for "myApp"]` + ); + }); + }); + test('creates an alert', async () => { const data = getMockData(); unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ @@ -933,8 +1123,9 @@ describe('enable()', () => { id: '1', type: 'alert', attributes: { + consumer: 'myApp', schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', enabled: false, }, version: '123', @@ -963,6 +1154,117 @@ describe('enable()', () => { }); }); + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + alertsClientParams.createAPIKey.mockResolvedValue({ + apiKeysEnabled: false, + }); + taskManager.schedule.mockResolvedValue({ + id: 'task-123', + scheduledAt: new Date(), + attempts: 0, + status: TaskStatus.Idle, + runAt: new Date(), + state: {}, + params: {}, + taskType: '', + startedAt: null, + retryAt: null, + ownerId: null, + }); + }); + + test('enable when user is authorised to enable this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/enable', + authorized: false, + }, + { + privilege: 'myType/myApp/enable', + authorized: true, + }, + ], + }); + + await alertsClientWithAuthorization.enable({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'enable' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'enable'); + }); + + test('enable when user is authorised to enable this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/enable', + authorized: true, + }, + { + privilege: 'myType/myApp/enable', + authorized: false, + }, + ], + }); + + await alertsClientWithAuthorization.enable({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'enable' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'enable'); + }); + + test('throws when user is not authorised to enable this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/enable', + authorized: false, + }, + { + privilege: 'myType/myApp/enable', + authorized: false, + }, + ], + }); + + expect(alertsClientWithAuthorization.enable({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to enable a "myType" alert for "myApp"]` + ); + }); + }); + test('enables an alert', async () => { await alertsClient.enable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); @@ -976,7 +1278,8 @@ describe('enable()', () => { '1', { schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', + consumer: 'myApp', enabled: true, updatedBy: 'elastic', apiKey: null, @@ -987,7 +1290,7 @@ describe('enable()', () => { } ); expect(taskManager.schedule).toHaveBeenCalledWith({ - taskType: `alerting:2`, + taskType: `alerting:myType`, params: { alertId: '1', spaceId: 'default', @@ -1049,7 +1352,8 @@ describe('enable()', () => { '1', { schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', + consumer: 'myApp', enabled: true, apiKey: Buffer.from('123:abc').toString('base64'), apiKeyOwner: 'elastic', @@ -1135,8 +1439,9 @@ describe('disable()', () => { id: '1', type: 'alert', attributes: { + consumer: 'myApp', schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', enabled: true, scheduledTaskId: 'task-123', }, @@ -1157,6 +1462,117 @@ describe('disable()', () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); }); + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + alertsClientParams.createAPIKey.mockResolvedValue({ + apiKeysEnabled: false, + }); + taskManager.schedule.mockResolvedValue({ + id: 'task-123', + scheduledAt: new Date(), + attempts: 0, + status: TaskStatus.Idle, + runAt: new Date(), + state: {}, + params: {}, + taskType: '', + startedAt: null, + retryAt: null, + ownerId: null, + }); + }); + + test('disables when user is authorised to disable this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/disable', + authorized: false, + }, + { + privilege: 'myType/myApp/disable', + authorized: true, + }, + ], + }); + + await alertsClientWithAuthorization.disable({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'disable' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'disable'); + }); + + test('disables when user is authorised to disable this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/disable', + authorized: true, + }, + { + privilege: 'myType/myApp/disable', + authorized: false, + }, + ], + }); + + await alertsClientWithAuthorization.disable({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'disable' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'disable'); + }); + + test('throws when user is not authorised to disable this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/disable', + authorized: false, + }, + { + privilege: 'myType/myApp/disable', + authorized: false, + }, + ], + }); + + expect(alertsClientWithAuthorization.disable({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to disable a "myType" alert for "myApp"]` + ); + }); + }); + test('disables an alert', async () => { await alertsClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); @@ -1167,8 +1583,9 @@ describe('disable()', () => { 'alert', '1', { + consumer: 'myApp', schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', apiKey: null, apiKeyOwner: null, enabled: false, @@ -1195,8 +1612,9 @@ describe('disable()', () => { 'alert', '1', { + consumer: 'myApp', schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', apiKey: null, apiKeyOwner: null, enabled: false, @@ -1290,6 +1708,109 @@ describe('muteAll()', () => { updatedBy: 'elastic', }); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + muteAll: false, + }, + references: [], + }); + }); + + test('mutes when user is authorised to muteAll this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/muteAll', + authorized: false, + }, + { + privilege: 'myType/myApp/muteAll', + authorized: true, + }, + ], + }); + + await alertsClientWithAuthorization.muteAll({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'muteAll' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); + }); + + test('mutes when user is authorised to muteAll this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/muteAll', + authorized: true, + }, + { + privilege: 'myType/myApp/muteAll', + authorized: false, + }, + ], + }); + + await alertsClientWithAuthorization.muteAll({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'muteAll' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); + }); + + test('throws when user is not authorised to muteAll this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/muteAll', + authorized: false, + }, + { + privilege: 'myType/myApp/muteAll', + authorized: false, + }, + ], + }); + + expect(alertsClientWithAuthorization.muteAll({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to muteAll a "myType" alert for "myApp"]` + ); + }); + }); }); describe('unmuteAll()', () => { @@ -1311,6 +1832,117 @@ describe('unmuteAll()', () => { updatedBy: 'elastic', }); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + muteAll: true, + }, + references: [], + }); + }); + + test('unmutes when user is authorised to unmuteAll this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/unmuteAll', + authorized: false, + }, + { + privilege: 'myType/myApp/unmuteAll', + authorized: true, + }, + ], + }); + + await alertsClientWithAuthorization.unmuteAll({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'unmuteAll' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'unmuteAll' + ); + }); + + test('unmutes when user is authorised to unmuteAll this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/unmuteAll', + authorized: true, + }, + { + privilege: 'myType/myApp/unmuteAll', + authorized: false, + }, + ], + }); + + await alertsClientWithAuthorization.unmuteAll({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'unmuteAll' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'unmuteAll' + ); + }); + + test('throws when user is not authorised to unmuteAll this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/unmuteAll', + authorized: false, + }, + { + privilege: 'myType/myApp/unmuteAll', + authorized: false, + }, + ], + }); + + expect(alertsClientWithAuthorization.unmuteAll({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to unmuteAll a "myType" alert for "myApp"]` + ); + }); + }); }); describe('muteInstance()', () => { @@ -1380,6 +2012,119 @@ describe('muteInstance()', () => { await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + muteAll: true, + }, + references: [], + }); + }); + + test('mutes instance when user is authorised to mute an instance on this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/muteInstance', + authorized: false, + }, + { + privilege: 'myType/myApp/muteInstance', + authorized: true, + }, + ], + }); + + await alertsClientWithAuthorization.muteInstance({ alertId: '1', alertInstanceId: '2' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'muteInstance' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'muteInstance' + ); + }); + + test('mutes instance when user is authorised to mute an instance on this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/muteInstance', + authorized: true, + }, + { + privilege: 'myType/myApp/muteInstance', + authorized: false, + }, + ], + }); + + await alertsClientWithAuthorization.muteInstance({ alertId: '1', alertInstanceId: '2' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'muteInstance' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'muteInstance' + ); + }); + + test('throws when user is not authorised to mute an instance on this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/muteInstance', + authorized: false, + }, + { + privilege: 'myType/myApp/muteInstance', + authorized: false, + }, + ], + }); + + expect( + alertsClientWithAuthorization.muteInstance({ alertId: '1', alertInstanceId: '2' }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to muteInstance a "myType" alert for "myApp"]` + ); + }); + }); }); describe('unmuteInstance()', () => { @@ -1449,6 +2194,119 @@ describe('unmuteInstance()', () => { await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + muteAll: true, + }, + references: [], + }); + }); + + test('unmutes instance when user is authorised to unmutes an instance on this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/unmuteInstance', + authorized: false, + }, + { + privilege: 'myType/myApp/unmuteInstance', + authorized: true, + }, + ], + }); + + await alertsClientWithAuthorization.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'unmuteInstance' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'unmuteInstance' + ); + }); + + test('unmutes instance when user is authorised to unmutes an instance on this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/unmuteInstance', + authorized: true, + }, + { + privilege: 'myType/myApp/unmuteInstance', + authorized: false, + }, + ], + }); + + await alertsClientWithAuthorization.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'unmuteInstance' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'unmuteInstance' + ); + }); + + test('throws when user is not authorised to unmutes an instance on this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/unmuteInstance', + authorized: false, + }, + { + privilege: 'myType/myApp/unmuteInstance', + authorized: false, + }, + ], + }); + + expect( + alertsClientWithAuthorization.unmuteInstance({ alertId: '1', alertInstanceId: '2' }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to unmuteInstance a "myType" alert for "myApp"]` + ); + }); + }); }); describe('get()', () => { @@ -1541,6 +2399,121 @@ describe('get()', () => { `"Reference action_0 not found"` ); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + }); + + function tryToExecuteOperation(): Promise { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + return alertsClientWithAuthorization.get({ id: '1' }); + } + + test('gets when user is authorised to get this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/get', + authorized: false, + }, + { + privilege: 'myType/myApp/get', + authorized: true, + }, + ], + }); + + await tryToExecuteOperation(); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); + }); + + test('gets when user is authorised to get this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/get', + authorized: true, + }, + { + privilege: 'myType/myApp/get', + authorized: false, + }, + ], + }); + + await tryToExecuteOperation(); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); + }); + + test('throws when user is not authorised to get this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/get', + authorized: false, + }, + { + privilege: 'myType/myApp/get', + authorized: false, + }, + ], + }); + + await expect(tryToExecuteOperation()).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "myType" alert for "myApp"]` + ); + }); + }); }); describe('getAlertState()', () => { @@ -1655,10 +2628,137 @@ describe('getAlertState()', () => { expect(taskManager.get).toHaveBeenCalledTimes(1); expect(taskManager.get).toHaveBeenCalledWith(scheduledTaskId); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + }); + + function tryToExecuteOperation(): Promise { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + return alertsClientWithAuthorization.getAlertState({ id: '1' }); + } + + test('gets AlertState when user is authorised to get this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/get', + authorized: false, + }, + { + privilege: 'myType/myApp/get', + authorized: true, + }, + ], + }); + + await tryToExecuteOperation(); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); + }); + + test('gets AlertState when user is authorised to get this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/get', + authorized: true, + }, + { + privilege: 'myType/myApp/get', + authorized: false, + }, + ], + }); + + await tryToExecuteOperation(); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); + }); + + test('throws when user is not authorised to get this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/get', + authorized: false, + }, + { + privilege: 'myType/myApp/get', + authorized: false, + }, + ], + }); + + await expect(tryToExecuteOperation()).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "myType" alert for "myApp"]` + ); + }); + }); }); describe('find()', () => { test('calls saved objects client with given params', async () => { + alertTypeRegistry.list.mockReturnValue( + new Set([ + { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myType', + name: 'myType', + producer: 'myApp', + }, + ]) + ); const alertsClient = new AlertsClient(alertsClientParams); unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ total: 1, @@ -1669,7 +2769,7 @@ describe('find()', () => { id: '1', type: 'alert', attributes: { - alertTypeId: '123', + alertTypeId: 'myType', schedule: { interval: '10s' }, params: { bar: true, @@ -1708,7 +2808,7 @@ describe('find()', () => { }, }, ], - "alertTypeId": "123", + "alertTypeId": "myType", "createdAt": 2019-02-12T21:01:22.479Z, "id": "1", "params": Object { @@ -1720,19 +2820,207 @@ describe('find()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, }, ], - "page": 1, - "perPage": 10, - "total": 1, - } - `); - expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "type": "alert", - }, - ] - `); + "page": 1, + "perPage": 10, + "total": 1, + } + `); + expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "filter": "alert.attributes.alertTypeId:(myType)", + "type": "alert", + }, + ] + `); + }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + function mockAlertSavedObject(alertTypeId: string) { + return { + id: uuid.v4(), + type: 'alert', + attributes: { + alertTypeId, + schedule: { interval: '10s' }, + params: {}, + actions: [], + }, + references: [], + }; + } + + beforeEach(() => { + authorization = mockAuthorization(); + + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + const myType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myType', + name: 'myType', + producer: 'myApp', + }; + const anUnauthorizedType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'anUnauthorizedType', + name: 'anUnauthorizedType', + producer: 'anUnauthorizedApp', + }; + const setOfAlertTypes = new Set([anUnauthorizedType, myType]); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + }); + + function tryToExecuteOperation( + options?: FindOptions['options'], + savedObjects: Array> = [ + mockAlertSavedObject('myType'), + ] + ): Promise { + unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ + total: 1, + per_page: 10, + page: 1, + saved_objects: savedObjects, + }); + return alertsClientWithAuthorization.find({ options }); + } + + test('includes types that a user is authorised to find under their producer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/find', + authorized: false, + }, + { + privilege: 'myType/myApp/find', + authorized: true, + }, + { + privilege: 'anUnauthorizedType/find', + authorized: false, + }, + { + privilege: 'anUnauthorizedType/anUnauthorizedApp/find', + authorized: false, + }, + ], + }); + + await tryToExecuteOperation(); + + expect(unsecuredSavedObjectsClient.find.mock.calls[0][0].filter).toMatchInlineSnapshot( + `"alert.attributes.alertTypeId:(myType)"` + ); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'find'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'find'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'anUnauthorizedType', + undefined, + 'find' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'anUnauthorizedType', + 'anUnauthorizedApp', + 'find' + ); + }); + + test('includes types that a user is authorised to get globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/find', + authorized: true, + }, + { + privilege: 'myType/myApp/find', + authorized: false, + }, + { + privilege: 'anUnauthorizedType/find', + authorized: false, + }, + { + privilege: 'anUnauthorizedType/anUnauthorizedApp/find', + authorized: false, + }, + ], + }); + + await tryToExecuteOperation(); + + expect(unsecuredSavedObjectsClient.find.mock.calls[0][0].filter).toMatchInlineSnapshot( + `"alert.attributes.alertTypeId:(myType)"` + ); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'find'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'find'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'anUnauthorizedType', + undefined, + 'find' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'anUnauthorizedType', + 'anUnauthorizedApp', + 'find' + ); + }); + + test('throws if a result contains a type the user is not authorised to find', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/find', + authorized: true, + }, + { + privilege: 'myType/myApp/find', + authorized: true, + }, + { + privilege: 'anUnauthorizedType/find', + authorized: false, + }, + { + privilege: 'anUnauthorizedType/anUnauthorizedApp/find', + authorized: false, + }, + ], + }); + + await expect( + tryToExecuteOperation({}, [ + mockAlertSavedObject('myType'), + mockAlertSavedObject('anUnauthorizedType'), + ]) + ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to find "anUnauthorizedType" alerts]`); + }); }); }); @@ -1742,7 +3030,8 @@ describe('delete()', () => { id: '1', type: 'alert', attributes: { - alertTypeId: '123', + alertTypeId: 'myType', + consumer: 'myApp', schedule: { interval: '10s' }, params: { bar: true, @@ -1871,6 +3160,97 @@ describe('delete()', () => { `"TM Fail"` ); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + }); + + test('deletes when user is authorised to delete this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/delete', + authorized: false, + }, + { + privilege: 'myType/myApp/delete', + authorized: true, + }, + ], + }); + + await alertsClientWithAuthorization.delete({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'delete' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'delete'); + }); + + test('deletes when user is authorised to delete this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/delete', + authorized: true, + }, + { + privilege: 'myType/myApp/delete', + authorized: false, + }, + ], + }); + + await alertsClientWithAuthorization.delete({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'delete' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'delete'); + }); + + test('throws when user is not authorised to delete this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/delete', + authorized: false, + }, + { + privilege: 'myType/myApp/delete', + authorized: false, + }, + ], + }); + + expect(alertsClientWithAuthorization.delete({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to delete a "myType" alert for "myApp"]` + ); + }); + }); }); describe('update()', () => { @@ -1880,7 +3260,8 @@ describe('update()', () => { type: 'alert', attributes: { enabled: true, - alertTypeId: '123', + alertTypeId: 'myType', + consumer: 'myApp', scheduledTaskId: 'task-123', }, references: [], @@ -2098,9 +3479,10 @@ describe('update()', () => { }, }, ], - "alertTypeId": "123", + "alertTypeId": "myType", "apiKey": null, "apiKeyOwner": null, + "consumer": "myApp", "enabled": true, "name": "abc", "params": Object { @@ -2253,9 +3635,10 @@ describe('update()', () => { }, }, ], - "alertTypeId": "123", + "alertTypeId": "myType", "apiKey": "MTIzOmFiYw==", "apiKeyOwner": "elastic", + "consumer": "myApp", "enabled": true, "name": "abc", "params": Object { @@ -2402,9 +3785,10 @@ describe('update()', () => { }, }, ], - "alertTypeId": "123", + "alertTypeId": "myType", "apiKey": null, "apiKeyOwner": null, + "consumer": "myApp", "enabled": false, "name": "abc", "params": Object { @@ -2888,6 +4272,206 @@ describe('update()', () => { ); }); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + }); + + function tryToExecuteOperation(options: UpdateOptions): Promise { + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + actionTypeId: 'test', + }, + references: [], + }, + { + id: '2', + type: 'action', + attributes: { + actionTypeId: 'test2', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: true, + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_1', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_2', + actionTypeId: 'test2', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: 'task-123', + createdAt: new Date().toISOString(), + }, + updated_at: new Date().toISOString(), + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + { + name: 'action_1', + type: 'action', + id: '1', + }, + { + name: 'action_2', + type: 'action', + id: '2', + }, + ], + }); + return alertsClientWithAuthorization.update(options); + } + + test('updates when user is authorised to update this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/update', + authorized: false, + }, + { + privilege: 'myType/myApp/update', + authorized: true, + }, + ], + }); + + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myApp', + }); + + await tryToExecuteOperation({ + id: '1', + data, + }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'update' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'update'); + }); + + test('updates when user is authorised to update this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/update', + authorized: true, + }, + { + privilege: 'myType/myApp/update', + authorized: false, + }, + ], + }); + + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myApp', + }); + + await tryToExecuteOperation({ + id: '1', + data, + }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'update' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'update'); + }); + + test('throws when user is not authorised to update this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/update', + authorized: false, + }, + { + privilege: 'myType/myApp/update', + authorized: false, + }, + ], + }); + + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myApp', + }); + + await expect( + tryToExecuteOperation({ + id: '1', + data, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to update a "myType" alert for "myApp"]` + ); + }); + }); }); describe('updateApiKey()', () => { @@ -2897,7 +4481,8 @@ describe('updateApiKey()', () => { type: 'alert', attributes: { schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', + consumer: 'myApp', enabled: true, }, version: '123', @@ -2932,7 +4517,8 @@ describe('updateApiKey()', () => { '1', { schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', + consumer: 'myApp', enabled: true, apiKey: Buffer.from('234:abc').toString('base64'), apiKeyOwner: 'elastic', @@ -2956,7 +4542,8 @@ describe('updateApiKey()', () => { '1', { schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', + consumer: 'myApp', enabled: true, apiKey: Buffer.from('234:abc').toString('base64'), apiKeyOwner: 'elastic', @@ -2996,4 +4583,205 @@ describe('updateApiKey()', () => { ); expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + }); + + test('updates when user is authorised to updateApiKey this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/updateApiKey', + authorized: false, + }, + { + privilege: 'myType/myApp/updateApiKey', + authorized: true, + }, + ], + }); + + await alertsClientWithAuthorization.updateApiKey({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'updateApiKey' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'updateApiKey' + ); + }); + + test('updates when user is authorised to updateApiKey this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/updateApiKey', + authorized: true, + }, + { + privilege: 'myType/myApp/updateApiKey', + authorized: false, + }, + ], + }); + + await alertsClientWithAuthorization.updateApiKey({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'updateApiKey' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'updateApiKey' + ); + }); + + test('throws when user is not authorised to updateApiKey this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/updateApiKey', + authorized: false, + }, + { + privilege: 'myType/myApp/updateApiKey', + authorized: false, + }, + ], + }); + + expect(alertsClientWithAuthorization.updateApiKey({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to updateApiKey a "myType" alert for "myApp"]` + ); + }); + }); +}); + +describe('listAlertTypes', () => { + let alertsClient: AlertsClient; + const alertingAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'alertingAlertType', + name: 'alertingAlertType', + producer: 'alerting', + }; + const myAppAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + }; + const setOfAlertTypes = new Set([myAppAlertType, alertingAlertType]); + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + }); + + test('should return a list of AlertTypes that exist in the registry', async () => { + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + expect(await alertsClient.listAlertTypes()).toEqual(setOfAlertTypes); + }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + }); + + test('should return a list of AlertTypes that exist in the registry only if the user is authorised to get them', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myAppAlertType/get', + authorized: false, + }, + { + privilege: 'myAppAlertType/alerting/get', + authorized: false, + }, + { + privilege: 'alertingAlertType/get', + authorized: true, + }, + { + privilege: 'alertingAlertType/alerting/get', + authorized: true, + }, + ], + }); + + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + expect(await alertsClientWithAuthorization.listAlertTypes()).toEqual( + new Set([alertingAlertType]) + ); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myAppAlertType', + 'myApp', + 'get' + ); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myAppAlertType', + undefined, + 'get' + ); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'alertingAlertType', + 'alerting', + 'get' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'alertingAlertType', + undefined, + 'get' + ); + }); + }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client.ts index 3149f11bedabf..bf4305998f5c5 100644 --- a/x-pack/plugins/alerting/server/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client.ts @@ -38,6 +38,8 @@ import { EncryptedSavedObjectsClient } from '../../../plugins/encrypted_saved_ob import { TaskManagerStartContract } from '../../../plugins/task_manager/server'; import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists'; +import { CheckPrivilegesResponse } from '../../security/server'; +import { RegistryAlertType } from './alert_type_registry'; type NormalizedAlertAction = Omit; export type CreateAPIKeyResult = @@ -107,7 +109,7 @@ export interface CreateOptions { }; } -interface UpdateOptions { +export interface UpdateOptions { id: string; data: { name: string; @@ -126,8 +128,8 @@ export class AlertsClient { private readonly namespace?: string; private readonly taskManager: TaskManagerStartContract; private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; - // private readonly request: KibanaRequest; - // private readonly authorization?: SecurityPluginSetup['authz']; + private readonly request: KibanaRequest; + private readonly authorization?: SecurityPluginSetup['authz']; private readonly alertTypeRegistry: AlertTypeRegistry; private readonly createAPIKey: () => Promise; private readonly invalidateAPIKey: ( @@ -139,8 +141,8 @@ export class AlertsClient { constructor({ alertTypeRegistry, unsecuredSavedObjectsClient, - // request, - // authorization, + request, + authorization, taskManager, logger, spaceId, @@ -158,8 +160,8 @@ export class AlertsClient { this.taskManager = taskManager; this.alertTypeRegistry = alertTypeRegistry; this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; - // this.request = request; - // this.authorization = authorization; + this.request = request; + this.authorization = authorization; this.createAPIKey = createAPIKey; this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; @@ -168,6 +170,7 @@ export class AlertsClient { public async create({ data, options }: CreateOptions): Promise { // Throws an error if alert type isn't registered + await this.ensureAuthorized(data.alertTypeId, data.consumer, 'create'); const alertType = this.alertTypeRegistry.get(data.alertTypeId); const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); const username = await this.getUserName(); @@ -222,6 +225,7 @@ export class AlertsClient { public async get({ id }: { id: string }): Promise { const result = await this.unsecuredSavedObjectsClient.get('alert', id); + await this.ensureAuthorized(result.attributes.alertTypeId, result.attributes.consumer, 'get'); return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references); } @@ -236,7 +240,28 @@ export class AlertsClient { } } - public async find({ options = {} }: FindOptions = {}): Promise { + public async find({ options: { filter, ...options } = {} }: FindOptions = {}): Promise< + FindResult + > { + const filters = filter ? [filter] : []; + + const authorizedAlertTypes = new Set( + pluck([...(await this.filterByAuthorized(this.alertTypeRegistry.list(), 'find'))], 'id') + ); + + if (!authorizedAlertTypes.size) { + // the current user isn't authorized to get any alertTypes + // we can short circuit here + return { + page: 0, + perPage: 0, + total: 0, + data: [], + }; + } + + filters.push(`alert.attributes.alertTypeId:(${[...authorizedAlertTypes].join(' or ')})`); + const { page, per_page: perPage, @@ -244,6 +269,7 @@ export class AlertsClient { saved_objects: data, } = await this.unsecuredSavedObjectsClient.find({ ...options, + filter: filters.join(` and `), type: 'alert', }); @@ -251,15 +277,19 @@ export class AlertsClient { page, perPage, total, - data: data.map(({ id, attributes, updated_at, references }) => - this.getAlertFromRaw(id, attributes, updated_at, references) - ), + data: data.map(({ id, attributes, updated_at, references }) => { + if (!authorizedAlertTypes.has(attributes.alertTypeId)) { + throw Boom.forbidden(`Unauthorized to find "${attributes.alertTypeId}" alerts`); + } + return this.getAlertFromRaw(id, attributes, updated_at, references); + }), }; } public async delete({ id }: { id: string }) { let taskIdToRemove: string | undefined; let apiKeyToInvalidate: string | null = null; + let attributes: RawAlert; try { const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser< @@ -267,6 +297,7 @@ export class AlertsClient { >('alert', id, { namespace: this.namespace }); apiKeyToInvalidate = decryptedAlert.attributes.apiKey; taskIdToRemove = decryptedAlert.attributes.scheduledTaskId; + attributes = decryptedAlert.attributes; } catch (e) { // We'll skip invalidating the API key since we failed to load the decrypted saved object this.logger.error( @@ -275,8 +306,11 @@ export class AlertsClient { // Still attempt to load the scheduledTaskId using SOC const alert = await this.unsecuredSavedObjectsClient.get('alert', id); taskIdToRemove = alert.attributes.scheduledTaskId; + attributes = alert.attributes; } + await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'delete'); + const removeResult = await this.unsecuredSavedObjectsClient.delete('alert', id); await Promise.all([ @@ -302,6 +336,11 @@ export class AlertsClient { // Still attempt to load the object using SOC alertSavedObject = await this.unsecuredSavedObjectsClient.get('alert', id); } + await this.ensureAuthorized( + alertSavedObject.attributes.alertTypeId, + alertSavedObject.attributes.consumer, + 'update' + ); const updateResult = await this.updateAlert({ id, data }, alertSavedObject); @@ -403,6 +442,7 @@ export class AlertsClient { attributes = alert.attributes; version = alert.version; } + await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'updateApiKey'); const username = await this.getUserName(); await this.unsecuredSavedObjectsClient.update( @@ -462,6 +502,8 @@ export class AlertsClient { version = alert.version; } + await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'enable'); + if (attributes.enabled === false) { const username = await this.getUserName(); await this.unsecuredSavedObjectsClient.update( @@ -508,6 +550,8 @@ export class AlertsClient { version = alert.version; } + await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'disable'); + if (attributes.enabled === true) { await this.unsecuredSavedObjectsClient.update( 'alert', @@ -533,6 +577,9 @@ export class AlertsClient { } public async muteAll({ id }: { id: string }) { + const { attributes } = await this.unsecuredSavedObjectsClient.get('alert', id); + await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'muteAll'); + await this.unsecuredSavedObjectsClient.update('alert', id, { muteAll: true, mutedInstanceIds: [], @@ -541,6 +588,9 @@ export class AlertsClient { } public async unmuteAll({ id }: { id: string }) { + const { attributes } = await this.unsecuredSavedObjectsClient.get('alert', id); + await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'unmuteAll'); + await this.unsecuredSavedObjectsClient.update('alert', id, { muteAll: false, mutedInstanceIds: [], @@ -559,6 +609,9 @@ export class AlertsClient { 'alert', alertId ); + + await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'muteInstance'); + const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) { mutedInstanceIds.push(alertInstanceId); @@ -585,6 +638,7 @@ export class AlertsClient { 'alert', alertId ); + await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'unmuteInstance'); const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { await this.unsecuredSavedObjectsClient.update( @@ -600,18 +654,72 @@ export class AlertsClient { } } - // private async ensureAuthorized(alertTypeId: string, operation: string) { - // if (this.authorization == null) { - // return; - // } - // const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest(this.request); - // const { hasAllRequested } = await checkPrivileges( - // this.authorization.actions.savedObject.get(alertTypeId, operation) - // ); - // if (!hasAllRequested) { - // throw Boom.forbidden(`Unable to ${operation} ${alertTypeId}`); - // } - // } + public async listAlertTypes() { + return await this.filterByAuthorized(this.alertTypeRegistry.list(), 'get'); + } + + private async ensureAuthorized(alertTypeId: string, consumer: string, operation: string) { + if (this.authorization) { + const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( + this.request + ); + if ( + !this.hasAnyPrivilege( + await checkPrivileges([ + // check for global access + this.authorization.actions.alerting.get(alertTypeId, undefined, operation), + // check for access at consumer level + this.authorization.actions.alerting.get(alertTypeId, consumer, operation), + ]) + ) + ) { + throw Boom.forbidden( + `Unauthorized to ${operation} a "${alertTypeId}" alert for "${consumer}"` + ); + } + } + } + + private async filterByAuthorized( + alertTypes: Set, + operation: string + ): Promise> { + if (!this.authorization) { + return alertTypes; + } + + const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest(this.request); + + const privilegeToAlertType = Array.from(alertTypes).reduce((privileges, alertType) => { + // check for global access + privileges.set( + this.authorization!.actions.alerting.get(alertType.id, undefined, operation), + alertType + ); + // check for access within the producer level + privileges.set( + this.authorization!.actions.alerting.get(alertType.id, alertType.producer, operation), + alertType + ); + return privileges; + }, new Map()); + const { hasAllRequested, privileges } = await checkPrivileges([...privilegeToAlertType.keys()]); + return hasAllRequested + ? alertTypes + : privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { + if (authorized && privilegeToAlertType.has(privilege)) { + authorizedAlertTypes.add(privilegeToAlertType.get(privilege)!); + } + return authorizedAlertTypes; + }, new Set()); + } + + private hasAnyPrivilege(checkPrivilegesResponse: CheckPrivilegesResponse): boolean { + return ( + checkPrivilegesResponse.hasAllRequested || + checkPrivilegesResponse.privileges.some(({ authorized }) => authorized) + ); + } private async scheduleAlert(id: string, alertTypeId: string) { return await this.taskManager.schedule({ diff --git a/x-pack/plugins/alerting/server/routes/create.test.ts b/x-pack/plugins/alerting/server/routes/create.test.ts index a4910495c8a40..5e0dce6491055 100644 --- a/x-pack/plugins/alerting/server/routes/create.test.ts +++ b/x-pack/plugins/alerting/server/routes/create.test.ts @@ -75,13 +75,6 @@ describe('createAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alert"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.create.mockResolvedValueOnce(createResult); diff --git a/x-pack/plugins/alerting/server/routes/create.ts b/x-pack/plugins/alerting/server/routes/create.ts index 0c038b6490483..82f4c586248c9 100644 --- a/x-pack/plugins/alerting/server/routes/create.ts +++ b/x-pack/plugins/alerting/server/routes/create.ts @@ -47,9 +47,6 @@ export const createAlertRoute = (router: IRouter, licenseState: LicenseState) => validate: { body: bodySchema, }, - options: { - tags: ['access:alerting-all'], - }, }, handleDisabledApiKeysError( router.handleLegacyErrors(async function( diff --git a/x-pack/plugins/alerting/server/routes/delete.test.ts b/x-pack/plugins/alerting/server/routes/delete.test.ts index 416628d015b5a..a2fa221f87098 100644 --- a/x-pack/plugins/alerting/server/routes/delete.test.ts +++ b/x-pack/plugins/alerting/server/routes/delete.test.ts @@ -30,13 +30,6 @@ describe('deleteAlertRoute', () => { const [config, handler] = router.delete.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.delete.mockResolvedValueOnce({}); diff --git a/x-pack/plugins/alerting/server/routes/delete.ts b/x-pack/plugins/alerting/server/routes/delete.ts index 7f6600b1ec48e..12c8d52e200a5 100644 --- a/x-pack/plugins/alerting/server/routes/delete.ts +++ b/x-pack/plugins/alerting/server/routes/delete.ts @@ -27,9 +27,6 @@ export const deleteAlertRoute = (router: IRouter, licenseState: LicenseState) => validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, router.handleLegacyErrors(async function( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerting/server/routes/disable.test.ts b/x-pack/plugins/alerting/server/routes/disable.test.ts index fde095e9145b6..622923594949d 100644 --- a/x-pack/plugins/alerting/server/routes/disable.test.ts +++ b/x-pack/plugins/alerting/server/routes/disable.test.ts @@ -30,13 +30,6 @@ describe('disableAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/_disable"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.disable.mockResolvedValueOnce(); diff --git a/x-pack/plugins/alerting/server/routes/disable.ts b/x-pack/plugins/alerting/server/routes/disable.ts index c7e7b1001f82d..c294dde42a94d 100644 --- a/x-pack/plugins/alerting/server/routes/disable.ts +++ b/x-pack/plugins/alerting/server/routes/disable.ts @@ -27,9 +27,6 @@ export const disableAlertRoute = (router: IRouter, licenseState: LicenseState) = validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, router.handleLegacyErrors(async function( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerting/server/routes/enable.test.ts b/x-pack/plugins/alerting/server/routes/enable.test.ts index e4e89e3f06380..5ea017092ef9e 100644 --- a/x-pack/plugins/alerting/server/routes/enable.test.ts +++ b/x-pack/plugins/alerting/server/routes/enable.test.ts @@ -29,13 +29,6 @@ describe('enableAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/_enable"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.enable.mockResolvedValueOnce(); diff --git a/x-pack/plugins/alerting/server/routes/enable.ts b/x-pack/plugins/alerting/server/routes/enable.ts index 3ed4fb0739d3d..65a42c17c7980 100644 --- a/x-pack/plugins/alerting/server/routes/enable.ts +++ b/x-pack/plugins/alerting/server/routes/enable.ts @@ -28,9 +28,6 @@ export const enableAlertRoute = (router: IRouter, licenseState: LicenseState) => validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, handleDisabledApiKeysError( router.handleLegacyErrors(async function( diff --git a/x-pack/plugins/alerting/server/routes/find.test.ts b/x-pack/plugins/alerting/server/routes/find.test.ts index cc601bd42b8ca..bf00623f83a0b 100644 --- a/x-pack/plugins/alerting/server/routes/find.test.ts +++ b/x-pack/plugins/alerting/server/routes/find.test.ts @@ -31,13 +31,6 @@ describe('findAlertRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alert/_find"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-read", - ], - } - `); const findResult = { page: 1, diff --git a/x-pack/plugins/alerting/server/routes/find.ts b/x-pack/plugins/alerting/server/routes/find.ts index c723419a965c5..3b6b7ade7c10e 100644 --- a/x-pack/plugins/alerting/server/routes/find.ts +++ b/x-pack/plugins/alerting/server/routes/find.ts @@ -49,9 +49,6 @@ export const findAlertRoute = (router: IRouter, licenseState: LicenseState) => { validate: { query: querySchema, }, - options: { - tags: ['access:alerting-read'], - }, }, router.handleLegacyErrors(async function( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerting/server/routes/get.test.ts b/x-pack/plugins/alerting/server/routes/get.test.ts index 7335f13c85a4d..50df4ae5d90af 100644 --- a/x-pack/plugins/alerting/server/routes/get.test.ts +++ b/x-pack/plugins/alerting/server/routes/get.test.ts @@ -61,13 +61,6 @@ describe('getAlertRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-read", - ], - } - `); alertsClient.get.mockResolvedValueOnce(mockedAlert); diff --git a/x-pack/plugins/alerting/server/routes/get.ts b/x-pack/plugins/alerting/server/routes/get.ts index 6d652d1304f65..a000c9a03226d 100644 --- a/x-pack/plugins/alerting/server/routes/get.ts +++ b/x-pack/plugins/alerting/server/routes/get.ts @@ -27,9 +27,6 @@ export const getAlertRoute = (router: IRouter, licenseState: LicenseState) => { validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-read'], - }, }, router.handleLegacyErrors(async function( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts b/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts index 20a420ca00986..a44dd81800c0d 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts @@ -48,13 +48,6 @@ describe('getAlertStateRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/state"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-read", - ], - } - `); alertsClient.getAlertState.mockResolvedValueOnce(mockedAlertState); diff --git a/x-pack/plugins/alerting/server/routes/get_alert_state.ts b/x-pack/plugins/alerting/server/routes/get_alert_state.ts index 552bfea22a42b..9c14570aa9cf9 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_state.ts +++ b/x-pack/plugins/alerting/server/routes/get_alert_state.ts @@ -27,9 +27,6 @@ export const getAlertStateRoute = (router: IRouter, licenseState: LicenseState) validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-read'], - }, }, router.handleLegacyErrors(async function( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts index e940b2d102045..81d5cff70450c 100644 --- a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts +++ b/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts @@ -28,13 +28,6 @@ describe('listAlertTypesRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alert/types"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-read", - ], - } - `); const listTypes = [ { diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.ts b/x-pack/plugins/alerting/server/routes/list_alert_types.ts index 7ab64cf932051..032d882afe25c 100644 --- a/x-pack/plugins/alerting/server/routes/list_alert_types.ts +++ b/x-pack/plugins/alerting/server/routes/list_alert_types.ts @@ -20,9 +20,6 @@ export const listAlertTypesRoute = (router: IRouter, licenseState: LicenseState) { path: `${BASE_ALERT_API_PATH}/types`, validate: {}, - options: { - tags: ['access:alerting-read'], - }, }, router.handleLegacyErrors(async function( context: RequestHandlerContext, @@ -34,7 +31,7 @@ export const listAlertTypesRoute = (router: IRouter, licenseState: LicenseState) return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); } return res.ok({ - body: context.alerting.listTypes(), + body: Array.from(await context.alerting.getAlertsClient().listAlertTypes()), }); }) ); diff --git a/x-pack/plugins/alerting/server/routes/mute_all.test.ts b/x-pack/plugins/alerting/server/routes/mute_all.test.ts index 5ef9e3694f8f4..cda2519582538 100644 --- a/x-pack/plugins/alerting/server/routes/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/routes/mute_all.test.ts @@ -29,13 +29,6 @@ describe('muteAllAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/_mute_all"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.muteAll.mockResolvedValueOnce(); diff --git a/x-pack/plugins/alerting/server/routes/mute_all.ts b/x-pack/plugins/alerting/server/routes/mute_all.ts index d1b4322bd1ccb..2c1ff65aaf15d 100644 --- a/x-pack/plugins/alerting/server/routes/mute_all.ts +++ b/x-pack/plugins/alerting/server/routes/mute_all.ts @@ -27,9 +27,6 @@ export const muteAllAlertRoute = (router: IRouter, licenseState: LicenseState) = validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, router.handleLegacyErrors(async function( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts b/x-pack/plugins/alerting/server/routes/mute_instance.test.ts index 2e6adedb76df9..1643312e5a098 100644 --- a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts +++ b/x-pack/plugins/alerting/server/routes/mute_instance.test.ts @@ -31,13 +31,6 @@ describe('muteAlertInstanceRoute', () => { expect(config.path).toMatchInlineSnapshot( `"/api/alert/{alertId}/alert_instance/{alertInstanceId}/_mute"` ); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.muteInstance.mockResolvedValueOnce(); diff --git a/x-pack/plugins/alerting/server/routes/mute_instance.ts b/x-pack/plugins/alerting/server/routes/mute_instance.ts index fbdda62836d74..bf96724b5ca6f 100644 --- a/x-pack/plugins/alerting/server/routes/mute_instance.ts +++ b/x-pack/plugins/alerting/server/routes/mute_instance.ts @@ -28,9 +28,6 @@ export const muteAlertInstanceRoute = (router: IRouter, licenseState: LicenseSta validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, router.handleLegacyErrors(async function( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts b/x-pack/plugins/alerting/server/routes/unmute_all.test.ts index 1756dbd3fb41d..e775fe51ddfe4 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_all.test.ts @@ -28,13 +28,6 @@ describe('unmuteAllAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/_unmute_all"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.unmuteAll.mockResolvedValueOnce(); diff --git a/x-pack/plugins/alerting/server/routes/unmute_all.ts b/x-pack/plugins/alerting/server/routes/unmute_all.ts index e09f2fe6b8b93..c252152385165 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_all.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_all.ts @@ -27,9 +27,6 @@ export const unmuteAllAlertRoute = (router: IRouter, licenseState: LicenseState) validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, router.handleLegacyErrors(async function( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts b/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts index 9b9542c606741..be4f7b85a3f34 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts @@ -31,13 +31,6 @@ describe('unmuteAlertInstanceRoute', () => { expect(config.path).toMatchInlineSnapshot( `"/api/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute"` ); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.unmuteInstance.mockResolvedValueOnce(); diff --git a/x-pack/plugins/alerting/server/routes/unmute_instance.ts b/x-pack/plugins/alerting/server/routes/unmute_instance.ts index 64ba22dc3ea0b..3df4bdd583a57 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_instance.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_instance.ts @@ -28,9 +28,6 @@ export const unmuteAlertInstanceRoute = (router: IRouter, licenseState: LicenseS validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, router.handleLegacyErrors(async function( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerting/server/routes/update.test.ts b/x-pack/plugins/alerting/server/routes/update.test.ts index cd96f289b8714..bf93ffb155acf 100644 --- a/x-pack/plugins/alerting/server/routes/update.test.ts +++ b/x-pack/plugins/alerting/server/routes/update.test.ts @@ -52,13 +52,6 @@ describe('updateAlertRoute', () => { const [config, handler] = router.put.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.update.mockResolvedValueOnce(mockedResponse); diff --git a/x-pack/plugins/alerting/server/routes/update.ts b/x-pack/plugins/alerting/server/routes/update.ts index 7f07749311598..a621d265af97f 100644 --- a/x-pack/plugins/alerting/server/routes/update.ts +++ b/x-pack/plugins/alerting/server/routes/update.ts @@ -49,9 +49,6 @@ export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) => body: bodySchema, params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, handleDisabledApiKeysError( router.handleLegacyErrors(async function( diff --git a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts b/x-pack/plugins/alerting/server/routes/update_api_key.test.ts index 0347feb24a235..ea713d24ed114 100644 --- a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/routes/update_api_key.test.ts @@ -29,13 +29,6 @@ describe('updateApiKeyRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alert/{id}/_update_api_key"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.updateApiKey.mockResolvedValueOnce(); diff --git a/x-pack/plugins/alerting/server/routes/update_api_key.ts b/x-pack/plugins/alerting/server/routes/update_api_key.ts index 9d0c34fc1a015..2c9a5b58f6283 100644 --- a/x-pack/plugins/alerting/server/routes/update_api_key.ts +++ b/x-pack/plugins/alerting/server/routes/update_api_key.ts @@ -28,9 +28,6 @@ export const updateApiKeyRoute = (router: IRouter, licenseState: LicenseState) = validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, handleDisabledApiKeysError( router.handleLegacyErrors(async function( diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index ae0f5510cd80e..92a4466b69b66 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { AlertType } from '../common/alert_types.ts'; export const APM_FEATURE = { id: 'apm', @@ -20,19 +21,15 @@ export const APM_FEATURE = { privileges: { all: { app: ['apm', 'kibana'], - api: [ - 'apm', - 'apm_write', - 'actions-read', - 'actions-all', - 'alerting-read', - 'alerting-all' - ], + api: ['apm', 'apm_write', 'actions-read', 'actions-all'], catalogue: ['apm'], savedObject: { all: ['alert', 'action', 'action_task_params'], read: [] }, + alerting: { + all: Object.values(AlertType) + }, ui: [ 'show', 'save', @@ -46,18 +43,15 @@ export const APM_FEATURE = { }, read: { app: ['apm', 'kibana'], - api: [ - 'apm', - 'actions-read', - 'actions-all', - 'alerting-read', - 'alerting-all' - ], + api: ['apm', 'actions-read', 'actions-all'], catalogue: ['apm'], savedObject: { all: ['alert', 'action', 'action_task_params'], read: [] }, + alerting: { + all: Object.values(AlertType) + }, ui: [ 'show', 'alerting:show', diff --git a/x-pack/plugins/features/common/feature_kibana_privileges.ts b/x-pack/plugins/features/common/feature_kibana_privileges.ts index 768c8c6ae1088..c642f3e5b6fd4 100644 --- a/x-pack/plugins/features/common/feature_kibana_privileges.ts +++ b/x-pack/plugins/features/common/feature_kibana_privileges.ts @@ -75,6 +75,59 @@ export interface FeatureKibanaPrivileges { */ app?: string[]; + /** + * If your feature registers its own Alert types you may specify the access privileges for them here. + */ + alerting?: { + /** + * List of alert types which users should have full read/write access to within the feature. + * @example + * ```ts + * { + * all: ['my-alert-type-within-my-feature'] + * } + * ``` + */ + all?: string[]; + + /** + * List of alert types which users should have read-only access to from within the feature. + * @example + * ```ts + * { + * read: ['my-alert-type'] + * } + * ``` + */ + read?: string[]; + + /** + * If your feature registers its own Alert types you may specify global access privileges for them here. + */ + globally?: { + /** + * List of alert types types which users should have full read/write access to throughout kibana. + * @example + * ```ts + * { + * all: ['my-alert-type-globally-available'] + * } + * ``` + */ + all?: string[]; + + /** + * List of alert types which users should have read-only access to throughout kibana. + * @example + * ```ts + * { + * read: ['my-alert-type'] + * } + * ``` + */ + read?: string[]; + }; + }; /** * If your feature requires access to specific saved objects, then specify your access needs here. */ diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 403d9586bf160..16361ddef605f 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -33,6 +33,14 @@ const privilegeSchema = Joi.object({ catalogue: catalogueSchema, api: Joi.array().items(Joi.string()), app: Joi.array().items(Joi.string()), + alerting: Joi.object({ + all: Joi.array().items(Joi.string()), + read: Joi.array().items(Joi.string()), + globally: Joi.object({ + all: Joi.array().items(Joi.string()), + read: Joi.array().items(Joi.string()), + }), + }), savedObject: Joi.object({ all: Joi.array() .items(Joi.string()) diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index fa228e03194a9..00dc2c9ac1b62 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -5,6 +5,9 @@ */ import { i18n } from '@kbn/i18n'; +import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../common/alerting/logs/types'; +import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/inventory_metric_threshold/types.ts'; +import { METRIC_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/metric_threshold/types.ts'; export const METRICS_FEATURE = { id: 'infrastructure', @@ -20,11 +23,20 @@ export const METRICS_FEATURE = { all: { app: ['infra', 'kibana'], catalogue: ['infraops'], - api: ['infra', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'], + api: ['infra', 'actions-read', 'actions-all'], savedObject: { all: ['infrastructure-ui-source', 'alert', 'action', 'action_task_params'], read: ['index-pattern'], }, + alerting: { + globally: { + all: [ + METRIC_THRESHOLD_ALERT_TYPE_ID, + METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + ], + }, + }, ui: [ 'show', 'configureSource', @@ -40,11 +52,20 @@ export const METRICS_FEATURE = { read: { app: ['infra', 'kibana'], catalogue: ['infraops'], - api: ['infra', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'], + api: ['infra', 'actions-read', 'actions-all'], savedObject: { all: ['alert', 'action', 'action_task_params'], read: ['infrastructure-ui-source', 'index-pattern'], }, + alerting: { + globally: { + all: [ + METRIC_THRESHOLD_ALERT_TYPE_ID, + METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + ], + }, + }, ui: [ 'show', 'alerting:show', diff --git a/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap b/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap new file mode 100644 index 0000000000000..e9cd8bf48a400 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#get alertType of "" throws error 1`] = `"alertTypeId is required and must be a string"`; + +exports[`#get alertType of {} throws error 1`] = `"alertTypeId is required and must be a string"`; + +exports[`#get alertType of 1 throws error 1`] = `"alertTypeId is required and must be a string"`; + +exports[`#get alertType of null throws error 1`] = `"alertTypeId is required and must be a string"`; + +exports[`#get alertType of true throws error 1`] = `"alertTypeId is required and must be a string"`; + +exports[`#get alertType of undefined throws error 1`] = `"alertTypeId is required and must be a string"`; + +exports[`#get consumer of "" throws error 1`] = `"consumer is optional but must be a string when specified"`; + +exports[`#get consumer of {} throws error 1`] = `"consumer is optional but must be a string when specified"`; + +exports[`#get consumer of 1 throws error 1`] = `"consumer is optional but must be a string when specified"`; + +exports[`#get consumer of null throws error 1`] = `"consumer is optional but must be a string when specified"`; + +exports[`#get consumer of true throws error 1`] = `"consumer is optional but must be a string when specified"`; + +exports[`#get operation of "" throws error 1`] = `"operation is required and must be a string"`; + +exports[`#get operation of {} throws error 1`] = `"operation is required and must be a string"`; + +exports[`#get operation of 1 throws error 1`] = `"operation is required and must be a string"`; + +exports[`#get operation of null throws error 1`] = `"operation is required and must be a string"`; + +exports[`#get operation of true throws error 1`] = `"operation is required and must be a string"`; + +exports[`#get operation of undefined throws error 1`] = `"operation is required and must be a string"`; diff --git a/x-pack/plugins/security/server/authorization/actions/actions.mock.ts b/x-pack/plugins/security/server/authorization/actions/actions.mock.ts new file mode 100644 index 0000000000000..f41faaa3dd52c --- /dev/null +++ b/x-pack/plugins/security/server/authorization/actions/actions.mock.ts @@ -0,0 +1,35 @@ +/* + * 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 { ApiActions } from './api'; +import { AppActions } from './app'; +import { SavedObjectActions } from './saved_object'; +import { SpaceActions } from './space'; +import { UIActions } from './ui'; +import { AlertingActions } from './alerting'; +import { Actions } from './actions'; + +jest.mock('./api'); +jest.mock('./app'); +jest.mock('./saved_object'); +jest.mock('./space'); +jest.mock('./ui'); +jest.mock('./alerting'); + +const create = (versionNumber: string) => { + const t = ({ + api: new ApiActions(versionNumber), + app: new AppActions(versionNumber), + login: 'login:', + savedObject: new SavedObjectActions(versionNumber), + alerting: new AlertingActions(versionNumber), + space: new SpaceActions(versionNumber), + ui: new UIActions(versionNumber), + version: `version:${versionNumber}`, + } as unknown) as jest.Mocked; + return t; +}; + +export const actionsMock = { create }; diff --git a/x-pack/plugins/security/server/authorization/actions/actions.ts b/x-pack/plugins/security/server/authorization/actions/actions.ts index 00293e88abe76..34258bdcf972d 100644 --- a/x-pack/plugins/security/server/authorization/actions/actions.ts +++ b/x-pack/plugins/security/server/authorization/actions/actions.ts @@ -9,6 +9,7 @@ import { AppActions } from './app'; import { SavedObjectActions } from './saved_object'; import { SpaceActions } from './space'; import { UIActions } from './ui'; +import { AlertingActions } from './alerting'; /** Actions are used to create the "actions" that are associated with Elasticsearch's * application privileges, and are used to perform the authorization checks implemented @@ -23,6 +24,8 @@ export class Actions { public readonly savedObject = new SavedObjectActions(this.versionNumber); + public readonly alerting = new AlertingActions(this.versionNumber); + public readonly space = new SpaceActions(this.versionNumber); public readonly ui = new UIActions(this.versionNumber); diff --git a/x-pack/plugins/security/server/authorization/actions/alerting.test.ts b/x-pack/plugins/security/server/authorization/actions/alerting.test.ts new file mode 100644 index 0000000000000..fcd1e4aea0628 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/actions/alerting.test.ts @@ -0,0 +1,52 @@ +/* + * 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 { AlertingActions } from './alerting'; + +const version = '1.0.0-zeta1'; + +describe('#get', () => { + [null, undefined, '', 1, true, {}].forEach((alertType: any) => { + test(`alertType of ${JSON.stringify(alertType)} throws error`, () => { + const alertingActions = new AlertingActions(version); + expect(() => + alertingActions.get(alertType, 'consumer', 'foo-action') + ).toThrowErrorMatchingSnapshot(); + }); + }); + + [null, undefined, '', 1, true, {}].forEach((operation: any) => { + test(`operation of ${JSON.stringify(operation)} throws error`, () => { + const alertingActions = new AlertingActions(version); + expect(() => + alertingActions.get('foo-alertType', 'consumer', operation) + ).toThrowErrorMatchingSnapshot(); + }); + }); + + [null, '', 1, true, {}].forEach((consumer: any) => { + test(`consumer of ${JSON.stringify(consumer)} throws error`, () => { + const alertingActions = new AlertingActions(version); + expect(() => + alertingActions.get('foo-alertType', consumer, 'operation') + ).toThrowErrorMatchingSnapshot(); + }); + }); + + test('returns `alerting:${alertType}/${consumer}/${operation}`', () => { + const alertingActions = new AlertingActions(version); + expect(alertingActions.get('foo-alertType', 'consumer', 'bar-operation')).toBe( + 'alerting:1.0.0-zeta1:foo-alertType/consumer/bar-operation' + ); + }); + + test('returns `alerting:${alertType}/${operation}` when no consumer is specified', () => { + const alertingActions = new AlertingActions(version); + expect(alertingActions.get('foo-alertType', undefined, 'bar-operation')).toBe( + 'alerting:1.0.0-zeta1:foo-alertType/bar-operation' + ); + }); +}); diff --git a/x-pack/plugins/security/server/authorization/actions/alerting.ts b/x-pack/plugins/security/server/authorization/actions/alerting.ts new file mode 100644 index 0000000000000..a3e56701a60d3 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/actions/alerting.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 { isString, isUndefined } from 'lodash'; + +export class AlertingActions { + private readonly prefix: string; + + constructor(versionNumber: string) { + this.prefix = `alerting:${versionNumber}:`; + } + + public get(alertTypeId: string, consumer: string | undefined, operation: string): string { + if (!alertTypeId || !isString(alertTypeId)) { + throw new Error('alertTypeId is required and must be a string'); + } + + if (!operation || !isString(operation)) { + throw new Error('operation is required and must be a string'); + } + + if (!isUndefined(consumer) && (!consumer || !isString(consumer))) { + throw new Error('consumer is optional but must be a string when specified'); + } + + return `${this.prefix}${alertTypeId}${consumer ? `/${consumer}` : ''}/${operation}`; + } +} diff --git a/x-pack/plugins/security/server/authorization/index.mock.ts b/x-pack/plugins/security/server/authorization/index.mock.ts index 930ede4157723..22eed47c17bfe 100644 --- a/x-pack/plugins/security/server/authorization/index.mock.ts +++ b/x-pack/plugins/security/server/authorization/index.mock.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Actions } from '.'; import { AuthorizationMode } from './mode'; +import { actionsMock } from './actions/actions.mock'; export const authorizationMock = { create: ({ version = 'mock-version', applicationName = 'mock-application', }: { version?: string; applicationName?: string } = {}) => ({ - actions: new Actions(version), + actions: actionsMock.create(version), checkPrivilegesWithRequest: jest.fn(), checkPrivilegesDynamicallyWithRequest: jest.fn(), checkSavedObjectsPrivilegesWithRequest: jest.fn(), diff --git a/x-pack/plugins/security/server/authorization/index.ts b/x-pack/plugins/security/server/authorization/index.ts index cf970a561b93f..06b9bad0af972 100644 --- a/x-pack/plugins/security/server/authorization/index.ts +++ b/x-pack/plugins/security/server/authorization/index.ts @@ -15,6 +15,7 @@ import { import { FeaturesService, SpacesService } from '../plugin'; import { Actions } from './actions'; import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges'; +export { CheckPrivilegesResponse } from './check_privileges'; import { CheckPrivilegesDynamicallyWithRequest, checkPrivilegesDynamicallyWithRequestFactory, diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts new file mode 100644 index 0000000000000..1eaebac2c78f6 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts @@ -0,0 +1,311 @@ +/* + * 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 { Actions } from '../../actions'; +import { FeaturePrivilegeAlertingBuilder } from './alerting'; +import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; + +const version = '1.0.0-zeta1'; + +describe(`feature_privilege_builder`, () => { + describe(`alerting`, () => { + test('grants no privileges by default', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + all: [], + read: [], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new Feature({ + id: 'my-feature', + name: 'my-feature', + app: [], + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toEqual([]); + }); + + describe(`within feature`, () => { + test('grants `read` privileges under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + all: [], + read: ['alert-type'], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new Feature({ + id: 'my-feature', + name: 'my-feature', + app: [], + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/my-feature/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", + "alerting:1.0.0-zeta1:alert-type/my-feature/find", + ] + `); + }); + + test('grants `all` privileges under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + all: ['alert-type'], + read: [], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new Feature({ + id: 'my-feature', + name: 'my-feature', + app: [], + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/my-feature/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", + "alerting:1.0.0-zeta1:alert-type/my-feature/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/create", + "alerting:1.0.0-zeta1:alert-type/my-feature/delete", + "alerting:1.0.0-zeta1:alert-type/my-feature/update", + "alerting:1.0.0-zeta1:alert-type/my-feature/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/my-feature/enable", + "alerting:1.0.0-zeta1:alert-type/my-feature/disable", + "alerting:1.0.0-zeta1:alert-type/my-feature/muteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/muteInstance", + "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteInstance", + ] + `); + }); + + test('grants both `all` and `read` privileges under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + all: ['alert-type'], + read: ['readonly-alert-type'], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new Feature({ + id: 'my-feature', + name: 'my-feature', + app: [], + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/my-feature/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", + "alerting:1.0.0-zeta1:alert-type/my-feature/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/create", + "alerting:1.0.0-zeta1:alert-type/my-feature/delete", + "alerting:1.0.0-zeta1:alert-type/my-feature/update", + "alerting:1.0.0-zeta1:alert-type/my-feature/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/my-feature/enable", + "alerting:1.0.0-zeta1:alert-type/my-feature/disable", + "alerting:1.0.0-zeta1:alert-type/my-feature/muteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/muteInstance", + "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteInstance", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/get", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/getAlertState", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/find", + ] + `); + }); + }); + + describe(`globally`, () => { + test('grants global `read` privileges under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + allGlobally: [], + readGlobally: ['alert-type'], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new Feature({ + id: 'my-feature', + name: 'my-feature', + app: [], + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/get", + "alerting:1.0.0-zeta1:alert-type/getAlertState", + "alerting:1.0.0-zeta1:alert-type/find", + ] + `); + }); + + test('grants global `all` privileges under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + allGlobally: ['alert-type'], + readGlobally: [], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new Feature({ + id: 'my-feature', + name: 'my-feature', + app: [], + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/get", + "alerting:1.0.0-zeta1:alert-type/getAlertState", + "alerting:1.0.0-zeta1:alert-type/find", + "alerting:1.0.0-zeta1:alert-type/create", + "alerting:1.0.0-zeta1:alert-type/delete", + "alerting:1.0.0-zeta1:alert-type/update", + "alerting:1.0.0-zeta1:alert-type/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/enable", + "alerting:1.0.0-zeta1:alert-type/disable", + "alerting:1.0.0-zeta1:alert-type/muteAll", + "alerting:1.0.0-zeta1:alert-type/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/muteInstance", + "alerting:1.0.0-zeta1:alert-type/unmuteInstance", + ] + `); + }); + + test('grants both global `all` and global `read` privileges under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + allGlobally: ['alert-type'], + readGlobally: ['readonly-alert-type'], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new Feature({ + id: 'my-feature', + name: 'my-feature', + app: [], + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/get", + "alerting:1.0.0-zeta1:alert-type/getAlertState", + "alerting:1.0.0-zeta1:alert-type/find", + "alerting:1.0.0-zeta1:alert-type/create", + "alerting:1.0.0-zeta1:alert-type/delete", + "alerting:1.0.0-zeta1:alert-type/update", + "alerting:1.0.0-zeta1:alert-type/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/enable", + "alerting:1.0.0-zeta1:alert-type/disable", + "alerting:1.0.0-zeta1:alert-type/muteAll", + "alerting:1.0.0-zeta1:alert-type/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/muteInstance", + "alerting:1.0.0-zeta1:alert-type/unmuteInstance", + "alerting:1.0.0-zeta1:readonly-alert-type/get", + "alerting:1.0.0-zeta1:readonly-alert-type/getAlertState", + "alerting:1.0.0-zeta1:readonly-alert-type/find", + ] + `); + }); + }); + }); +}); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts new file mode 100644 index 0000000000000..7935959c331ce --- /dev/null +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -0,0 +1,48 @@ +/* + * 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 { flatten, uniq } from 'lodash'; +import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; + +const readOperations: string[] = ['get', 'getAlertState', 'find']; +const writeOperations: string[] = [ + 'create', + 'delete', + 'update', + 'updateApiKey', + 'enable', + 'disable', + 'muteAll', + 'unmuteAll', + 'muteInstance', + 'unmuteInstance', +]; +const allOperations: string[] = [...readOperations, ...writeOperations]; + +export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder { + public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + const allOperationsWithinConsumer = (privileges: string[], consumer?: string) => + flatten( + privileges.map(type => [ + ...allOperations.map(operation => this.actions.alerting.get(type, consumer, operation)), + ]) + ); + const readOperationsWithinConsumer = (privileges: string[], consumer?: string) => + flatten( + privileges.map(type => [ + ...readOperations.map(operation => this.actions.alerting.get(type, consumer, operation)), + ]) + ); + + return uniq([ + ...allOperationsWithinConsumer(privilegeDefinition.alerting?.all ?? [], feature.id), + ...readOperationsWithinConsumer(privilegeDefinition.alerting?.read ?? [], feature.id), + ...allOperationsWithinConsumer(privilegeDefinition.alerting?.globally?.all ?? []), + ...readOperationsWithinConsumer(privilegeDefinition.alerting?.globally?.read ?? []), + ]); + } +} diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts index c293319070419..42792cb1797cd 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts @@ -14,6 +14,7 @@ import { FeaturePrivilegeBuilder } from './feature_privilege_builder'; import { FeaturePrivilegeManagementBuilder } from './management'; import { FeaturePrivilegeNavlinkBuilder } from './navlink'; import { FeaturePrivilegeSavedObjectBuilder } from './saved_object'; +import { FeaturePrivilegeAlertingBuilder } from './alerting'; import { FeaturePrivilegeUIBuilder } from './ui'; export { FeaturePrivilegeBuilder }; @@ -26,6 +27,7 @@ export const featurePrivilegeBuilderFactory = (actions: Actions): FeaturePrivile new FeaturePrivilegeNavlinkBuilder(actions), new FeaturePrivilegeSavedObjectBuilder(actions), new FeaturePrivilegeUIBuilder(actions), + new FeaturePrivilegeAlertingBuilder(actions), ]; return { diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts index 9a8935f80a174..1fa1b21083921 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -89,7 +89,6 @@ export function privilegesFactory( delete featurePrivileges[feature.id]; } } - return { features: featurePrivileges, global: { diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 0011737d85734..013330ec652b9 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -29,6 +29,7 @@ export { } from './authentication'; export { SecurityPluginSetup }; export { AuthenticatedUser } from '../common/model'; +export { Actions, CheckPrivilegesResponse } from './authorization'; export const config: PluginConfigDescriptor> = { schema: ConfigSchema, diff --git a/x-pack/plugins/siem/server/plugin.ts b/x-pack/plugins/siem/server/plugin.ts index 3c336991f3d9d..b96c6a8cc4160 100644 --- a/x-pack/plugins/siem/server/plugin.ts +++ b/x-pack/plugins/siem/server/plugin.ts @@ -35,7 +35,7 @@ import { initSavedObjects, savedObjectTypes } from './saved_objects'; import { SiemClientFactory } from './client'; import { createConfig$, ConfigType } from './config'; import { initUiSettings } from './ui_settings'; -import { APP_ID, APP_ICON } from '../common/constants'; +import { APP_ID, APP_ICON, SIGNALS_ID, NOTIFICATIONS_ID } from '../common/constants'; import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerResolverRoutes } from './endpoint/routes/resolver'; import { registerAlertRoutes } from './endpoint/alerts/routes'; @@ -136,7 +136,7 @@ export class Plugin implements IPlugin { - it('should return 200 with list of alert types', async () => { + it('should return 200 with list of globally available alert types', async () => { const response = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/api/alert/types`) .auth(user.username, user.password); + expect(response.statusCode).to.eql(200); switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - expect(response.statusCode).to.eql(404); - expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', - }); + expect(response.body).to.eql([]); break; case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': - expect(response.statusCode).to.eql(200); const fixtureAlertType = response.body.find( (alertType: any) => alertType.id === 'test.noop' ); @@ -48,7 +43,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { state: [], context: [], }, - producer: 'alerting', + producer: 'alertsFixture', }); break; default: 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 index ce11fb8052b45..ff850544463d5 100644 --- 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 @@ -13,6 +13,7 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, + getUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -44,11 +45,11 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('muteAll', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': 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 index f91b54514ae05..d830003fc6e62 100644 --- 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 @@ -13,6 +13,7 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, + getUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -44,11 +45,11 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('muteInstance', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': @@ -93,11 +94,11 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('muteInstance', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': 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 index f2598ff7c5493..c3e449264b209 100644 --- 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 @@ -13,6 +13,7 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, + getUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -49,11 +50,11 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('unmuteAll', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': 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 index ca58b58e5e822..22cb7c09e9131 100644 --- 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 @@ -13,6 +13,7 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, + getUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -49,11 +50,15 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage( + 'unmuteInstance', + 'test.noop', + 'alertsFixture' + ), + statusCode: 403, }); break; case 'superuser at space1': 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 f7ccc6c97bcf0..df5bc44564016 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 @@ -13,6 +13,7 @@ import { getTestAlertData, ObjectRemover, ensureDatetimeIsWithinRange, + getUnauthorizedErrorMessage, } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -65,11 +66,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('update', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': @@ -79,7 +80,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { ...updatedData, id: createdAlert.id, alertTypeId: 'test.noop', - consumer: 'bar', + consumer: 'alertsFixture', createdBy: 'elastic', enabled: true, updatedBy: user.username, @@ -146,11 +147,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('update', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': @@ -160,7 +161,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { ...updatedData, id: createdAlert.id, alertTypeId: 'test.noop', - consumer: 'bar', + consumer: 'alertsFixture', createdBy: 'elastic', enabled: true, updatedBy: user.username, @@ -218,12 +219,6 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { 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', - message: 'Not Found', - }); - break; case 'superuser at space1': expect(response.body).to.eql({ statusCode: 404, @@ -264,13 +259,6 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { 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(400); @@ -296,13 +284,6 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { 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(400); @@ -349,11 +330,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('update', 'test.validation', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': @@ -388,13 +369,6 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { 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(400); @@ -448,11 +422,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('update', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': 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 cd821a739a9eb..fc2aebfe2b604 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 @@ -13,6 +13,7 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, + getUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -39,16 +40,15 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte objectRemover.add(space.id, createdAlert.id, 'alert'); const response = await alertUtils.getUpdateApiKeyRequest(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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('updateApiKey', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': @@ -98,11 +98,11 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('updateApiKey', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': @@ -143,12 +143,6 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'global_read at space1': - 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.body).to.eql({ 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 319834452a212..8f38277a86f53 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 @@ -69,7 +69,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { ], enabled: true, alertTypeId: 'test.noop', - consumer: 'bar', + consumer: 'alertsFixture', params: {}, createdBy: null, schedule: { interval: '1m' }, 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 5f50c0d64f353..ff2d338bd26a5 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 @@ -42,7 +42,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { name: 'abc', tags: ['foo'], alertTypeId: 'test.noop', - consumer: 'bar', + consumer: 'alertsFixture', schedule: { interval: '1m' }, enabled: true, actions: [], 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 66cd8a7244081..777864fbdd402 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 @@ -36,7 +36,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { name: 'abc', tags: ['foo'], alertTypeId: 'test.noop', - consumer: 'bar', + consumer: 'alertsFixture', schedule: { interval: '1m' }, enabled: true, actions: [], diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts index 6f1aec901760e..24d72714f4ac4 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts @@ -44,7 +44,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont name: 'abc', tags: ['foo'], alertTypeId: 'test.cumulative-firing', - consumer: 'bar', + consumer: 'alertsFixture', schedule: { interval: '5s' }, throttle: '5s', actions: [], diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts index 845a6f7955739..963e6f07ffc13 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts @@ -27,7 +27,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { state: [], context: [], }, - producer: 'alerting', + producer: 'alertsFixture', }); }); 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 5a35d4bf83865..3ef899b631946 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 @@ -47,7 +47,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { id: createdAlert.id, tags: ['bar'], alertTypeId: 'test.noop', - consumer: 'bar', + consumer: 'alertsFixture', createdBy: null, enabled: true, updatedBy: null, From 95da803e63b537885b0a114030c169ba9d796bff Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 21 May 2020 09:50:32 +0100 Subject: [PATCH 005/126] made SO client unsecure in alerting --- x-pack/plugins/alerts/server/alerts_client.ts | 99 ++++++++++++------- .../alerts/server/alerts_client_factory.ts | 14 +-- x-pack/plugins/alerts/server/plugin.ts | 14 +-- x-pack/plugins/security/server/plugin.ts | 10 +- 4 files changed, 85 insertions(+), 52 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 6b091a5a4503b..42b4044394408 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -12,6 +12,7 @@ import { SavedObjectsClientContract, SavedObjectReference, SavedObject, + KibanaRequest, } from 'src/core/server'; import { ActionsClient } from '../../actions/server'; import { @@ -30,6 +31,7 @@ import { InvalidateAPIKeyParams, GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, + SecurityPluginSetup, } from '../../security/server'; import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../task_manager/server'; @@ -47,7 +49,9 @@ export type InvalidateAPIKeyResult = interface ConstructorOptions { logger: Logger; taskManager: TaskManagerStartContract; - savedObjectsClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; + authorization?: SecurityPluginSetup['authz']; + request: KibanaRequest; alertTypeRegistry: AlertTypeRegistry; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; spaceId?: string; @@ -127,7 +131,9 @@ export class AlertsClient { private readonly spaceId?: string; private readonly namespace?: string; private readonly taskManager: TaskManagerStartContract; - private readonly savedObjectsClient: SavedObjectsClientContract; + private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; + private readonly request: KibanaRequest; + private readonly authorization?: SecurityPluginSetup['authz']; private readonly alertTypeRegistry: AlertTypeRegistry; private readonly createAPIKey: () => Promise; private readonly invalidateAPIKey: ( @@ -138,7 +144,9 @@ export class AlertsClient { constructor({ alertTypeRegistry, - savedObjectsClient, + unsecuredSavedObjectsClient, + request, + authorization, taskManager, logger, spaceId, @@ -155,7 +163,9 @@ export class AlertsClient { this.namespace = namespace; this.taskManager = taskManager; this.alertTypeRegistry = alertTypeRegistry; - this.savedObjectsClient = savedObjectsClient; + this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; + this.request = request; + this.authorization = authorization; this.createAPIKey = createAPIKey; this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; @@ -183,7 +193,7 @@ export class AlertsClient { muteAll: false, mutedInstanceIds: [], }; - const createdAlert = await this.savedObjectsClient.create('alert', rawAlert, { + const createdAlert = await this.unsecuredSavedObjectsClient.create('alert', rawAlert, { ...options, references, }); @@ -194,7 +204,7 @@ export class AlertsClient { } catch (e) { // Cleanup data, something went wrong scheduling the task try { - await this.savedObjectsClient.delete('alert', createdAlert.id); + await this.unsecuredSavedObjectsClient.delete('alert', createdAlert.id); } catch (err) { // Skip the cleanup error and throw the task manager error to avoid confusion this.logger.error( @@ -203,7 +213,7 @@ export class AlertsClient { } throw e; } - await this.savedObjectsClient.update('alert', createdAlert.id, { + await this.unsecuredSavedObjectsClient.update('alert', createdAlert.id, { scheduledTaskId: scheduledTask.id, }); createdAlert.attributes.scheduledTaskId = scheduledTask.id; @@ -217,7 +227,7 @@ export class AlertsClient { } public async get({ id }: { id: string }): Promise { - const result = await this.savedObjectsClient.get('alert', id); + const result = await this.unsecuredSavedObjectsClient.get('alert', id); return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references); } @@ -238,7 +248,7 @@ export class AlertsClient { per_page: perPage, total, saved_objects: data, - } = await this.savedObjectsClient.find({ + } = await this.unsecuredSavedObjectsClient.find({ ...options, type: 'alert', }); @@ -269,11 +279,11 @@ export class AlertsClient { `delete(): Failed to load API key to invalidate on alert ${id}: ${e.message}` ); // Still attempt to load the scheduledTaskId using SOC - const alert = await this.savedObjectsClient.get('alert', id); + const alert = await this.unsecuredSavedObjectsClient.get('alert', id); taskIdToRemove = alert.attributes.scheduledTaskId; } - const removeResult = await this.savedObjectsClient.delete('alert', id); + const removeResult = await this.unsecuredSavedObjectsClient.delete('alert', id); await Promise.all([ taskIdToRemove ? deleteTaskIfItExists(this.taskManager, taskIdToRemove) : null, @@ -296,7 +306,7 @@ export class AlertsClient { `update(): Failed to load API key to invalidate on alert ${id}: ${e.message}` ); // Still attempt to load the object using SOC - alertSavedObject = await this.savedObjectsClient.get('alert', id); + alertSavedObject = await this.unsecuredSavedObjectsClient.get('alert', id); } const updateResult = await this.updateAlert({ id, data }, alertSavedObject); @@ -337,7 +347,7 @@ export class AlertsClient { const createdAPIKey = attributes.enabled ? await this.createAPIKey() : null; const apiKeyAttributes = this.apiKeyAsAlertAttributes(createdAPIKey, username); - const updatedObject = await this.savedObjectsClient.update( + const updatedObject = await this.unsecuredSavedObjectsClient.update( 'alert', id, { @@ -395,13 +405,13 @@ export class AlertsClient { `updateApiKey(): Failed to load API key to invalidate on alert ${id}: ${e.message}` ); // Still attempt to load the attributes and version using SOC - const alert = await this.savedObjectsClient.get('alert', id); + const alert = await this.unsecuredSavedObjectsClient.get('alert', id); attributes = alert.attributes; version = alert.version; } const username = await this.getUserName(); - await this.savedObjectsClient.update( + await this.unsecuredSavedObjectsClient.update( 'alert', id, { @@ -423,7 +433,9 @@ export class AlertsClient { } try { - const apiKeyId = Buffer.from(apiKey, 'base64').toString().split(':')[0]; + const apiKeyId = Buffer.from(apiKey, 'base64') + .toString() + .split(':')[0]; const response = await this.invalidateAPIKey({ id: apiKeyId }); if (response.apiKeysEnabled === true && response.result.error_count > 0) { this.logger.error(`Failed to invalidate API Key [id="${apiKeyId}"]`); @@ -451,14 +463,14 @@ export class AlertsClient { `enable(): Failed to load API key to invalidate on alert ${id}: ${e.message}` ); // Still attempt to load the attributes and version using SOC - const alert = await this.savedObjectsClient.get('alert', id); + const alert = await this.unsecuredSavedObjectsClient.get('alert', id); attributes = alert.attributes; version = alert.version; } if (attributes.enabled === false) { const username = await this.getUserName(); - await this.savedObjectsClient.update( + await this.unsecuredSavedObjectsClient.update( 'alert', id, { @@ -470,7 +482,9 @@ export class AlertsClient { { version } ); const scheduledTask = await this.scheduleAlert(id, attributes.alertTypeId); - await this.savedObjectsClient.update('alert', id, { scheduledTaskId: scheduledTask.id }); + await this.unsecuredSavedObjectsClient.update('alert', id, { + scheduledTaskId: scheduledTask.id, + }); if (apiKeyToInvalidate) { await this.invalidateApiKey({ apiKey: apiKeyToInvalidate }); } @@ -495,13 +509,13 @@ export class AlertsClient { `disable(): Failed to load API key to invalidate on alert ${id}: ${e.message}` ); // Still attempt to load the attributes and version using SOC - const alert = await this.savedObjectsClient.get('alert', id); + const alert = await this.unsecuredSavedObjectsClient.get('alert', id); attributes = alert.attributes; version = alert.version; } if (attributes.enabled === true) { - await this.savedObjectsClient.update( + await this.unsecuredSavedObjectsClient.update( 'alert', id, { @@ -525,7 +539,7 @@ export class AlertsClient { } public async muteAll({ id }: { id: string }) { - await this.savedObjectsClient.update('alert', id, { + await this.unsecuredSavedObjectsClient.update('alert', id, { muteAll: true, mutedInstanceIds: [], updatedBy: await this.getUserName(), @@ -533,7 +547,7 @@ export class AlertsClient { } public async unmuteAll({ id }: { id: string }) { - await this.savedObjectsClient.update('alert', id, { + await this.unsecuredSavedObjectsClient.update('alert', id, { muteAll: false, mutedInstanceIds: [], updatedBy: await this.getUserName(), @@ -541,11 +555,14 @@ export class AlertsClient { } public async muteInstance({ alertId, alertInstanceId }: MuteOptions) { - const { attributes, version } = await this.savedObjectsClient.get('alert', alertId); + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + 'alert', + alertId + ); const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) { mutedInstanceIds.push(alertInstanceId); - await this.savedObjectsClient.update( + await this.unsecuredSavedObjectsClient.update( 'alert', alertId, { @@ -564,10 +581,13 @@ export class AlertsClient { alertId: string; alertInstanceId: string; }) { - const { attributes, version } = await this.savedObjectsClient.get('alert', alertId); + const { attributes, version } = await this.unsecuredSavedObjectsClient.get( + 'alert', + alertId + ); const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { - await this.savedObjectsClient.update( + await this.unsecuredSavedObjectsClient.update( 'alert', alertId, { @@ -580,6 +600,19 @@ export class AlertsClient { } } + private async ensureAuthorized(alertTypeId: string, operation: string) { + if (this.authorization == null) { + return; + } + const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest(this.request); + const { hasAllRequested } = await checkPrivileges( + this.authorization.actions.savedObject.get(alertTypeId, operation) + ); + if (!hasAllRequested) { + throw Boom.forbidden(`Unable to ${operation} ${alertTypeId}`); + } + } + private async scheduleAlert(id: string, alertTypeId: string) { return await this.taskManager.schedule({ taskType: `alerting:${alertTypeId}`, @@ -600,8 +633,8 @@ export class AlertsClient { actions: RawAlert['actions'], references: SavedObjectReference[] ) { - return actions.map((action) => { - const reference = references.find((ref) => ref.name === action.actionRef); + return actions.map(action => { + const reference = references.find(ref => ref.name === action.actionRef); if (!reference) { throw new Error(`Reference ${action.actionRef} not found`); } @@ -646,10 +679,10 @@ export class AlertsClient { private validateActions(alertType: AlertType, actions: NormalizedAlertAction[]): void { const { actionGroups: alertTypeActionGroups } = alertType; - const usedAlertActionGroups = actions.map((action) => action.group); + const usedAlertActionGroups = actions.map(action => action.group); const availableAlertTypeActionGroups = new Set(pluck(alertTypeActionGroups, 'id')); const invalidActionGroups = usedAlertActionGroups.filter( - (group) => !availableAlertTypeActionGroups.has(group) + group => !availableAlertTypeActionGroups.has(group) ); if (invalidActionGroups.length) { throw Boom.badRequest( @@ -667,11 +700,11 @@ export class AlertsClient { alertActions: NormalizedAlertAction[] ): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> { const actionsClient = await this.getActionsClient(); - const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))]; + const actionIds = [...new Set(alertActions.map(alertAction => alertAction.id))]; const actionResults = await actionsClient.getBulk(actionIds); const references: SavedObjectReference[] = []; const actions = alertActions.map(({ id, ...alertAction }, i) => { - const actionResultValue = actionResults.find((action) => action.id === id); + const actionResultValue = actionResults.find(action => action.id === id); if (actionResultValue) { const actionRef = `action_${i}`; references.push({ diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index af546f965d7df..b7d1bf6e8cf31 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -7,7 +7,7 @@ import { PluginStartContract as ActionsPluginStartContract } from '../../actions/server'; import { AlertsClient } from './alerts_client'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; -import { KibanaRequest, Logger, SavedObjectsClientContract } from '../../../../src/core/server'; +import { KibanaRequest, Logger, SavedObjectsServiceStart } from '../../../../src/core/server'; import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../security/server'; import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../task_manager/server'; @@ -49,10 +49,7 @@ export class AlertsClientFactory { this.actions = options.actions; } - public create( - request: KibanaRequest, - savedObjectsClient: SavedObjectsClientContract - ): AlertsClient { + public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): AlertsClient { const { securityPluginSetup, actions } = this; const spaceId = this.getSpaceId(request); return new AlertsClient({ @@ -60,7 +57,12 @@ export class AlertsClientFactory { logger: this.logger, taskManager: this.taskManager, alertTypeRegistry: this.alertTypeRegistry, - savedObjectsClient, + unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, { + excludedWrappers: ['security'], + includedHiddenTypes: ['alert', 'action'], + }), + authorization: this.securityPluginSetup?.authz, + request, namespace: this.spaceIdToNamespace(spaceId), encryptedSavedObjectsClient: this.encryptedSavedObjectsClient, async getUserName() { diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 324bc9fbfb72b..e46be4efeaf9a 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -240,10 +240,7 @@ export class AlertingPlugin { `Unable to create alerts client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` ); } - return alertsClientFactory!.create( - request, - this.getScopedClientWithAlertSavedObjectType(core.savedObjects, request) - ); + return alertsClientFactory!.create(request, core.savedObjects); }, }; } @@ -252,14 +249,11 @@ export class AlertingPlugin { core: CoreSetup ): IContextProvider, 'alerting'> => { const { alertTypeRegistry, alertsClientFactory } = this; - return async (context, request) => { + return async function alertsRouteHandlerContext(context, request) { const [{ savedObjects }] = await core.getStartServices(); return { getAlertsClient: () => { - return alertsClientFactory!.create( - request, - this.getScopedClientWithAlertSavedObjectType(savedObjects, request) - ); + return alertsClientFactory!.create(request, savedObjects); }, listTypes: alertTypeRegistry!.list.bind(alertTypeRegistry!), }; @@ -270,7 +264,7 @@ export class AlertingPlugin { savedObjects: SavedObjectsServiceStart, elasticsearch: ElasticsearchServiceStart ): (request: KibanaRequest) => Services { - return (request) => ({ + return request => ({ callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser, savedObjectsClient: this.getScopedClientWithAlertSavedObjectType(savedObjects, request), getScopedCallCluster(clusterClient: IClusterClient) { diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index bdda0be9b15a7..b947292eb0aef 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -48,7 +48,10 @@ export interface SecurityPluginSetup { | 'grantAPIKeyAsInternalUser' | 'invalidateAPIKeyAsInternalUser' >; - authz: Pick; + authz: Pick< + Authorization, + 'actions' | 'checkPrivilegesWithRequest' | 'checkPrivilegesDynamicallyWithRequest' | 'mode' + >; license: SecurityLicense; audit: Pick; @@ -98,7 +101,7 @@ export class Plugin { public async setup(core: CoreSetup, { features, licensing }: PluginSetupDependencies) { const [config, legacyConfig] = await combineLatest([ this.initializerContext.config.create>().pipe( - map((rawConfig) => + map(rawConfig => createConfig(rawConfig, this.initializerContext.logger.get('config'), { isTLSEnabled: core.http.isTlsEnabled, }) @@ -180,12 +183,13 @@ export class Plugin { authz: { actions: authz.actions, checkPrivilegesWithRequest: authz.checkPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: authz.checkPrivilegesDynamicallyWithRequest, mode: authz.mode, }, license, - registerSpacesService: (service) => { + registerSpacesService: service => { if (this.wasSpacesServiceAccessed()) { throw new Error('Spaces service has been accessed before registration.'); } From 341afdba54f75c0bd822a375cbbfb1879e6e2ca6 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 21 May 2020 11:57:29 +0100 Subject: [PATCH 006/126] fixed typing, commented unused authz --- .../alerts/server/alerts_client.test.ts | 286 +++++++++--------- x-pack/plugins/alerts/server/alerts_client.ts | 36 +-- .../server/alerts_client_factory.test.ts | 65 +++- x-pack/plugins/security/server/mocks.ts | 1 + 4 files changed, 220 insertions(+), 168 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 9685f58b8fb31..0e19708947584 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -5,6 +5,7 @@ */ import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; +import { KibanaRequest } from 'kibana/server'; import { AlertsClient, CreateOptions } from './alerts_client'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../src/core/server/mocks'; import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; @@ -17,13 +18,14 @@ import { actionsClientMock } from '../../actions/server/mocks'; const taskManager = taskManagerMock.start(); const alertTypeRegistry = alertTypeRegistryMock.create(); -const savedObjectsClient = savedObjectsClientMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const alertsClientParams = { taskManager, alertTypeRegistry, - savedObjectsClient, + unsecuredSavedObjectsClient, + request: {} as KibanaRequest, spaceId: 'default', namespace: 'default', getUserName: jest.fn(), @@ -126,7 +128,7 @@ describe('create()', () => { test('creates an alert', async () => { const data = getMockData(); - savedObjectsClient.create.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -168,7 +170,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -208,10 +210,10 @@ describe('create()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.create.mock.calls[0]).toHaveLength(3); - expect(savedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); - expect(savedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -246,7 +248,7 @@ describe('create()', () => { "updatedBy": "elastic", } `); - expect(savedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` Object { "references": Array [ Object { @@ -277,11 +279,11 @@ describe('create()', () => { }, ] `); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.update.mock.calls[0]).toHaveLength(3); - 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(` + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` Object { "scheduledTaskId": "task-123", } @@ -314,7 +316,7 @@ describe('create()', () => { }, ], }); - savedObjectsClient.create.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -382,7 +384,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -436,7 +438,7 @@ describe('create()', () => { test('creates a disabled alert', async () => { const data = getMockData({ enabled: false }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -448,7 +450,7 @@ describe('create()', () => { }, ], }); - savedObjectsClient.create.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -504,7 +506,7 @@ describe('create()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); expect(taskManager.schedule).toHaveBeenCalledTimes(0); }); @@ -542,13 +544,13 @@ describe('create()', () => { await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Test Error"` ); - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); }); test('throws error if create saved object fails', async () => { const data = getMockData(); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -560,7 +562,7 @@ describe('create()', () => { }, ], }); - savedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure')); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure')); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Test failure"` ); @@ -569,7 +571,7 @@ describe('create()', () => { test('attempts to remove saved object if scheduling failed', async () => { const data = getMockData(); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -581,7 +583,7 @@ describe('create()', () => { }, ], }); - savedObjectsClient.create.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -610,12 +612,12 @@ describe('create()', () => { ], }); taskManager.schedule.mockRejectedValueOnce(new Error('Test failure')); - savedObjectsClient.delete.mockResolvedValueOnce({}); + unsecuredSavedObjectsClient.delete.mockResolvedValueOnce({}); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Test failure"` ); - expect(savedObjectsClient.delete).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alert", "1", @@ -625,7 +627,7 @@ describe('create()', () => { test('returns task manager error if cleanup fails, logs to console', async () => { const data = getMockData(); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -637,7 +639,7 @@ describe('create()', () => { }, ], }); - savedObjectsClient.create.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -666,7 +668,9 @@ describe('create()', () => { ], }); taskManager.schedule.mockRejectedValueOnce(new Error('Task manager error')); - savedObjectsClient.delete.mockRejectedValueOnce(new Error('Saved object delete error')); + unsecuredSavedObjectsClient.delete.mockRejectedValueOnce( + new Error('Saved object delete error') + ); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Task manager error"` ); @@ -691,7 +695,7 @@ describe('create()', () => { apiKeysEnabled: true, result: { id: '123', api_key: 'abc' }, }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -703,7 +707,7 @@ describe('create()', () => { }, ], }); - savedObjectsClient.create.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -744,7 +748,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -761,7 +765,7 @@ describe('create()', () => { await alertsClient.create({ data }); expect(alertsClientParams.createAPIKey).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.create).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( 'alert', { actions: [ @@ -802,7 +806,7 @@ describe('create()', () => { test(`doesn't create API key for disabled alerts`, async () => { const data = getMockData({ enabled: false }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -814,7 +818,7 @@ describe('create()', () => { }, ], }); - savedObjectsClient.create.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -855,7 +859,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -872,7 +876,7 @@ describe('create()', () => { await alertsClient.create({ data }); expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(savedObjectsClient.create).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( 'alert', { actions: [ @@ -929,7 +933,7 @@ describe('enable()', () => { beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); - savedObjectsClient.get.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false, }); @@ -950,13 +954,13 @@ describe('enable()', () => { test('enables an alert', async () => { await alertsClient.enable({ id: '1' }); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(savedObjectsClient.update).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { @@ -984,7 +988,7 @@ describe('enable()', () => { }, scope: ['alerting'], }); - expect(savedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { scheduledTaskId: 'task-123', }); }); @@ -999,7 +1003,7 @@ describe('enable()', () => { }); await alertsClient.enable({ id: '1' }); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); @@ -1018,7 +1022,7 @@ describe('enable()', () => { await alertsClient.enable({ id: '1' }); expect(alertsClientParams.getUserName).not.toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(savedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); }); @@ -1029,7 +1033,7 @@ describe('enable()', () => { }); await alertsClient.enable({ id: '1' }); - expect(savedObjectsClient.update).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { @@ -1050,7 +1054,7 @@ describe('enable()', () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); await alertsClient.enable({ id: '1' }); - expect(savedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'enable(): Failed to load API key to invalidate on alert 1: Fail' ); @@ -1058,45 +1062,47 @@ describe('enable()', () => { test('throws error when failing to load the saved object using SOC', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - savedObjectsClient.get.mockRejectedValueOnce(new Error('Fail to get')); + unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('Fail to get')); await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Fail to get"` ); expect(alertsClientParams.getUserName).not.toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(savedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); }); test('throws error when failing to update the first time', async () => { - savedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update')); + unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update')); await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Fail to update"` ); expect(alertsClientParams.getUserName).toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); expect(taskManager.schedule).not.toHaveBeenCalled(); }); test('throws error when failing to update the second time', async () => { - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ ...existingAlert, attributes: { ...existingAlert.attributes, enabled: true, }, }); - savedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update second time')); + unsecuredSavedObjectsClient.update.mockRejectedValueOnce( + new Error('Fail to update second time') + ); await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Fail to update second time"` ); expect(alertsClientParams.getUserName).toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(2); expect(taskManager.schedule).toHaveBeenCalled(); }); @@ -1108,7 +1114,7 @@ describe('enable()', () => { ); expect(alertsClientParams.getUserName).toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); }); }); @@ -1136,17 +1142,17 @@ describe('disable()', () => { beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); }); test('disables an alert', async () => { await alertsClient.disable({ id: '1' }); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(savedObjectsClient.update).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { @@ -1170,11 +1176,11 @@ describe('disable()', () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); await alertsClient.disable({ id: '1' }); - expect(savedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(savedObjectsClient.update).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { @@ -1204,7 +1210,7 @@ describe('disable()', () => { }); await alertsClient.disable({ id: '1' }); - expect(savedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); expect(taskManager.remove).not.toHaveBeenCalled(); expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); }); @@ -1220,7 +1226,7 @@ describe('disable()', () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); await alertsClient.disable({ id: '1' }); - expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); expect(taskManager.remove).toHaveBeenCalled(); expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( @@ -1228,8 +1234,8 @@ describe('disable()', () => { ); }); - test('throws when savedObjectsClient update fails', async () => { - savedObjectsClient.update.mockRejectedValueOnce(new Error('Failed to update')); + test('throws when unsecuredSavedObjectsClient update fails', async () => { + unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Failed to update')); await expect(alertsClient.disable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Failed to update"` @@ -1257,7 +1263,7 @@ describe('disable()', () => { describe('muteAll()', () => { test('mutes an alert', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1267,7 +1273,7 @@ describe('muteAll()', () => { }); await alertsClient.muteAll({ id: '1' }); - expect(savedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { muteAll: true, mutedInstanceIds: [], updatedBy: 'elastic', @@ -1278,7 +1284,7 @@ describe('muteAll()', () => { describe('unmuteAll()', () => { test('unmutes an alert', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1288,7 +1294,7 @@ describe('unmuteAll()', () => { }); await alertsClient.unmuteAll({ id: '1' }); - expect(savedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { muteAll: false, mutedInstanceIds: [], updatedBy: 'elastic', @@ -1299,7 +1305,7 @@ describe('unmuteAll()', () => { describe('muteInstance()', () => { test('mutes an alert instance', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1314,7 +1320,7 @@ describe('muteInstance()', () => { }); await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(savedObjectsClient.update).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { @@ -1327,7 +1333,7 @@ describe('muteInstance()', () => { test('skips muting when alert instance already muted', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1341,12 +1347,12 @@ describe('muteInstance()', () => { }); await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(savedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); }); test('skips muting when alert is muted', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1361,14 +1367,14 @@ describe('muteInstance()', () => { }); await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(savedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); }); }); describe('unmuteInstance()', () => { test('unmutes an alert instance', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1383,7 +1389,7 @@ describe('unmuteInstance()', () => { }); await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(savedObjectsClient.update).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { @@ -1396,7 +1402,7 @@ describe('unmuteInstance()', () => { test('skips unmuting when alert instance not muted', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1410,12 +1416,12 @@ describe('unmuteInstance()', () => { }); await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(savedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); }); test('skips unmuting when alert is muted', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1430,14 +1436,14 @@ describe('unmuteInstance()', () => { }); await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(savedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); }); }); describe('get()', () => { test('calls saved objects client with given params', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1488,8 +1494,8 @@ describe('get()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alert", "1", @@ -1499,7 +1505,7 @@ describe('get()', () => { test(`throws an error when references aren't found`, async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1529,7 +1535,7 @@ describe('get()', () => { describe('getAlertState()', () => { test('calls saved objects client with given params', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1572,8 +1578,8 @@ describe('getAlertState()', () => { }); await alertsClient.getAlertState({ id: '1' }); - expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alert", "1", @@ -1586,7 +1592,7 @@ describe('getAlertState()', () => { const scheduledTaskId = 'task-123'; - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1643,7 +1649,7 @@ describe('getAlertState()', () => { describe('find()', () => { test('calls saved objects client with given params', async () => { const alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.find.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ total: 1, per_page: 10, page: 1, @@ -1708,8 +1714,8 @@ describe('find()', () => { "total": 1, } `); - expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "type": "alert", @@ -1759,8 +1765,8 @@ describe('delete()', () => { beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValue(existingAlert); - savedObjectsClient.delete.mockResolvedValue({ + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.delete.mockResolvedValue({ success: true, }); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); @@ -1769,13 +1775,13 @@ describe('delete()', () => { test('successfully removes an alert', async () => { const result = await alertsClient.delete({ id: '1' }); expect(result).toEqual({ success: true }); - expect(savedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); expect(taskManager.remove).toHaveBeenCalledWith('task-123'); expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); }); test('falls back to SOC.get when getDecryptedAsInternalUser throws an error', async () => { @@ -1783,10 +1789,10 @@ describe('delete()', () => { const result = await alertsClient.delete({ id: '1' }); expect(result).toEqual({ success: true }); - expect(savedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); expect(taskManager.remove).toHaveBeenCalledWith('task-123'); expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - expect(savedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'delete(): Failed to load API key to invalidate on alert 1: Fail' ); @@ -1838,9 +1844,9 @@ describe('delete()', () => { ); }); - test('throws error when savedObjectsClient.get throws an error', async () => { + test('throws error when unsecuredSavedObjectsClient.get throws an error', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - savedObjectsClient.get.mockRejectedValue(new Error('SOC Fail')); + unsecuredSavedObjectsClient.get.mockRejectedValue(new Error('SOC Fail')); await expect(alertsClient.delete({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"SOC Fail"` @@ -1879,7 +1885,7 @@ describe('update()', () => { beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); alertTypeRegistry.get.mockReturnValue({ id: '123', @@ -1892,7 +1898,7 @@ describe('update()', () => { }); test('updates given parameters', async () => { - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -2028,12 +2034,12 @@ describe('update()', () => { expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); - 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(` + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -2080,7 +2086,7 @@ describe('update()', () => { "updatedBy": "elastic", } `); - expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` Object { "references": Array [ Object { @@ -2105,7 +2111,7 @@ describe('update()', () => { }); it('calls the createApiKey function', async () => { - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -2121,7 +2127,7 @@ describe('update()', () => { apiKeysEnabled: true, result: { id: '123', api_key: 'abc' }, }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -2200,11 +2206,11 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - 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(` + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -2235,7 +2241,7 @@ describe('update()', () => { "updatedBy": "elastic", } `); - expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` Object { "references": Array [ Object { @@ -2257,7 +2263,7 @@ describe('update()', () => { enabled: false, }, }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -2269,7 +2275,7 @@ describe('update()', () => { }, ], }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -2349,11 +2355,11 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - 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(` + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -2384,7 +2390,7 @@ describe('update()', () => { "updatedBy": "elastic", } `); - expect(savedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` Object { "references": Array [ Object { @@ -2441,7 +2447,7 @@ describe('update()', () => { it('swallows error when invalidate API key throws', async () => { alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -2453,7 +2459,7 @@ describe('update()', () => { }, ], }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -2510,7 +2516,7 @@ describe('update()', () => { it('swallows error when getDecryptedAsInternalUser throws', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -2530,7 +2536,7 @@ describe('update()', () => { }, ], }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -2622,7 +2628,7 @@ describe('update()', () => { ], }, }); - expect(savedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'update(): Failed to load API key to invalidate on alert 1: Fail' ); @@ -2644,7 +2650,7 @@ describe('update()', () => { async executor() {}, producer: 'alerting', }); - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -2682,7 +2688,7 @@ describe('update()', () => { params: {}, ownerId: null, }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: alertId, type: 'alert', attributes: { @@ -2774,7 +2780,7 @@ describe('update()', () => { expect(taskManager.runNow).not.toHaveBeenCalled(); }); - test('updating the alert should not wait for the rerun the task to complete', async (done) => { + test('updating the alert should not wait for the rerun the task to complete', async done => { const alertId = uuid.v4(); const taskId = uuid.v4(); @@ -2876,7 +2882,7 @@ describe('updateApiKey()', () => { beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); - savedObjectsClient.get.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingEncryptedAlert); alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, @@ -2886,11 +2892,11 @@ describe('updateApiKey()', () => { test('updates the API key for the alert', async () => { await alertsClient.updateApiKey({ id: '1' }); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(savedObjectsClient.update).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { @@ -2910,11 +2916,11 @@ describe('updateApiKey()', () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); await alertsClient.updateApiKey({ id: '1' }); - expect(savedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { namespace: 'default', }); - expect(savedObjectsClient.update).toHaveBeenCalledWith( + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( 'alert', '1', { @@ -2937,7 +2943,7 @@ describe('updateApiKey()', () => { expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'Failed to invalidate API Key: Fail' ); - expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); }); test('swallows error when getting decrypted object throws', async () => { @@ -2947,12 +2953,12 @@ describe('updateApiKey()', () => { expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'updateApiKey(): Failed to load API key to invalidate on alert 1: Fail' ); - expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); }); - test('throws when savedObjectsClient update fails', async () => { - savedObjectsClient.update.mockRejectedValueOnce(new Error('Fail')); + test('throws when unsecuredSavedObjectsClient update fails', async () => { + unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail')); await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( `"Fail"` diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 42b4044394408..5fcaac17a7d32 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -132,8 +132,8 @@ export class AlertsClient { private readonly namespace?: string; private readonly taskManager: TaskManagerStartContract; private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; - private readonly request: KibanaRequest; - private readonly authorization?: SecurityPluginSetup['authz']; + // private readonly request: KibanaRequest; + // private readonly authorization?: SecurityPluginSetup['authz']; private readonly alertTypeRegistry: AlertTypeRegistry; private readonly createAPIKey: () => Promise; private readonly invalidateAPIKey: ( @@ -145,8 +145,8 @@ export class AlertsClient { constructor({ alertTypeRegistry, unsecuredSavedObjectsClient, - request, - authorization, + // request, + // authorization, taskManager, logger, spaceId, @@ -164,8 +164,8 @@ export class AlertsClient { this.taskManager = taskManager; this.alertTypeRegistry = alertTypeRegistry; this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; - this.request = request; - this.authorization = authorization; + // this.request = request; + // this.authorization = authorization; this.createAPIKey = createAPIKey; this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; @@ -600,18 +600,18 @@ export class AlertsClient { } } - private async ensureAuthorized(alertTypeId: string, operation: string) { - if (this.authorization == null) { - return; - } - const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested } = await checkPrivileges( - this.authorization.actions.savedObject.get(alertTypeId, operation) - ); - if (!hasAllRequested) { - throw Boom.forbidden(`Unable to ${operation} ${alertTypeId}`); - } - } + // private async ensureAuthorized(alertTypeId: string, operation: string) { + // if (this.authorization == null) { + // return; + // } + // const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest(this.request); + // const { hasAllRequested } = await checkPrivileges( + // this.authorization.actions.savedObject.get(alertTypeId, operation) + // ); + // if (!hasAllRequested) { + // throw Boom.forbidden(`Unable to ${operation} ${alertTypeId}`); + // } + // } private async scheduleAlert(id: string, alertTypeId: string) { return await this.taskManager.schedule({ diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 50dafba00a7e4..10d7e7f9cafdf 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -9,7 +9,11 @@ import { AlertsClientFactory, AlertsClientFactoryOpts } from './alerts_client_fa import { alertTypeRegistryMock } from './alert_type_registry.mock'; import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { KibanaRequest } from '../../../../src/core/server'; -import { loggingServiceMock, savedObjectsClientMock } from '../../../../src/core/server/mocks'; +import { + loggingServiceMock, + savedObjectsClientMock, + savedObjectsServiceMock, +} from '../../../../src/core/server/mocks'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { AuthenticatedUser } from '../../security/public'; import { securityMock } from '../../security/server/mocks'; @@ -18,6 +22,8 @@ import { actionsMock } from '../../actions/server/mocks'; jest.mock('./alerts_client'); const savedObjectsClient = savedObjectsClientMock.create(); +const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(); + const securityPluginSetup = securityMock.createSetup(); const alertsClientFactoryParams: jest.Mocked = { logger: loggingServiceMock.create().get(), @@ -50,13 +56,52 @@ beforeEach(() => { alertsClientFactoryParams.spaceIdToNamespace.mockReturnValue('default'); }); +test('creates an alerts client with proper constructor arguments when security is enabled', async () => { + const factory = new AlertsClientFactory(); + factory.initialize({ securityPluginSetup, ...alertsClientFactoryParams }); + const request = KibanaRequest.from(fakeRequest); + + savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient); + + factory.create(request, savedObjectsService); + + expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, { + excludedWrappers: ['security'], + }); + + expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ + unsecuredSavedObjectsClient: savedObjectsClient, + request, + authorization: securityPluginSetup.authz, + logger: alertsClientFactoryParams.logger, + taskManager: alertsClientFactoryParams.taskManager, + alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, + spaceId: 'default', + namespace: 'default', + getUserName: expect.any(Function), + createAPIKey: expect.any(Function), + invalidateAPIKey: expect.any(Function), + encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, + preconfiguredActions: [], + }); +}); + test('creates an alerts client with proper constructor arguments', async () => { const factory = new AlertsClientFactory(); factory.initialize(alertsClientFactoryParams); - factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + const request = KibanaRequest.from(fakeRequest); + + savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient); + + factory.create(request, savedObjectsService); + + expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, { + excludedWrappers: ['security'], + }); expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ - savedObjectsClient, + unsecuredSavedObjectsClient: savedObjectsClient, + request, logger: alertsClientFactoryParams.logger, taskManager: alertsClientFactoryParams.taskManager, alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, @@ -73,7 +118,7 @@ test('creates an alerts client with proper constructor arguments', async () => { test('getUserName() returns null when security is disabled', async () => { const factory = new AlertsClientFactory(); factory.initialize(alertsClientFactoryParams); - factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; const userNameResult = await constructorCall.getUserName(); @@ -86,7 +131,7 @@ test('getUserName() returns a name when security is enabled', async () => { ...alertsClientFactoryParams, securityPluginSetup, }); - factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; securityPluginSetup.authc.getCurrentUser.mockReturnValueOnce(({ @@ -99,7 +144,7 @@ test('getUserName() returns a name when security is enabled', async () => { test('getActionsClient() returns ActionsClient', async () => { const factory = new AlertsClientFactory(); factory.initialize(alertsClientFactoryParams); - factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; const actionsClient = await constructorCall.getActionsClient(); @@ -109,7 +154,7 @@ test('getActionsClient() returns ActionsClient', async () => { test('createAPIKey() returns { apiKeysEnabled: false } when security is disabled', async () => { const factory = new AlertsClientFactory(); factory.initialize(alertsClientFactoryParams); - factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; const createAPIKeyResult = await constructorCall.createAPIKey(); @@ -119,7 +164,7 @@ test('createAPIKey() returns { apiKeysEnabled: false } when security is disabled test('createAPIKey() returns { apiKeysEnabled: false } when security is enabled but ES security is disabled', async () => { const factory = new AlertsClientFactory(); factory.initialize(alertsClientFactoryParams); - factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce(null); @@ -133,7 +178,7 @@ test('createAPIKey() returns an API key when security is enabled', async () => { ...alertsClientFactoryParams, securityPluginSetup, }); - factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce({ @@ -154,7 +199,7 @@ test('createAPIKey() throws when security plugin createAPIKey throws an error', ...alertsClientFactoryParams, securityPluginSetup, }); - factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); + factory.create(KibanaRequest.from(fakeRequest), savedObjectsService); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockRejectedValueOnce( diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index 72a946d6c5155..c2adcc74f1473 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -19,6 +19,7 @@ function createSetupMock() { authz: { actions: mockAuthz.actions, checkPrivilegesWithRequest: mockAuthz.checkPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockAuthz.checkPrivilegesDynamicallyWithRequest, mode: mockAuthz.mode, }, registerSpacesService: jest.fn(), From c8e23f0dd4dd3457e2a5e2d418e5ec284598433f Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 21 May 2020 16:51:50 +0100 Subject: [PATCH 007/126] fixed unit test --- x-pack/plugins/security/server/plugin.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 3e30ff9447f3e..42c9b5c5be4e5 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -84,6 +84,7 @@ describe('Security Plugin', () => { "version": "version:version", "versionNumber": "version", }, + "checkPrivilegesDynamicallyWithRequest": [Function], "checkPrivilegesWithRequest": [Function], "mode": Object { "useRbacForRequest": [Function], From 1afad8ef15790073b1ced07fb09445ebf23cf9dd Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 3 Jun 2020 19:56:53 +0100 Subject: [PATCH 008/126] added rbac in alerting --- .../alerts/server/alert_type_registry.test.ts | 6 +- .../alerts/server/alert_type_registry.ts | 33 +- .../alerts/server/alerts_client.mock.ts | 1 + .../alerts/server/alerts_client.test.ts | 1852 ++++++++++++++++- x-pack/plugins/alerts/server/alerts_client.ts | 154 +- .../alerts/server/routes/create.test.ts | 7 - x-pack/plugins/alerts/server/routes/create.ts | 3 - .../alerts/server/routes/delete.test.ts | 7 - x-pack/plugins/alerts/server/routes/delete.ts | 3 - .../alerts/server/routes/disable.test.ts | 7 - .../plugins/alerts/server/routes/disable.ts | 3 - .../alerts/server/routes/enable.test.ts | 7 - x-pack/plugins/alerts/server/routes/enable.ts | 3 - .../plugins/alerts/server/routes/find.test.ts | 7 - x-pack/plugins/alerts/server/routes/find.ts | 3 - .../plugins/alerts/server/routes/get.test.ts | 7 - x-pack/plugins/alerts/server/routes/get.ts | 3 - .../server/routes/get_alert_state.test.ts | 7 - .../alerts/server/routes/get_alert_state.ts | 3 - .../server/routes/list_alert_types.test.ts | 7 - .../alerts/server/routes/list_alert_types.ts | 5 +- .../alerts/server/routes/mute_all.test.ts | 7 - .../plugins/alerts/server/routes/mute_all.ts | 3 - .../server/routes/mute_instance.test.ts | 7 - .../alerts/server/routes/mute_instance.ts | 3 - .../alerts/server/routes/unmute_all.test.ts | 7 - .../alerts/server/routes/unmute_all.ts | 3 - .../server/routes/unmute_instance.test.ts | 7 - .../alerts/server/routes/unmute_instance.ts | 3 - .../alerts/server/routes/update.test.ts | 7 - x-pack/plugins/alerts/server/routes/update.ts | 3 - .../server/routes/update_api_key.test.ts | 7 - .../alerts/server/routes/update_api_key.ts | 3 - x-pack/plugins/apm/server/feature.ts | 42 +- .../common/feature_kibana_privileges.ts | 53 + .../plugins/features/server/feature_schema.ts | 8 + x-pack/plugins/infra/server/features.ts | 25 +- .../__snapshots__/alerting.test.ts.snap | 35 + .../authorization/actions/actions.mock.ts | 35 + .../server/authorization/actions/actions.ts | 3 + .../authorization/actions/alerting.test.ts | 52 + .../server/authorization/actions/alerting.ts | 31 + .../server/authorization/index.mock.ts | 4 +- .../security/server/authorization/index.ts | 1 + .../alerting.test.ts | 311 +++ .../feature_privilege_builder/alerting.ts | 48 + .../feature_privilege_builder/index.ts | 2 + .../authorization/privileges/privileges.ts | 1 - x-pack/plugins/security/server/index.ts | 1 + x-pack/plugins/siem/server/plugin.ts | 12 +- x-pack/plugins/uptime/server/kibana.index.ts | 17 +- .../plugins/alerts/server/alert_types.ts | 20 +- .../fixtures/plugins/alerts/server/plugin.ts | 34 +- .../common/lib/alert_utils.ts | 12 +- .../common/lib/get_test_alert_data.ts | 2 +- .../common/lib/index.ts | 2 +- .../security_and_spaces/scenarios.ts | 2 + .../tests/alerting/alerts.ts | 133 +- .../tests/alerting/create.ts | 69 +- .../tests/alerting/delete.ts | 29 +- .../tests/alerting/disable.ts | 23 +- .../tests/alerting/enable.ts | 23 +- .../tests/alerting/find.ts | 43 +- .../security_and_spaces/tests/alerting/get.ts | 30 +- .../tests/alerting/get_alert_state.ts | 28 +- .../tests/alerting/list_alert_types.ts | 13 +- .../tests/alerting/mute_all.ts | 9 +- .../tests/alerting/mute_instance.ts | 17 +- .../tests/alerting/unmute_all.ts | 9 +- .../tests/alerting/unmute_instance.ts | 13 +- .../tests/alerting/update.ts | 64 +- .../tests/alerting/update_api_key.ts | 24 +- .../spaces_only/tests/alerting/create.ts | 2 +- .../spaces_only/tests/alerting/find.ts | 2 +- .../spaces_only/tests/alerting/get.ts | 2 +- .../tests/alerting/get_alert_state.ts | 2 +- .../tests/alerting/list_alert_types.ts | 2 +- .../spaces_only/tests/alerting/update.ts | 2 +- 78 files changed, 2933 insertions(+), 547 deletions(-) create mode 100644 x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap create mode 100644 x-pack/plugins/security/server/authorization/actions/actions.mock.ts create mode 100644 x-pack/plugins/security/server/authorization/actions/alerting.test.ts create mode 100644 x-pack/plugins/security/server/authorization/actions/alerting.ts create mode 100644 x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts create mode 100644 x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts diff --git a/x-pack/plugins/alerts/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts index 6d7cf621ab0ca..ae3633cdde62b 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts @@ -177,7 +177,7 @@ describe('list()', () => { test('should return empty when nothing is registered', () => { const registry = new AlertTypeRegistry(alertTypeRegistryParams); const result = registry.list(); - expect(result).toMatchInlineSnapshot(`Array []`); + expect(result).toMatchInlineSnapshot(`Set {}`); }); test('should return registered types', () => { @@ -197,7 +197,7 @@ describe('list()', () => { }); const result = registry.list(); expect(result).toMatchInlineSnapshot(` - Array [ + Set { Object { "actionGroups": Array [ Object { @@ -214,7 +214,7 @@ describe('list()', () => { "name": "Test", "producer": "alerting", }, - ] + } `); }); diff --git a/x-pack/plugins/alerts/server/alert_type_registry.ts b/x-pack/plugins/alerts/server/alert_type_registry.ts index 8f36afe062aa5..300cfc5b5f549 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.ts @@ -15,6 +15,14 @@ interface ConstructorOptions { taskRunnerFactory: TaskRunnerFactory; } +export interface RegistryAlertType + extends Pick< + AlertType, + 'name' | 'actionGroups' | 'defaultActionGroupId' | 'actionVariables' | 'producer' + > { + id: string; +} + export class AlertTypeRegistry { private readonly taskManager: TaskManagerSetupContract; private readonly alertTypes: Map = new Map(); @@ -66,15 +74,22 @@ export class AlertTypeRegistry { return this.alertTypes.get(id)!; } - public list() { - return Array.from(this.alertTypes).map(([alertTypeId, alertType]) => ({ - id: alertTypeId, - name: alertType.name, - actionGroups: alertType.actionGroups, - defaultActionGroupId: alertType.defaultActionGroupId, - actionVariables: alertType.actionVariables, - producer: alertType.producer, - })); + public list(): Set { + return new Set( + Array.from(this.alertTypes).map( + ([id, { name, actionGroups, defaultActionGroupId, actionVariables, producer }]: [ + string, + AlertType + ]) => ({ + id, + name, + actionGroups, + defaultActionGroupId, + actionVariables, + producer, + }) + ) + ); } } diff --git a/x-pack/plugins/alerts/server/alerts_client.mock.ts b/x-pack/plugins/alerts/server/alerts_client.mock.ts index 1848b3432ae5a..be70e441b6fc5 100644 --- a/x-pack/plugins/alerts/server/alerts_client.mock.ts +++ b/x-pack/plugins/alerts/server/alerts_client.mock.ts @@ -24,6 +24,7 @@ const createAlertsClientMock = () => { unmuteAll: jest.fn(), muteInstance: jest.fn(), unmuteInstance: jest.fn(), + listAlertTypes: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 0e19708947584..4abf9bcd12b78 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -6,15 +6,17 @@ import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; import { KibanaRequest } from 'kibana/server'; -import { AlertsClient, CreateOptions } from './alerts_client'; +import { AlertsClient, CreateOptions, UpdateOptions, FindOptions } from './alerts_client'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../src/core/server/mocks'; import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; import { TaskStatus } from '../../task_manager/server'; -import { IntervalSchedule } from './types'; +import { IntervalSchedule, PartialAlert } from './types'; import { resolvable } from './test_utils'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { actionsClientMock } from '../../actions/server/mocks'; +import { SecurityPluginSetup } from '../../../plugins/security/server'; +import { securityMock } from '../../../plugins/security/server/mocks'; const taskManager = taskManagerMock.start(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -36,6 +38,15 @@ const alertsClientParams = { getActionsClient: jest.fn(), }; +function mockAuthorization() { + const authorization = securityMock.createSetup().authz; + // typescript is havingtrouble inferring jest's automocking + (authorization.actions.alerting.get as jest.MockedFunction< + typeof authorization.actions.alerting.get + >).mockImplementation((type, app, operation) => `${type}${app ? `/${app}` : ``}/${operation}`); + return authorization; +} + beforeEach(() => { jest.resetAllMocks(); alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false }); @@ -126,6 +137,185 @@ describe('create()', () => { }); }); + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + }); + + function tryToExecuteOperation(options: CreateOptions): Promise { + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: '2019-02-12T21:01:22.479Z', + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + scheduledTaskId: 'task-123', + }, + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + }); + + return alertsClientWithAuthorization.create(options); + } + + test('create when user is authorised to create this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/create', + authorized: false, + }, + { + privilege: 'myType/myApp/create', + authorized: true, + }, + ], + }); + + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myApp', + }); + + await tryToExecuteOperation({ data }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'create' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); + }); + + test('create when user is authorised to create this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/create', + authorized: true, + }, + { + privilege: 'myType/myApp/create', + authorized: false, + }, + ], + }); + + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myApp', + }); + + await tryToExecuteOperation({ data }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'create' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); + }); + + test('throws when user is not authorised to create this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/create', + authorized: false, + }, + { + privilege: 'myType/myApp/create', + authorized: false, + }, + ], + }); + + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myApp', + }); + + await expect(tryToExecuteOperation({ data })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to create a "myType" alert for "myApp"]` + ); + }); + }); + test('creates an alert', async () => { const data = getMockData(); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ @@ -922,8 +1112,9 @@ describe('enable()', () => { id: '1', type: 'alert', attributes: { + consumer: 'myApp', schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', enabled: false, }, version: '123', @@ -952,6 +1143,117 @@ describe('enable()', () => { }); }); + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + alertsClientParams.createAPIKey.mockResolvedValue({ + apiKeysEnabled: false, + }); + taskManager.schedule.mockResolvedValue({ + id: 'task-123', + scheduledAt: new Date(), + attempts: 0, + status: TaskStatus.Idle, + runAt: new Date(), + state: {}, + params: {}, + taskType: '', + startedAt: null, + retryAt: null, + ownerId: null, + }); + }); + + test('enable when user is authorised to enable this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/enable', + authorized: false, + }, + { + privilege: 'myType/myApp/enable', + authorized: true, + }, + ], + }); + + await alertsClientWithAuthorization.enable({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'enable' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'enable'); + }); + + test('enable when user is authorised to enable this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/enable', + authorized: true, + }, + { + privilege: 'myType/myApp/enable', + authorized: false, + }, + ], + }); + + await alertsClientWithAuthorization.enable({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'enable' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'enable'); + }); + + test('throws when user is not authorised to enable this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/enable', + authorized: false, + }, + { + privilege: 'myType/myApp/enable', + authorized: false, + }, + ], + }); + + expect(alertsClientWithAuthorization.enable({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to enable a "myType" alert for "myApp"]` + ); + }); + }); + test('enables an alert', async () => { await alertsClient.enable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); @@ -965,7 +1267,8 @@ describe('enable()', () => { '1', { schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', + consumer: 'myApp', enabled: true, updatedBy: 'elastic', apiKey: null, @@ -976,7 +1279,7 @@ describe('enable()', () => { } ); expect(taskManager.schedule).toHaveBeenCalledWith({ - taskType: `alerting:2`, + taskType: `alerting:myType`, params: { alertId: '1', spaceId: 'default', @@ -1038,7 +1341,8 @@ describe('enable()', () => { '1', { schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', + consumer: 'myApp', enabled: true, apiKey: Buffer.from('123:abc').toString('base64'), apiKeyOwner: 'elastic', @@ -1124,8 +1428,9 @@ describe('disable()', () => { id: '1', type: 'alert', attributes: { + consumer: 'myApp', schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', enabled: true, scheduledTaskId: 'task-123', }, @@ -1146,6 +1451,117 @@ describe('disable()', () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); }); + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + alertsClientParams.createAPIKey.mockResolvedValue({ + apiKeysEnabled: false, + }); + taskManager.schedule.mockResolvedValue({ + id: 'task-123', + scheduledAt: new Date(), + attempts: 0, + status: TaskStatus.Idle, + runAt: new Date(), + state: {}, + params: {}, + taskType: '', + startedAt: null, + retryAt: null, + ownerId: null, + }); + }); + + test('disables when user is authorised to disable this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/disable', + authorized: false, + }, + { + privilege: 'myType/myApp/disable', + authorized: true, + }, + ], + }); + + await alertsClientWithAuthorization.disable({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'disable' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'disable'); + }); + + test('disables when user is authorised to disable this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/disable', + authorized: true, + }, + { + privilege: 'myType/myApp/disable', + authorized: false, + }, + ], + }); + + await alertsClientWithAuthorization.disable({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'disable' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'disable'); + }); + + test('throws when user is not authorised to disable this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/disable', + authorized: false, + }, + { + privilege: 'myType/myApp/disable', + authorized: false, + }, + ], + }); + + expect(alertsClientWithAuthorization.disable({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to disable a "myType" alert for "myApp"]` + ); + }); + }); + test('disables an alert', async () => { await alertsClient.disable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); @@ -1156,8 +1572,9 @@ describe('disable()', () => { 'alert', '1', { + consumer: 'myApp', schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', apiKey: null, apiKeyOwner: null, enabled: false, @@ -1184,8 +1601,9 @@ describe('disable()', () => { 'alert', '1', { + consumer: 'myApp', schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', apiKey: null, apiKeyOwner: null, enabled: false, @@ -1279,6 +1697,109 @@ describe('muteAll()', () => { updatedBy: 'elastic', }); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + muteAll: false, + }, + references: [], + }); + }); + + test('mutes when user is authorised to muteAll this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/muteAll', + authorized: false, + }, + { + privilege: 'myType/myApp/muteAll', + authorized: true, + }, + ], + }); + + await alertsClientWithAuthorization.muteAll({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'muteAll' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); + }); + + test('mutes when user is authorised to muteAll this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/muteAll', + authorized: true, + }, + { + privilege: 'myType/myApp/muteAll', + authorized: false, + }, + ], + }); + + await alertsClientWithAuthorization.muteAll({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'muteAll' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); + }); + + test('throws when user is not authorised to muteAll this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/muteAll', + authorized: false, + }, + { + privilege: 'myType/myApp/muteAll', + authorized: false, + }, + ], + }); + + expect(alertsClientWithAuthorization.muteAll({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to muteAll a "myType" alert for "myApp"]` + ); + }); + }); }); describe('unmuteAll()', () => { @@ -1300,6 +1821,117 @@ describe('unmuteAll()', () => { updatedBy: 'elastic', }); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + muteAll: true, + }, + references: [], + }); + }); + + test('unmutes when user is authorised to unmuteAll this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/unmuteAll', + authorized: false, + }, + { + privilege: 'myType/myApp/unmuteAll', + authorized: true, + }, + ], + }); + + await alertsClientWithAuthorization.unmuteAll({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'unmuteAll' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'unmuteAll' + ); + }); + + test('unmutes when user is authorised to unmuteAll this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/unmuteAll', + authorized: true, + }, + { + privilege: 'myType/myApp/unmuteAll', + authorized: false, + }, + ], + }); + + await alertsClientWithAuthorization.unmuteAll({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'unmuteAll' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'unmuteAll' + ); + }); + + test('throws when user is not authorised to unmuteAll this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/unmuteAll', + authorized: false, + }, + { + privilege: 'myType/myApp/unmuteAll', + authorized: false, + }, + ], + }); + + expect(alertsClientWithAuthorization.unmuteAll({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to unmuteAll a "myType" alert for "myApp"]` + ); + }); + }); }); describe('muteInstance()', () => { @@ -1369,6 +2001,119 @@ describe('muteInstance()', () => { await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + muteAll: true, + }, + references: [], + }); + }); + + test('mutes instance when user is authorised to mute an instance on this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/muteInstance', + authorized: false, + }, + { + privilege: 'myType/myApp/muteInstance', + authorized: true, + }, + ], + }); + + await alertsClientWithAuthorization.muteInstance({ alertId: '1', alertInstanceId: '2' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'muteInstance' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'muteInstance' + ); + }); + + test('mutes instance when user is authorised to mute an instance on this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/muteInstance', + authorized: true, + }, + { + privilege: 'myType/myApp/muteInstance', + authorized: false, + }, + ], + }); + + await alertsClientWithAuthorization.muteInstance({ alertId: '1', alertInstanceId: '2' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'muteInstance' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'muteInstance' + ); + }); + + test('throws when user is not authorised to mute an instance on this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/muteInstance', + authorized: false, + }, + { + privilege: 'myType/myApp/muteInstance', + authorized: false, + }, + ], + }); + + expect( + alertsClientWithAuthorization.muteInstance({ alertId: '1', alertInstanceId: '2' }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to muteInstance a "myType" alert for "myApp"]` + ); + }); + }); }); describe('unmuteInstance()', () => { @@ -1438,6 +2183,119 @@ describe('unmuteInstance()', () => { await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + muteAll: true, + }, + references: [], + }); + }); + + test('unmutes instance when user is authorised to unmutes an instance on this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/unmuteInstance', + authorized: false, + }, + { + privilege: 'myType/myApp/unmuteInstance', + authorized: true, + }, + ], + }); + + await alertsClientWithAuthorization.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'unmuteInstance' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'unmuteInstance' + ); + }); + + test('unmutes instance when user is authorised to unmutes an instance on this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/unmuteInstance', + authorized: true, + }, + { + privilege: 'myType/myApp/unmuteInstance', + authorized: false, + }, + ], + }); + + await alertsClientWithAuthorization.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'unmuteInstance' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'unmuteInstance' + ); + }); + + test('throws when user is not authorised to unmutes an instance on this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/unmuteInstance', + authorized: false, + }, + { + privilege: 'myType/myApp/unmuteInstance', + authorized: false, + }, + ], + }); + + expect( + alertsClientWithAuthorization.unmuteInstance({ alertId: '1', alertInstanceId: '2' }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to unmuteInstance a "myType" alert for "myApp"]` + ); + }); + }); }); describe('get()', () => { @@ -1530,6 +2388,121 @@ describe('get()', () => { `"Reference action_0 not found"` ); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + }); + + function tryToExecuteOperation(): Promise { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + return alertsClientWithAuthorization.get({ id: '1' }); + } + + test('gets when user is authorised to get this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/get', + authorized: false, + }, + { + privilege: 'myType/myApp/get', + authorized: true, + }, + ], + }); + + await tryToExecuteOperation(); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); + }); + + test('gets when user is authorised to get this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/get', + authorized: true, + }, + { + privilege: 'myType/myApp/get', + authorized: false, + }, + ], + }); + + await tryToExecuteOperation(); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); + }); + + test('throws when user is not authorised to get this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/get', + authorized: false, + }, + { + privilege: 'myType/myApp/get', + authorized: false, + }, + ], + }); + + await expect(tryToExecuteOperation()).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "myType" alert for "myApp"]` + ); + }); + }); }); describe('getAlertState()', () => { @@ -1644,10 +2617,137 @@ describe('getAlertState()', () => { expect(taskManager.get).toHaveBeenCalledTimes(1); expect(taskManager.get).toHaveBeenCalledWith(scheduledTaskId); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + }); + + function tryToExecuteOperation(): Promise { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + return alertsClientWithAuthorization.getAlertState({ id: '1' }); + } + + test('gets AlertState when user is authorised to get this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/get', + authorized: false, + }, + { + privilege: 'myType/myApp/get', + authorized: true, + }, + ], + }); + + await tryToExecuteOperation(); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); + }); + + test('gets AlertState when user is authorised to get this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/get', + authorized: true, + }, + { + privilege: 'myType/myApp/get', + authorized: false, + }, + ], + }); + + await tryToExecuteOperation(); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); + }); + + test('throws when user is not authorised to get this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/get', + authorized: false, + }, + { + privilege: 'myType/myApp/get', + authorized: false, + }, + ], + }); + + await expect(tryToExecuteOperation()).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "myType" alert for "myApp"]` + ); + }); + }); }); describe('find()', () => { test('calls saved objects client with given params', async () => { + alertTypeRegistry.list.mockReturnValue( + new Set([ + { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myType', + name: 'myType', + producer: 'myApp', + }, + ]) + ); const alertsClient = new AlertsClient(alertsClientParams); unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ total: 1, @@ -1658,7 +2758,7 @@ describe('find()', () => { id: '1', type: 'alert', attributes: { - alertTypeId: '123', + alertTypeId: 'myType', schedule: { interval: '10s' }, params: { bar: true, @@ -1697,7 +2797,7 @@ describe('find()', () => { }, }, ], - "alertTypeId": "123", + "alertTypeId": "myType", "createdAt": 2019-02-12T21:01:22.479Z, "id": "1", "params": Object { @@ -1709,19 +2809,207 @@ describe('find()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, }, ], - "page": 1, - "perPage": 10, - "total": 1, - } - `); - expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "type": "alert", - }, - ] - `); + "page": 1, + "perPage": 10, + "total": 1, + } + `); + expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "filter": "alert.attributes.alertTypeId:(myType)", + "type": "alert", + }, + ] + `); + }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + function mockAlertSavedObject(alertTypeId: string) { + return { + id: uuid.v4(), + type: 'alert', + attributes: { + alertTypeId, + schedule: { interval: '10s' }, + params: {}, + actions: [], + }, + references: [], + }; + } + + beforeEach(() => { + authorization = mockAuthorization(); + + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + const myType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myType', + name: 'myType', + producer: 'myApp', + }; + const anUnauthorizedType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'anUnauthorizedType', + name: 'anUnauthorizedType', + producer: 'anUnauthorizedApp', + }; + const setOfAlertTypes = new Set([anUnauthorizedType, myType]); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + }); + + function tryToExecuteOperation( + options?: FindOptions, + savedObjects: Array> = [ + mockAlertSavedObject('myType'), + ] + ): Promise { + unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ + total: 1, + per_page: 10, + page: 1, + saved_objects: savedObjects, + }); + return alertsClientWithAuthorization.find({ options }); + } + + test('includes types that a user is authorised to find under their producer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/find', + authorized: false, + }, + { + privilege: 'myType/myApp/find', + authorized: true, + }, + { + privilege: 'anUnauthorizedType/find', + authorized: false, + }, + { + privilege: 'anUnauthorizedType/anUnauthorizedApp/find', + authorized: false, + }, + ], + }); + + await tryToExecuteOperation(); + + expect(unsecuredSavedObjectsClient.find.mock.calls[0][0].filter).toMatchInlineSnapshot( + `"alert.attributes.alertTypeId:(myType)"` + ); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'find'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'find'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'anUnauthorizedType', + undefined, + 'find' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'anUnauthorizedType', + 'anUnauthorizedApp', + 'find' + ); + }); + + test('includes types that a user is authorised to get globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/find', + authorized: true, + }, + { + privilege: 'myType/myApp/find', + authorized: false, + }, + { + privilege: 'anUnauthorizedType/find', + authorized: false, + }, + { + privilege: 'anUnauthorizedType/anUnauthorizedApp/find', + authorized: false, + }, + ], + }); + + await tryToExecuteOperation(); + + expect(unsecuredSavedObjectsClient.find.mock.calls[0][0].filter).toMatchInlineSnapshot( + `"alert.attributes.alertTypeId:(myType)"` + ); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'find'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'find'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'anUnauthorizedType', + undefined, + 'find' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'anUnauthorizedType', + 'anUnauthorizedApp', + 'find' + ); + }); + + test('throws if a result contains a type the user is not authorised to find', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/find', + authorized: true, + }, + { + privilege: 'myType/myApp/find', + authorized: true, + }, + { + privilege: 'anUnauthorizedType/find', + authorized: false, + }, + { + privilege: 'anUnauthorizedType/anUnauthorizedApp/find', + authorized: false, + }, + ], + }); + + await expect( + tryToExecuteOperation({}, [ + mockAlertSavedObject('myType'), + mockAlertSavedObject('anUnauthorizedType'), + ]) + ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to find "anUnauthorizedType" alerts]`); + }); }); }); @@ -1731,7 +3019,8 @@ describe('delete()', () => { id: '1', type: 'alert', attributes: { - alertTypeId: '123', + alertTypeId: 'myType', + consumer: 'myApp', schedule: { interval: '10s' }, params: { bar: true, @@ -1860,6 +3149,97 @@ describe('delete()', () => { `"TM Fail"` ); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + }); + + test('deletes when user is authorised to delete this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/delete', + authorized: false, + }, + { + privilege: 'myType/myApp/delete', + authorized: true, + }, + ], + }); + + await alertsClientWithAuthorization.delete({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'delete' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'delete'); + }); + + test('deletes when user is authorised to delete this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/delete', + authorized: true, + }, + { + privilege: 'myType/myApp/delete', + authorized: false, + }, + ], + }); + + await alertsClientWithAuthorization.delete({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'delete' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'delete'); + }); + + test('throws when user is not authorised to delete this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/delete', + authorized: false, + }, + { + privilege: 'myType/myApp/delete', + authorized: false, + }, + ], + }); + + expect(alertsClientWithAuthorization.delete({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to delete a "myType" alert for "myApp"]` + ); + }); + }); }); describe('update()', () => { @@ -1869,7 +3249,8 @@ describe('update()', () => { type: 'alert', attributes: { enabled: true, - alertTypeId: '123', + alertTypeId: 'myType', + consumer: 'myApp', scheduledTaskId: 'task-123', }, references: [], @@ -2067,9 +3448,10 @@ describe('update()', () => { }, }, ], - "alertTypeId": "123", + "alertTypeId": "myType", "apiKey": null, "apiKeyOwner": null, + "consumer": "myApp", "enabled": true, "name": "abc", "params": Object { @@ -2222,9 +3604,10 @@ describe('update()', () => { }, }, ], - "alertTypeId": "123", + "alertTypeId": "myType", "apiKey": "MTIzOmFiYw==", "apiKeyOwner": "elastic", + "consumer": "myApp", "enabled": true, "name": "abc", "params": Object { @@ -2371,9 +3754,10 @@ describe('update()', () => { }, }, ], - "alertTypeId": "123", + "alertTypeId": "myType", "apiKey": null, "apiKeyOwner": null, + "consumer": "myApp", "enabled": false, "name": "abc", "params": Object { @@ -2857,6 +4241,206 @@ describe('update()', () => { ); }); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + }); + + function tryToExecuteOperation(options: UpdateOptions): Promise { + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + actionTypeId: 'test', + }, + references: [], + }, + { + id: '2', + type: 'action', + attributes: { + actionTypeId: 'test2', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: true, + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_1', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_2', + actionTypeId: 'test2', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: 'task-123', + createdAt: new Date().toISOString(), + }, + updated_at: new Date().toISOString(), + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + { + name: 'action_1', + type: 'action', + id: '1', + }, + { + name: 'action_2', + type: 'action', + id: '2', + }, + ], + }); + return alertsClientWithAuthorization.update(options); + } + + test('updates when user is authorised to update this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/update', + authorized: false, + }, + { + privilege: 'myType/myApp/update', + authorized: true, + }, + ], + }); + + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myApp', + }); + + await tryToExecuteOperation({ + id: '1', + data, + }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'update' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'update'); + }); + + test('updates when user is authorised to update this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/update', + authorized: true, + }, + { + privilege: 'myType/myApp/update', + authorized: false, + }, + ], + }); + + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myApp', + }); + + await tryToExecuteOperation({ + id: '1', + data, + }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'update' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'update'); + }); + + test('throws when user is not authorised to update this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/update', + authorized: false, + }, + { + privilege: 'myType/myApp/update', + authorized: false, + }, + ], + }); + + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myApp', + }); + + await expect( + tryToExecuteOperation({ + id: '1', + data, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to update a "myType" alert for "myApp"]` + ); + }); + }); }); describe('updateApiKey()', () => { @@ -2866,7 +4450,8 @@ describe('updateApiKey()', () => { type: 'alert', attributes: { schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', + consumer: 'myApp', enabled: true, }, version: '123', @@ -2901,7 +4486,8 @@ describe('updateApiKey()', () => { '1', { schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', + consumer: 'myApp', enabled: true, apiKey: Buffer.from('234:abc').toString('base64'), apiKeyOwner: 'elastic', @@ -2925,7 +4511,8 @@ describe('updateApiKey()', () => { '1', { schedule: { interval: '10s' }, - alertTypeId: '2', + alertTypeId: 'myType', + consumer: 'myApp', enabled: true, apiKey: Buffer.from('234:abc').toString('base64'), apiKeyOwner: 'elastic', @@ -2965,4 +4552,205 @@ describe('updateApiKey()', () => { ); expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + }); + + test('updates when user is authorised to updateApiKey this type of alert type for the specified consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/updateApiKey', + authorized: false, + }, + { + privilege: 'myType/myApp/updateApiKey', + authorized: true, + }, + ], + }); + + await alertsClientWithAuthorization.updateApiKey({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'updateApiKey' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'updateApiKey' + ); + }); + + test('updates when user is authorised to updateApiKey this type of alert type globally', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/updateApiKey', + authorized: true, + }, + { + privilege: 'myType/myApp/updateApiKey', + authorized: false, + }, + ], + }); + + await alertsClientWithAuthorization.updateApiKey({ id: '1' }); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + undefined, + 'updateApiKey' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'updateApiKey' + ); + }); + + test('throws when user is not authorised to updateApiKey this type of alert at all', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/updateApiKey', + authorized: false, + }, + { + privilege: 'myType/myApp/updateApiKey', + authorized: false, + }, + ], + }); + + expect(alertsClientWithAuthorization.updateApiKey({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to updateApiKey a "myType" alert for "myApp"]` + ); + }); + }); +}); + +describe('listAlertTypes', () => { + let alertsClient: AlertsClient; + const alertingAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'alertingAlertType', + name: 'alertingAlertType', + producer: 'alerting', + }; + const myAppAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + }; + const setOfAlertTypes = new Set([myAppAlertType, alertingAlertType]); + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + }); + + test('should return a list of AlertTypes that exist in the registry', async () => { + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + expect(await alertsClient.listAlertTypes()).toEqual(setOfAlertTypes); + }); + + describe('authorization', () => { + let authorization: jest.Mocked; + let alertsClientWithAuthorization: AlertsClient; + let checkPrivileges: jest.MockedFunction>; + + beforeEach(() => { + authorization = mockAuthorization(); + alertsClientWithAuthorization = new AlertsClient({ + authorization, + ...alertsClientParams, + }); + checkPrivileges = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + }); + + test('should return a list of AlertTypes that exist in the registry only if the user is authorised to get them', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myAppAlertType/get', + authorized: false, + }, + { + privilege: 'myAppAlertType/alerting/get', + authorized: false, + }, + { + privilege: 'alertingAlertType/get', + authorized: true, + }, + { + privilege: 'alertingAlertType/alerting/get', + authorized: true, + }, + ], + }); + + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + expect(await alertsClientWithAuthorization.listAlertTypes()).toEqual( + new Set([alertingAlertType]) + ); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myAppAlertType', + 'myApp', + 'get' + ); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myAppAlertType', + undefined, + 'get' + ); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'alertingAlertType', + 'alerting', + 'get' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'alertingAlertType', + undefined, + 'get' + ); + }); + }); }); diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 5fcaac17a7d32..f1ea77829a5be 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -37,6 +37,8 @@ import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/serve import { TaskManagerStartContract } from '../../task_manager/server'; import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists'; +import { CheckPrivilegesResponse } from '../../security/server'; +import { RegistryAlertType } from './alert_type_registry'; type NormalizedAlertAction = Omit; export type CreateAPIKeyResult = @@ -113,7 +115,7 @@ export interface CreateOptions { }; } -interface UpdateOptions { +export interface UpdateOptions { id: string; data: { name: string; @@ -132,8 +134,8 @@ export class AlertsClient { private readonly namespace?: string; private readonly taskManager: TaskManagerStartContract; private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; - // private readonly request: KibanaRequest; - // private readonly authorization?: SecurityPluginSetup['authz']; + private readonly request: KibanaRequest; + private readonly authorization?: SecurityPluginSetup['authz']; private readonly alertTypeRegistry: AlertTypeRegistry; private readonly createAPIKey: () => Promise; private readonly invalidateAPIKey: ( @@ -145,8 +147,8 @@ export class AlertsClient { constructor({ alertTypeRegistry, unsecuredSavedObjectsClient, - // request, - // authorization, + request, + authorization, taskManager, logger, spaceId, @@ -164,8 +166,8 @@ export class AlertsClient { this.taskManager = taskManager; this.alertTypeRegistry = alertTypeRegistry; this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; - // this.request = request; - // this.authorization = authorization; + this.request = request; + this.authorization = authorization; this.createAPIKey = createAPIKey; this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; @@ -174,6 +176,7 @@ export class AlertsClient { public async create({ data, options }: CreateOptions): Promise { // Throws an error if alert type isn't registered + await this.ensureAuthorized(data.alertTypeId, data.consumer, 'create'); const alertType = this.alertTypeRegistry.get(data.alertTypeId); const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); const username = await this.getUserName(); @@ -228,6 +231,7 @@ export class AlertsClient { public async get({ id }: { id: string }): Promise { const result = await this.unsecuredSavedObjectsClient.get('alert', id); + await this.ensureAuthorized(result.attributes.alertTypeId, result.attributes.consumer, 'get'); return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references); } @@ -242,7 +246,28 @@ export class AlertsClient { } } - public async find({ options = {} }: { options: FindOptions }): Promise { + public async find({ + options: { filter, ...options } = {}, + }: { options?: FindOptions } = {}): Promise { + const filters = filter ? [filter] : []; + + const authorizedAlertTypes = new Set( + pluck([...(await this.filterByAuthorized(this.alertTypeRegistry.list(), 'find'))], 'id') + ); + + if (!authorizedAlertTypes.size) { + // the current user isn't authorized to get any alertTypes + // we can short circuit here + return { + page: 0, + perPage: 0, + total: 0, + data: [], + }; + } + + filters.push(`alert.attributes.alertTypeId:(${[...authorizedAlertTypes].join(' or ')})`); + const { page, per_page: perPage, @@ -250,6 +275,7 @@ export class AlertsClient { saved_objects: data, } = await this.unsecuredSavedObjectsClient.find({ ...options, + filter: filters.join(` and `), type: 'alert', }); @@ -257,15 +283,19 @@ export class AlertsClient { page, perPage, total, - data: data.map(({ id, attributes, updated_at, references }) => - this.getAlertFromRaw(id, attributes, updated_at, references) - ), + data: data.map(({ id, attributes, updated_at, references }) => { + if (!authorizedAlertTypes.has(attributes.alertTypeId)) { + throw Boom.forbidden(`Unauthorized to find "${attributes.alertTypeId}" alerts`); + } + return this.getAlertFromRaw(id, attributes, updated_at, references); + }), }; } public async delete({ id }: { id: string }) { let taskIdToRemove: string | undefined; let apiKeyToInvalidate: string | null = null; + let attributes: RawAlert; try { const decryptedAlert = await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser< @@ -273,6 +303,7 @@ export class AlertsClient { >('alert', id, { namespace: this.namespace }); apiKeyToInvalidate = decryptedAlert.attributes.apiKey; taskIdToRemove = decryptedAlert.attributes.scheduledTaskId; + attributes = decryptedAlert.attributes; } catch (e) { // We'll skip invalidating the API key since we failed to load the decrypted saved object this.logger.error( @@ -281,8 +312,11 @@ export class AlertsClient { // Still attempt to load the scheduledTaskId using SOC const alert = await this.unsecuredSavedObjectsClient.get('alert', id); taskIdToRemove = alert.attributes.scheduledTaskId; + attributes = alert.attributes; } + await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'delete'); + const removeResult = await this.unsecuredSavedObjectsClient.delete('alert', id); await Promise.all([ @@ -308,6 +342,11 @@ export class AlertsClient { // Still attempt to load the object using SOC alertSavedObject = await this.unsecuredSavedObjectsClient.get('alert', id); } + await this.ensureAuthorized( + alertSavedObject.attributes.alertTypeId, + alertSavedObject.attributes.consumer, + 'update' + ); const updateResult = await this.updateAlert({ id, data }, alertSavedObject); @@ -409,6 +448,7 @@ export class AlertsClient { attributes = alert.attributes; version = alert.version; } + await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'updateApiKey'); const username = await this.getUserName(); await this.unsecuredSavedObjectsClient.update( @@ -468,6 +508,8 @@ export class AlertsClient { version = alert.version; } + await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'enable'); + if (attributes.enabled === false) { const username = await this.getUserName(); await this.unsecuredSavedObjectsClient.update( @@ -514,6 +556,8 @@ export class AlertsClient { version = alert.version; } + await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'disable'); + if (attributes.enabled === true) { await this.unsecuredSavedObjectsClient.update( 'alert', @@ -539,6 +583,9 @@ export class AlertsClient { } public async muteAll({ id }: { id: string }) { + const { attributes } = await this.unsecuredSavedObjectsClient.get('alert', id); + await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'muteAll'); + await this.unsecuredSavedObjectsClient.update('alert', id, { muteAll: true, mutedInstanceIds: [], @@ -547,6 +594,9 @@ export class AlertsClient { } public async unmuteAll({ id }: { id: string }) { + const { attributes } = await this.unsecuredSavedObjectsClient.get('alert', id); + await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'unmuteAll'); + await this.unsecuredSavedObjectsClient.update('alert', id, { muteAll: false, mutedInstanceIds: [], @@ -559,6 +609,9 @@ export class AlertsClient { 'alert', alertId ); + + await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'muteInstance'); + const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) { mutedInstanceIds.push(alertInstanceId); @@ -585,6 +638,7 @@ export class AlertsClient { 'alert', alertId ); + await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'unmuteInstance'); const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { await this.unsecuredSavedObjectsClient.update( @@ -600,18 +654,72 @@ export class AlertsClient { } } - // private async ensureAuthorized(alertTypeId: string, operation: string) { - // if (this.authorization == null) { - // return; - // } - // const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest(this.request); - // const { hasAllRequested } = await checkPrivileges( - // this.authorization.actions.savedObject.get(alertTypeId, operation) - // ); - // if (!hasAllRequested) { - // throw Boom.forbidden(`Unable to ${operation} ${alertTypeId}`); - // } - // } + public async listAlertTypes() { + return await this.filterByAuthorized(this.alertTypeRegistry.list(), 'get'); + } + + private async ensureAuthorized(alertTypeId: string, consumer: string, operation: string) { + if (this.authorization) { + const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( + this.request + ); + if ( + !this.hasAnyPrivilege( + await checkPrivileges([ + // check for global access + this.authorization.actions.alerting.get(alertTypeId, undefined, operation), + // check for access at consumer level + this.authorization.actions.alerting.get(alertTypeId, consumer, operation), + ]) + ) + ) { + throw Boom.forbidden( + `Unauthorized to ${operation} a "${alertTypeId}" alert for "${consumer}"` + ); + } + } + } + + private async filterByAuthorized( + alertTypes: Set, + operation: string + ): Promise> { + if (!this.authorization) { + return alertTypes; + } + + const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest(this.request); + + const privilegeToAlertType = Array.from(alertTypes).reduce((privileges, alertType) => { + // check for global access + privileges.set( + this.authorization!.actions.alerting.get(alertType.id, undefined, operation), + alertType + ); + // check for access within the producer level + privileges.set( + this.authorization!.actions.alerting.get(alertType.id, alertType.producer, operation), + alertType + ); + return privileges; + }, new Map()); + const { hasAllRequested, privileges } = await checkPrivileges([...privilegeToAlertType.keys()]); + return hasAllRequested + ? alertTypes + : privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { + if (authorized && privilegeToAlertType.has(privilege)) { + authorizedAlertTypes.add(privilegeToAlertType.get(privilege)!); + } + return authorizedAlertTypes; + }, new Set()); + } + + private hasAnyPrivilege(checkPrivilegesResponse: CheckPrivilegesResponse): boolean { + return ( + checkPrivilegesResponse.hasAllRequested || + checkPrivilegesResponse.privileges.some(({ authorized }) => authorized) + ); + } private async scheduleAlert(id: string, alertTypeId: string) { return await this.taskManager.schedule({ diff --git a/x-pack/plugins/alerts/server/routes/create.test.ts b/x-pack/plugins/alerts/server/routes/create.test.ts index 9e941903eeaed..274acaf01c475 100644 --- a/x-pack/plugins/alerts/server/routes/create.test.ts +++ b/x-pack/plugins/alerts/server/routes/create.test.ts @@ -75,13 +75,6 @@ describe('createAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.create.mockResolvedValueOnce(createResult); diff --git a/x-pack/plugins/alerts/server/routes/create.ts b/x-pack/plugins/alerts/server/routes/create.ts index 6238fca024e55..91a81f6d84b71 100644 --- a/x-pack/plugins/alerts/server/routes/create.ts +++ b/x-pack/plugins/alerts/server/routes/create.ts @@ -47,9 +47,6 @@ export const createAlertRoute = (router: IRouter, licenseState: LicenseState) => validate: { body: bodySchema, }, - options: { - tags: ['access:alerting-all'], - }, }, handleDisabledApiKeysError( router.handleLegacyErrors(async function ( diff --git a/x-pack/plugins/alerts/server/routes/delete.test.ts b/x-pack/plugins/alerts/server/routes/delete.test.ts index 9ba4e20312e17..d9c5aa2d59c87 100644 --- a/x-pack/plugins/alerts/server/routes/delete.test.ts +++ b/x-pack/plugins/alerts/server/routes/delete.test.ts @@ -30,13 +30,6 @@ describe('deleteAlertRoute', () => { const [config, handler] = router.delete.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.delete.mockResolvedValueOnce({}); diff --git a/x-pack/plugins/alerts/server/routes/delete.ts b/x-pack/plugins/alerts/server/routes/delete.ts index 2034bd21fbed6..b073c59149171 100644 --- a/x-pack/plugins/alerts/server/routes/delete.ts +++ b/x-pack/plugins/alerts/server/routes/delete.ts @@ -27,9 +27,6 @@ export const deleteAlertRoute = (router: IRouter, licenseState: LicenseState) => validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerts/server/routes/disable.test.ts b/x-pack/plugins/alerts/server/routes/disable.test.ts index a82d09854a604..74f7b2eb8a570 100644 --- a/x-pack/plugins/alerts/server/routes/disable.test.ts +++ b/x-pack/plugins/alerts/server/routes/disable.test.ts @@ -30,13 +30,6 @@ describe('disableAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_disable"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.disable.mockResolvedValueOnce(); diff --git a/x-pack/plugins/alerts/server/routes/disable.ts b/x-pack/plugins/alerts/server/routes/disable.ts index dfc5dfbdd5aa2..234f8ed959a5d 100644 --- a/x-pack/plugins/alerts/server/routes/disable.ts +++ b/x-pack/plugins/alerts/server/routes/disable.ts @@ -27,9 +27,6 @@ export const disableAlertRoute = (router: IRouter, licenseState: LicenseState) = validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerts/server/routes/enable.test.ts b/x-pack/plugins/alerts/server/routes/enable.test.ts index 4ee3a12a59dc7..c9575ef87f767 100644 --- a/x-pack/plugins/alerts/server/routes/enable.test.ts +++ b/x-pack/plugins/alerts/server/routes/enable.test.ts @@ -29,13 +29,6 @@ describe('enableAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_enable"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.enable.mockResolvedValueOnce(); diff --git a/x-pack/plugins/alerts/server/routes/enable.ts b/x-pack/plugins/alerts/server/routes/enable.ts index b6f86b97d6a3a..c162b4a9844b3 100644 --- a/x-pack/plugins/alerts/server/routes/enable.ts +++ b/x-pack/plugins/alerts/server/routes/enable.ts @@ -28,9 +28,6 @@ export const enableAlertRoute = (router: IRouter, licenseState: LicenseState) => validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, handleDisabledApiKeysError( router.handleLegacyErrors(async function ( diff --git a/x-pack/plugins/alerts/server/routes/find.test.ts b/x-pack/plugins/alerts/server/routes/find.test.ts index f20ee0a54dcd9..46702f96a2e10 100644 --- a/x-pack/plugins/alerts/server/routes/find.test.ts +++ b/x-pack/plugins/alerts/server/routes/find.test.ts @@ -31,13 +31,6 @@ describe('findAlertRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_find"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-read", - ], - } - `); const findResult = { page: 1, diff --git a/x-pack/plugins/alerts/server/routes/find.ts b/x-pack/plugins/alerts/server/routes/find.ts index 80c9c20eec7da..632772eaddded 100644 --- a/x-pack/plugins/alerts/server/routes/find.ts +++ b/x-pack/plugins/alerts/server/routes/find.ts @@ -50,9 +50,6 @@ export const findAlertRoute = (router: IRouter, licenseState: LicenseState) => { validate: { query: querySchema, }, - options: { - tags: ['access:alerting-read'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerts/server/routes/get.test.ts b/x-pack/plugins/alerts/server/routes/get.test.ts index b11224ff4794e..8c4b06adf70f7 100644 --- a/x-pack/plugins/alerts/server/routes/get.test.ts +++ b/x-pack/plugins/alerts/server/routes/get.test.ts @@ -61,13 +61,6 @@ describe('getAlertRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-read", - ], - } - `); alertsClient.get.mockResolvedValueOnce(mockedAlert); diff --git a/x-pack/plugins/alerts/server/routes/get.ts b/x-pack/plugins/alerts/server/routes/get.ts index ae9ebe1299371..0f3fc4b2f3e41 100644 --- a/x-pack/plugins/alerts/server/routes/get.ts +++ b/x-pack/plugins/alerts/server/routes/get.ts @@ -27,9 +27,6 @@ export const getAlertRoute = (router: IRouter, licenseState: LicenseState) => { validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-read'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts b/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts index 8c9051093f85b..6fa12a17eb9f3 100644 --- a/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts +++ b/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts @@ -48,13 +48,6 @@ describe('getAlertStateRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-read", - ], - } - `); alertsClient.getAlertState.mockResolvedValueOnce(mockedAlertState); diff --git a/x-pack/plugins/alerts/server/routes/get_alert_state.ts b/x-pack/plugins/alerts/server/routes/get_alert_state.ts index b27ae3758e1b9..089fc80fca355 100644 --- a/x-pack/plugins/alerts/server/routes/get_alert_state.ts +++ b/x-pack/plugins/alerts/server/routes/get_alert_state.ts @@ -27,9 +27,6 @@ export const getAlertStateRoute = (router: IRouter, licenseState: LicenseState) validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-read'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts index 3192154f6664c..5cf36cb2c0786 100644 --- a/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts +++ b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts @@ -28,13 +28,6 @@ describe('listAlertTypesRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-read", - ], - } - `); const listTypes = [ { diff --git a/x-pack/plugins/alerts/server/routes/list_alert_types.ts b/x-pack/plugins/alerts/server/routes/list_alert_types.ts index 51a4558108e29..bf516120fbe93 100644 --- a/x-pack/plugins/alerts/server/routes/list_alert_types.ts +++ b/x-pack/plugins/alerts/server/routes/list_alert_types.ts @@ -20,9 +20,6 @@ export const listAlertTypesRoute = (router: IRouter, licenseState: LicenseState) { path: `${BASE_ALERT_API_PATH}/list_alert_types`, validate: {}, - options: { - tags: ['access:alerting-read'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, @@ -34,7 +31,7 @@ export const listAlertTypesRoute = (router: IRouter, licenseState: LicenseState) return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); } return res.ok({ - body: context.alerting.listTypes(), + body: Array.from(await context.alerting.getAlertsClient().listAlertTypes()), }); }) ); diff --git a/x-pack/plugins/alerts/server/routes/mute_all.test.ts b/x-pack/plugins/alerts/server/routes/mute_all.test.ts index bcdb8cbd022ac..efa3cdebad8ff 100644 --- a/x-pack/plugins/alerts/server/routes/mute_all.test.ts +++ b/x-pack/plugins/alerts/server/routes/mute_all.test.ts @@ -29,13 +29,6 @@ describe('muteAllAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_mute_all"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.muteAll.mockResolvedValueOnce(); diff --git a/x-pack/plugins/alerts/server/routes/mute_all.ts b/x-pack/plugins/alerts/server/routes/mute_all.ts index 5b05d7231c385..6735121d4edb0 100644 --- a/x-pack/plugins/alerts/server/routes/mute_all.ts +++ b/x-pack/plugins/alerts/server/routes/mute_all.ts @@ -27,9 +27,6 @@ export const muteAllAlertRoute = (router: IRouter, licenseState: LicenseState) = validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerts/server/routes/mute_instance.test.ts b/x-pack/plugins/alerts/server/routes/mute_instance.test.ts index c382c12de21cd..6e700e4e3fd46 100644 --- a/x-pack/plugins/alerts/server/routes/mute_instance.test.ts +++ b/x-pack/plugins/alerts/server/routes/mute_instance.test.ts @@ -31,13 +31,6 @@ describe('muteAlertInstanceRoute', () => { expect(config.path).toMatchInlineSnapshot( `"/api/alerts/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute"` ); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.muteInstance.mockResolvedValueOnce(); diff --git a/x-pack/plugins/alerts/server/routes/mute_instance.ts b/x-pack/plugins/alerts/server/routes/mute_instance.ts index 00550f4af3418..5e2ffc7d519ed 100644 --- a/x-pack/plugins/alerts/server/routes/mute_instance.ts +++ b/x-pack/plugins/alerts/server/routes/mute_instance.ts @@ -30,9 +30,6 @@ export const muteAlertInstanceRoute = (router: IRouter, licenseState: LicenseSta validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerts/server/routes/unmute_all.test.ts b/x-pack/plugins/alerts/server/routes/unmute_all.test.ts index e13af38fe4cb1..81fdc5bb4dd76 100644 --- a/x-pack/plugins/alerts/server/routes/unmute_all.test.ts +++ b/x-pack/plugins/alerts/server/routes/unmute_all.test.ts @@ -28,13 +28,6 @@ describe('unmuteAllAlertRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_unmute_all"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.unmuteAll.mockResolvedValueOnce(); diff --git a/x-pack/plugins/alerts/server/routes/unmute_all.ts b/x-pack/plugins/alerts/server/routes/unmute_all.ts index 1efc9ed40054e..a987380541696 100644 --- a/x-pack/plugins/alerts/server/routes/unmute_all.ts +++ b/x-pack/plugins/alerts/server/routes/unmute_all.ts @@ -27,9 +27,6 @@ export const unmuteAllAlertRoute = (router: IRouter, licenseState: LicenseState) validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerts/server/routes/unmute_instance.test.ts b/x-pack/plugins/alerts/server/routes/unmute_instance.test.ts index b2e2f24e91de9..04e97dbe5e538 100644 --- a/x-pack/plugins/alerts/server/routes/unmute_instance.test.ts +++ b/x-pack/plugins/alerts/server/routes/unmute_instance.test.ts @@ -31,13 +31,6 @@ describe('unmuteAlertInstanceRoute', () => { expect(config.path).toMatchInlineSnapshot( `"/api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute"` ); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.unmuteInstance.mockResolvedValueOnce(); diff --git a/x-pack/plugins/alerts/server/routes/unmute_instance.ts b/x-pack/plugins/alerts/server/routes/unmute_instance.ts index 967f9f890c9fb..15b882e585804 100644 --- a/x-pack/plugins/alerts/server/routes/unmute_instance.ts +++ b/x-pack/plugins/alerts/server/routes/unmute_instance.ts @@ -28,9 +28,6 @@ export const unmuteAlertInstanceRoute = (router: IRouter, licenseState: LicenseS validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, diff --git a/x-pack/plugins/alerts/server/routes/update.test.ts b/x-pack/plugins/alerts/server/routes/update.test.ts index c7d23f2670b45..dedb08a9972c2 100644 --- a/x-pack/plugins/alerts/server/routes/update.test.ts +++ b/x-pack/plugins/alerts/server/routes/update.test.ts @@ -52,13 +52,6 @@ describe('updateAlertRoute', () => { const [config, handler] = router.put.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.update.mockResolvedValueOnce(mockedResponse); diff --git a/x-pack/plugins/alerts/server/routes/update.ts b/x-pack/plugins/alerts/server/routes/update.ts index 99b81dfc5b56e..9b2fe9a43810b 100644 --- a/x-pack/plugins/alerts/server/routes/update.ts +++ b/x-pack/plugins/alerts/server/routes/update.ts @@ -49,9 +49,6 @@ export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) => body: bodySchema, params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, handleDisabledApiKeysError( router.handleLegacyErrors(async function ( diff --git a/x-pack/plugins/alerts/server/routes/update_api_key.test.ts b/x-pack/plugins/alerts/server/routes/update_api_key.test.ts index babae59553b5b..5aa91d215be90 100644 --- a/x-pack/plugins/alerts/server/routes/update_api_key.test.ts +++ b/x-pack/plugins/alerts/server/routes/update_api_key.test.ts @@ -29,13 +29,6 @@ describe('updateApiKeyRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/_update_api_key"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-all", - ], - } - `); alertsClient.updateApiKey.mockResolvedValueOnce(); diff --git a/x-pack/plugins/alerts/server/routes/update_api_key.ts b/x-pack/plugins/alerts/server/routes/update_api_key.ts index 4736351a25cbd..d44649b05b929 100644 --- a/x-pack/plugins/alerts/server/routes/update_api_key.ts +++ b/x-pack/plugins/alerts/server/routes/update_api_key.ts @@ -28,9 +28,6 @@ export const updateApiKeyRoute = (router: IRouter, licenseState: LicenseState) = validate: { params: paramSchema, }, - options: { - tags: ['access:alerting-all'], - }, }, handleDisabledApiKeysError( router.handleLegacyErrors(async function ( diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index 60a7be9391eea..ee6e5a445f996 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -5,11 +5,12 @@ */ import { i18n } from '@kbn/i18n'; +import { AlertType } from '../common/alert_types'; export const APM_FEATURE = { id: 'apm', name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', { - defaultMessage: 'APM', + defaultMessage: 'APM' }), order: 900, icon: 'apmApp', @@ -20,18 +21,14 @@ export const APM_FEATURE = { privileges: { all: { app: ['apm', 'kibana'], - api: [ - 'apm', - 'apm_write', - 'actions-read', - 'actions-all', - 'alerting-read', - 'alerting-all', - ], + api: ['apm', 'apm_write', 'actions-read', 'actions-all'], catalogue: ['apm'], savedObject: { all: ['alert', 'action', 'action_task_params'], - read: [], + read: [] + }, + alerting: { + all: Object.values(AlertType) }, ui: [ 'show', @@ -41,22 +38,19 @@ export const APM_FEATURE = { 'alerting:save', 'actions:save', 'alerting:delete', - 'actions:delete', - ], + 'actions:delete' + ] }, read: { app: ['apm', 'kibana'], - api: [ - 'apm', - 'actions-read', - 'actions-all', - 'alerting-read', - 'alerting-all', - ], + api: ['apm', 'actions-read', 'actions-all'], catalogue: ['apm'], savedObject: { all: ['alert', 'action', 'action_task_params'], - read: [], + read: [] + }, + alerting: { + all: Object.values(AlertType) }, ui: [ 'show', @@ -65,8 +59,8 @@ export const APM_FEATURE = { 'alerting:save', 'actions:save', 'alerting:delete', - 'actions:delete', - ], - }, - }, + 'actions:delete' + ] + } + } }; diff --git a/x-pack/plugins/features/common/feature_kibana_privileges.ts b/x-pack/plugins/features/common/feature_kibana_privileges.ts index 768c8c6ae1088..c642f3e5b6fd4 100644 --- a/x-pack/plugins/features/common/feature_kibana_privileges.ts +++ b/x-pack/plugins/features/common/feature_kibana_privileges.ts @@ -75,6 +75,59 @@ export interface FeatureKibanaPrivileges { */ app?: string[]; + /** + * If your feature registers its own Alert types you may specify the access privileges for them here. + */ + alerting?: { + /** + * List of alert types which users should have full read/write access to within the feature. + * @example + * ```ts + * { + * all: ['my-alert-type-within-my-feature'] + * } + * ``` + */ + all?: string[]; + + /** + * List of alert types which users should have read-only access to from within the feature. + * @example + * ```ts + * { + * read: ['my-alert-type'] + * } + * ``` + */ + read?: string[]; + + /** + * If your feature registers its own Alert types you may specify global access privileges for them here. + */ + globally?: { + /** + * List of alert types types which users should have full read/write access to throughout kibana. + * @example + * ```ts + * { + * all: ['my-alert-type-globally-available'] + * } + * ``` + */ + all?: string[]; + + /** + * List of alert types which users should have read-only access to throughout kibana. + * @example + * ```ts + * { + * read: ['my-alert-type'] + * } + * ``` + */ + read?: string[]; + }; + }; /** * If your feature requires access to specific saved objects, then specify your access needs here. */ diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 7497548cf8904..86c7bc4852742 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -33,6 +33,14 @@ const privilegeSchema = Joi.object({ catalogue: catalogueSchema, api: Joi.array().items(Joi.string()), app: Joi.array().items(Joi.string()), + alerting: Joi.object({ + all: Joi.array().items(Joi.string()), + read: Joi.array().items(Joi.string()), + globally: Joi.object({ + all: Joi.array().items(Joi.string()), + read: Joi.array().items(Joi.string()), + }), + }), savedObject: Joi.object({ all: Joi.array().items(Joi.string()).required(), read: Joi.array().items(Joi.string()).required(), diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index fa228e03194a9..00dc2c9ac1b62 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -5,6 +5,9 @@ */ import { i18n } from '@kbn/i18n'; +import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../common/alerting/logs/types'; +import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/inventory_metric_threshold/types.ts'; +import { METRIC_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/metric_threshold/types.ts'; export const METRICS_FEATURE = { id: 'infrastructure', @@ -20,11 +23,20 @@ export const METRICS_FEATURE = { all: { app: ['infra', 'kibana'], catalogue: ['infraops'], - api: ['infra', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'], + api: ['infra', 'actions-read', 'actions-all'], savedObject: { all: ['infrastructure-ui-source', 'alert', 'action', 'action_task_params'], read: ['index-pattern'], }, + alerting: { + globally: { + all: [ + METRIC_THRESHOLD_ALERT_TYPE_ID, + METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + ], + }, + }, ui: [ 'show', 'configureSource', @@ -40,11 +52,20 @@ export const METRICS_FEATURE = { read: { app: ['infra', 'kibana'], catalogue: ['infraops'], - api: ['infra', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'], + api: ['infra', 'actions-read', 'actions-all'], savedObject: { all: ['alert', 'action', 'action_task_params'], read: ['infrastructure-ui-source', 'index-pattern'], }, + alerting: { + globally: { + all: [ + METRIC_THRESHOLD_ALERT_TYPE_ID, + METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + ], + }, + }, ui: [ 'show', 'alerting:show', diff --git a/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap b/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap new file mode 100644 index 0000000000000..e9cd8bf48a400 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#get alertType of "" throws error 1`] = `"alertTypeId is required and must be a string"`; + +exports[`#get alertType of {} throws error 1`] = `"alertTypeId is required and must be a string"`; + +exports[`#get alertType of 1 throws error 1`] = `"alertTypeId is required and must be a string"`; + +exports[`#get alertType of null throws error 1`] = `"alertTypeId is required and must be a string"`; + +exports[`#get alertType of true throws error 1`] = `"alertTypeId is required and must be a string"`; + +exports[`#get alertType of undefined throws error 1`] = `"alertTypeId is required and must be a string"`; + +exports[`#get consumer of "" throws error 1`] = `"consumer is optional but must be a string when specified"`; + +exports[`#get consumer of {} throws error 1`] = `"consumer is optional but must be a string when specified"`; + +exports[`#get consumer of 1 throws error 1`] = `"consumer is optional but must be a string when specified"`; + +exports[`#get consumer of null throws error 1`] = `"consumer is optional but must be a string when specified"`; + +exports[`#get consumer of true throws error 1`] = `"consumer is optional but must be a string when specified"`; + +exports[`#get operation of "" throws error 1`] = `"operation is required and must be a string"`; + +exports[`#get operation of {} throws error 1`] = `"operation is required and must be a string"`; + +exports[`#get operation of 1 throws error 1`] = `"operation is required and must be a string"`; + +exports[`#get operation of null throws error 1`] = `"operation is required and must be a string"`; + +exports[`#get operation of true throws error 1`] = `"operation is required and must be a string"`; + +exports[`#get operation of undefined throws error 1`] = `"operation is required and must be a string"`; diff --git a/x-pack/plugins/security/server/authorization/actions/actions.mock.ts b/x-pack/plugins/security/server/authorization/actions/actions.mock.ts new file mode 100644 index 0000000000000..f41faaa3dd52c --- /dev/null +++ b/x-pack/plugins/security/server/authorization/actions/actions.mock.ts @@ -0,0 +1,35 @@ +/* + * 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 { ApiActions } from './api'; +import { AppActions } from './app'; +import { SavedObjectActions } from './saved_object'; +import { SpaceActions } from './space'; +import { UIActions } from './ui'; +import { AlertingActions } from './alerting'; +import { Actions } from './actions'; + +jest.mock('./api'); +jest.mock('./app'); +jest.mock('./saved_object'); +jest.mock('./space'); +jest.mock('./ui'); +jest.mock('./alerting'); + +const create = (versionNumber: string) => { + const t = ({ + api: new ApiActions(versionNumber), + app: new AppActions(versionNumber), + login: 'login:', + savedObject: new SavedObjectActions(versionNumber), + alerting: new AlertingActions(versionNumber), + space: new SpaceActions(versionNumber), + ui: new UIActions(versionNumber), + version: `version:${versionNumber}`, + } as unknown) as jest.Mocked; + return t; +}; + +export const actionsMock = { create }; diff --git a/x-pack/plugins/security/server/authorization/actions/actions.ts b/x-pack/plugins/security/server/authorization/actions/actions.ts index 00293e88abe76..34258bdcf972d 100644 --- a/x-pack/plugins/security/server/authorization/actions/actions.ts +++ b/x-pack/plugins/security/server/authorization/actions/actions.ts @@ -9,6 +9,7 @@ import { AppActions } from './app'; import { SavedObjectActions } from './saved_object'; import { SpaceActions } from './space'; import { UIActions } from './ui'; +import { AlertingActions } from './alerting'; /** Actions are used to create the "actions" that are associated with Elasticsearch's * application privileges, and are used to perform the authorization checks implemented @@ -23,6 +24,8 @@ export class Actions { public readonly savedObject = new SavedObjectActions(this.versionNumber); + public readonly alerting = new AlertingActions(this.versionNumber); + public readonly space = new SpaceActions(this.versionNumber); public readonly ui = new UIActions(this.versionNumber); diff --git a/x-pack/plugins/security/server/authorization/actions/alerting.test.ts b/x-pack/plugins/security/server/authorization/actions/alerting.test.ts new file mode 100644 index 0000000000000..fcd1e4aea0628 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/actions/alerting.test.ts @@ -0,0 +1,52 @@ +/* + * 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 { AlertingActions } from './alerting'; + +const version = '1.0.0-zeta1'; + +describe('#get', () => { + [null, undefined, '', 1, true, {}].forEach((alertType: any) => { + test(`alertType of ${JSON.stringify(alertType)} throws error`, () => { + const alertingActions = new AlertingActions(version); + expect(() => + alertingActions.get(alertType, 'consumer', 'foo-action') + ).toThrowErrorMatchingSnapshot(); + }); + }); + + [null, undefined, '', 1, true, {}].forEach((operation: any) => { + test(`operation of ${JSON.stringify(operation)} throws error`, () => { + const alertingActions = new AlertingActions(version); + expect(() => + alertingActions.get('foo-alertType', 'consumer', operation) + ).toThrowErrorMatchingSnapshot(); + }); + }); + + [null, '', 1, true, {}].forEach((consumer: any) => { + test(`consumer of ${JSON.stringify(consumer)} throws error`, () => { + const alertingActions = new AlertingActions(version); + expect(() => + alertingActions.get('foo-alertType', consumer, 'operation') + ).toThrowErrorMatchingSnapshot(); + }); + }); + + test('returns `alerting:${alertType}/${consumer}/${operation}`', () => { + const alertingActions = new AlertingActions(version); + expect(alertingActions.get('foo-alertType', 'consumer', 'bar-operation')).toBe( + 'alerting:1.0.0-zeta1:foo-alertType/consumer/bar-operation' + ); + }); + + test('returns `alerting:${alertType}/${operation}` when no consumer is specified', () => { + const alertingActions = new AlertingActions(version); + expect(alertingActions.get('foo-alertType', undefined, 'bar-operation')).toBe( + 'alerting:1.0.0-zeta1:foo-alertType/bar-operation' + ); + }); +}); diff --git a/x-pack/plugins/security/server/authorization/actions/alerting.ts b/x-pack/plugins/security/server/authorization/actions/alerting.ts new file mode 100644 index 0000000000000..a3e56701a60d3 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/actions/alerting.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 { isString, isUndefined } from 'lodash'; + +export class AlertingActions { + private readonly prefix: string; + + constructor(versionNumber: string) { + this.prefix = `alerting:${versionNumber}:`; + } + + public get(alertTypeId: string, consumer: string | undefined, operation: string): string { + if (!alertTypeId || !isString(alertTypeId)) { + throw new Error('alertTypeId is required and must be a string'); + } + + if (!operation || !isString(operation)) { + throw new Error('operation is required and must be a string'); + } + + if (!isUndefined(consumer) && (!consumer || !isString(consumer))) { + throw new Error('consumer is optional but must be a string when specified'); + } + + return `${this.prefix}${alertTypeId}${consumer ? `/${consumer}` : ''}/${operation}`; + } +} diff --git a/x-pack/plugins/security/server/authorization/index.mock.ts b/x-pack/plugins/security/server/authorization/index.mock.ts index 930ede4157723..22eed47c17bfe 100644 --- a/x-pack/plugins/security/server/authorization/index.mock.ts +++ b/x-pack/plugins/security/server/authorization/index.mock.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Actions } from '.'; import { AuthorizationMode } from './mode'; +import { actionsMock } from './actions/actions.mock'; export const authorizationMock = { create: ({ version = 'mock-version', applicationName = 'mock-application', }: { version?: string; applicationName?: string } = {}) => ({ - actions: new Actions(version), + actions: actionsMock.create(version), checkPrivilegesWithRequest: jest.fn(), checkPrivilegesDynamicallyWithRequest: jest.fn(), checkSavedObjectsPrivilegesWithRequest: jest.fn(), diff --git a/x-pack/plugins/security/server/authorization/index.ts b/x-pack/plugins/security/server/authorization/index.ts index cf970a561b93f..06b9bad0af972 100644 --- a/x-pack/plugins/security/server/authorization/index.ts +++ b/x-pack/plugins/security/server/authorization/index.ts @@ -15,6 +15,7 @@ import { import { FeaturesService, SpacesService } from '../plugin'; import { Actions } from './actions'; import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges'; +export { CheckPrivilegesResponse } from './check_privileges'; import { CheckPrivilegesDynamicallyWithRequest, checkPrivilegesDynamicallyWithRequestFactory, diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts new file mode 100644 index 0000000000000..1eaebac2c78f6 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts @@ -0,0 +1,311 @@ +/* + * 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 { Actions } from '../../actions'; +import { FeaturePrivilegeAlertingBuilder } from './alerting'; +import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; + +const version = '1.0.0-zeta1'; + +describe(`feature_privilege_builder`, () => { + describe(`alerting`, () => { + test('grants no privileges by default', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + all: [], + read: [], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new Feature({ + id: 'my-feature', + name: 'my-feature', + app: [], + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toEqual([]); + }); + + describe(`within feature`, () => { + test('grants `read` privileges under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + all: [], + read: ['alert-type'], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new Feature({ + id: 'my-feature', + name: 'my-feature', + app: [], + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/my-feature/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", + "alerting:1.0.0-zeta1:alert-type/my-feature/find", + ] + `); + }); + + test('grants `all` privileges under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + all: ['alert-type'], + read: [], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new Feature({ + id: 'my-feature', + name: 'my-feature', + app: [], + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/my-feature/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", + "alerting:1.0.0-zeta1:alert-type/my-feature/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/create", + "alerting:1.0.0-zeta1:alert-type/my-feature/delete", + "alerting:1.0.0-zeta1:alert-type/my-feature/update", + "alerting:1.0.0-zeta1:alert-type/my-feature/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/my-feature/enable", + "alerting:1.0.0-zeta1:alert-type/my-feature/disable", + "alerting:1.0.0-zeta1:alert-type/my-feature/muteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/muteInstance", + "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteInstance", + ] + `); + }); + + test('grants both `all` and `read` privileges under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + all: ['alert-type'], + read: ['readonly-alert-type'], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new Feature({ + id: 'my-feature', + name: 'my-feature', + app: [], + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/my-feature/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", + "alerting:1.0.0-zeta1:alert-type/my-feature/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/create", + "alerting:1.0.0-zeta1:alert-type/my-feature/delete", + "alerting:1.0.0-zeta1:alert-type/my-feature/update", + "alerting:1.0.0-zeta1:alert-type/my-feature/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/my-feature/enable", + "alerting:1.0.0-zeta1:alert-type/my-feature/disable", + "alerting:1.0.0-zeta1:alert-type/my-feature/muteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/muteInstance", + "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteInstance", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/get", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/getAlertState", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/find", + ] + `); + }); + }); + + describe(`globally`, () => { + test('grants global `read` privileges under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + allGlobally: [], + readGlobally: ['alert-type'], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new Feature({ + id: 'my-feature', + name: 'my-feature', + app: [], + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/get", + "alerting:1.0.0-zeta1:alert-type/getAlertState", + "alerting:1.0.0-zeta1:alert-type/find", + ] + `); + }); + + test('grants global `all` privileges under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + allGlobally: ['alert-type'], + readGlobally: [], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new Feature({ + id: 'my-feature', + name: 'my-feature', + app: [], + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/get", + "alerting:1.0.0-zeta1:alert-type/getAlertState", + "alerting:1.0.0-zeta1:alert-type/find", + "alerting:1.0.0-zeta1:alert-type/create", + "alerting:1.0.0-zeta1:alert-type/delete", + "alerting:1.0.0-zeta1:alert-type/update", + "alerting:1.0.0-zeta1:alert-type/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/enable", + "alerting:1.0.0-zeta1:alert-type/disable", + "alerting:1.0.0-zeta1:alert-type/muteAll", + "alerting:1.0.0-zeta1:alert-type/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/muteInstance", + "alerting:1.0.0-zeta1:alert-type/unmuteInstance", + ] + `); + }); + + test('grants both global `all` and global `read` privileges under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + allGlobally: ['alert-type'], + readGlobally: ['readonly-alert-type'], + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new Feature({ + id: 'my-feature', + name: 'my-feature', + app: [], + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/get", + "alerting:1.0.0-zeta1:alert-type/getAlertState", + "alerting:1.0.0-zeta1:alert-type/find", + "alerting:1.0.0-zeta1:alert-type/create", + "alerting:1.0.0-zeta1:alert-type/delete", + "alerting:1.0.0-zeta1:alert-type/update", + "alerting:1.0.0-zeta1:alert-type/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/enable", + "alerting:1.0.0-zeta1:alert-type/disable", + "alerting:1.0.0-zeta1:alert-type/muteAll", + "alerting:1.0.0-zeta1:alert-type/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/muteInstance", + "alerting:1.0.0-zeta1:alert-type/unmuteInstance", + "alerting:1.0.0-zeta1:readonly-alert-type/get", + "alerting:1.0.0-zeta1:readonly-alert-type/getAlertState", + "alerting:1.0.0-zeta1:readonly-alert-type/find", + ] + `); + }); + }); + }); +}); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts new file mode 100644 index 0000000000000..7935959c331ce --- /dev/null +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -0,0 +1,48 @@ +/* + * 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 { flatten, uniq } from 'lodash'; +import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; + +const readOperations: string[] = ['get', 'getAlertState', 'find']; +const writeOperations: string[] = [ + 'create', + 'delete', + 'update', + 'updateApiKey', + 'enable', + 'disable', + 'muteAll', + 'unmuteAll', + 'muteInstance', + 'unmuteInstance', +]; +const allOperations: string[] = [...readOperations, ...writeOperations]; + +export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder { + public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + const allOperationsWithinConsumer = (privileges: string[], consumer?: string) => + flatten( + privileges.map(type => [ + ...allOperations.map(operation => this.actions.alerting.get(type, consumer, operation)), + ]) + ); + const readOperationsWithinConsumer = (privileges: string[], consumer?: string) => + flatten( + privileges.map(type => [ + ...readOperations.map(operation => this.actions.alerting.get(type, consumer, operation)), + ]) + ); + + return uniq([ + ...allOperationsWithinConsumer(privilegeDefinition.alerting?.all ?? [], feature.id), + ...readOperationsWithinConsumer(privilegeDefinition.alerting?.read ?? [], feature.id), + ...allOperationsWithinConsumer(privilegeDefinition.alerting?.globally?.all ?? []), + ...readOperationsWithinConsumer(privilegeDefinition.alerting?.globally?.read ?? []), + ]); + } +} diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts index 3d6dfbdac0251..76b664cbbe2a7 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts @@ -14,6 +14,7 @@ import { FeaturePrivilegeBuilder } from './feature_privilege_builder'; import { FeaturePrivilegeManagementBuilder } from './management'; import { FeaturePrivilegeNavlinkBuilder } from './navlink'; import { FeaturePrivilegeSavedObjectBuilder } from './saved_object'; +import { FeaturePrivilegeAlertingBuilder } from './alerting'; import { FeaturePrivilegeUIBuilder } from './ui'; export { FeaturePrivilegeBuilder }; @@ -26,6 +27,7 @@ export const featurePrivilegeBuilderFactory = (actions: Actions): FeaturePrivile new FeaturePrivilegeNavlinkBuilder(actions), new FeaturePrivilegeSavedObjectBuilder(actions), new FeaturePrivilegeUIBuilder(actions), + new FeaturePrivilegeAlertingBuilder(actions), ]; return { diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts index f3b2881e79ece..a84eea3933eea 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -91,7 +91,6 @@ export function privilegesFactory( delete featurePrivileges[feature.id]; } } - return { features: featurePrivileges, global: { diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index a0a06b537213d..e25613fc5936f 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -30,6 +30,7 @@ export { export { AuditLogger } from './audit'; export { SecurityPluginSetup }; export { AuthenticatedUser } from '../common/model'; +export { Actions, CheckPrivilegesResponse } from './authorization'; export const config: PluginConfigDescriptor> = { schema: ConfigSchema, diff --git a/x-pack/plugins/siem/server/plugin.ts b/x-pack/plugins/siem/server/plugin.ts index a8858c91d677c..ea379451dc198 100644 --- a/x-pack/plugins/siem/server/plugin.ts +++ b/x-pack/plugins/siem/server/plugin.ts @@ -36,7 +36,7 @@ import { initSavedObjects, savedObjectTypes } from './saved_objects'; import { SiemClientFactory } from './client'; import { createConfig$, ConfigType } from './config'; import { initUiSettings } from './ui_settings'; -import { APP_ID, APP_ICON } from '../common/constants'; +import { APP_ID, APP_ICON, SIGNALS_ID, NOTIFICATIONS_ID } from '../common/constants'; import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerResolverRoutes } from './endpoint/routes/resolver'; import { registerAlertRoutes } from './endpoint/alerts/routes'; @@ -138,7 +138,7 @@ export class Plugin implements IPlugin { + times(runCount, index => { services .alertInstanceFactory(`instance-${index}`) .replaceState({ instanceStateValue: true }) @@ -111,7 +111,7 @@ export function defineAlertTypes( name: 'Default', }, ], - producer: 'alerting', + producer: 'alertsFixture', defaultActionGroupId: 'default', async executor({ services, params, state }: AlertExecutorOptions) { await services.callCluster('index', { @@ -138,7 +138,7 @@ export function defineAlertTypes( name: 'Default', }, ], - producer: 'alerting', + producer: 'alertsFixture', defaultActionGroupId: 'default', async executor({ services, params, state }: AlertExecutorOptions) { await services.callCluster('index', { @@ -164,7 +164,7 @@ export function defineAlertTypes( }, ], defaultActionGroupId: 'default', - producer: 'alerting', + producer: 'alertsFixture', validate: { params: schema.object({ callClusterAuthorizationIndex: schema.string(), @@ -247,7 +247,7 @@ export function defineAlertTypes( name: 'Default', }, ], - producer: 'alerting', + producer: 'alertsFixture', defaultActionGroupId: 'default', validate: { params: schema.object({ @@ -260,7 +260,7 @@ export function defineAlertTypes( id: 'test.noop', name: 'Test: Noop', actionGroups: [{ id: 'default', name: 'Default' }], - producer: 'alerting', + producer: 'alertsFixture', defaultActionGroupId: 'default', async executor({ services, params, state }: AlertExecutorOptions) {}, }; @@ -268,7 +268,7 @@ export function defineAlertTypes( id: 'test.onlyContextVariables', name: 'Test: Only Context Variables', actionGroups: [{ id: 'default', name: 'Default' }], - producer: 'alerting', + producer: 'alertsFixture', defaultActionGroupId: 'default', actionVariables: { context: [{ name: 'aContextVariable', description: 'this is a context variable' }], @@ -279,7 +279,7 @@ export function defineAlertTypes( id: 'test.onlyStateVariables', name: 'Test: Only State Variables', actionGroups: [{ id: 'default', name: 'Default' }], - producer: 'alerting', + producer: 'alertsFixture', defaultActionGroupId: 'default', actionVariables: { state: [{ name: 'aStateVariable', description: 'this is a state variable' }], diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index 47563f8a5f078..504d67352be1a 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -26,7 +26,7 @@ export interface FixtureStartDeps { export class FixturePlugin implements Plugin { public setup(core: CoreSetup, { features, actions, alerts }: FixtureSetupDeps) { features.registerFeature({ - id: 'alerts', + id: 'alertsFixture', name: 'Alerts', app: ['alerts', 'kibana'], privileges: { @@ -36,8 +36,22 @@ export class FixturePlugin implements Plugin { - it('should return 200 with list of alert types', async () => { + it('should return 200 with list of globally available alert types', async () => { const response = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/api/alerts/list_alert_types`) .auth(user.username, user.password); + expect(response.statusCode).to.eql(200); switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - expect(response.statusCode).to.eql(404); - expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', - }); + expect(response.body).to.eql([]); break; case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': - expect(response.statusCode).to.eql(200); const fixtureAlertType = response.body.find( (alertType: any) => alertType.id === 'test.noop' ); @@ -48,7 +43,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { state: [], context: [], }, - producer: 'alerting', + producer: 'alertsFixture', }); break; default: 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 index 2416bc2ea1d12..ce210f1128fa6 100644 --- 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 @@ -13,6 +13,7 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, + getUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -44,11 +45,11 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('muteAll', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': 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 index c59b9f4503a03..885443bfbd1b2 100644 --- 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 @@ -13,6 +13,7 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, + getUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -44,11 +45,11 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('muteInstance', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': @@ -95,11 +96,11 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('muteInstance', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': 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 index fd22752ccc11a..9a649a3b7af73 100644 --- 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 @@ -13,6 +13,7 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, + getUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -49,11 +50,11 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('unmuteAll', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': 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 index 72b524282354a..6f9e7ce5a6e08 100644 --- 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 @@ -13,6 +13,7 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, + getUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -51,11 +52,15 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage( + 'unmuteInstance', + 'test.noop', + 'alertsFixture' + ), + statusCode: 403, }); break; case 'superuser at space1': 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 2bcc035beb7a9..bf3ccf6ec479e 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 @@ -13,6 +13,7 @@ import { getTestAlertData, ObjectRemover, ensureDatetimeIsWithinRange, + getUnauthorizedErrorMessage, } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -65,11 +66,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('update', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': @@ -79,7 +80,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { ...updatedData, id: createdAlert.id, alertTypeId: 'test.noop', - consumer: 'bar', + consumer: 'alertsFixture', createdBy: 'elastic', enabled: true, updatedBy: user.username, @@ -148,11 +149,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('update', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': @@ -162,7 +163,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { ...updatedData, id: createdAlert.id, alertTypeId: 'test.noop', - consumer: 'bar', + consumer: 'alertsFixture', createdBy: 'elastic', enabled: true, updatedBy: user.username, @@ -220,12 +221,6 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { 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', - message: 'Not Found', - }); - break; case 'superuser at space1': expect(response.body).to.eql({ statusCode: 404, @@ -266,13 +261,6 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { 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(400); @@ -298,13 +286,6 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { 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(400); @@ -351,11 +332,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('update', 'test.validation', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': @@ -390,13 +371,6 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { 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(400); @@ -450,11 +424,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('update', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': 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 bf72b970dc0f1..d441be8a82fd3 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 @@ -13,6 +13,7 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, + getUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -39,16 +40,15 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); const response = await alertUtils.getUpdateApiKeyRequest(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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('updateApiKey', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': @@ -100,11 +100,11 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte 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.statusCode).to.eql(403); expect(response.body).to.eql({ - statusCode: 404, - error: 'Not Found', - message: 'Not Found', + error: 'Forbidden', + message: getUnauthorizedErrorMessage('updateApiKey', 'test.noop', 'alertsFixture'), + statusCode: 403, }); break; case 'superuser at space1': @@ -145,12 +145,6 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'global_read at space1': - 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.body).to.eql({ 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 fa256712a012b..8f42f12347728 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 @@ -69,7 +69,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { ], enabled: true, alertTypeId: 'test.noop', - consumer: 'bar', + consumer: 'alertsFixture', params: {}, createdBy: null, schedule: { interval: '1m' }, 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 06f27d666c3da..b28ce89b30472 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 @@ -42,7 +42,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { name: 'abc', tags: ['foo'], alertTypeId: 'test.noop', - consumer: 'bar', + consumer: 'alertsFixture', schedule: { interval: '1m' }, enabled: true, actions: [], 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 ff671e16654b5..165eaa09126a8 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 @@ -36,7 +36,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { name: 'abc', tags: ['foo'], alertTypeId: 'test.noop', - consumer: 'bar', + consumer: 'alertsFixture', schedule: { interval: '1m' }, enabled: true, actions: [], diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts index d3f08d7c509a0..e3f87a9be00ba 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts @@ -44,7 +44,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont name: 'abc', tags: ['foo'], alertTypeId: 'test.cumulative-firing', - consumer: 'bar', + consumer: 'alertsFixture', schedule: { interval: '5s' }, throttle: '5s', actions: [], diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts index aef87eefba2ad..4baae603f2960 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts @@ -29,7 +29,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { state: [], context: [], }, - producer: 'alerting', + producer: 'alertsFixture', }); }); 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 b01a1b140f2d6..9c8e6f6b8d94c 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 @@ -47,7 +47,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { id: createdAlert.id, tags: ['bar'], alertTypeId: 'test.noop', - consumer: 'bar', + consumer: 'alertsFixture', createdBy: null, enabled: true, updatedBy: null, From 06495a6ae8e385720804e46e375fce59fa483456 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 4 Jun 2020 12:11:17 +0100 Subject: [PATCH 009/126] fixed unit test --- x-pack/plugins/alerts/server/alerts_client.ts | 18 +++---- .../server/alerts_client_factory.test.ts | 4 +- .../alerts/server/alerts_client_factory.ts | 2 +- .../server/routes/get_alert_state.test.ts | 14 ----- .../server/routes/list_alert_types.test.ts | 51 ++++++++++--------- x-pack/plugins/infra/server/features.ts | 4 +- .../alerting.test.ts | 18 ++++--- .../tests/alerting/find.ts | 7 +-- 8 files changed, 55 insertions(+), 63 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index f1ea77829a5be..53c6d01c1d418 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -32,12 +32,12 @@ import { GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, SecurityPluginSetup, + CheckPrivilegesResponse, } from '../../security/server'; import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../task_manager/server'; import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists'; -import { CheckPrivilegesResponse } from '../../security/server'; import { RegistryAlertType } from './alert_type_registry'; type NormalizedAlertAction = Omit; @@ -473,9 +473,7 @@ export class AlertsClient { } try { - const apiKeyId = Buffer.from(apiKey, 'base64') - .toString() - .split(':')[0]; + const apiKeyId = Buffer.from(apiKey, 'base64').toString().split(':')[0]; const response = await this.invalidateAPIKey({ id: apiKeyId }); if (response.apiKeysEnabled === true && response.result.error_count > 0) { this.logger.error(`Failed to invalidate API Key [id="${apiKeyId}"]`); @@ -741,8 +739,8 @@ export class AlertsClient { actions: RawAlert['actions'], references: SavedObjectReference[] ) { - return actions.map(action => { - const reference = references.find(ref => ref.name === action.actionRef); + return actions.map((action) => { + const reference = references.find((ref) => ref.name === action.actionRef); if (!reference) { throw new Error(`Reference ${action.actionRef} not found`); } @@ -787,10 +785,10 @@ export class AlertsClient { private validateActions(alertType: AlertType, actions: NormalizedAlertAction[]): void { const { actionGroups: alertTypeActionGroups } = alertType; - const usedAlertActionGroups = actions.map(action => action.group); + const usedAlertActionGroups = actions.map((action) => action.group); const availableAlertTypeActionGroups = new Set(pluck(alertTypeActionGroups, 'id')); const invalidActionGroups = usedAlertActionGroups.filter( - group => !availableAlertTypeActionGroups.has(group) + (group) => !availableAlertTypeActionGroups.has(group) ); if (invalidActionGroups.length) { throw Boom.badRequest( @@ -808,11 +806,11 @@ export class AlertsClient { alertActions: NormalizedAlertAction[] ): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> { const actionsClient = await this.getActionsClient(); - const actionIds = [...new Set(alertActions.map(alertAction => alertAction.id))]; + const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))]; const actionResults = await actionsClient.getBulk(actionIds); const references: SavedObjectReference[] = []; const actions = alertActions.map(({ id, ...alertAction }, i) => { - const actionResultValue = actionResults.find(action => action.id === id); + const actionResultValue = actionResults.find((action) => action.id === id); if (actionResultValue) { const actionRef = `action_${i}`; references.push({ diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 10d7e7f9cafdf..7278c7ab2c837 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -67,6 +67,7 @@ test('creates an alerts client with proper constructor arguments when security i expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, { excludedWrappers: ['security'], + includedHiddenTypes: ['alert'], }); expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ @@ -79,10 +80,10 @@ test('creates an alerts client with proper constructor arguments when security i spaceId: 'default', namespace: 'default', getUserName: expect.any(Function), + getActionsClient: expect.any(Function), createAPIKey: expect.any(Function), invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, - preconfiguredActions: [], }); }); @@ -97,6 +98,7 @@ test('creates an alerts client with proper constructor arguments', async () => { expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, { excludedWrappers: ['security'], + includedHiddenTypes: ['alert'], }); expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index b7d1bf6e8cf31..7ebe505913b95 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -59,7 +59,7 @@ export class AlertsClientFactory { alertTypeRegistry: this.alertTypeRegistry, unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, { excludedWrappers: ['security'], - includedHiddenTypes: ['alert', 'action'], + includedHiddenTypes: ['alert'], }), authorization: this.securityPluginSetup?.authz, request, diff --git a/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts b/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts index 6fa12a17eb9f3..d5bf9737d39ab 100644 --- a/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts +++ b/x-pack/plugins/alerts/server/routes/get_alert_state.test.ts @@ -84,13 +84,6 @@ describe('getAlertStateRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-read", - ], - } - `); alertsClient.getAlertState.mockResolvedValueOnce(undefined); @@ -127,13 +120,6 @@ describe('getAlertStateRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alerts/alert/{id}/state"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-read", - ], - } - `); alertsClient.getAlertState = jest .fn() diff --git a/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts index 5cf36cb2c0786..14143021290af 100644 --- a/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts +++ b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts @@ -9,6 +9,9 @@ import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { alertsClientMock } from '../alerts_client.mock'; + +const alertsClient = alertsClientMock.create(); jest.mock('../lib/license_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -40,12 +43,16 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', - actionVariables: [], + actionVariables: { + context: [], + state: [], + }, producer: 'test', }, ]; + alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes)); - const [context, req, res] = mockHandlerArguments({ listTypes }, {}, ['ok']); + const [context, req, res] = mockHandlerArguments({ alertsClient }, {}, ['ok']); expect(await handler(context, req, res)).toMatchInlineSnapshot(` Object { @@ -57,7 +64,10 @@ describe('listAlertTypesRoute', () => { "name": "Default", }, ], - "actionVariables": Array [], + "actionVariables": Object { + "context": Array [], + "state": Array [], + }, "defaultActionGroupId": "default", "id": "1", "name": "name", @@ -67,7 +77,7 @@ describe('listAlertTypesRoute', () => { } `); - expect(context.alerting!.listTypes).toHaveBeenCalledTimes(1); + expect(alertsClient.listAlertTypes).toHaveBeenCalledTimes(1); expect(res.ok).toHaveBeenCalledWith({ body: listTypes, @@ -83,19 +93,11 @@ describe('listAlertTypesRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-read", - ], - } - `); const listTypes = [ { id: '1', name: 'name', - enabled: true, actionGroups: [ { id: 'default', @@ -103,13 +105,18 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', - actionVariables: [], + actionVariables: { + context: [], + state: [], + }, producer: 'alerting', }, ]; + alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes)); + const [context, req, res] = mockHandlerArguments( - { listTypes }, + { alertsClient }, { params: { id: '1' }, }, @@ -134,13 +141,6 @@ describe('listAlertTypesRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/alerts/list_alert_types"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:alerting-read", - ], - } - `); const listTypes = [ { @@ -153,13 +153,18 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', - actionVariables: [], + actionVariables: { + context: [], + state: [], + }, producer: 'alerting', }, ]; + alertsClient.listAlertTypes.mockResolvedValueOnce(new Set(listTypes)); + const [context, req, res] = mockHandlerArguments( - { listTypes }, + { alertsClient }, { params: { id: '1' }, }, diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index 00dc2c9ac1b62..598e619a21143 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -6,8 +6,8 @@ import { i18n } from '@kbn/i18n'; import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../common/alerting/logs/types'; -import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/inventory_metric_threshold/types.ts'; -import { METRIC_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/metric_threshold/types.ts'; +import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/inventory_metric_threshold/types'; +import { METRIC_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/metric_threshold/types'; export const METRICS_FEATURE = { id: 'infrastructure', diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts index 1eaebac2c78f6..2add0dbb20302 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts @@ -182,8 +182,10 @@ describe(`feature_privilege_builder`, () => { const privilege: FeatureKibanaPrivileges = { alerting: { - allGlobally: [], - readGlobally: ['alert-type'], + globally: { + all: [], + read: ['alert-type'], + }, }, savedObject: { @@ -218,8 +220,10 @@ describe(`feature_privilege_builder`, () => { const privilege: FeatureKibanaPrivileges = { alerting: { - allGlobally: ['alert-type'], - readGlobally: [], + globally: { + all: ['alert-type'], + read: [], + }, }, savedObject: { @@ -264,8 +268,10 @@ describe(`feature_privilege_builder`, () => { const privilege: FeatureKibanaPrivileges = { alerting: { - allGlobally: ['alert-type'], - readGlobally: ['readonly-alert-type'], + globally: { + all: ['alert-type'], + read: ['readonly-alert-type'], + }, }, savedObject: { 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 f21cb9f486fb5..b54767cb5ebc5 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 @@ -6,12 +6,7 @@ import expect from '@kbn/expect'; import { UserAtSpaceScenarios } from '../../scenarios'; -import { - getUrlPrefix, - getTestAlertData, - ObjectRemover, - getUnauthorizedErrorMessage, -} from '../../../common/lib'; +import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export From 77348f11889e9bf47b481072bc8dcebd3f4ff66c Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 4 Jun 2020 13:35:40 +0100 Subject: [PATCH 010/126] provide default global privileges over builtin types --- x-pack/plugins/alerting_builtins/kibana.json | 2 +- .../alerting_builtins/server/plugin.ts | 38 ++++++++++++++++++- .../plugins/alerting_builtins/server/types.ts | 2 + .../fixtures/plugins/alerts/kibana.json | 2 +- .../fixtures/plugins/alerts/server/plugin.ts | 35 ++++++++++++++++- 5 files changed, 75 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/alerting_builtins/kibana.json b/x-pack/plugins/alerting_builtins/kibana.json index cc613d5247ef4..dd70e53604f16 100644 --- a/x-pack/plugins/alerting_builtins/kibana.json +++ b/x-pack/plugins/alerting_builtins/kibana.json @@ -3,7 +3,7 @@ "server": true, "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["alerts"], + "requiredPlugins": ["alerts", "features"], "configPath": ["xpack", "alerting_builtins"], "ui": false } diff --git a/x-pack/plugins/alerting_builtins/server/plugin.ts b/x-pack/plugins/alerting_builtins/server/plugin.ts index 12d1b080c7c63..d49321016daa6 100644 --- a/x-pack/plugins/alerting_builtins/server/plugin.ts +++ b/x-pack/plugins/alerting_builtins/server/plugin.ts @@ -9,6 +9,7 @@ import { Plugin, Logger, CoreSetup, CoreStart, PluginInitializerContext } from ' import { Service, IService, AlertingBuiltinsDeps } from './types'; import { getService as getServiceIndexThreshold } from './alert_types/index_threshold'; import { registerBuiltInAlertTypes } from './alert_types'; +import { ID as IndexThresholdId } from './alert_types/index_threshold/alert_type'; export class AlertingBuiltinsPlugin implements Plugin { private readonly logger: Logger; @@ -22,7 +23,42 @@ export class AlertingBuiltinsPlugin implements Plugin { }; } - public async setup(core: CoreSetup, { alerts }: AlertingBuiltinsDeps): Promise { + public async setup( + core: CoreSetup, + { alerts, features }: AlertingBuiltinsDeps + ): Promise { + features.registerFeature({ + id: 'alerts', + name: 'alerts', + app: [], + privileges: { + all: { + alerting: { + globally: { + all: [IndexThresholdId], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + read: { + alerting: { + globally: { + all: [IndexThresholdId], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, + }); + registerBuiltInAlertTypes({ service: this.service, router: core.http.createRouter(), diff --git a/x-pack/plugins/alerting_builtins/server/types.ts b/x-pack/plugins/alerting_builtins/server/types.ts index 95d34371a6d1e..38a9b6c52ecb7 100644 --- a/x-pack/plugins/alerting_builtins/server/types.ts +++ b/x-pack/plugins/alerting_builtins/server/types.ts @@ -5,6 +5,7 @@ */ import { Logger, ScopedClusterClient } from '../../../../src/core/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { PluginSetupContract as AlertingSetup } from '../../alerts/server'; import { getService as getServiceIndexThreshold } from './alert_types/index_threshold'; @@ -19,6 +20,7 @@ export { // this plugin's dependendencies export interface AlertingBuiltinsDeps { alerts: AlertingSetup; + features: FeaturesPluginSetup; } // external service exposed through plugin setup/start diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json index 74f740f52a8b2..4ad7aa3126e88 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/kibana.json @@ -3,7 +3,7 @@ "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack"], - "requiredPlugins": ["alerts", "triggers_actions_ui"], + "requiredPlugins": ["alerts", "triggers_actions_ui", "features"], "server": true, "ui": true } diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index fb431351a382d..5f4afd84522cf 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -9,16 +9,49 @@ import { PluginSetupContract as AlertingSetup, AlertType, } from '../../../../../../plugins/alerts/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../plugins/features/server'; // this plugin's dependendencies export interface AlertingExampleDeps { alerts: AlertingSetup; + features: FeaturesPluginSetup; } export class AlertingFixturePlugin implements Plugin { - public setup(core: CoreSetup, { alerts }: AlertingExampleDeps) { + public setup(core: CoreSetup, { alerts, features }: AlertingExampleDeps) { createNoopAlertType(alerts); createAlwaysFiringAlertType(alerts); + features.registerFeature({ + id: 'alerting_fixture', + name: 'alerting_fixture', + app: [], + privileges: { + all: { + alerting: { + globally: { + all: ['test.always-firing', 'test.noop'], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + read: { + alerting: { + globally: { + all: ['test.always-firing', 'test.noop'], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, + }); } public start() {} From 492f78abffcfc8c59ef51ff53f018ed0655493bc Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 4 Jun 2020 14:41:17 +0100 Subject: [PATCH 011/126] fixed lintin errors --- x-pack/plugins/alerts/server/plugin.ts | 2 +- .../privileges/feature_privilege_builder/alerting.ts | 10 ++++++---- x-pack/plugins/security/server/plugin.ts | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index e46be4efeaf9a..0a3dd8f825e49 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -264,7 +264,7 @@ export class AlertingPlugin { savedObjects: SavedObjectsServiceStart, elasticsearch: ElasticsearchServiceStart ): (request: KibanaRequest) => Services { - return request => ({ + return (request) => ({ callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser, savedObjectsClient: this.getScopedClientWithAlertSavedObjectType(savedObjects, request), getScopedCallCluster(clusterClient: IClusterClient) { diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts index 7935959c331ce..a47d1ffd5185c 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -27,14 +27,16 @@ export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { const allOperationsWithinConsumer = (privileges: string[], consumer?: string) => flatten( - privileges.map(type => [ - ...allOperations.map(operation => this.actions.alerting.get(type, consumer, operation)), + privileges.map((type) => [ + ...allOperations.map((operation) => this.actions.alerting.get(type, consumer, operation)), ]) ); const readOperationsWithinConsumer = (privileges: string[], consumer?: string) => flatten( - privileges.map(type => [ - ...readOperations.map(operation => this.actions.alerting.get(type, consumer, operation)), + privileges.map((type) => [ + ...readOperations.map((operation) => + this.actions.alerting.get(type, consumer, operation) + ), ]) ); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index b947292eb0aef..fc2d6a7472043 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -101,7 +101,7 @@ export class Plugin { public async setup(core: CoreSetup, { features, licensing }: PluginSetupDependencies) { const [config, legacyConfig] = await combineLatest([ this.initializerContext.config.create>().pipe( - map(rawConfig => + map((rawConfig) => createConfig(rawConfig, this.initializerContext.logger.get('config'), { isTLSEnabled: core.http.isTlsEnabled, }) @@ -189,7 +189,7 @@ export class Plugin { license, - registerSpacesService: service => { + registerSpacesService: (service) => { if (this.wasSpacesServiceAccessed()) { throw new Error('Spaces service has been accessed before registration.'); } From 076ebdf15300234bcd34e00117bc069b015e1cc2 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 4 Jun 2020 15:09:09 +0100 Subject: [PATCH 012/126] moved feature into main alerts plugin --- x-pack/plugins/alerting_builtins/kibana.json | 2 +- .../plugins/alerting_builtins/server/index.ts | 1 + .../alerting_builtins/server/plugin.ts | 38 +---------------- .../plugins/alerting_builtins/server/types.ts | 2 - x-pack/plugins/alerts/kibana.json | 2 +- x-pack/plugins/alerts/server/feature.ts | 41 +++++++++++++++++++ x-pack/plugins/alerts/server/plugin.ts | 4 ++ 7 files changed, 49 insertions(+), 41 deletions(-) create mode 100644 x-pack/plugins/alerts/server/feature.ts diff --git a/x-pack/plugins/alerting_builtins/kibana.json b/x-pack/plugins/alerting_builtins/kibana.json index dd70e53604f16..cc613d5247ef4 100644 --- a/x-pack/plugins/alerting_builtins/kibana.json +++ b/x-pack/plugins/alerting_builtins/kibana.json @@ -3,7 +3,7 @@ "server": true, "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["alerts", "features"], + "requiredPlugins": ["alerts"], "configPath": ["xpack", "alerting_builtins"], "ui": false } diff --git a/x-pack/plugins/alerting_builtins/server/index.ts b/x-pack/plugins/alerting_builtins/server/index.ts index 00613213d5aed..029547344fa66 100644 --- a/x-pack/plugins/alerting_builtins/server/index.ts +++ b/x-pack/plugins/alerting_builtins/server/index.ts @@ -7,6 +7,7 @@ import { PluginInitializerContext } from 'src/core/server'; import { AlertingBuiltinsPlugin } from './plugin'; import { configSchema } from './config'; +export { ID as IndexThresholdId } from './alert_types/index_threshold/alert_type'; export const plugin = (ctx: PluginInitializerContext) => new AlertingBuiltinsPlugin(ctx); diff --git a/x-pack/plugins/alerting_builtins/server/plugin.ts b/x-pack/plugins/alerting_builtins/server/plugin.ts index d49321016daa6..12d1b080c7c63 100644 --- a/x-pack/plugins/alerting_builtins/server/plugin.ts +++ b/x-pack/plugins/alerting_builtins/server/plugin.ts @@ -9,7 +9,6 @@ import { Plugin, Logger, CoreSetup, CoreStart, PluginInitializerContext } from ' import { Service, IService, AlertingBuiltinsDeps } from './types'; import { getService as getServiceIndexThreshold } from './alert_types/index_threshold'; import { registerBuiltInAlertTypes } from './alert_types'; -import { ID as IndexThresholdId } from './alert_types/index_threshold/alert_type'; export class AlertingBuiltinsPlugin implements Plugin { private readonly logger: Logger; @@ -23,42 +22,7 @@ export class AlertingBuiltinsPlugin implements Plugin { }; } - public async setup( - core: CoreSetup, - { alerts, features }: AlertingBuiltinsDeps - ): Promise { - features.registerFeature({ - id: 'alerts', - name: 'alerts', - app: [], - privileges: { - all: { - alerting: { - globally: { - all: [IndexThresholdId], - }, - }, - savedObject: { - all: [], - read: [], - }, - ui: [], - }, - read: { - alerting: { - globally: { - all: [IndexThresholdId], - }, - }, - savedObject: { - all: [], - read: [], - }, - ui: [], - }, - }, - }); - + public async setup(core: CoreSetup, { alerts }: AlertingBuiltinsDeps): Promise { registerBuiltInAlertTypes({ service: this.service, router: core.http.createRouter(), diff --git a/x-pack/plugins/alerting_builtins/server/types.ts b/x-pack/plugins/alerting_builtins/server/types.ts index 38a9b6c52ecb7..95d34371a6d1e 100644 --- a/x-pack/plugins/alerting_builtins/server/types.ts +++ b/x-pack/plugins/alerting_builtins/server/types.ts @@ -5,7 +5,6 @@ */ import { Logger, ScopedClusterClient } from '../../../../src/core/server'; -import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { PluginSetupContract as AlertingSetup } from '../../alerts/server'; import { getService as getServiceIndexThreshold } from './alert_types/index_threshold'; @@ -20,7 +19,6 @@ export { // this plugin's dependendencies export interface AlertingBuiltinsDeps { alerts: AlertingSetup; - features: FeaturesPluginSetup; } // external service exposed through plugin setup/start diff --git a/x-pack/plugins/alerts/kibana.json b/x-pack/plugins/alerts/kibana.json index 3509f79dbbe4d..e10d9dc4a4db2 100644 --- a/x-pack/plugins/alerts/kibana.json +++ b/x-pack/plugins/alerts/kibana.json @@ -5,6 +5,6 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "alerts"], - "requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "actions", "eventLog"], + "requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "actions", "eventLog", "features"], "optionalPlugins": ["usageCollection", "spaces", "security"] } diff --git a/x-pack/plugins/alerts/server/feature.ts b/x-pack/plugins/alerts/server/feature.ts new file mode 100644 index 0000000000000..8f92c6cc3b567 --- /dev/null +++ b/x-pack/plugins/alerts/server/feature.ts @@ -0,0 +1,41 @@ +/* + * 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 { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { IndexThresholdId } from '../../alerting_builtins/server'; + +export function registerFeature(features: FeaturesPluginSetup) { + features.registerFeature({ + id: 'alerts', + name: 'alerts', + app: [], + privileges: { + all: { + alerting: { + globally: { + all: [IndexThresholdId], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + read: { + alerting: { + globally: { + all: [IndexThresholdId], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, + }); +} diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 0a3dd8f825e49..932e3f0dfb46f 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -58,7 +58,9 @@ import { Services } from './types'; import { registerAlertsUsageCollector } from './usage'; import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/task'; import { IEventLogger, IEventLogService } from '../../event_log/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { setupSavedObjects } from './saved_objects'; +import { registerFeature } from './feature'; const EVENT_LOG_PROVIDER = 'alerting'; export const EVENT_LOG_ACTIONS = { @@ -85,6 +87,7 @@ export interface AlertingPluginsSetup { spaces?: SpacesPluginSetup; usageCollection?: UsageCollectionSetup; eventLog: IEventLogService; + features: FeaturesPluginSetup; } export interface AlertingPluginsStart { actions: ActionsPluginStartContract; @@ -136,6 +139,7 @@ export class AlertingPlugin { ); } + registerFeature(plugins.features); setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects); plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS)); From bdd5d2812e13c45d2077da6591012cc956abcc7e Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 4 Jun 2020 16:59:14 +0100 Subject: [PATCH 013/126] fixed security tests --- x-pack/plugins/alerts/server/feature.ts | 2 +- x-pack/plugins/alerts/server/plugin.test.ts | 98 +++++++++++++++++++ .../apis/security/privileges.ts | 1 + 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/alerts/server/feature.ts b/x-pack/plugins/alerts/server/feature.ts index 8f92c6cc3b567..119a9f06a4844 100644 --- a/x-pack/plugins/alerts/server/feature.ts +++ b/x-pack/plugins/alerts/server/feature.ts @@ -27,7 +27,7 @@ export function registerFeature(features: FeaturesPluginSetup) { read: { alerting: { globally: { - all: [IndexThresholdId], + read: [IndexThresholdId], }, }, savedObject: { diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index 008a9bb804c5b..b676c099e490f 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -11,6 +11,7 @@ import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/ import { taskManagerMock } from '../../task_manager/server/mocks'; import { eventLogServiceMock } from '../../event_log/server/event_log_service.mock'; import { KibanaRequest, CoreSetup } from 'kibana/server'; +import { featuresPluginMock } from '../../features/server/mocks'; describe('Alerting Plugin', () => { describe('setup()', () => { @@ -33,6 +34,7 @@ describe('Alerting Plugin', () => { encryptedSavedObjects: encryptedSavedObjectsSetup, taskManager: taskManagerMock.createSetup(), eventLog: eventLogServiceMock.create(), + features: featuresPluginMock.createSetup(), } as unknown) as AlertingPluginsSetup ); @@ -41,6 +43,100 @@ describe('Alerting Plugin', () => { 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.' ); }); + + it('should grant global `all` priviliges to built in AlertTypes for anyone with `all` priviliges to alerts', async () => { + const context = coreMock.createPluginInitializerContext(); + const plugin = new AlertingPlugin(context); + + const coreSetup = coreMock.createSetup(); + const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); + const features = featuresPluginMock.createSetup(); + await plugin.setup( + ({ + ...coreSetup, + http: { + ...coreSetup.http, + route: jest.fn(), + }, + } as unknown) as CoreSetup, + ({ + licensing: licensingMock.createSetup(), + encryptedSavedObjects: encryptedSavedObjectsSetup, + taskManager: taskManagerMock.createSetup(), + eventLog: eventLogServiceMock.create(), + features, + } as unknown) as AlertingPluginsSetup + ); + + expect(features.registerFeature).toHaveBeenCalledTimes(1); + const { privileges } = features.registerFeature.mock.calls[0][0]; + + expect(privileges?.all.alerting).toMatchInlineSnapshot(` + Object { + "globally": Object { + "all": Array [ + ".index-threshold", + ], + }, + } + `); + expect(privileges?.read.alerting).toMatchInlineSnapshot(` + Object { + "globally": Object { + "read": Array [ + ".index-threshold", + ], + }, + } + `); + }); + + it('should grant global `read` priviliges to built in AlertTypes for anyone with `read` priviliges to alerts', async () => { + const context = coreMock.createPluginInitializerContext(); + const plugin = new AlertingPlugin(context); + + const coreSetup = coreMock.createSetup(); + const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); + const features = featuresPluginMock.createSetup(); + await plugin.setup( + ({ + ...coreSetup, + http: { + ...coreSetup.http, + route: jest.fn(), + }, + } as unknown) as CoreSetup, + ({ + licensing: licensingMock.createSetup(), + encryptedSavedObjects: encryptedSavedObjectsSetup, + taskManager: taskManagerMock.createSetup(), + eventLog: eventLogServiceMock.create(), + features, + } as unknown) as AlertingPluginsSetup + ); + + expect(features.registerFeature).toHaveBeenCalledTimes(1); + const { privileges } = features.registerFeature.mock.calls[0][0]; + + expect(privileges?.all.alerting).toMatchInlineSnapshot(` + Object { + "globally": Object { + "all": Array [ + ".index-threshold", + ], + }, + } + `); + expect(privileges?.read.alerting).toMatchInlineSnapshot(` + Object { + "globally": Object { + "read": Array [ + ".index-threshold", + ], + }, + } + `); + }); }); describe('start()', () => { @@ -71,6 +167,7 @@ describe('Alerting Plugin', () => { encryptedSavedObjects: encryptedSavedObjectsSetup, taskManager: taskManagerMock.createSetup(), eventLog: eventLogServiceMock.create(), + features: featuresPluginMock.createSetup(), } as unknown) as AlertingPluginsSetup ); @@ -115,6 +212,7 @@ describe('Alerting Plugin', () => { encryptedSavedObjects: encryptedSavedObjectsSetup, taskManager: taskManagerMock.createSetup(), eventLog: eventLogServiceMock.create(), + features: featuresPluginMock.createSetup(), } as unknown) as AlertingPluginsSetup ); diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index bcadd4fa06360..6599c476b8bae 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -37,6 +37,7 @@ export default function ({ getService }: FtrProviderContext) { apm: ['all', 'read'], securitySolution: ['all', 'read'], ingestManager: ['all', 'read'], + alerts: ['all', 'read'], }, global: ['all', 'read'], space: ['all', 'read'], From 711fdbac29766db3953ee16314cd33c31595fe8a Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 4 Jun 2020 17:18:45 +0100 Subject: [PATCH 014/126] fixed security unit tests --- .../server/authorization/disable_ui_capabilities.test.ts | 3 +++ x-pack/plugins/security/server/plugin.test.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts index 082484d5fa6b4..93cf6dccc8a6d 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts @@ -20,6 +20,9 @@ const mockRequest = httpServerMock.createKibanaRequest(); const createMockAuthz = (options: MockAuthzOptions) => { const mock = authorizationMock.create({ version: '1.0.0-zeta1' }); + // plug actual ui actions into mock Actions with + mock.actions = actions; + mock.checkPrivilegesDynamicallyWithRequest.mockImplementation((request) => { expect(request).toBe(mockRequest); diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 42c9b5c5be4e5..8b60f375bfa52 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -65,6 +65,9 @@ describe('Security Plugin', () => { }, "authz": Object { "actions": Actions { + "alerting": AlertingActions { + "prefix": "alerting:version:", + }, "api": ApiActions { "prefix": "api:version:", }, From a15c7d9f546163b7785a9d3c6ce6f50812536588 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 4 Jun 2020 18:55:14 +0100 Subject: [PATCH 015/126] added _global namespace before global privileges --- .../authorization/actions/alerting.test.ts | 8 +- .../server/authorization/actions/alerting.ts | 4 +- .../alerting.test.ts | 128 +++++++++--------- .../feature_privilege_builder/alerting.ts | 28 ++-- .../tests/alerting/find.ts | 7 +- 5 files changed, 83 insertions(+), 92 deletions(-) diff --git a/x-pack/plugins/security/server/authorization/actions/alerting.test.ts b/x-pack/plugins/security/server/authorization/actions/alerting.test.ts index fcd1e4aea0628..75d5a70e9302c 100644 --- a/x-pack/plugins/security/server/authorization/actions/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/actions/alerting.test.ts @@ -36,17 +36,17 @@ describe('#get', () => { }); }); - test('returns `alerting:${alertType}/${consumer}/${operation}`', () => { + test('returns `alerting:${alertType}/feature/${consumer}/${operation}`', () => { const alertingActions = new AlertingActions(version); expect(alertingActions.get('foo-alertType', 'consumer', 'bar-operation')).toBe( - 'alerting:1.0.0-zeta1:foo-alertType/consumer/bar-operation' + 'alerting:1.0.0-zeta1:foo-alertType/feature/consumer/bar-operation' ); }); - test('returns `alerting:${alertType}/${operation}` when no consumer is specified', () => { + test('returns `alerting:${alertType}/_global/${operation}` when no consumer is specified', () => { const alertingActions = new AlertingActions(version); expect(alertingActions.get('foo-alertType', undefined, 'bar-operation')).toBe( - 'alerting:1.0.0-zeta1:foo-alertType/bar-operation' + 'alerting:1.0.0-zeta1:foo-alertType/_global/bar-operation' ); }); }); diff --git a/x-pack/plugins/security/server/authorization/actions/alerting.ts b/x-pack/plugins/security/server/authorization/actions/alerting.ts index a3e56701a60d3..e8c7e8005b5d2 100644 --- a/x-pack/plugins/security/server/authorization/actions/alerting.ts +++ b/x-pack/plugins/security/server/authorization/actions/alerting.ts @@ -26,6 +26,8 @@ export class AlertingActions { throw new Error('consumer is optional but must be a string when specified'); } - return `${this.prefix}${alertTypeId}${consumer ? `/${consumer}` : ''}/${operation}`; + return `${this.prefix}${alertTypeId}/${ + consumer ? `feature/${consumer}` : '_global' + }/${operation}`; } } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts index 2add0dbb20302..4036154aef9a6 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts @@ -72,9 +72,9 @@ describe(`feature_privilege_builder`, () => { expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "alerting:1.0.0-zeta1:alert-type/my-feature/get", - "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", - "alerting:1.0.0-zeta1:alert-type/my-feature/find", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/get", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/getAlertState", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/find", ] `); }); @@ -108,19 +108,19 @@ describe(`feature_privilege_builder`, () => { expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "alerting:1.0.0-zeta1:alert-type/my-feature/get", - "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", - "alerting:1.0.0-zeta1:alert-type/my-feature/find", - "alerting:1.0.0-zeta1:alert-type/my-feature/create", - "alerting:1.0.0-zeta1:alert-type/my-feature/delete", - "alerting:1.0.0-zeta1:alert-type/my-feature/update", - "alerting:1.0.0-zeta1:alert-type/my-feature/updateApiKey", - "alerting:1.0.0-zeta1:alert-type/my-feature/enable", - "alerting:1.0.0-zeta1:alert-type/my-feature/disable", - "alerting:1.0.0-zeta1:alert-type/my-feature/muteAll", - "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteAll", - "alerting:1.0.0-zeta1:alert-type/my-feature/muteInstance", - "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteInstance", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/get", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/getAlertState", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/find", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/create", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/delete", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/update", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/enable", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/disable", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/muteAll", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/muteInstance", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/unmuteInstance", ] `); }); @@ -154,22 +154,22 @@ describe(`feature_privilege_builder`, () => { expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "alerting:1.0.0-zeta1:alert-type/my-feature/get", - "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", - "alerting:1.0.0-zeta1:alert-type/my-feature/find", - "alerting:1.0.0-zeta1:alert-type/my-feature/create", - "alerting:1.0.0-zeta1:alert-type/my-feature/delete", - "alerting:1.0.0-zeta1:alert-type/my-feature/update", - "alerting:1.0.0-zeta1:alert-type/my-feature/updateApiKey", - "alerting:1.0.0-zeta1:alert-type/my-feature/enable", - "alerting:1.0.0-zeta1:alert-type/my-feature/disable", - "alerting:1.0.0-zeta1:alert-type/my-feature/muteAll", - "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteAll", - "alerting:1.0.0-zeta1:alert-type/my-feature/muteInstance", - "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteInstance", - "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/get", - "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/getAlertState", - "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/find", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/get", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/getAlertState", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/find", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/create", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/delete", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/update", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/enable", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/disable", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/muteAll", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/muteInstance", + "alerting:1.0.0-zeta1:alert-type/feature/my-feature/unmuteInstance", + "alerting:1.0.0-zeta1:readonly-alert-type/feature/my-feature/get", + "alerting:1.0.0-zeta1:readonly-alert-type/feature/my-feature/getAlertState", + "alerting:1.0.0-zeta1:readonly-alert-type/feature/my-feature/find", ] `); }); @@ -207,9 +207,9 @@ describe(`feature_privilege_builder`, () => { expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "alerting:1.0.0-zeta1:alert-type/get", - "alerting:1.0.0-zeta1:alert-type/getAlertState", - "alerting:1.0.0-zeta1:alert-type/find", + "alerting:1.0.0-zeta1:alert-type/_global/get", + "alerting:1.0.0-zeta1:alert-type/_global/getAlertState", + "alerting:1.0.0-zeta1:alert-type/_global/find", ] `); }); @@ -245,19 +245,19 @@ describe(`feature_privilege_builder`, () => { expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "alerting:1.0.0-zeta1:alert-type/get", - "alerting:1.0.0-zeta1:alert-type/getAlertState", - "alerting:1.0.0-zeta1:alert-type/find", - "alerting:1.0.0-zeta1:alert-type/create", - "alerting:1.0.0-zeta1:alert-type/delete", - "alerting:1.0.0-zeta1:alert-type/update", - "alerting:1.0.0-zeta1:alert-type/updateApiKey", - "alerting:1.0.0-zeta1:alert-type/enable", - "alerting:1.0.0-zeta1:alert-type/disable", - "alerting:1.0.0-zeta1:alert-type/muteAll", - "alerting:1.0.0-zeta1:alert-type/unmuteAll", - "alerting:1.0.0-zeta1:alert-type/muteInstance", - "alerting:1.0.0-zeta1:alert-type/unmuteInstance", + "alerting:1.0.0-zeta1:alert-type/_global/get", + "alerting:1.0.0-zeta1:alert-type/_global/getAlertState", + "alerting:1.0.0-zeta1:alert-type/_global/find", + "alerting:1.0.0-zeta1:alert-type/_global/create", + "alerting:1.0.0-zeta1:alert-type/_global/delete", + "alerting:1.0.0-zeta1:alert-type/_global/update", + "alerting:1.0.0-zeta1:alert-type/_global/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/_global/enable", + "alerting:1.0.0-zeta1:alert-type/_global/disable", + "alerting:1.0.0-zeta1:alert-type/_global/muteAll", + "alerting:1.0.0-zeta1:alert-type/_global/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/_global/muteInstance", + "alerting:1.0.0-zeta1:alert-type/_global/unmuteInstance", ] `); }); @@ -293,22 +293,22 @@ describe(`feature_privilege_builder`, () => { expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "alerting:1.0.0-zeta1:alert-type/get", - "alerting:1.0.0-zeta1:alert-type/getAlertState", - "alerting:1.0.0-zeta1:alert-type/find", - "alerting:1.0.0-zeta1:alert-type/create", - "alerting:1.0.0-zeta1:alert-type/delete", - "alerting:1.0.0-zeta1:alert-type/update", - "alerting:1.0.0-zeta1:alert-type/updateApiKey", - "alerting:1.0.0-zeta1:alert-type/enable", - "alerting:1.0.0-zeta1:alert-type/disable", - "alerting:1.0.0-zeta1:alert-type/muteAll", - "alerting:1.0.0-zeta1:alert-type/unmuteAll", - "alerting:1.0.0-zeta1:alert-type/muteInstance", - "alerting:1.0.0-zeta1:alert-type/unmuteInstance", - "alerting:1.0.0-zeta1:readonly-alert-type/get", - "alerting:1.0.0-zeta1:readonly-alert-type/getAlertState", - "alerting:1.0.0-zeta1:readonly-alert-type/find", + "alerting:1.0.0-zeta1:alert-type/_global/get", + "alerting:1.0.0-zeta1:alert-type/_global/getAlertState", + "alerting:1.0.0-zeta1:alert-type/_global/find", + "alerting:1.0.0-zeta1:alert-type/_global/create", + "alerting:1.0.0-zeta1:alert-type/_global/delete", + "alerting:1.0.0-zeta1:alert-type/_global/update", + "alerting:1.0.0-zeta1:alert-type/_global/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/_global/enable", + "alerting:1.0.0-zeta1:alert-type/_global/disable", + "alerting:1.0.0-zeta1:alert-type/_global/muteAll", + "alerting:1.0.0-zeta1:alert-type/_global/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/_global/muteInstance", + "alerting:1.0.0-zeta1:alert-type/_global/unmuteInstance", + "alerting:1.0.0-zeta1:readonly-alert-type/_global/get", + "alerting:1.0.0-zeta1:readonly-alert-type/_global/getAlertState", + "alerting:1.0.0-zeta1:readonly-alert-type/_global/find", ] `); }); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts index a47d1ffd5185c..4ef93f5f07276 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -25,26 +25,20 @@ const allOperations: string[] = [...readOperations, ...writeOperations]; export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder { public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { - const allOperationsWithinConsumer = (privileges: string[], consumer?: string) => - flatten( - privileges.map((type) => [ - ...allOperations.map((operation) => this.actions.alerting.get(type, consumer, operation)), - ]) - ); - const readOperationsWithinConsumer = (privileges: string[], consumer?: string) => - flatten( - privileges.map((type) => [ - ...readOperations.map((operation) => - this.actions.alerting.get(type, consumer, operation) - ), - ]) + const getAlertingPrivilege = ( + operations: string[], + privilegedTypes: string[], + consumer?: string + ) => + privilegedTypes.flatMap((type) => + operations.map((operation) => this.actions.alerting.get(type, consumer, operation)) ); return uniq([ - ...allOperationsWithinConsumer(privilegeDefinition.alerting?.all ?? [], feature.id), - ...readOperationsWithinConsumer(privilegeDefinition.alerting?.read ?? [], feature.id), - ...allOperationsWithinConsumer(privilegeDefinition.alerting?.globally?.all ?? []), - ...readOperationsWithinConsumer(privilegeDefinition.alerting?.globally?.read ?? []), + ...getAlertingPrivilege(allOperations, privilegeDefinition.alerting?.all ?? [], feature.id), + ...getAlertingPrivilege(readOperations, privilegeDefinition.alerting?.read ?? [], feature.id), + ...getAlertingPrivilege(allOperations, privilegeDefinition.alerting?.globally?.all ?? []), + ...getAlertingPrivilege(readOperations, privilegeDefinition.alerting?.globally?.read ?? []), ]); } } 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 f21cb9f486fb5..b54767cb5ebc5 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 @@ -6,12 +6,7 @@ import expect from '@kbn/expect'; import { UserAtSpaceScenarios } from '../../scenarios'; -import { - getUrlPrefix, - getTestAlertData, - ObjectRemover, - getUnauthorizedErrorMessage, -} from '../../../common/lib'; +import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export From 87d099f761ec9dbbb2c1c97c9ae6afc6013699d8 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 4 Jun 2020 20:10:26 +0100 Subject: [PATCH 016/126] fixed security acceptance tests --- x-pack/test/api_integration/apis/features/features/features.ts | 1 + x-pack/test/api_integration/apis/security/privileges_basic.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index ed93b627f003c..bcb32c6743821 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -112,6 +112,7 @@ export default function ({ getService }: FtrProviderContext) { 'uptime', 'securitySolution', 'ingestManager', + 'alerts', ].sort() ); }); diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 1270c03b8a977..880f6c7548025 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -35,6 +35,7 @@ export default function ({ getService }: FtrProviderContext) { apm: ['all', 'read'], securitySolution: ['all', 'read'], ingestManager: ['all', 'read'], + alerts: ['all', 'read'], }, global: ['all', 'read'], space: ['all', 'read'], From ade2c4c3d34eb8dc4c5d6d12b83ff7c238af5ecd Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 4 Jun 2020 23:40:12 +0100 Subject: [PATCH 017/126] fixed lint --- .../privileges/feature_privilege_builder/alerting.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts index 4ef93f5f07276..e512330335418 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flatten, uniq } from 'lodash'; +import { uniq } from 'lodash'; import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; From 0ace530bdbe059d1abcf5864270f5fd1f545e9df Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 5 Jun 2020 11:25:46 +0100 Subject: [PATCH 018/126] use alerts privileges in the alertsExample feature --- examples/alerting_example/kibana.json | 2 +- examples/alerting_example/server/plugin.ts | 36 +++++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/examples/alerting_example/kibana.json b/examples/alerting_example/kibana.json index 2b6389649cef9..aa5b0dd45895e 100644 --- a/examples/alerting_example/kibana.json +++ b/examples/alerting_example/kibana.json @@ -4,6 +4,6 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["triggers_actions_ui", "charts", "data", "alerts", "actions"], + "requiredPlugins": ["triggers_actions_ui", "charts", "data", "alerts", "actions", "features"], "optionalPlugins": [] } diff --git a/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts index cdb005feca35c..9128281fb72e5 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/examples/alerting_example/server/plugin.ts @@ -19,6 +19,7 @@ import { Plugin, CoreSetup } from 'kibana/server'; import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerts/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../../x-pack/plugins/features/server'; import { alertType as alwaysFiringAlert } from './alert_types/always_firing'; import { alertType as peopleInSpaceAlert } from './alert_types/astros'; @@ -26,12 +27,45 @@ import { alertType as peopleInSpaceAlert } from './alert_types/astros'; // this plugin's dependendencies export interface AlertingExampleDeps { alerts: AlertingSetup; + features: FeaturesPluginSetup; } export class AlertingExamplePlugin implements Plugin { - public setup(core: CoreSetup, { alerts }: AlertingExampleDeps) { + public setup(core: CoreSetup, { alerts, features }: AlertingExampleDeps) { alerts.registerType(alwaysFiringAlert); alerts.registerType(peopleInSpaceAlert); + + features.registerFeature({ + id: 'alertsExample', + name: 'alertsExample', + app: [], + privileges: { + all: { + alerting: { + globally: { + all: [alwaysFiringAlert.id, peopleInSpaceAlert.id], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + read: { + alerting: { + globally: { + read: [alwaysFiringAlert.id, peopleInSpaceAlert.id], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, + }); } public start() {} From efdb521b09f1165f6d23bb83b01d360cc47eb834 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 8 Jun 2020 11:48:32 +0100 Subject: [PATCH 019/126] added more acceptance tests around alert creation and auth --- x-pack/plugins/alerts/common/index.ts | 1 + .../alerts/server/alerts_client.test.ts | 3258 +++++++++-------- x-pack/plugins/alerts/server/alerts_client.ts | 151 +- .../server/alerts_client_factory.test.ts | 5 + .../alerts/server/alerts_client_factory.ts | 7 +- x-pack/plugins/alerts/server/feature.ts | 8 +- x-pack/plugins/alerts/server/plugin.test.ts | 32 +- x-pack/plugins/alerts/server/plugin.ts | 7 +- .../server/routes/list_alert_types.test.ts | 4 + .../common/feature_kibana_privileges.ts | 35 +- x-pack/plugins/infra/server/features.ts | 24 +- .../authorization/actions/alerting.test.ts | 13 +- .../server/authorization/actions/alerting.ts | 12 +- .../alerting.test.ts | 203 +- .../feature_privilege_builder/alerting.ts | 4 +- .../sections/alert_form/alert_form.tsx | 3 +- .../alerts_list/components/alerts_list.tsx | 3 +- .../alerts/server/builtin_alert_types.ts | 23 + .../fixtures/plugins/alerts/server/plugin.ts | 11 +- .../plugins/alerts_restricted/kibana.json | 10 + .../plugins/alerts_restricted/package.json | 20 + .../alerts_restricted/server/alert_types.ts | 33 + .../plugins/alerts_restricted/server/index.ts | 9 + .../alerts_restricted/server/plugin.ts | 66 + .../security_and_spaces/scenarios.ts | 51 +- .../tests/actions/create.ts | 5 + .../tests/actions/delete.ts | 4 + .../tests/actions/execute.ts | 7 + .../security_and_spaces/tests/actions/get.ts | 3 + .../tests/actions/get_all.ts | 3 + .../tests/actions/list_action_types.ts | 1 + .../tests/actions/update.ts | 7 + .../tests/alerting/alerts.ts | 11 + .../tests/alerting/create.ts | 161 +- .../tests/alerting/delete.ts | 3 + .../tests/alerting/disable.ts | 3 + .../tests/alerting/enable.ts | 3 + .../tests/alerting/find.ts | 3 + .../security_and_spaces/tests/alerting/get.ts | 3 + .../tests/alerting/get_alert_state.ts | 3 + .../tests/alerting/list_alert_types.ts | 40 +- .../tests/alerting/mute_all.ts | 1 + .../tests/alerting/mute_instance.ts | 2 + .../tests/alerting/unmute_all.ts | 1 + .../tests/alerting/unmute_instance.ts | 1 + .../tests/alerting/update.ts | 8 + .../tests/alerting/update_api_key.ts | 3 + 47 files changed, 2358 insertions(+), 1911 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/builtin_alert_types.ts create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/kibana.json create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/package.json create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/alert_types.ts create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/index.ts create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts diff --git a/x-pack/plugins/alerts/common/index.ts b/x-pack/plugins/alerts/common/index.ts index 88a8da5a3e575..af067e08d73f6 100644 --- a/x-pack/plugins/alerts/common/index.ts +++ b/x-pack/plugins/alerts/common/index.ts @@ -21,3 +21,4 @@ export interface AlertingFrameworkHealth { } export const BASE_ALERT_API_PATH = '/api/alerts'; +export const AlertsFeatureId = 'alerts'; diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 07d5ba5829ec8..f0f35717a5d22 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -6,27 +6,38 @@ import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; import { KibanaRequest } from 'kibana/server'; -import { AlertsClient, CreateOptions, UpdateOptions, FindOptions } from './alerts_client'; +import { + AlertsClient, + CreateOptions, + // , UpdateOptions, FindOptions +} from './alerts_client'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../src/core/server/mocks'; import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; import { TaskStatus } from '../../task_manager/server'; -import { IntervalSchedule, PartialAlert } from './types'; +import { + IntervalSchedule, + // PartialAlert +} from './types'; import { resolvable } from './test_utils'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { actionsClientMock } from '../../actions/server/mocks'; import { SecurityPluginSetup } from '../../../plugins/security/server'; import { securityMock } from '../../../plugins/security/server/mocks'; +import { PluginStartContract as FeaturesStartContract, Feature } from '../../features/server'; +import { featuresPluginMock } from '../../features/server/mocks'; const taskManager = taskManagerMock.start(); const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const features: jest.Mocked = featuresPluginMock.createStart(); const alertsClientParams = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, + features, request: {} as KibanaRequest, spaceId: 'default', namespace: 'default', @@ -43,10 +54,43 @@ function mockAuthorization() { // typescript is havingtrouble inferring jest's automocking (authorization.actions.alerting.get as jest.MockedFunction< typeof authorization.actions.alerting.get - >).mockImplementation((type, app, operation) => `${type}${app ? `/${app}` : ``}/${operation}`); + >).mockImplementation((type, app, operation) => `${type}/${app}/${operation}`); return authorization; } +function mockFeature(appName: string, typeName: string, requiredApps: string[] = []) { + return new Feature({ + id: appName, + name: appName, + app: requiredApps, + privileges: { + all: { + alerting: { + all: [typeName], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + read: { + alerting: { + read: [typeName], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, + }); +} +const alertsFeature = mockFeature('alerts', 'myBuiltInType'); +const myAppFeature = mockFeature('myApp', 'myType', ['alerts']); +const myOtherAppFeature = mockFeature('myOtherApp', 'myType', ['alerts']); + beforeEach(() => { jest.resetAllMocks(); alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false }); @@ -84,6 +128,27 @@ beforeEach(() => { }, ]); alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); + + alertTypeRegistry.get.mockImplementation((id) => + id !== 'myType' + ? { + id: '123', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + async executor() {}, + producer: 'alerts', + } + : { + id: 'myType', + name: 'My Alert Type', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + async executor() {}, + producer: 'myApp', + } + ); + features.getFeatures.mockReturnValue([alertsFeature, myAppFeature, myOtherAppFeature]); }); const mockedDate = new Date('2019-02-12T21:01:22.479Z'); @@ -127,14 +192,6 @@ describe('create()', () => { beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); - alertTypeRegistry.get.mockReturnValue({ - id: '123', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - async executor() {}, - producer: 'alerting', - }); }); describe('authorization', () => { @@ -227,15 +284,11 @@ describe('create()', () => { return alertsClientWithAuthorization.create(options); } - test('create when user is authorised to create this type of alert type for the specified consumer', async () => { + test('create when user is authorised to create this type of alert type for the producer', async () => { checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, + hasAllRequested: true, username: '', privileges: [ - { - privilege: 'myType/create', - authorized: false, - }, { privilege: 'myType/myApp/create', authorized: true, @@ -250,43 +303,38 @@ describe('create()', () => { await tryToExecuteOperation({ data }); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'create' - ); expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); }); - test('create when user is authorised to create this type of alert type globally', async () => { + test('create when user is authorised to create this type of alert type for the specified consumer and producer', async () => { checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, + hasAllRequested: true, username: '', privileges: [ { - privilege: 'myType/create', + privilege: 'myType/myApp/create', authorized: true, }, { - privilege: 'myType/myApp/create', - authorized: false, + privilege: 'myType/myOtherApp/create', + authorized: true, }, ], }); const data = getMockData({ alertTypeId: 'myType', - consumer: 'myApp', + consumer: 'myOtherApp', }); await tryToExecuteOperation({ data }); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'myType', - undefined, + 'myOtherApp', 'create' ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); }); test('throws when user is not authorised to create this type of alert at all', async () => { @@ -295,11 +343,11 @@ describe('create()', () => { username: '', privileges: [ { - privilege: 'myType/create', + privilege: 'myType/myApp/create', authorized: false, }, { - privilege: 'myType/myApp/create', + privilege: 'myType/myOtherApp/create', authorized: false, }, ], @@ -314,6 +362,32 @@ describe('create()', () => { `[Error: Unauthorized to create a "myType" alert for "myApp"]` ); }); + + test('throws when user is not authorised to create this type of alert at consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + username: '', + privileges: [ + { + privilege: 'myType/myApp/create', + authorized: true, + }, + { + privilege: 'myType/myOtherApp/create', + authorized: false, + }, + ], + }); + + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myOtherApp', + }); + + await expect(tryToExecuteOperation({ data })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to create a "myType" alert for "myOtherApp"]` + ); + }); }); test('creates an alert', async () => { @@ -719,7 +793,7 @@ describe('create()', () => { }), }, async executor() {}, - producer: 'alerting', + producer: 'alerts', }); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"params invalid: [param1]: expected value of type [string] but got [undefined]"` @@ -1120,6 +1194,18 @@ describe('enable()', () => { version: '123', references: [], }; + const alertInOtherFeature = { + id: '2', + type: 'alert', + attributes: { + consumer: 'myOtherApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + enabled: false, + }, + version: '123', + references: [], + }; beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); @@ -1180,17 +1266,17 @@ describe('enable()', () => { }); }); - test('enable when user is authorised to enable this type of alert type for the specified consumer', async () => { + test('enable when user is authorised to enable this type of alert type for the producer', async () => { checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, + hasAllRequested: true, username: '', privileges: [ { - privilege: 'myType/enable', - authorized: false, + privilege: 'myType/myApp/enable', + authorized: true, }, { - privilege: 'myType/myApp/enable', + privilege: 'myType/myOtherApp/enable', authorized: true, }, ], @@ -1198,58 +1284,59 @@ describe('enable()', () => { await alertsClientWithAuthorization.enable({ id: '1' }); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'enable' - ); expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'enable'); }); - test('enable when user is authorised to enable this type of alert type globally', async () => { + test('enable when user is authorised to enable this type of alert type for producer and consumer', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(alertInOtherFeature); + unsecuredSavedObjectsClient.get.mockResolvedValue(alertInOtherFeature); checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, + hasAllRequested: true, username: '', privileges: [ { - privilege: 'myType/enable', + privilege: 'myType/myApp/enable', authorized: true, }, { - privilege: 'myType/myApp/enable', - authorized: false, + privilege: 'myType/myOtherApp/enable', + authorized: true, }, ], }); - await alertsClientWithAuthorization.enable({ id: '1' }); + await alertsClientWithAuthorization.enable({ id: alertInOtherFeature.id }); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'enable'); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'myType', - undefined, + 'myOtherApp', 'enable' ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'enable'); }); test('throws when user is not authorised to enable this type of alert at all', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(alertInOtherFeature); + unsecuredSavedObjectsClient.get.mockResolvedValue(alertInOtherFeature); checkPrivileges.mockResolvedValueOnce({ hasAllRequested: false, username: '', privileges: [ { - privilege: 'myType/enable', - authorized: false, + privilege: 'myType/myApp/enable', + authorized: true, }, { - privilege: 'myType/myApp/enable', + privilege: 'myType/myOtherApp/enable', authorized: false, }, ], }); - expect(alertsClientWithAuthorization.enable({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to enable a "myType" alert for "myApp"]` + expect( + alertsClientWithAuthorization.enable({ id: alertInOtherFeature.id }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to enable a "myType" alert for "myOtherApp"]` ); }); }); @@ -1451,116 +1538,116 @@ describe('disable()', () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); }); - describe('authorization', () => { - let authorization: jest.Mocked; - let alertsClientWithAuthorization: AlertsClient; - let checkPrivileges: jest.MockedFunction>; - - beforeEach(() => { - authorization = mockAuthorization(); - alertsClientWithAuthorization = new AlertsClient({ - authorization, - ...alertsClientParams, - }); - - checkPrivileges = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); - alertsClientParams.createAPIKey.mockResolvedValue({ - apiKeysEnabled: false, - }); - taskManager.schedule.mockResolvedValue({ - id: 'task-123', - scheduledAt: new Date(), - attempts: 0, - status: TaskStatus.Idle, - runAt: new Date(), - state: {}, - params: {}, - taskType: '', - startedAt: null, - retryAt: null, - ownerId: null, - }); - }); - - test('disables when user is authorised to disable this type of alert type for the specified consumer', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/disable', - authorized: false, - }, - { - privilege: 'myType/myApp/disable', - authorized: true, - }, - ], - }); - - await alertsClientWithAuthorization.disable({ id: '1' }); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'disable' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'disable'); - }); - - test('disables when user is authorised to disable this type of alert type globally', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/disable', - authorized: true, - }, - { - privilege: 'myType/myApp/disable', - authorized: false, - }, - ], - }); - - await alertsClientWithAuthorization.disable({ id: '1' }); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'disable' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'disable'); - }); - - test('throws when user is not authorised to disable this type of alert at all', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/disable', - authorized: false, - }, - { - privilege: 'myType/myApp/disable', - authorized: false, - }, - ], - }); - - expect(alertsClientWithAuthorization.disable({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to disable a "myType" alert for "myApp"]` - ); - }); - }); + // describe('authorization', () => { + // let authorization: jest.Mocked; + // let alertsClientWithAuthorization: AlertsClient; + // let checkPrivileges: jest.MockedFunction>; + + // beforeEach(() => { + // authorization = mockAuthorization(); + // alertsClientWithAuthorization = new AlertsClient({ + // authorization, + // ...alertsClientParams, + // }); + + // checkPrivileges = jest.fn(); + // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + // encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); + // unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + // alertsClientParams.createAPIKey.mockResolvedValue({ + // apiKeysEnabled: false, + // }); + // taskManager.schedule.mockResolvedValue({ + // id: 'task-123', + // scheduledAt: new Date(), + // attempts: 0, + // status: TaskStatus.Idle, + // runAt: new Date(), + // state: {}, + // params: {}, + // taskType: '', + // startedAt: null, + // retryAt: null, + // ownerId: null, + // }); + // }); + + // test('disables when user is authorised to disable this type of alert type for the specified consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/disable', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/disable', + // authorized: true, + // }, + // ], + // }); + + // await alertsClientWithAuthorization.disable({ id: '1' }); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // undefined, + // 'disable' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'disable'); + // }); + + // test('disables when user is authorised to disable this type of alert type producer and consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/disable', + // authorized: true, + // }, + // { + // privilege: 'myType/myApp/disable', + // authorized: false, + // }, + // ], + // }); + + // await alertsClientWithAuthorization.disable({ id: '1' }); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // undefined, + // 'disable' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'disable'); + // }); + + // test('throws when user is not authorised to disable this type of alert at all', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/disable', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/disable', + // authorized: false, + // }, + // ], + // }); + + // expect(alertsClientWithAuthorization.disable({ id: '1' })).rejects.toMatchInlineSnapshot( + // `[Error: Unauthorized to disable a "myType" alert for "myApp"]` + // ); + // }); + // }); test('disables an alert', async () => { await alertsClient.disable({ id: '1' }); @@ -1698,257 +1785,257 @@ describe('muteAll()', () => { }); }); - describe('authorization', () => { - let authorization: jest.Mocked; - let alertsClientWithAuthorization: AlertsClient; - let checkPrivileges: jest.MockedFunction>; - - beforeEach(() => { - authorization = mockAuthorization(); - alertsClientWithAuthorization = new AlertsClient({ - authorization, - ...alertsClientParams, - }); - - checkPrivileges = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + // describe('authorization', () => { + // let authorization: jest.Mocked; + // let alertsClientWithAuthorization: AlertsClient; + // let checkPrivileges: jest.MockedFunction>; + + // beforeEach(() => { + // authorization = mockAuthorization(); + // alertsClientWithAuthorization = new AlertsClient({ + // authorization, + // ...alertsClientParams, + // }); + + // checkPrivileges = jest.fn(); + // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + // unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + // id: '1', + // type: 'alert', + // attributes: { + // alertTypeId: 'myType', + // consumer: 'myApp', + // muteAll: false, + // }, + // references: [], + // }); + // }); + + // test('mutes when user is authorised to muteAll this type of alert type for the specified consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/muteAll', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/muteAll', + // authorized: true, + // }, + // ], + // }); + + // await alertsClientWithAuthorization.muteAll({ id: '1' }); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // undefined, + // 'muteAll' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); + // }); + + // test('mutes when user is authorised to muteAll this type of alert type producer and consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/muteAll', + // authorized: true, + // }, + // { + // privilege: 'myType/myApp/muteAll', + // authorized: false, + // }, + // ], + // }); + + // await alertsClientWithAuthorization.muteAll({ id: '1' }); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // undefined, + // 'muteAll' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); + // }); + + // test('throws when user is not authorised to muteAll this type of alert at all', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/muteAll', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/muteAll', + // authorized: false, + // }, + // ], + // }); + + // expect(alertsClientWithAuthorization.muteAll({ id: '1' })).rejects.toMatchInlineSnapshot( + // `[Error: Unauthorized to muteAll a "myType" alert for "myApp"]` + // ); + // }); + // }); +}); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: 'myType', - consumer: 'myApp', - muteAll: false, - }, - references: [], - }); +describe('unmuteAll()', () => { + test('unmutes an alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + muteAll: true, + }, + references: [], }); - test('mutes when user is authorised to muteAll this type of alert type for the specified consumer', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/muteAll', - authorized: false, - }, - { - privilege: 'myType/myApp/muteAll', - authorized: true, - }, - ], - }); + await alertsClient.unmuteAll({ id: '1' }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { + muteAll: false, + mutedInstanceIds: [], + updatedBy: 'elastic', + }); + }); - await alertsClientWithAuthorization.muteAll({ id: '1' }); + // describe('authorization', () => { + // let authorization: jest.Mocked; + // let alertsClientWithAuthorization: AlertsClient; + // let checkPrivileges: jest.MockedFunction>; + + // beforeEach(() => { + // authorization = mockAuthorization(); + // alertsClientWithAuthorization = new AlertsClient({ + // authorization, + // ...alertsClientParams, + // }); + + // checkPrivileges = jest.fn(); + // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + // unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + // id: '1', + // type: 'alert', + // attributes: { + // alertTypeId: 'myType', + // consumer: 'myApp', + // muteAll: true, + // }, + // references: [], + // }); + // }); + + // test('unmutes when user is authorised to unmuteAll this type of alert type for the specified consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/unmuteAll', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/unmuteAll', + // authorized: true, + // }, + // ], + // }); + + // await alertsClientWithAuthorization.unmuteAll({ id: '1' }); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // undefined, + // 'unmuteAll' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // 'myApp', + // 'unmuteAll' + // ); + // }); + + // test('unmutes when user is authorised to unmuteAll this type of alert type producer and consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/unmuteAll', + // authorized: true, + // }, + // { + // privilege: 'myType/myApp/unmuteAll', + // authorized: false, + // }, + // ], + // }); + + // await alertsClientWithAuthorization.unmuteAll({ id: '1' }); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // undefined, + // 'unmuteAll' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // 'myApp', + // 'unmuteAll' + // ); + // }); + + // test('throws when user is not authorised to unmuteAll this type of alert at all', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/unmuteAll', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/unmuteAll', + // authorized: false, + // }, + // ], + // }); + + // expect(alertsClientWithAuthorization.unmuteAll({ id: '1' })).rejects.toMatchInlineSnapshot( + // `[Error: Unauthorized to unmuteAll a "myType" alert for "myApp"]` + // ); + // }); + // }); +}); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'muteAll' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); - }); - - test('mutes when user is authorised to muteAll this type of alert type globally', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/muteAll', - authorized: true, - }, - { - privilege: 'myType/myApp/muteAll', - authorized: false, - }, - ], - }); - - await alertsClientWithAuthorization.muteAll({ id: '1' }); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'muteAll' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); - }); - - test('throws when user is not authorised to muteAll this type of alert at all', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/muteAll', - authorized: false, - }, - { - privilege: 'myType/myApp/muteAll', - authorized: false, - }, - ], - }); - - expect(alertsClientWithAuthorization.muteAll({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to muteAll a "myType" alert for "myApp"]` - ); - }); - }); -}); - -describe('unmuteAll()', () => { - test('unmutes an alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - muteAll: true, - }, - references: [], - }); - - await alertsClient.unmuteAll({ id: '1' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { - muteAll: false, - mutedInstanceIds: [], - updatedBy: 'elastic', - }); - }); - - describe('authorization', () => { - let authorization: jest.Mocked; - let alertsClientWithAuthorization: AlertsClient; - let checkPrivileges: jest.MockedFunction>; - - beforeEach(() => { - authorization = mockAuthorization(); - alertsClientWithAuthorization = new AlertsClient({ - authorization, - ...alertsClientParams, - }); - - checkPrivileges = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: 'myType', - consumer: 'myApp', - muteAll: true, - }, - references: [], - }); - }); - - test('unmutes when user is authorised to unmuteAll this type of alert type for the specified consumer', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/unmuteAll', - authorized: false, - }, - { - privilege: 'myType/myApp/unmuteAll', - authorized: true, - }, - ], - }); - - await alertsClientWithAuthorization.unmuteAll({ id: '1' }); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'unmuteAll' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'unmuteAll' - ); - }); - - test('unmutes when user is authorised to unmuteAll this type of alert type globally', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/unmuteAll', - authorized: true, - }, - { - privilege: 'myType/myApp/unmuteAll', - authorized: false, - }, - ], - }); - - await alertsClientWithAuthorization.unmuteAll({ id: '1' }); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'unmuteAll' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'unmuteAll' - ); - }); - - test('throws when user is not authorised to unmuteAll this type of alert at all', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/unmuteAll', - authorized: false, - }, - { - privilege: 'myType/myApp/unmuteAll', - authorized: false, - }, - ], - }); - - expect(alertsClientWithAuthorization.unmuteAll({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to unmuteAll a "myType" alert for "myApp"]` - ); - }); - }); -}); - -describe('muteInstance()', () => { - test('mutes an alert instance', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - schedule: { interval: '10s' }, - alertTypeId: '2', - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: [], - }, - version: '123', - references: [], +describe('muteInstance()', () => { + test('mutes an alert instance', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + schedule: { interval: '10s' }, + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: [], + }, + version: '123', + references: [], }); await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); @@ -2002,118 +2089,118 @@ describe('muteInstance()', () => { expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); }); - describe('authorization', () => { - let authorization: jest.Mocked; - let alertsClientWithAuthorization: AlertsClient; - let checkPrivileges: jest.MockedFunction>; - - beforeEach(() => { - authorization = mockAuthorization(); - alertsClientWithAuthorization = new AlertsClient({ - authorization, - ...alertsClientParams, - }); - - checkPrivileges = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: 'myType', - consumer: 'myApp', - muteAll: true, - }, - references: [], - }); - }); - - test('mutes instance when user is authorised to mute an instance on this type of alert type for the specified consumer', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/muteInstance', - authorized: false, - }, - { - privilege: 'myType/myApp/muteInstance', - authorized: true, - }, - ], - }); - - await alertsClientWithAuthorization.muteInstance({ alertId: '1', alertInstanceId: '2' }); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'muteInstance' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'muteInstance' - ); - }); - - test('mutes instance when user is authorised to mute an instance on this type of alert type globally', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/muteInstance', - authorized: true, - }, - { - privilege: 'myType/myApp/muteInstance', - authorized: false, - }, - ], - }); - - await alertsClientWithAuthorization.muteInstance({ alertId: '1', alertInstanceId: '2' }); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'muteInstance' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'muteInstance' - ); - }); - - test('throws when user is not authorised to mute an instance on this type of alert at all', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/muteInstance', - authorized: false, - }, - { - privilege: 'myType/myApp/muteInstance', - authorized: false, - }, - ], - }); - - expect( - alertsClientWithAuthorization.muteInstance({ alertId: '1', alertInstanceId: '2' }) - ).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to muteInstance a "myType" alert for "myApp"]` - ); - }); - }); + // describe('authorization', () => { + // let authorization: jest.Mocked; + // let alertsClientWithAuthorization: AlertsClient; + // let checkPrivileges: jest.MockedFunction>; + + // beforeEach(() => { + // authorization = mockAuthorization(); + // alertsClientWithAuthorization = new AlertsClient({ + // authorization, + // ...alertsClientParams, + // }); + + // checkPrivileges = jest.fn(); + // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + // unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + // id: '1', + // type: 'alert', + // attributes: { + // alertTypeId: 'myType', + // consumer: 'myApp', + // muteAll: true, + // }, + // references: [], + // }); + // }); + + // test('mutes instance when user is authorised to mute an instance on this type of alert type for the specified consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/muteInstance', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/muteInstance', + // authorized: true, + // }, + // ], + // }); + + // await alertsClientWithAuthorization.muteInstance({ alertId: '1', alertInstanceId: '2' }); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // undefined, + // 'muteInstance' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // 'myApp', + // 'muteInstance' + // ); + // }); + + // test('mutes instance when user is authorised to mute an instance on this type of alert type producer and consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/muteInstance', + // authorized: true, + // }, + // { + // privilege: 'myType/myApp/muteInstance', + // authorized: false, + // }, + // ], + // }); + + // await alertsClientWithAuthorization.muteInstance({ alertId: '1', alertInstanceId: '2' }); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // undefined, + // 'muteInstance' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // 'myApp', + // 'muteInstance' + // ); + // }); + + // test('throws when user is not authorised to mute an instance on this type of alert at all', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/muteInstance', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/muteInstance', + // authorized: false, + // }, + // ], + // }); + + // expect( + // alertsClientWithAuthorization.muteInstance({ alertId: '1', alertInstanceId: '2' }) + // ).rejects.toMatchInlineSnapshot( + // `[Error: Unauthorized to muteInstance a "myType" alert for "myApp"]` + // ); + // }); + // }); }); describe('unmuteInstance()', () => { @@ -2184,118 +2271,118 @@ describe('unmuteInstance()', () => { expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); }); - describe('authorization', () => { - let authorization: jest.Mocked; - let alertsClientWithAuthorization: AlertsClient; - let checkPrivileges: jest.MockedFunction>; - - beforeEach(() => { - authorization = mockAuthorization(); - alertsClientWithAuthorization = new AlertsClient({ - authorization, - ...alertsClientParams, - }); - - checkPrivileges = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: 'myType', - consumer: 'myApp', - muteAll: true, - }, - references: [], - }); - }); - - test('unmutes instance when user is authorised to unmutes an instance on this type of alert type for the specified consumer', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/unmuteInstance', - authorized: false, - }, - { - privilege: 'myType/myApp/unmuteInstance', - authorized: true, - }, - ], - }); - - await alertsClientWithAuthorization.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'unmuteInstance' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'unmuteInstance' - ); - }); - - test('unmutes instance when user is authorised to unmutes an instance on this type of alert type globally', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/unmuteInstance', - authorized: true, - }, - { - privilege: 'myType/myApp/unmuteInstance', - authorized: false, - }, - ], - }); - - await alertsClientWithAuthorization.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'unmuteInstance' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'unmuteInstance' - ); - }); - - test('throws when user is not authorised to unmutes an instance on this type of alert at all', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/unmuteInstance', - authorized: false, - }, - { - privilege: 'myType/myApp/unmuteInstance', - authorized: false, - }, - ], - }); - - expect( - alertsClientWithAuthorization.unmuteInstance({ alertId: '1', alertInstanceId: '2' }) - ).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to unmuteInstance a "myType" alert for "myApp"]` - ); - }); - }); + // describe('authorization', () => { + // let authorization: jest.Mocked; + // let alertsClientWithAuthorization: AlertsClient; + // let checkPrivileges: jest.MockedFunction>; + + // beforeEach(() => { + // authorization = mockAuthorization(); + // alertsClientWithAuthorization = new AlertsClient({ + // authorization, + // ...alertsClientParams, + // }); + + // checkPrivileges = jest.fn(); + // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + // unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + // id: '1', + // type: 'alert', + // attributes: { + // alertTypeId: 'myType', + // consumer: 'myApp', + // muteAll: true, + // }, + // references: [], + // }); + // }); + + // test('unmutes instance when user is authorised to unmutes an instance on this type of alert type for the specified consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/unmuteInstance', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/unmuteInstance', + // authorized: true, + // }, + // ], + // }); + + // await alertsClientWithAuthorization.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // undefined, + // 'unmuteInstance' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // 'myApp', + // 'unmuteInstance' + // ); + // }); + + // test('unmutes instance when user is authorised to unmutes an instance on this type of alert type producer and consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/unmuteInstance', + // authorized: true, + // }, + // { + // privilege: 'myType/myApp/unmuteInstance', + // authorized: false, + // }, + // ], + // }); + + // await alertsClientWithAuthorization.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // undefined, + // 'unmuteInstance' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // 'myApp', + // 'unmuteInstance' + // ); + // }); + + // test('throws when user is not authorised to unmutes an instance on this type of alert at all', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/unmuteInstance', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/unmuteInstance', + // authorized: false, + // }, + // ], + // }); + + // expect( + // alertsClientWithAuthorization.unmuteInstance({ alertId: '1', alertInstanceId: '2' }) + // ).rejects.toMatchInlineSnapshot( + // `[Error: Unauthorized to unmuteInstance a "myType" alert for "myApp"]` + // ); + // }); + // }); }); describe('get()', () => { @@ -2370,139 +2457,139 @@ describe('get()', () => { alertTypeId: '123', schedule: { interval: '10s' }, params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - references: [], - }); - await expect(alertsClient.get({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Reference action_0 not found"` - ); - }); - - describe('authorization', () => { - let authorization: jest.Mocked; - let alertsClientWithAuthorization: AlertsClient; - let checkPrivileges: jest.MockedFunction>; - - beforeEach(() => { - authorization = mockAuthorization(); - alertsClientWithAuthorization = new AlertsClient({ - authorization, - ...alertsClientParams, - }); - checkPrivileges = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - }); - - function tryToExecuteOperation(): Promise { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: 'myType', - consumer: 'myApp', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - return alertsClientWithAuthorization.get({ id: '1' }); - } - - test('gets when user is authorised to get this type of alert type for the specified consumer', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/get', - authorized: false, - }, - { - privilege: 'myType/myApp/get', - authorized: true, - }, - ], - }); - - await tryToExecuteOperation(); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); - }); - - test('gets when user is authorised to get this type of alert type globally', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/get', - authorized: true, - }, - { - privilege: 'myType/myApp/get', - authorized: false, - }, - ], - }); - - await tryToExecuteOperation(); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); - }); - - test('throws when user is not authorised to get this type of alert at all', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/get', - authorized: false, - }, + bar: true, + }, + actions: [ { - privilege: 'myType/myApp/get', - authorized: false, + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, }, ], - }); - - await expect(tryToExecuteOperation()).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to get a "myType" alert for "myApp"]` - ); + }, + references: [], }); + await expect(alertsClient.get({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Reference action_0 not found"` + ); }); + + // describe('authorization', () => { + // let authorization: jest.Mocked; + // let alertsClientWithAuthorization: AlertsClient; + // let checkPrivileges: jest.MockedFunction>; + + // beforeEach(() => { + // authorization = mockAuthorization(); + // alertsClientWithAuthorization = new AlertsClient({ + // authorization, + // ...alertsClientParams, + // }); + // checkPrivileges = jest.fn(); + // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + // }); + + // function tryToExecuteOperation(): Promise { + // unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + // id: '1', + // type: 'alert', + // attributes: { + // alertTypeId: 'myType', + // consumer: 'myApp', + // schedule: { interval: '10s' }, + // params: { + // bar: true, + // }, + // actions: [ + // { + // group: 'default', + // actionRef: 'action_0', + // params: { + // foo: true, + // }, + // }, + // ], + // }, + // references: [ + // { + // name: 'action_0', + // type: 'action', + // id: '1', + // }, + // ], + // }); + // return alertsClientWithAuthorization.get({ id: '1' }); + // } + + // test('gets when user is authorised to get this type of alert type for the specified consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/get', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/get', + // authorized: true, + // }, + // ], + // }); + + // await tryToExecuteOperation(); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); + // }); + + // test('gets when user is authorised to get this type of alert type producer and consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/get', + // authorized: true, + // }, + // { + // privilege: 'myType/myApp/get', + // authorized: false, + // }, + // ], + // }); + + // await tryToExecuteOperation(); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); + // }); + + // test('throws when user is not authorised to get this type of alert at all', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/get', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/get', + // authorized: false, + // }, + // ], + // }); + + // await expect(tryToExecuteOperation()).rejects.toMatchInlineSnapshot( + // `[Error: Unauthorized to get a "myType" alert for "myApp"]` + // ); + // }); + // }); }); describe('getAlertState()', () => { @@ -2618,120 +2705,120 @@ describe('getAlertState()', () => { expect(taskManager.get).toHaveBeenCalledWith(scheduledTaskId); }); - describe('authorization', () => { - let authorization: jest.Mocked; - let alertsClientWithAuthorization: AlertsClient; - let checkPrivileges: jest.MockedFunction>; - - beforeEach(() => { - authorization = mockAuthorization(); - alertsClientWithAuthorization = new AlertsClient({ - authorization, - ...alertsClientParams, - }); - checkPrivileges = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - }); - - function tryToExecuteOperation(): Promise { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: 'myType', - consumer: 'myApp', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - return alertsClientWithAuthorization.getAlertState({ id: '1' }); - } - - test('gets AlertState when user is authorised to get this type of alert type for the specified consumer', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/get', - authorized: false, - }, - { - privilege: 'myType/myApp/get', - authorized: true, - }, - ], - }); - - await tryToExecuteOperation(); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); - }); - - test('gets AlertState when user is authorised to get this type of alert type globally', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/get', - authorized: true, - }, - { - privilege: 'myType/myApp/get', - authorized: false, - }, - ], - }); - - await tryToExecuteOperation(); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); - }); - - test('throws when user is not authorised to get this type of alert at all', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/get', - authorized: false, - }, - { - privilege: 'myType/myApp/get', - authorized: false, - }, - ], - }); - - await expect(tryToExecuteOperation()).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to get a "myType" alert for "myApp"]` - ); - }); - }); + // describe('authorization', () => { + // let authorization: jest.Mocked; + // let alertsClientWithAuthorization: AlertsClient; + // let checkPrivileges: jest.MockedFunction>; + + // beforeEach(() => { + // authorization = mockAuthorization(); + // alertsClientWithAuthorization = new AlertsClient({ + // authorization, + // ...alertsClientParams, + // }); + // checkPrivileges = jest.fn(); + // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + // }); + + // function tryToExecuteOperation(): Promise { + // unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + // id: '1', + // type: 'alert', + // attributes: { + // alertTypeId: 'myType', + // consumer: 'myApp', + // schedule: { interval: '10s' }, + // params: { + // bar: true, + // }, + // actions: [ + // { + // group: 'default', + // actionRef: 'action_0', + // params: { + // foo: true, + // }, + // }, + // ], + // }, + // references: [ + // { + // name: 'action_0', + // type: 'action', + // id: '1', + // }, + // ], + // }); + // return alertsClientWithAuthorization.getAlertState({ id: '1' }); + // } + + // test('gets AlertState when user is authorised to get this type of alert type for the specified consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/get', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/get', + // authorized: true, + // }, + // ], + // }); + + // await tryToExecuteOperation(); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); + // }); + + // test('gets AlertState when user is authorised to get this type of alert type producer and consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/get', + // authorized: true, + // }, + // { + // privilege: 'myType/myApp/get', + // authorized: false, + // }, + // ], + // }); + + // await tryToExecuteOperation(); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); + // }); + + // test('throws when user is not authorised to get this type of alert at all', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/get', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/get', + // authorized: false, + // }, + // ], + // }); + + // await expect(tryToExecuteOperation()).rejects.toMatchInlineSnapshot( + // `[Error: Unauthorized to get a "myType" alert for "myApp"]` + // ); + // }); + // }); }); describe('find()', () => { @@ -2782,235 +2869,235 @@ describe('find()', () => { ], }, ], - }); - const result = await alertsClient.find({ options: {} }); - expect(result).toMatchInlineSnapshot(` - Object { - "data": Array [ - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "myType", - "createdAt": 2019-02-12T21:01:22.479Z, - "id": "1", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "updatedAt": 2019-02-12T21:01:22.479Z, - }, - ], - "page": 1, - "perPage": 10, - "total": 1, - } - `); - expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "filter": "alert.attributes.alertTypeId:(myType)", - "type": "alert", - }, - ] - `); - }); - - describe('authorization', () => { - let authorization: jest.Mocked; - let alertsClientWithAuthorization: AlertsClient; - let checkPrivileges: jest.MockedFunction>; - - function mockAlertSavedObject(alertTypeId: string) { - return { - id: uuid.v4(), - type: 'alert', - attributes: { - alertTypeId, - schedule: { interval: '10s' }, - params: {}, - actions: [], - }, - references: [], - }; - } - - beforeEach(() => { - authorization = mockAuthorization(); - - alertsClientWithAuthorization = new AlertsClient({ - authorization, - ...alertsClientParams, - }); - checkPrivileges = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - - const myType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - id: 'myType', - name: 'myType', - producer: 'myApp', - }; - const anUnauthorizedType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - id: 'anUnauthorizedType', - name: 'anUnauthorizedType', - producer: 'anUnauthorizedApp', - }; - const setOfAlertTypes = new Set([anUnauthorizedType, myType]); - alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - }); - - function tryToExecuteOperation( - options?: FindOptions, - savedObjects: Array> = [ - mockAlertSavedObject('myType'), - ] - ): Promise { - unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ - total: 1, - per_page: 10, - page: 1, - saved_objects: savedObjects, - }); - return alertsClientWithAuthorization.find({ options }); - } - - test('includes types that a user is authorised to find under their producer', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/find', - authorized: false, - }, - { - privilege: 'myType/myApp/find', - authorized: true, - }, - { - privilege: 'anUnauthorizedType/find', - authorized: false, - }, - { - privilege: 'anUnauthorizedType/anUnauthorizedApp/find', - authorized: false, - }, - ], - }); - - await tryToExecuteOperation(); - - expect(unsecuredSavedObjectsClient.find.mock.calls[0][0].filter).toMatchInlineSnapshot( - `"alert.attributes.alertTypeId:(myType)"` - ); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'find'); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'find'); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'anUnauthorizedType', - undefined, - 'find' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'anUnauthorizedType', - 'anUnauthorizedApp', - 'find' - ); - }); - - test('includes types that a user is authorised to get globally', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/find', - authorized: true, - }, - { - privilege: 'myType/myApp/find', - authorized: false, - }, - { - privilege: 'anUnauthorizedType/find', - authorized: false, - }, - { - privilege: 'anUnauthorizedType/anUnauthorizedApp/find', - authorized: false, - }, - ], - }); - - await tryToExecuteOperation(); - - expect(unsecuredSavedObjectsClient.find.mock.calls[0][0].filter).toMatchInlineSnapshot( - `"alert.attributes.alertTypeId:(myType)"` - ); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'find'); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'find'); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'anUnauthorizedType', - undefined, - 'find' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'anUnauthorizedType', - 'anUnauthorizedApp', - 'find' - ); - }); - - test('throws if a result contains a type the user is not authorised to find', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/find', - authorized: true, - }, - { - privilege: 'myType/myApp/find', - authorized: true, - }, - { - privilege: 'anUnauthorizedType/find', - authorized: false, - }, - { - privilege: 'anUnauthorizedType/anUnauthorizedApp/find', - authorized: false, + }); + const result = await alertsClient.find({ options: {} }); + expect(result).toMatchInlineSnapshot(` + Object { + "data": Array [ + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "myType", + "createdAt": 2019-02-12T21:01:22.479Z, + "id": "1", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "updatedAt": 2019-02-12T21:01:22.479Z, }, ], - }); - - await expect( - tryToExecuteOperation({}, [ - mockAlertSavedObject('myType'), - mockAlertSavedObject('anUnauthorizedType'), - ]) - ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to find "anUnauthorizedType" alerts]`); - }); + "page": 1, + "perPage": 10, + "total": 1, + } + `); + expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "filter": "((alert.attributes.alertTypeId:myType and alert.attributes.consumer:alerts) or (alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myType and alert.attributes.consumer:myOtherApp))", + "type": "alert", + }, + ] + `); }); + + // describe('authorization', () => { + // let authorization: jest.Mocked; + // let alertsClientWithAuthorization: AlertsClient; + // let checkPrivileges: jest.MockedFunction>; + + // function mockAlertSavedObject(alertTypeId: string) { + // return { + // id: uuid.v4(), + // type: 'alert', + // attributes: { + // alertTypeId, + // schedule: { interval: '10s' }, + // params: {}, + // actions: [], + // }, + // references: [], + // }; + // } + + // beforeEach(() => { + // authorization = mockAuthorization(); + + // alertsClientWithAuthorization = new AlertsClient({ + // authorization, + // ...alertsClientParams, + // }); + // checkPrivileges = jest.fn(); + // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + // const myType = { + // actionGroups: [], + // actionVariables: undefined, + // defaultActionGroupId: 'default', + // id: 'myType', + // name: 'myType', + // producer: 'myApp', + // }; + // const anUnauthorizedType = { + // actionGroups: [], + // actionVariables: undefined, + // defaultActionGroupId: 'default', + // id: 'anUnauthorizedType', + // name: 'anUnauthorizedType', + // producer: 'anUnauthorizedApp', + // }; + // const setOfAlertTypes = new Set([anUnauthorizedType, myType]); + // alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + // }); + + // function tryToExecuteOperation( + // options?: FindOptions, + // savedObjects: Array> = [ + // mockAlertSavedObject('myType'), + // ] + // ): Promise { + // unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ + // total: 1, + // per_page: 10, + // page: 1, + // saved_objects: savedObjects, + // }); + // return alertsClientWithAuthorization.find({ options }); + // } + + // test('includes types that a user is authorised to find under their producer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/find', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/find', + // authorized: true, + // }, + // { + // privilege: 'anUnauthorizedType/find', + // authorized: false, + // }, + // { + // privilege: 'anUnauthorizedType/anUnauthorizedApp/find', + // authorized: false, + // }, + // ], + // }); + + // await tryToExecuteOperation(); + + // expect(unsecuredSavedObjectsClient.find.mock.calls[0][0].filter).toMatchInlineSnapshot( + // `"alert.attributes.alertTypeId:(myType)"` + // ); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'find'); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'find'); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'anUnauthorizedType', + // undefined, + // 'find' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'anUnauthorizedType', + // 'anUnauthorizedApp', + // 'find' + // ); + // }); + + // test('includes types that a user is authorised to get producer and consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/find', + // authorized: true, + // }, + // { + // privilege: 'myType/myApp/find', + // authorized: false, + // }, + // { + // privilege: 'anUnauthorizedType/find', + // authorized: false, + // }, + // { + // privilege: 'anUnauthorizedType/anUnauthorizedApp/find', + // authorized: false, + // }, + // ], + // }); + + // await tryToExecuteOperation(); + + // expect(unsecuredSavedObjectsClient.find.mock.calls[0][0].filter).toMatchInlineSnapshot( + // `"alert.attributes.alertTypeId:(myType)"` + // ); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'find'); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'find'); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'anUnauthorizedType', + // undefined, + // 'find' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'anUnauthorizedType', + // 'anUnauthorizedApp', + // 'find' + // ); + // }); + + // test('throws if a result contains a type the user is not authorised to find', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/find', + // authorized: true, + // }, + // { + // privilege: 'myType/myApp/find', + // authorized: true, + // }, + // { + // privilege: 'anUnauthorizedType/find', + // authorized: false, + // }, + // { + // privilege: 'anUnauthorizedType/anUnauthorizedApp/find', + // authorized: false, + // }, + // ], + // }); + + // await expect( + // tryToExecuteOperation({}, [ + // mockAlertSavedObject('myType'), + // mockAlertSavedObject('anUnauthorizedType'), + // ]) + // ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to find "anUnauthorizedType" alerts]`); + // }); + // }); }); describe('delete()', () => { @@ -3150,96 +3237,96 @@ describe('delete()', () => { ); }); - describe('authorization', () => { - let authorization: jest.Mocked; - let alertsClientWithAuthorization: AlertsClient; - let checkPrivileges: jest.MockedFunction>; - - beforeEach(() => { - authorization = mockAuthorization(); - alertsClientWithAuthorization = new AlertsClient({ - authorization, - ...alertsClientParams, - }); - checkPrivileges = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - }); - - test('deletes when user is authorised to delete this type of alert type for the specified consumer', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/delete', - authorized: false, - }, - { - privilege: 'myType/myApp/delete', - authorized: true, - }, - ], - }); - - await alertsClientWithAuthorization.delete({ id: '1' }); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'delete' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'delete'); - }); - - test('deletes when user is authorised to delete this type of alert type globally', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/delete', - authorized: true, - }, - { - privilege: 'myType/myApp/delete', - authorized: false, - }, - ], - }); - - await alertsClientWithAuthorization.delete({ id: '1' }); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'delete' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'delete'); - }); - - test('throws when user is not authorised to delete this type of alert at all', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/delete', - authorized: false, - }, - { - privilege: 'myType/myApp/delete', - authorized: false, - }, - ], - }); - - expect(alertsClientWithAuthorization.delete({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to delete a "myType" alert for "myApp"]` - ); - }); - }); + // describe('authorization', () => { + // let authorization: jest.Mocked; + // let alertsClientWithAuthorization: AlertsClient; + // let checkPrivileges: jest.MockedFunction>; + + // beforeEach(() => { + // authorization = mockAuthorization(); + // alertsClientWithAuthorization = new AlertsClient({ + // authorization, + // ...alertsClientParams, + // }); + // checkPrivileges = jest.fn(); + // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + // }); + + // test('deletes when user is authorised to delete this type of alert type for the specified consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/delete', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/delete', + // authorized: true, + // }, + // ], + // }); + + // await alertsClientWithAuthorization.delete({ id: '1' }); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // undefined, + // 'delete' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'delete'); + // }); + + // test('deletes when user is authorised to delete this type of alert type producer and consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/delete', + // authorized: true, + // }, + // { + // privilege: 'myType/myApp/delete', + // authorized: false, + // }, + // ], + // }); + + // await alertsClientWithAuthorization.delete({ id: '1' }); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // undefined, + // 'delete' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'delete'); + // }); + + // test('throws when user is not authorised to delete this type of alert at all', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/delete', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/delete', + // authorized: false, + // }, + // ], + // }); + + // expect(alertsClientWithAuthorization.delete({ id: '1' })).rejects.toMatchInlineSnapshot( + // `[Error: Unauthorized to delete a "myType" alert for "myApp"]` + // ); + // }); + // }); }); describe('update()', () => { @@ -3274,7 +3361,7 @@ describe('update()', () => { actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', async executor() {}, - producer: 'alerting', + producer: 'alerts', }); }); @@ -3800,7 +3887,7 @@ describe('update()', () => { }), }, async executor() {}, - producer: 'alerting', + producer: 'alerts', }); await expect( alertsClient.update({ @@ -4032,7 +4119,7 @@ describe('update()', () => { actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', async executor() {}, - producer: 'alerting', + producer: 'alerts', }); unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ @@ -4141,306 +4228,306 @@ describe('update()', () => { await alertsClient.update({ id: alertId, - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - - expect(taskManager.runNow).not.toHaveBeenCalled(); - }); - - test('updating the alert should not wait for the rerun the task to complete', async (done) => { - const alertId = uuid.v4(); - const taskId = uuid.v4(); - - mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' }); - - const resolveAfterAlertUpdatedCompletes = resolvable<{ id: string }>(); - resolveAfterAlertUpdatedCompletes.then(() => done()); - - taskManager.runNow.mockReset(); - taskManager.runNow.mockReturnValue(resolveAfterAlertUpdatedCompletes); - - await alertsClient.update({ - id: alertId, - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - - expect(taskManager.runNow).toHaveBeenCalled(); - - resolveAfterAlertUpdatedCompletes.resolve({ id: alertId }); - }); - - test('logs when the rerun of an alerts underlying task fails', async () => { - const alertId = uuid.v4(); - const taskId = uuid.v4(); - - mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' }); - - taskManager.runNow.mockReset(); - taskManager.runNow.mockRejectedValue(new Error('Failed to run alert')); - - await alertsClient.update({ - id: alertId, - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - - expect(taskManager.runNow).toHaveBeenCalled(); - - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - `Alert update failed to run its underlying task. TaskManager runNow failed with Error: Failed to run alert` - ); - }); - }); - - describe('authorization', () => { - let authorization: jest.Mocked; - let alertsClientWithAuthorization: AlertsClient; - let checkPrivileges: jest.MockedFunction>; - - beforeEach(() => { - authorization = mockAuthorization(); - alertsClientWithAuthorization = new AlertsClient({ - authorization, - ...alertsClientParams, - }); - checkPrivileges = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - }); - - function tryToExecuteOperation(options: UpdateOptions): Promise { - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - alertTypeId: 'myType', - consumer: 'myApp', - actionTypeId: 'test', - }, - references: [], - }, - { - id: '2', - type: 'action', - attributes: { - actionTypeId: 'test2', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: true, + data: { schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], params: { bar: true, }, + throttle: null, actions: [ { group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - { - group: 'default', - actionRef: 'action_1', - actionTypeId: 'test', + id: '1', params: { foo: true, }, }, + ], + }, + }); + + expect(taskManager.runNow).not.toHaveBeenCalled(); + }); + + test('updating the alert should not wait for the rerun the task to complete', async (done) => { + const alertId = uuid.v4(); + const taskId = uuid.v4(); + + mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' }); + + const resolveAfterAlertUpdatedCompletes = resolvable<{ id: string }>(); + resolveAfterAlertUpdatedCompletes.then(() => done()); + + taskManager.runNow.mockReset(); + taskManager.runNow.mockReturnValue(resolveAfterAlertUpdatedCompletes); + + await alertsClient.update({ + id: alertId, + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ { group: 'default', - actionRef: 'action_2', - actionTypeId: 'test2', + id: '1', params: { foo: true, }, }, ], - scheduledTaskId: 'task-123', - createdAt: new Date().toISOString(), }, - updated_at: new Date().toISOString(), - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - { - name: 'action_1', - type: 'action', - id: '1', - }, - { - name: 'action_2', - type: 'action', - id: '2', - }, - ], - }); - return alertsClientWithAuthorization.update(options); - } - - test('updates when user is authorised to update this type of alert type for the specified consumer', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/update', - authorized: false, - }, - { - privilege: 'myType/myApp/update', - authorized: true, - }, - ], - }); - - const data = getMockData({ - alertTypeId: 'myType', - consumer: 'myApp', }); - await tryToExecuteOperation({ - id: '1', - data, - }); + expect(taskManager.runNow).toHaveBeenCalled(); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'update' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'update'); + resolveAfterAlertUpdatedCompletes.resolve({ id: alertId }); }); - test('updates when user is authorised to update this type of alert type globally', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/update', - authorized: true, - }, - { - privilege: 'myType/myApp/update', - authorized: false, - }, - ], - }); - - const data = getMockData({ - alertTypeId: 'myType', - consumer: 'myApp', - }); + test('logs when the rerun of an alerts underlying task fails', async () => { + const alertId = uuid.v4(); + const taskId = uuid.v4(); - await tryToExecuteOperation({ - id: '1', - data, - }); + mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' }); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'update' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'update'); - }); + taskManager.runNow.mockReset(); + taskManager.runNow.mockRejectedValue(new Error('Failed to run alert')); - test('throws when user is not authorised to update this type of alert at all', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/update', - authorized: false, - }, - { - privilege: 'myType/myApp/update', - authorized: false, + await alertsClient.update({ + id: alertId, + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, }, - ], + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, }); - const data = getMockData({ - alertTypeId: 'myType', - consumer: 'myApp', - }); + expect(taskManager.runNow).toHaveBeenCalled(); - await expect( - tryToExecuteOperation({ - id: '1', - data, - }) - ).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to update a "myType" alert for "myApp"]` + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + `Alert update failed to run its underlying task. TaskManager runNow failed with Error: Failed to run alert` ); }); }); + + // describe('authorization', () => { + // let authorization: jest.Mocked; + // let alertsClientWithAuthorization: AlertsClient; + // let checkPrivileges: jest.MockedFunction>; + + // beforeEach(() => { + // authorization = mockAuthorization(); + // alertsClientWithAuthorization = new AlertsClient({ + // authorization, + // ...alertsClientParams, + // }); + // checkPrivileges = jest.fn(); + // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + // }); + + // function tryToExecuteOperation(options: UpdateOptions): Promise { + // unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + // saved_objects: [ + // { + // id: '1', + // type: 'action', + // attributes: { + // alertTypeId: 'myType', + // consumer: 'myApp', + // actionTypeId: 'test', + // }, + // references: [], + // }, + // { + // id: '2', + // type: 'action', + // attributes: { + // actionTypeId: 'test2', + // }, + // references: [], + // }, + // ], + // }); + // unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + // id: '1', + // type: 'alert', + // attributes: { + // enabled: true, + // schedule: { interval: '10s' }, + // params: { + // bar: true, + // }, + // actions: [ + // { + // group: 'default', + // actionRef: 'action_0', + // actionTypeId: 'test', + // params: { + // foo: true, + // }, + // }, + // { + // group: 'default', + // actionRef: 'action_1', + // actionTypeId: 'test', + // params: { + // foo: true, + // }, + // }, + // { + // group: 'default', + // actionRef: 'action_2', + // actionTypeId: 'test2', + // params: { + // foo: true, + // }, + // }, + // ], + // scheduledTaskId: 'task-123', + // createdAt: new Date().toISOString(), + // }, + // updated_at: new Date().toISOString(), + // references: [ + // { + // name: 'action_0', + // type: 'action', + // id: '1', + // }, + // { + // name: 'action_1', + // type: 'action', + // id: '1', + // }, + // { + // name: 'action_2', + // type: 'action', + // id: '2', + // }, + // ], + // }); + // return alertsClientWithAuthorization.update(options); + // } + + // test('updates when user is authorised to update this type of alert type for the specified consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/update', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/update', + // authorized: true, + // }, + // ], + // }); + + // const data = getMockData({ + // alertTypeId: 'myType', + // consumer: 'myApp', + // }); + + // await tryToExecuteOperation({ + // id: '1', + // data, + // }); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // undefined, + // 'update' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'update'); + // }); + + // test('updates when user is authorised to update this type of alert type producer and consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/update', + // authorized: true, + // }, + // { + // privilege: 'myType/myApp/update', + // authorized: false, + // }, + // ], + // }); + + // const data = getMockData({ + // alertTypeId: 'myType', + // consumer: 'myApp', + // }); + + // await tryToExecuteOperation({ + // id: '1', + // data, + // }); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // undefined, + // 'update' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'update'); + // }); + + // test('throws when user is not authorised to update this type of alert at all', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/update', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/update', + // authorized: false, + // }, + // ], + // }); + + // const data = getMockData({ + // alertTypeId: 'myType', + // consumer: 'myApp', + // }); + + // await expect( + // tryToExecuteOperation({ + // id: '1', + // data, + // }) + // ).rejects.toMatchInlineSnapshot( + // `[Error: Unauthorized to update a "myType" alert for "myApp"]` + // ); + // }); + // }); }); describe('updateApiKey()', () => { @@ -4553,104 +4640,104 @@ describe('updateApiKey()', () => { expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); }); - describe('authorization', () => { - let authorization: jest.Mocked; - let alertsClientWithAuthorization: AlertsClient; - let checkPrivileges: jest.MockedFunction>; - - beforeEach(() => { - authorization = mockAuthorization(); - alertsClientWithAuthorization = new AlertsClient({ - authorization, - ...alertsClientParams, - }); - checkPrivileges = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - }); - - test('updates when user is authorised to updateApiKey this type of alert type for the specified consumer', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/updateApiKey', - authorized: false, - }, - { - privilege: 'myType/myApp/updateApiKey', - authorized: true, - }, - ], - }); - - await alertsClientWithAuthorization.updateApiKey({ id: '1' }); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'updateApiKey' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'updateApiKey' - ); - }); - - test('updates when user is authorised to updateApiKey this type of alert type globally', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/updateApiKey', - authorized: true, - }, - { - privilege: 'myType/myApp/updateApiKey', - authorized: false, - }, - ], - }); - - await alertsClientWithAuthorization.updateApiKey({ id: '1' }); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - undefined, - 'updateApiKey' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'updateApiKey' - ); - }); - - test('throws when user is not authorised to updateApiKey this type of alert at all', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/updateApiKey', - authorized: false, - }, - { - privilege: 'myType/myApp/updateApiKey', - authorized: false, - }, - ], - }); - - expect(alertsClientWithAuthorization.updateApiKey({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to updateApiKey a "myType" alert for "myApp"]` - ); - }); - }); + // describe('authorization', () => { + // let authorization: jest.Mocked; + // let alertsClientWithAuthorization: AlertsClient; + // let checkPrivileges: jest.MockedFunction>; + + // beforeEach(() => { + // authorization = mockAuthorization(); + // alertsClientWithAuthorization = new AlertsClient({ + // authorization, + // ...alertsClientParams, + // }); + // checkPrivileges = jest.fn(); + // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + // }); + + // test('updates when user is authorised to updateApiKey this type of alert type for the specified consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/updateApiKey', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/updateApiKey', + // authorized: true, + // }, + // ], + // }); + + // await alertsClientWithAuthorization.updateApiKey({ id: '1' }); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // undefined, + // 'updateApiKey' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // 'myApp', + // 'updateApiKey' + // ); + // }); + + // test('updates when user is authorised to updateApiKey this type of alert type producer and consumer', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/updateApiKey', + // authorized: true, + // }, + // { + // privilege: 'myType/myApp/updateApiKey', + // authorized: false, + // }, + // ], + // }); + + // await alertsClientWithAuthorization.updateApiKey({ id: '1' }); + + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // undefined, + // 'updateApiKey' + // ); + // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + // 'myType', + // 'myApp', + // 'updateApiKey' + // ); + // }); + + // test('throws when user is not authorised to updateApiKey this type of alert at all', async () => { + // checkPrivileges.mockResolvedValueOnce({ + // hasAllRequested: false, + // username: '', + // privileges: [ + // { + // privilege: 'myType/updateApiKey', + // authorized: false, + // }, + // { + // privilege: 'myType/myApp/updateApiKey', + // authorized: false, + // }, + // ], + // }); + + // expect(alertsClientWithAuthorization.updateApiKey({ id: '1' })).rejects.toMatchInlineSnapshot( + // `[Error: Unauthorized to updateApiKey a "myType" alert for "myApp"]` + // ); + // }); + // }); }); describe('listAlertTypes', () => { @@ -4661,7 +4748,7 @@ describe('listAlertTypes', () => { defaultActionGroupId: 'default', id: 'alertingAlertType', name: 'alertingAlertType', - producer: 'alerting', + producer: 'alerts', }; const myAppAlertType = { actionGroups: [], @@ -4679,7 +4766,12 @@ describe('listAlertTypes', () => { test('should return a list of AlertTypes that exist in the registry', async () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - expect(await alertsClient.listAlertTypes()).toEqual(setOfAlertTypes); + expect(await alertsClient.listAlertTypes()).toEqual( + new Set([ + { ...myAppAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, + { ...alertingAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, + ]) + ); }); describe('authorization', () => { @@ -4705,19 +4797,27 @@ describe('listAlertTypes', () => { username: '', privileges: [ { - privilege: 'myAppAlertType/get', - authorized: false, + privilege: 'myAppAlertType/myApp/get', + authorized: true, }, { - privilege: 'myAppAlertType/alerting/get', + privilege: 'myAppAlertType/myOtherApp/get', authorized: false, }, { - privilege: 'alertingAlertType/get', + privilege: 'myAppAlertType/alerts/get', + authorized: true, + }, + { + privilege: 'alertingAlertType/myApp/get', + authorized: true, + }, + { + privilege: 'alertingAlertType/myOtherApp/get', authorized: true, }, { - privilege: 'alertingAlertType/alerting/get', + privilege: 'alertingAlertType/alerts/get', authorized: true, }, ], @@ -4726,29 +4826,41 @@ describe('listAlertTypes', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); expect(await alertsClientWithAuthorization.listAlertTypes()).toEqual( - new Set([alertingAlertType]) + new Set([ + { ...myAppAlertType, authorizedConsumers: ['myApp', 'alerts'] }, + { ...alertingAlertType, authorizedConsumers: ['myApp', 'myOtherApp', 'alerts'] }, + ]) ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myAppAlertType', + 'alerts', + 'get' + ); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'myAppAlertType', 'myApp', 'get' ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'myAppAlertType', - undefined, + 'myOtherApp', 'get' ); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'alertingAlertType', - 'alerting', + 'alerts', + 'get' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'alertingAlertType', + 'myOtherApp', 'get' ); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'alertingAlertType', - undefined, + 'myApp', 'get' ); }); diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 53c6d01c1d418..172d2ceea4864 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -15,6 +15,7 @@ import { KibanaRequest, } from 'src/core/server'; import { ActionsClient } from '../../actions/server'; +import { AlertsFeatureId } from '../common'; import { Alert, PartialAlert, @@ -32,14 +33,17 @@ import { GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, SecurityPluginSetup, - CheckPrivilegesResponse, } from '../../security/server'; import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../task_manager/server'; import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists'; import { RegistryAlertType } from './alert_type_registry'; +import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; +export interface RegistryAlertTypeWithAuth extends RegistryAlertType { + authorizedConsumers: string[]; +} type NormalizedAlertAction = Omit; export type CreateAPIKeyResult = | { apiKeysEnabled: false } @@ -58,6 +62,7 @@ interface ConstructorOptions { encryptedSavedObjectsClient: EncryptedSavedObjectsClient; spaceId?: string; namespace?: string; + features: FeaturesPluginStart; getUserName: () => Promise; createAPIKey: () => Promise; invalidateAPIKey: (params: InvalidateAPIKeyParams) => Promise; @@ -130,6 +135,7 @@ export interface UpdateOptions { export class AlertsClient { private readonly logger: Logger; private readonly getUserName: () => Promise; + private readonly features: FeaturesPluginStart; private readonly spaceId?: string; private readonly namespace?: string; private readonly taskManager: TaskManagerStartContract; @@ -158,6 +164,7 @@ export class AlertsClient { invalidateAPIKey, encryptedSavedObjectsClient, getActionsClient, + features, }: ConstructorOptions) { this.logger = logger; this.getUserName = getUserName; @@ -172,6 +179,7 @@ export class AlertsClient { this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; this.getActionsClient = getActionsClient; + this.features = features; } public async create({ data, options }: CreateOptions): Promise { @@ -251,9 +259,11 @@ export class AlertsClient { }: { options?: FindOptions } = {}): Promise { const filters = filter ? [filter] : []; - const authorizedAlertTypes = new Set( - pluck([...(await this.filterByAuthorized(this.alertTypeRegistry.list(), 'find'))], 'id') + const authorizedAlertTypes = await this.filterByAuthorized( + this.alertTypeRegistry.list(), + 'find' ); + const authorizedAlertTypeIds = new Set(pluck([...authorizedAlertTypes], 'id')); if (!authorizedAlertTypes.size) { // the current user isn't authorized to get any alertTypes @@ -266,7 +276,7 @@ export class AlertsClient { }; } - filters.push(`alert.attributes.alertTypeId:(${[...authorizedAlertTypes].join(' or ')})`); + filters.push(`(${asFiltersByAlertTypeAndConsumer(authorizedAlertTypes).join(' or ')})`); const { page, @@ -284,7 +294,7 @@ export class AlertsClient { perPage, total, data: data.map(({ id, attributes, updated_at, references }) => { - if (!authorizedAlertTypes.has(attributes.alertTypeId)) { + if (!authorizedAlertTypeIds.has(attributes.alertTypeId)) { throw Boom.forbidden(`Unauthorized to find "${attributes.alertTypeId}" alerts`); } return this.getAlertFromRaw(id, attributes, updated_at, references); @@ -657,20 +667,30 @@ export class AlertsClient { } private async ensureAuthorized(alertTypeId: string, consumer: string, operation: string) { - if (this.authorization) { - const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( - this.request - ); - if ( - !this.hasAnyPrivilege( - await checkPrivileges([ - // check for global access - this.authorization.actions.alerting.get(alertTypeId, undefined, operation), - // check for access at consumer level - this.authorization.actions.alerting.get(alertTypeId, consumer, operation), - ]) + const { authorization } = this; + if (authorization) { + const alertType = this.alertTypeRegistry.get(alertTypeId); + const requiredPrivilegedScopes = + consumer === AlertsFeatureId || consumer === alertType.producer + ? [ + // skip consumer privilege checks under `alerts` as all alert types can + // be created under `alerts` if you have producer level privileges + alertType.producer, + ] + : [ + // check for access at consumer level + consumer, + // check for access at producer level + alertType.producer, + ]; + + const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); + const { hasAllRequested } = await checkPrivileges( + requiredPrivilegedScopes.map((scope) => + authorization.actions.alerting.get(alertTypeId, scope, operation) ) - ) { + ); + if (!hasAllRequested) { throw Boom.forbidden( `Unauthorized to ${operation} a "${alertTypeId}" alert for "${consumer}"` ); @@ -681,42 +701,50 @@ export class AlertsClient { private async filterByAuthorized( alertTypes: Set, operation: string - ): Promise> { + ): Promise> { + const featuresIds = this.features.getFeatures().map((feature) => feature.id); + if (!this.authorization) { - return alertTypes; - } + return augmentWithAuthorizedConsumers(alertTypes, featuresIds); + } else { + const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( + this.request + ); - const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest(this.request); + // add an empty `authorizedConsumers` array on each alertType + const alertTypesWithAutherization = augmentWithAuthorizedConsumers(alertTypes); + + // map from privilege to alertType which we can refer back to when analyzing the result + // of checkPrivileges + const privilegeToAlertType = new Map(); + // as we can't ask ES for the user's individual privileges we need to ask for each feature + // and alertType in the system whether this user has this privilege + for (const alertType of alertTypesWithAutherization) { + for (const feature of featuresIds) { + privilegeToAlertType.set( + this.authorization!.actions.alerting.get(alertType.id, feature, operation), + [alertType, feature] + ); + } + } - const privilegeToAlertType = Array.from(alertTypes).reduce((privileges, alertType) => { - // check for global access - privileges.set( - this.authorization!.actions.alerting.get(alertType.id, undefined, operation), - alertType - ); - // check for access within the producer level - privileges.set( - this.authorization!.actions.alerting.get(alertType.id, alertType.producer, operation), - alertType - ); - return privileges; - }, new Map()); - const { hasAllRequested, privileges } = await checkPrivileges([...privilegeToAlertType.keys()]); - return hasAllRequested - ? alertTypes - : privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { - if (authorized && privilegeToAlertType.has(privilege)) { - authorizedAlertTypes.add(privilegeToAlertType.get(privilege)!); - } - return authorizedAlertTypes; - }, new Set()); - } + const { hasAllRequested, privileges } = await checkPrivileges([ + ...privilegeToAlertType.keys(), + ]); - private hasAnyPrivilege(checkPrivilegesResponse: CheckPrivilegesResponse): boolean { - return ( - checkPrivilegesResponse.hasAllRequested || - checkPrivilegesResponse.privileges.some(({ authorized }) => authorized) - ); + return hasAllRequested + ? // has access to all features + augmentWithAuthorizedConsumers(alertTypes, featuresIds) + : // only has some of the required privileges + privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { + if (authorized && privilegeToAlertType.has(privilege)) { + const [alertType, consumer] = privilegeToAlertType.get(privilege)!; + alertType.authorizedConsumers.push(consumer); + authorizedAlertTypes.add(alertType); + } + return authorizedAlertTypes; + }, new Set()); + } } private async scheduleAlert(id: string, alertTypeId: string) { @@ -837,3 +865,26 @@ export class AlertsClient { }; } } + +function augmentWithAuthorizedConsumers( + alertTypes: Set, + authorizedConsumers?: string[] +): Set { + return new Set( + Array.from(alertTypes).map((alertType) => ({ + ...alertType, + authorizedConsumers: authorizedConsumers ?? [], + })) + ); +} + +function asFiltersByAlertTypeAndConsumer(alertTypes: Set): string[] { + return Array.from(alertTypes).reduce((filters, { id, authorizedConsumers }) => { + for (const consumer of authorizedConsumers) { + filters.push( + `(alert.attributes.alertTypeId:${id} and alert.attributes.consumer:${consumer})` + ); + } + return filters; + }, []); +} diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 7278c7ab2c837..1952aeb27d219 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -18,11 +18,13 @@ import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/ import { AuthenticatedUser } from '../../security/public'; import { securityMock } from '../../security/server/mocks'; import { actionsMock } from '../../actions/server/mocks'; +import { featuresPluginMock } from '../../features/server/mocks'; jest.mock('./alerts_client'); const savedObjectsClient = savedObjectsClientMock.create(); const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(); +const features = featuresPluginMock.createStart(); const securityPluginSetup = securityMock.createSetup(); const alertsClientFactoryParams: jest.Mocked = { @@ -33,6 +35,7 @@ const alertsClientFactoryParams: jest.Mocked = { spaceIdToNamespace: jest.fn(), encryptedSavedObjectsClient: encryptedSavedObjectsMock.createClient(), actions: actionsMock.createStart(), + features, }; const fakeRequest = ({ headers: {}, @@ -84,6 +87,7 @@ test('creates an alerts client with proper constructor arguments when security i createAPIKey: expect.any(Function), invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, + features: alertsClientFactoryParams.features, }); }); @@ -113,6 +117,7 @@ test('creates an alerts client with proper constructor arguments', async () => { createAPIKey: expect.any(Function), invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, + features: alertsClientFactoryParams.features, getActionsClient: expect.any(Function), }); }); diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index 7ebe505913b95..c84b1c1b1bd15 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -11,6 +11,7 @@ import { KibanaRequest, Logger, SavedObjectsServiceStart } from '../../../../src import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../security/server'; import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../task_manager/server'; +import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; export interface AlertsClientFactoryOpts { logger: Logger; @@ -21,6 +22,7 @@ export interface AlertsClientFactoryOpts { spaceIdToNamespace: SpaceIdToNamespaceFunction; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; actions: ActionsPluginStartContract; + features: FeaturesPluginStart; } export class AlertsClientFactory { @@ -33,6 +35,7 @@ export class AlertsClientFactory { private spaceIdToNamespace!: SpaceIdToNamespaceFunction; private encryptedSavedObjectsClient!: EncryptedSavedObjectsClient; private actions!: ActionsPluginStartContract; + private features!: FeaturesPluginStart; public initialize(options: AlertsClientFactoryOpts) { if (this.isInitialized) { @@ -47,14 +50,16 @@ export class AlertsClientFactory { this.spaceIdToNamespace = options.spaceIdToNamespace; this.encryptedSavedObjectsClient = options.encryptedSavedObjectsClient; this.actions = options.actions; + this.features = options.features; } public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): AlertsClient { - const { securityPluginSetup, actions } = this; + const { securityPluginSetup, actions, features } = this; const spaceId = this.getSpaceId(request); return new AlertsClient({ spaceId, logger: this.logger, + features: features!, taskManager: this.taskManager, alertTypeRegistry: this.alertTypeRegistry, unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, { diff --git a/x-pack/plugins/alerts/server/feature.ts b/x-pack/plugins/alerts/server/feature.ts index 119a9f06a4844..108e3e4300251 100644 --- a/x-pack/plugins/alerts/server/feature.ts +++ b/x-pack/plugins/alerts/server/feature.ts @@ -14,9 +14,7 @@ export function registerFeature(features: FeaturesPluginSetup) { privileges: { all: { alerting: { - globally: { - all: [IndexThresholdId], - }, + all: [IndexThresholdId], }, savedObject: { all: [], @@ -26,9 +24,7 @@ export function registerFeature(features: FeaturesPluginSetup) { }, read: { alerting: { - globally: { - read: [IndexThresholdId], - }, + read: [IndexThresholdId], }, savedObject: { all: [], diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index b676c099e490f..1cea6778e7c42 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -73,20 +73,16 @@ describe('Alerting Plugin', () => { expect(privileges?.all.alerting).toMatchInlineSnapshot(` Object { - "globally": Object { - "all": Array [ - ".index-threshold", - ], - }, + "all": Array [ + ".index-threshold", + ], } `); expect(privileges?.read.alerting).toMatchInlineSnapshot(` Object { - "globally": Object { - "read": Array [ - ".index-threshold", - ], - }, + "read": Array [ + ".index-threshold", + ], } `); }); @@ -120,20 +116,16 @@ describe('Alerting Plugin', () => { expect(privileges?.all.alerting).toMatchInlineSnapshot(` Object { - "globally": Object { - "all": Array [ - ".index-threshold", - ], - }, + "all": Array [ + ".index-threshold", + ], } `); expect(privileges?.read.alerting).toMatchInlineSnapshot(` Object { - "globally": Object { - "read": Array [ - ".index-threshold", - ], - }, + "read": Array [ + ".index-threshold", + ], } `); }); diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 932e3f0dfb46f..fb917cfc8c476 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -58,7 +58,10 @@ import { Services } from './types'; import { registerAlertsUsageCollector } from './usage'; import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/task'; import { IEventLogger, IEventLogService } from '../../event_log/server'; -import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { + PluginSetupContract as FeaturesPluginSetup, + PluginStartContract as FeaturesPluginStart, +} from '../../features/server'; import { setupSavedObjects } from './saved_objects'; import { registerFeature } from './feature'; @@ -93,6 +96,7 @@ export interface AlertingPluginsStart { actions: ActionsPluginStartContract; taskManager: TaskManagerStartContract; encryptedSavedObjects: EncryptedSavedObjectsPluginStart; + features: FeaturesPluginStart; } export class AlertingPlugin { @@ -221,6 +225,7 @@ export class AlertingPlugin { return spaces?.getSpaceId(request); }, actions: plugins.actions, + features: plugins.features, }); taskRunnerFactory.initialize({ diff --git a/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts index 14143021290af..276915973a391 100644 --- a/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts +++ b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts @@ -43,6 +43,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', + authorizedConsumers: [], actionVariables: { context: [], state: [], @@ -68,6 +69,7 @@ describe('listAlertTypesRoute', () => { "context": Array [], "state": Array [], }, + "authorizedConsumers": Array [], "defaultActionGroupId": "default", "id": "1", "name": "name", @@ -105,6 +107,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', + authorizedConsumers: [], actionVariables: { context: [], state: [], @@ -153,6 +156,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', + authorizedConsumers: [], actionVariables: { context: [], state: [], diff --git a/x-pack/plugins/features/common/feature_kibana_privileges.ts b/x-pack/plugins/features/common/feature_kibana_privileges.ts index c642f3e5b6fd4..dd77073a62834 100644 --- a/x-pack/plugins/features/common/feature_kibana_privileges.ts +++ b/x-pack/plugins/features/common/feature_kibana_privileges.ts @@ -76,11 +76,13 @@ export interface FeatureKibanaPrivileges { app?: string[]; /** - * If your feature registers its own Alert types you may specify the access privileges for them here. + * If your feature requires access to specific Alert Types, then specify your access needs here. + * Include both Alert Types registered by the feature and external Alert Types such as built-in + * Alert Types and Alert Types provided by other features to which you wish to grant access. */ alerting?: { /** - * List of alert types which users should have full read/write access to within the feature. + * List of alert types which users should have full read/write access to when granted this privilege. * @example * ```ts * { @@ -91,7 +93,7 @@ export interface FeatureKibanaPrivileges { all?: string[]; /** - * List of alert types which users should have read-only access to from within the feature. + * List of alert types which users should have read-only access to when granted this privilege. * @example * ```ts * { @@ -100,33 +102,6 @@ export interface FeatureKibanaPrivileges { * ``` */ read?: string[]; - - /** - * If your feature registers its own Alert types you may specify global access privileges for them here. - */ - globally?: { - /** - * List of alert types types which users should have full read/write access to throughout kibana. - * @example - * ```ts - * { - * all: ['my-alert-type-globally-available'] - * } - * ``` - */ - all?: string[]; - - /** - * List of alert types which users should have read-only access to throughout kibana. - * @example - * ```ts - * { - * read: ['my-alert-type'] - * } - * ``` - */ - read?: string[]; - }; }; /** * If your feature requires access to specific saved objects, then specify your access needs here. diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index 598e619a21143..2c16493a61445 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -29,13 +29,11 @@ export const METRICS_FEATURE = { read: ['index-pattern'], }, alerting: { - globally: { - all: [ - METRIC_THRESHOLD_ALERT_TYPE_ID, - METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, - LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, - ], - }, + all: [ + METRIC_THRESHOLD_ALERT_TYPE_ID, + METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + ], }, ui: [ 'show', @@ -58,13 +56,11 @@ export const METRICS_FEATURE = { read: ['infrastructure-ui-source', 'index-pattern'], }, alerting: { - globally: { - all: [ - METRIC_THRESHOLD_ALERT_TYPE_ID, - METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, - LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, - ], - }, + all: [ + METRIC_THRESHOLD_ALERT_TYPE_ID, + METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + ], }, ui: [ 'show', diff --git a/x-pack/plugins/security/server/authorization/actions/alerting.test.ts b/x-pack/plugins/security/server/authorization/actions/alerting.test.ts index 75d5a70e9302c..744543f38a914 100644 --- a/x-pack/plugins/security/server/authorization/actions/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/actions/alerting.test.ts @@ -27,7 +27,7 @@ describe('#get', () => { }); }); - [null, '', 1, true, {}].forEach((consumer: any) => { + [null, '', 1, true, undefined, {}].forEach((consumer: any) => { test(`consumer of ${JSON.stringify(consumer)} throws error`, () => { const alertingActions = new AlertingActions(version); expect(() => @@ -36,17 +36,10 @@ describe('#get', () => { }); }); - test('returns `alerting:${alertType}/feature/${consumer}/${operation}`', () => { + test('returns `alerting:${alertType}/${consumer}/${operation}`', () => { const alertingActions = new AlertingActions(version); expect(alertingActions.get('foo-alertType', 'consumer', 'bar-operation')).toBe( - 'alerting:1.0.0-zeta1:foo-alertType/feature/consumer/bar-operation' - ); - }); - - test('returns `alerting:${alertType}/_global/${operation}` when no consumer is specified', () => { - const alertingActions = new AlertingActions(version); - expect(alertingActions.get('foo-alertType', undefined, 'bar-operation')).toBe( - 'alerting:1.0.0-zeta1:foo-alertType/_global/bar-operation' + 'alerting:1.0.0-zeta1:foo-alertType/consumer/bar-operation' ); }); }); diff --git a/x-pack/plugins/security/server/authorization/actions/alerting.ts b/x-pack/plugins/security/server/authorization/actions/alerting.ts index e8c7e8005b5d2..99d04efe6892d 100644 --- a/x-pack/plugins/security/server/authorization/actions/alerting.ts +++ b/x-pack/plugins/security/server/authorization/actions/alerting.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isString, isUndefined } from 'lodash'; +import { isString } from 'lodash'; export class AlertingActions { private readonly prefix: string; @@ -13,7 +13,7 @@ export class AlertingActions { this.prefix = `alerting:${versionNumber}:`; } - public get(alertTypeId: string, consumer: string | undefined, operation: string): string { + public get(alertTypeId: string, consumer: string, operation: string): string { if (!alertTypeId || !isString(alertTypeId)) { throw new Error('alertTypeId is required and must be a string'); } @@ -22,12 +22,10 @@ export class AlertingActions { throw new Error('operation is required and must be a string'); } - if (!isUndefined(consumer) && (!consumer || !isString(consumer))) { - throw new Error('consumer is optional but must be a string when specified'); + if (!consumer || !isString(consumer)) { + throw new Error('consumer is required and must be a string'); } - return `${this.prefix}${alertTypeId}/${ - consumer ? `feature/${consumer}` : '_global' - }/${operation}`; + return `${this.prefix}${alertTypeId}/${consumer}/${operation}`; } } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts index 4036154aef9a6..99d69602db137 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts @@ -72,9 +72,9 @@ describe(`feature_privilege_builder`, () => { expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/get", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/getAlertState", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", + "alerting:1.0.0-zeta1:alert-type/my-feature/find", ] `); }); @@ -108,19 +108,19 @@ describe(`feature_privilege_builder`, () => { expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/get", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/getAlertState", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/find", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/create", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/delete", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/update", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/updateApiKey", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/enable", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/disable", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/muteAll", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/unmuteAll", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/muteInstance", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/unmuteInstance", + "alerting:1.0.0-zeta1:alert-type/my-feature/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", + "alerting:1.0.0-zeta1:alert-type/my-feature/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/create", + "alerting:1.0.0-zeta1:alert-type/my-feature/delete", + "alerting:1.0.0-zeta1:alert-type/my-feature/update", + "alerting:1.0.0-zeta1:alert-type/my-feature/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/my-feature/enable", + "alerting:1.0.0-zeta1:alert-type/my-feature/disable", + "alerting:1.0.0-zeta1:alert-type/my-feature/muteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/muteInstance", + "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteInstance", ] `); }); @@ -154,161 +154,22 @@ describe(`feature_privilege_builder`, () => { expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/get", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/getAlertState", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/find", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/create", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/delete", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/update", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/updateApiKey", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/enable", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/disable", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/muteAll", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/unmuteAll", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/muteInstance", - "alerting:1.0.0-zeta1:alert-type/feature/my-feature/unmuteInstance", - "alerting:1.0.0-zeta1:readonly-alert-type/feature/my-feature/get", - "alerting:1.0.0-zeta1:readonly-alert-type/feature/my-feature/getAlertState", - "alerting:1.0.0-zeta1:readonly-alert-type/feature/my-feature/find", - ] - `); - }); - }); - - describe(`globally`, () => { - test('grants global `read` privileges under feature consumer', () => { - const actions = new Actions(version); - const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); - - const privilege: FeatureKibanaPrivileges = { - alerting: { - globally: { - all: [], - read: ['alert-type'], - }, - }, - - savedObject: { - all: [], - read: [], - }, - ui: [], - }; - - const feature = new Feature({ - id: 'my-feature', - name: 'my-feature', - app: [], - privileges: { - all: privilege, - read: privilege, - }, - }); - - expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` - Array [ - "alerting:1.0.0-zeta1:alert-type/_global/get", - "alerting:1.0.0-zeta1:alert-type/_global/getAlertState", - "alerting:1.0.0-zeta1:alert-type/_global/find", - ] - `); - }); - - test('grants global `all` privileges under feature consumer', () => { - const actions = new Actions(version); - const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); - - const privilege: FeatureKibanaPrivileges = { - alerting: { - globally: { - all: ['alert-type'], - read: [], - }, - }, - - savedObject: { - all: [], - read: [], - }, - ui: [], - }; - - const feature = new Feature({ - id: 'my-feature', - name: 'my-feature', - app: [], - privileges: { - all: privilege, - read: privilege, - }, - }); - - expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` - Array [ - "alerting:1.0.0-zeta1:alert-type/_global/get", - "alerting:1.0.0-zeta1:alert-type/_global/getAlertState", - "alerting:1.0.0-zeta1:alert-type/_global/find", - "alerting:1.0.0-zeta1:alert-type/_global/create", - "alerting:1.0.0-zeta1:alert-type/_global/delete", - "alerting:1.0.0-zeta1:alert-type/_global/update", - "alerting:1.0.0-zeta1:alert-type/_global/updateApiKey", - "alerting:1.0.0-zeta1:alert-type/_global/enable", - "alerting:1.0.0-zeta1:alert-type/_global/disable", - "alerting:1.0.0-zeta1:alert-type/_global/muteAll", - "alerting:1.0.0-zeta1:alert-type/_global/unmuteAll", - "alerting:1.0.0-zeta1:alert-type/_global/muteInstance", - "alerting:1.0.0-zeta1:alert-type/_global/unmuteInstance", - ] - `); - }); - - test('grants both global `all` and global `read` privileges under feature consumer', () => { - const actions = new Actions(version); - const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); - - const privilege: FeatureKibanaPrivileges = { - alerting: { - globally: { - all: ['alert-type'], - read: ['readonly-alert-type'], - }, - }, - - savedObject: { - all: [], - read: [], - }, - ui: [], - }; - - const feature = new Feature({ - id: 'my-feature', - name: 'my-feature', - app: [], - privileges: { - all: privilege, - read: privilege, - }, - }); - - expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` - Array [ - "alerting:1.0.0-zeta1:alert-type/_global/get", - "alerting:1.0.0-zeta1:alert-type/_global/getAlertState", - "alerting:1.0.0-zeta1:alert-type/_global/find", - "alerting:1.0.0-zeta1:alert-type/_global/create", - "alerting:1.0.0-zeta1:alert-type/_global/delete", - "alerting:1.0.0-zeta1:alert-type/_global/update", - "alerting:1.0.0-zeta1:alert-type/_global/updateApiKey", - "alerting:1.0.0-zeta1:alert-type/_global/enable", - "alerting:1.0.0-zeta1:alert-type/_global/disable", - "alerting:1.0.0-zeta1:alert-type/_global/muteAll", - "alerting:1.0.0-zeta1:alert-type/_global/unmuteAll", - "alerting:1.0.0-zeta1:alert-type/_global/muteInstance", - "alerting:1.0.0-zeta1:alert-type/_global/unmuteInstance", - "alerting:1.0.0-zeta1:readonly-alert-type/_global/get", - "alerting:1.0.0-zeta1:readonly-alert-type/_global/getAlertState", - "alerting:1.0.0-zeta1:readonly-alert-type/_global/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/getAlertState", + "alerting:1.0.0-zeta1:alert-type/my-feature/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/create", + "alerting:1.0.0-zeta1:alert-type/my-feature/delete", + "alerting:1.0.0-zeta1:alert-type/my-feature/update", + "alerting:1.0.0-zeta1:alert-type/my-feature/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/my-feature/enable", + "alerting:1.0.0-zeta1:alert-type/my-feature/disable", + "alerting:1.0.0-zeta1:alert-type/my-feature/muteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/muteInstance", + "alerting:1.0.0-zeta1:alert-type/my-feature/unmuteInstance", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/get", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/getAlertState", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/find", ] `); }); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts index e512330335418..d697884e25104 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -28,7 +28,7 @@ export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder const getAlertingPrivilege = ( operations: string[], privilegedTypes: string[], - consumer?: string + consumer: string ) => privilegedTypes.flatMap((type) => operations.map((operation) => this.actions.alerting.get(type, consumer, operation)) @@ -37,8 +37,6 @@ export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder return uniq([ ...getAlertingPrivilege(allOperations, privilegeDefinition.alerting?.all ?? [], feature.id), ...getAlertingPrivilege(readOperations, privilegeDefinition.alerting?.read ?? [], feature.id), - ...getAlertingPrivilege(allOperations, privilegeDefinition.alerting?.globally?.all ?? []), - ...getAlertingPrivilege(readOperations, privilegeDefinition.alerting?.globally?.read ?? []), ]); } } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 874091b2bb7a8..69f1b0a183766 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -38,6 +38,7 @@ import { AlertTypeModel, Alert, IErrorObject, AlertAction, AlertTypeIndex } from import { getTimeOptions } from '../../../common/lib/get_time_options'; import { useAlertsContext } from '../../context/alerts_context'; import { ActionForm } from '../action_connector_form'; +import { AlertsFeatureId } from '../../../../../alerts/common'; export function validateBaseProperties(alertObject: Alert) { const validationResult = { errors: {} }; @@ -168,7 +169,7 @@ export const AlertForm = ({ : null; const alertTypeRegistryList = - alert.consumer === 'alerts' + alert.consumer === AlertsFeatureId ? alertTypeRegistry .list() .filter( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 2929ce6defeaf..4c6e8d3984b01 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -37,6 +37,7 @@ import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib import { routeToAlertDetails, DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation'; import { EmptyPrompt } from '../../../components/prompts/empty_prompt'; +import { AlertsFeatureId } from '../../../../../../alerts/common'; const ENTER_KEY = 13; @@ -439,7 +440,7 @@ export const AlertsList: React.FunctionComponent = () => { }} > diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/builtin_alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/builtin_alert_types.ts new file mode 100644 index 0000000000000..304410744c604 --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/builtin_alert_types.ts @@ -0,0 +1,23 @@ +/* + * 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 { FixtureSetupDeps } from './plugin'; +import { AlertType, AlertExecutorOptions } from '../../../../../../../plugins/alerts/server'; +import { AlertsFeatureId } from '../../../../../../../plugins/alerts/common'; + +export function defineFakeBuiltinAlertTypes({ alerts }: Pick) { + const noopBuiltinAlertType: AlertType = { + id: 'test.fake-built-in', + name: 'Test: Fake Built-in Noop', + actionGroups: [{ id: 'default', name: 'Default' }], + // this is a fake built in! + // privileges are special cased for built-in alerts + producer: AlertsFeatureId, + defaultActionGroupId: 'default', + async executor({ services, params, state }: AlertExecutorOptions) {}, + }; + alerts.registerType(noopBuiltinAlertType); +} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index 504d67352be1a..43c99f8c329fb 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -10,6 +10,7 @@ import { PluginSetupContract as AlertingPluginSetup } from '../../../../../../.. import { EncryptedSavedObjectsPluginStart } from '../../../../../../../plugins/encrypted_saved_objects/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; import { defineAlertTypes } from './alert_types'; +// import { defineFakeBuiltinAlertTypes } from './builtin_alert_types'; import { defineActionTypes } from './action_types'; import { defineRoutes } from './routes'; @@ -46,10 +47,9 @@ export class FixturePlugin implements Plugin, + { alerts }: Pick +) { + const noopRestrictedAlertType: AlertType = { + id: 'test.restricted-noop', + name: 'Test: Restricted Noop', + actionGroups: [{ id: 'default', name: 'Default' }], + producer: 'alertsRestrictedFixture', + defaultActionGroupId: 'default', + async executor({ services, params, state }: AlertExecutorOptions) {}, + }; + const noopUnrestrictedAlertType: AlertType = { + id: 'test.unrestricted-noop', + name: 'Test: Unrestricted Noop', + actionGroups: [{ id: 'default', name: 'Default' }], + producer: 'alertsRestrictedFixture', + defaultActionGroupId: 'default', + async executor({ services, params, state }: AlertExecutorOptions) {}, + }; + alerts.registerType(noopRestrictedAlertType); + alerts.registerType(noopUnrestrictedAlertType); +} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/index.ts new file mode 100644 index 0000000000000..54d6de50cff4d --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { FixturePlugin } from './plugin'; + +export const plugin = () => new FixturePlugin(); diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts new file mode 100644 index 0000000000000..044ad8444dd05 --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.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 { Plugin, CoreSetup } from 'kibana/server'; +import { PluginSetupContract as ActionsPluginSetup } from '../../../../../../../plugins/actions/server/plugin'; +import { PluginSetupContract as AlertingPluginSetup } from '../../../../../../../plugins/alerts/server/plugin'; +import { EncryptedSavedObjectsPluginStart } from '../../../../../../../plugins/encrypted_saved_objects/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; +import { defineAlertTypes } from './alert_types'; + +export interface FixtureSetupDeps { + features: FeaturesPluginSetup; + actions: ActionsPluginSetup; + alerts: AlertingPluginSetup; +} + +export interface FixtureStartDeps { + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; +} + +export class FixturePlugin implements Plugin { + public setup(core: CoreSetup, { features, alerts }: FixtureSetupDeps) { + features.registerFeature({ + id: 'alertsRestrictedFixture', + name: 'AlertRestricted', + app: ['alerts', 'kibana'], + privileges: { + all: { + app: ['alerts', 'kibana'], + savedObject: { + all: ['alert'], + read: [], + }, + alerting: { + all: [ + 'test.restricted-noop', + 'test.unrestricted-noop', + 'test.fake-built-in', + 'test.noop', + ], + }, + ui: [], + }, + read: { + app: ['alerts', 'kibana'], + savedObject: { + all: [], + read: ['alert'], + }, + alerting: { + read: ['test.restricted-noop', 'test.unrestricted-noop', 'test.fake-built-in'], + }, + ui: [], + }, + }, + }); + + defineAlertTypes(core, { alerts }); + } + + public start() {} + public stop() {} +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts index 7506f1d42bf0f..89ea7c768f28b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts @@ -79,6 +79,7 @@ const Space1All: User = { alerts: ['all'], actions: ['all'], alertsFixture: ['all'], + alertsRestrictedFixture: ['read'], }, spaces: ['space1'], }, @@ -96,7 +97,43 @@ const Space1All: User = { }, }; -export const Users: User[] = [NoKibanaPrivileges, Superuser, GlobalRead, Space1All]; +const Space1AllWithRestrictedFixture: User = { + username: 'space_1_all_with_restricted_fixture', + fullName: 'space_1_all_with_restricted_fixture', + password: 'space_1_all_with_restricted_fixture-password', + role: { + name: 'space_1_all_with_restricted_fixture_role', + kibana: [ + { + feature: { + alerts: ['all'], + actions: ['all'], + alertsFixture: ['all'], + alertsRestrictedFixture: ['all'], + }, + spaces: ['space1'], + }, + ], + elasticsearch: { + // TODO: Remove once Elasticsearch doesn't require the permission for own keys + cluster: ['manage_api_key'], + indices: [ + { + names: [`${ES_TEST_INDEX_NAME}*`], + privileges: ['all'], + }, + ], + }, + }, +}; + +export const Users: User[] = [ + NoKibanaPrivileges, + Superuser, + GlobalRead, + Space1All, + Space1AllWithRestrictedFixture, +]; const Space1: Space = { id: 'space1', @@ -162,6 +199,14 @@ const Space1AllAtSpace1: Space1AllAtSpace1 = { user: Space1All, space: Space1, }; +interface Space1AllWithRestrictedFixtureAtSpace1 extends Scenario { + id: 'space_1_all_with_restricted_fixture at space1'; +} +const Space1AllWithRestrictedFixtureAtSpace1: Space1AllWithRestrictedFixtureAtSpace1 = { + id: 'space_1_all_with_restricted_fixture at space1', + user: Space1AllWithRestrictedFixture, + space: Space1, +}; interface Space1AllAtSpace2 extends Scenario { id: 'space_1_all at space2'; @@ -177,11 +222,13 @@ export const UserAtSpaceScenarios: [ SuperuserAtSpace1, GlobalReadAtSpace1, Space1AllAtSpace1, - Space1AllAtSpace2 + Space1AllAtSpace2, + Space1AllWithRestrictedFixtureAtSpace1 ] = [ NoKibanaPrivilegesAtSpace1, SuperuserAtSpace1, GlobalReadAtSpace1, Space1AllAtSpace1, Space1AllAtSpace2, + Space1AllWithRestrictedFixtureAtSpace1, ]; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts index 69dcb7c813815..27d35f0bf5392 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts @@ -51,6 +51,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); objectRemover.add(space.id, response.body.id, 'action', 'actions'); expect(response.body).to.eql({ @@ -100,6 +101,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ statusCode: 400, @@ -132,6 +134,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ statusCode: 400, @@ -170,6 +173,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ statusCode: 400, @@ -206,6 +210,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ statusCode: 403, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts index d96ffc5bb3be3..86930a58a4f6f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts @@ -58,6 +58,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); break; @@ -94,6 +95,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'space_1_all at space2': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.body).to.eql({ statusCode: 404, error: 'Not Found', @@ -131,6 +133,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(404); break; default: @@ -157,6 +160,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.body).to.eql({ statusCode: 400, error: 'Bad Request', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts index 70a3663c1c798..20e751747087e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts @@ -84,6 +84,7 @@ export default function ({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body).to.be.an('object'); const searchResult = await esTestIndexTool.search( @@ -148,6 +149,7 @@ export default function ({ getService }: FtrProviderContext) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.body).to.eql({ statusCode: 404, error: 'Not Found', @@ -224,6 +226,7 @@ export default function ({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body).to.be.an('object'); const searchResult = await esTestIndexTool.search( @@ -275,6 +278,7 @@ export default function ({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(404); expect(response.body).to.eql({ statusCode: 404, @@ -307,6 +311,7 @@ export default function ({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ statusCode: 400, @@ -383,6 +388,7 @@ export default function ({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); break; default: @@ -430,6 +436,7 @@ export default function ({ getService }: FtrProviderContext) { break; case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); searchResult = await esTestIndexTool.search('action:test.authorization', reference); expect(searchResult.hits.total.value).to.eql(1); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts index c610ac670f690..78eafcf684ce6 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts @@ -56,6 +56,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body).to.eql({ id: createdAction.id, @@ -98,6 +99,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.body).to.eql({ statusCode: 404, error: 'Not Found', @@ -135,6 +137,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body).to.eql({ id: 'my-slack1', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index 45491aa2d28fc..c7eeda14733cc 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -56,6 +56,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body).to.eql([ { @@ -161,6 +162,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body).to.eql([ { @@ -233,6 +235,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(404); expect(response.body).to.eql({ statusCode: 404, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts index 22c89a1a8148f..1e1659636aa31 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts @@ -41,6 +41,7 @@ export default function listActionTypesTests({ getService }: FtrProviderContext) case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); // Check for values explicitly in order to avoid this test failing each time plugins register // a new action type diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts index cb0e0efda0b1a..f86656531b267 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts @@ -66,6 +66,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body).to.eql({ id: createdAction.id, @@ -126,6 +127,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.body).to.eql({ statusCode: 404, error: 'Not Found', @@ -167,6 +169,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ statusCode: 400, @@ -207,6 +210,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(404); expect(response.body).to.eql({ statusCode: 404, @@ -239,6 +243,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ statusCode: 400, @@ -296,6 +301,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ statusCode: 400, @@ -337,6 +343,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.body).to.eql({ statusCode: 400, error: 'Bad Request', 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 54e8a66b40337..b3dd4664b4b6f 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 @@ -97,6 +97,7 @@ export default function alertTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); // Wait for the action to index a document before disabling the alert and waiting for tasks to finish @@ -195,6 +196,7 @@ instanceStateValue: true break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); // Wait for the action to index a document before disabling the alert and waiting for tasks to finish @@ -386,6 +388,7 @@ instanceStateValue: true break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); @@ -473,6 +476,7 @@ instanceStateValue: true }); break; case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); @@ -591,6 +595,7 @@ instanceStateValue: true }); break; case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); @@ -679,6 +684,7 @@ instanceStateValue: true }); break; case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': expect(response.statusCode).to.eql(200); // Wait until alerts scheduled actions 3 times before disabling the alert and waiting for tasks to finish @@ -749,6 +755,7 @@ instanceStateValue: true }); break; case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': expect(response.statusCode).to.eql(200); // Wait for actions to execute twice before disabling the alert and waiting for tasks to finish @@ -803,6 +810,7 @@ instanceStateValue: true }); break; case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': expect(response.statusCode).to.eql(200); // Actions should execute twice before widning things down @@ -849,6 +857,7 @@ instanceStateValue: true }); break; case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': await alertUtils.muteAll(response.body.id); await alertUtils.enable(response.body.id); @@ -898,6 +907,7 @@ instanceStateValue: true }); break; case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': await alertUtils.muteInstance(response.body.id, '1'); await alertUtils.enable(response.body.id); @@ -947,6 +957,7 @@ instanceStateValue: true }); break; case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': await alertUtils.muteInstance(response.body.id, '1'); await alertUtils.muteAll(response.body.id); 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 2148b3710d893..b7e22da90be38 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 @@ -77,6 +77,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); expect(response.body).to.eql({ @@ -130,44 +131,51 @@ export default function createAlertTests({ getService }: FtrProviderContext) { } }); - it('should handle create alert request appropriately when an alert is disabled ', async () => { + it('should handle create alert request appropriately when consumer is the same as producer', async () => { const response = await supertestWithoutAuth .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) - .send(getTestAlertData({ enabled: false })); + .send( + getTestAlertData({ + alertTypeId: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + }) + ); switch (scenario.id) { case 'no_kibana_privileges at space1': case 'global_read at space1': case 'space_1_all at space2': + case 'space_1_all at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('create', 'test.noop', 'alertsFixture'), + message: getUnauthorizedErrorMessage( + 'create', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), statusCode: 403, }); break; case 'superuser at space1': - case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); - expect(response.body.scheduledTaskId).to.eql(undefined); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); - it('should handle create alert request appropriately when alert type is unregistered', async () => { + it('should handle create alert request appropriately when consumer is not the producer', async () => { const response = await supertestWithoutAuth .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) .send( - getTestAlertData({ - alertTypeId: 'test.unregistered-alert-type', - }) + getTestAlertData({ alertTypeId: 'test.unrestricted-noop', consumer: 'alertsFixture' }) ); switch (scenario.id) { @@ -180,12 +188,141 @@ export default function createAlertTests({ getService }: FtrProviderContext) { error: 'Forbidden', message: getUnauthorizedErrorMessage( 'create', - 'test.unregistered-alert-type', + 'test.unrestricted-noop', 'alertsFixture' ), statusCode: 403, }); break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + // it.only('should handle create alert request appropriately when alert type is a built-in type', async () => { + // const response = await supertestWithoutAuth + // .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + // .set('kbn-xsrf', 'foo') + // .auth(user.username, user.password) + // .send( + // getTestAlertData({ + // alertTypeId: 'test.fake-built-in', + // consumer: 'alertsRestrictedFixture', + // }) + // ); + + // switch (scenario.id) { + // case 'no_kibana_privileges at space1': + // case 'global_read at space1': + // case 'space_1_all at space2': + // case 'space_1_all at space1': + // expect(response.statusCode).to.eql(403); + // expect(response.body).to.eql({ + // error: 'Forbidden', + // message: getUnauthorizedErrorMessage( + // 'create', + // 'test.fake-built-in', + // 'alertsRestrictedFixture' + // ), + // statusCode: 403, + // }); + // break; + // case 'superuser at space1': + // case 'space_1_all_with_restricted_fixture at space1': + // expect(response.statusCode).to.eql(200); + // objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); + // break; + // default: + // throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + // } + // }); + + it('should handle create alert request appropriately when consumer is "alerts"', async () => { + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send( + getTestAlertData({ + alertTypeId: 'test.noop', + consumer: 'alerts', + }) + ); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getUnauthorizedErrorMessage('create', 'test.noop', 'alerts'), + statusCode: 403, + }); + break; + case 'space_1_all at space1': + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle create alert request appropriately when an alert is disabled ', async () => { + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send(getTestAlertData({ enabled: false })); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getUnauthorizedErrorMessage('create', 'test.noop', 'alertsFixture'), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); + expect(response.body.scheduledTaskId).to.eql(undefined); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle create alert request appropriately when alert type is unregistered', async () => { + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send( + getTestAlertData({ + alertTypeId: 'test.unregistered-alert-type', + }) + ); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all at space2': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ @@ -212,6 +349,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ statusCode: 400, @@ -248,6 +386,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ statusCode: 400, @@ -274,6 +413,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ error: 'Bad Request', @@ -299,6 +439,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ error: 'Bad Request', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts index 5092519e8d155..0e997add9e6e3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts @@ -63,6 +63,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); try { @@ -96,6 +97,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': expect(response.body).to.eql({ statusCode: 404, @@ -148,6 +150,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); try { 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 acd3340927b79..76ebe3c8bd902 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 @@ -64,6 +64,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); try { @@ -122,6 +123,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); try { @@ -160,6 +162,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture 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 ff31375ed1367..ca3947478af2f 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 @@ -62,6 +62,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth @@ -125,6 +126,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth @@ -170,6 +172,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture 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 b54767cb5ebc5..0ebb532ecb201 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 @@ -50,6 +50,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.body.page).to.equal(1); expect(response.body.perPage).to.be.greaterThan(0); expect(response.body.total).to.be.greaterThan(0); @@ -131,6 +132,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body.page).to.equal(1); expect(response.body.perPage).to.be.greaterThan(0); @@ -188,6 +190,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.body.page).to.equal(0); expect(response.body.perPage).to.equal(0); expect(response.body.total).to.equal(0); 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 3ad19450ffc0c..9cf635139bab1 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 @@ -52,6 +52,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body).to.eql({ id: createdAlert.id, @@ -98,6 +99,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': case 'global_read at space1': case 'superuser at space1': expect(response.body).to.eql({ @@ -122,6 +124,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(404); expect(response.body).to.eql({ statusCode: 404, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts index 7b8e8e838a475..3ed32614f5f2a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts @@ -52,6 +52,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body).to.key('alertInstances', 'previousStartedAt'); break; @@ -77,6 +78,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': case 'global_read at space1': case 'superuser at space1': expect(response.body).to.eql({ @@ -101,6 +103,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(404); expect(response.body).to.eql({ statusCode: 404, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index a02e95c80c95d..f8feff24fc1d0 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -23,18 +23,46 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { .auth(user.username, user.password); expect(response.statusCode).to.eql(200); + const noOpAlertType = response.body.find( + (alertType: any) => alertType.id === 'test.noop' + ); switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': expect(response.body).to.eql([]); break; case 'global_read at space1': - case 'superuser at space1': case 'space_1_all at space1': - const fixtureAlertType = response.body.find( - (alertType: any) => alertType.id === 'test.noop' - ); - expect(fixtureAlertType).to.eql({ + expect(noOpAlertType).to.eql({ + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + id: 'test.noop', + name: 'Test: Noop', + actionVariables: { + state: [], + context: [], + }, + authorizedConsumers: ['alertsFixture'], + producer: 'alertsFixture', + }); + break; + case 'space_1_all_with_restricted_fixture at space1': + expect(noOpAlertType).to.eql({ + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + id: 'test.noop', + name: 'Test: Noop', + actionVariables: { + state: [], + context: [], + }, + authorizedConsumers: ['alertsRestrictedFixture', 'alertsFixture'], + producer: 'alertsFixture', + }); + break; + case 'superuser at space1': + const { authorizedConsumers, ...superUserFixtureAlertType } = noOpAlertType; + expect(superUserFixtureAlertType).to.eql({ actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', id: 'test.noop', @@ -45,6 +73,8 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { }, producer: 'alertsFixture', }); + expect(authorizedConsumers).to.contain('alertsFixture'); + expect(authorizedConsumers).to.contain('alertsRestrictedFixture'); 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_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts index ce210f1128fa6..9c850e08a7138 100644 --- 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 @@ -54,6 +54,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth 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 index 885443bfbd1b2..e6907bc3aa9d9 100644 --- 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 @@ -54,6 +54,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth @@ -105,6 +106,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth 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 index 9a649a3b7af73..ac72d2d772516 100644 --- 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 @@ -59,6 +59,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth 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 index 6f9e7ce5a6e08..448289af8e011 100644 --- 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 @@ -65,6 +65,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth 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 bf3ccf6ec479e..89d2b45685a90 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 @@ -75,6 +75,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body).to.eql({ ...updatedData, @@ -158,6 +159,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body).to.eql({ ...updatedData, @@ -221,6 +223,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': expect(response.body).to.eql({ statusCode: 404, @@ -263,6 +266,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ statusCode: 400, @@ -288,6 +292,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ statusCode: 400, @@ -341,6 +346,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ statusCode: 400, @@ -373,6 +379,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ statusCode: 400, @@ -433,6 +440,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); await retry.try(async () => { const alertTask = (await getAlertingTaskById(createdAlert.scheduledTaskId)).docs[0]; 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 d441be8a82fd3..502ee0299f7f7 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 @@ -53,6 +53,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth @@ -109,6 +110,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); const { body: updatedAlert } = await supertestWithoutAuth @@ -147,6 +149,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': expect(response.body).to.eql({ statusCode: 404, error: 'Not Found', From 11bbf163c458c4ad113d1706e758e844288d4fa2 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 8 Jun 2020 12:11:14 +0100 Subject: [PATCH 020/126] fixed secuirty interface --- x-pack/plugins/security/server/plugin.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 54f9c5e11a8d1..62de3a3242f20 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -51,7 +51,10 @@ export interface SecurityPluginSetup { | 'grantAPIKeyAsInternalUser' | 'invalidateAPIKeyAsInternalUser' >; - authz: Pick; + authz: Pick< + AuthorizationServiceSetup, + 'actions' | 'checkPrivilegesDynamicallyWithRequest' | 'checkPrivilegesWithRequest' | 'mode' + >; license: SecurityLicense; audit: Pick; From 2b849022b21dbf0b8681606104a9bacf958d8232 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 8 Jun 2020 12:14:11 +0100 Subject: [PATCH 021/126] removed unused test fixture --- .../alerts/server/builtin_alert_types.ts | 23 ----------- .../fixtures/plugins/alerts/server/plugin.ts | 2 - .../tests/alerting/create.ts | 38 ------------------- 3 files changed, 63 deletions(-) delete mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/builtin_alert_types.ts diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/builtin_alert_types.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/builtin_alert_types.ts deleted file mode 100644 index 304410744c604..0000000000000 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/builtin_alert_types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 { FixtureSetupDeps } from './plugin'; -import { AlertType, AlertExecutorOptions } from '../../../../../../../plugins/alerts/server'; -import { AlertsFeatureId } from '../../../../../../../plugins/alerts/common'; - -export function defineFakeBuiltinAlertTypes({ alerts }: Pick) { - const noopBuiltinAlertType: AlertType = { - id: 'test.fake-built-in', - name: 'Test: Fake Built-in Noop', - actionGroups: [{ id: 'default', name: 'Default' }], - // this is a fake built in! - // privileges are special cased for built-in alerts - producer: AlertsFeatureId, - defaultActionGroupId: 'default', - async executor({ services, params, state }: AlertExecutorOptions) {}, - }; - alerts.registerType(noopBuiltinAlertType); -} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index 43c99f8c329fb..36147fa1f4961 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -10,7 +10,6 @@ import { PluginSetupContract as AlertingPluginSetup } from '../../../../../../.. import { EncryptedSavedObjectsPluginStart } from '../../../../../../../plugins/encrypted_saved_objects/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; import { defineAlertTypes } from './alert_types'; -// import { defineFakeBuiltinAlertTypes } from './builtin_alert_types'; import { defineActionTypes } from './action_types'; import { defineRoutes } from './routes'; @@ -79,7 +78,6 @@ export class FixturePlugin implements Plugin { - // const response = await supertestWithoutAuth - // .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) - // .set('kbn-xsrf', 'foo') - // .auth(user.username, user.password) - // .send( - // getTestAlertData({ - // alertTypeId: 'test.fake-built-in', - // consumer: 'alertsRestrictedFixture', - // }) - // ); - - // switch (scenario.id) { - // case 'no_kibana_privileges at space1': - // case 'global_read at space1': - // case 'space_1_all at space2': - // case 'space_1_all at space1': - // expect(response.statusCode).to.eql(403); - // expect(response.body).to.eql({ - // error: 'Forbidden', - // message: getUnauthorizedErrorMessage( - // 'create', - // 'test.fake-built-in', - // 'alertsRestrictedFixture' - // ), - // statusCode: 403, - // }); - // break; - // case 'superuser at space1': - // case 'space_1_all_with_restricted_fixture at space1': - // expect(response.statusCode).to.eql(200); - // objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); - // break; - // default: - // throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); - // } - // }); - it('should handle create alert request appropriately when consumer is "alerts"', async () => { const response = await supertestWithoutAuth .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) From e15946c8217885dfb3ca956f43b3a57d17fb4bb7 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 8 Jun 2020 16:48:18 +0100 Subject: [PATCH 022/126] added more acceptance tests around alert deletion, enabling, find and get with auth --- examples/alerting_example/server/plugin.ts | 8 +- x-pack/plugins/alerts/server/alerts_client.ts | 38 ++-- .../plugins/features/server/feature_schema.ts | 4 - .../__snapshots__/alerting.test.ts.snap | 12 +- x-pack/plugins/security/server/index.ts | 2 +- .../alerts_restricted/server/plugin.ts | 9 +- .../common/lib/alert_utils.ts | 10 +- .../common/lib/index.ts | 6 +- .../security_and_spaces/scenarios.ts | 1 - .../tests/alerting/alerts.ts | 24 +-- .../tests/alerting/create.ts | 40 +++- .../tests/alerting/delete.ts | 178 +++++++++++++++++- .../tests/alerting/disable.ts | 175 ++++++++++++++++- .../tests/alerting/enable.ts | 175 ++++++++++++++++- .../tests/alerting/find.ts | 85 +++++++++ .../security_and_spaces/tests/alerting/get.ts | 142 +++++++++++++- .../tests/alerting/get_alert_state.ts | 54 +++++- .../tests/alerting/list_alert_types.ts | 13 -- .../tests/alerting/mute_all.ts | 8 +- .../tests/alerting/mute_instance.ts | 14 +- .../tests/alerting/unmute_all.ts | 8 +- .../tests/alerting/unmute_instance.ts | 4 +- .../tests/alerting/update.ts | 26 ++- .../tests/alerting/update_api_key.ts | 14 +- .../fixtures/plugins/alerts/server/plugin.ts | 8 +- 25 files changed, 950 insertions(+), 108 deletions(-) diff --git a/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts index 9128281fb72e5..9a93a6f8f4d6e 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/examples/alerting_example/server/plugin.ts @@ -42,9 +42,7 @@ export class AlertingExamplePlugin implements Plugin - authorization.actions.alerting.get(alertTypeId, scope, operation) - ) + requiredPrivilegesByScope.producer, + ] ); + if (!hasAllRequested) { + const authorizedPrivileges = pluck( + privileges.filter((privilege) => privilege.authorized), + 'privilege' + ); + const unauthorizedScopes = mapValues( + requiredPrivilegesByScope, + (privilege) => !authorizedPrivileges.includes(privilege) + ); + throw Boom.forbidden( - `Unauthorized to ${operation} a "${alertTypeId}" alert for "${consumer}"` + `Unauthorized to ${operation} a "${alertTypeId}" alert ${ + unauthorizedScopes.consumer ? `for "${consumer}"` : `by "${alertType.producer}"` + }` ); } } diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 86c7bc4852742..ccc455cb2de5b 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -36,10 +36,6 @@ const privilegeSchema = Joi.object({ alerting: Joi.object({ all: Joi.array().items(Joi.string()), read: Joi.array().items(Joi.string()), - globally: Joi.object({ - all: Joi.array().items(Joi.string()), - read: Joi.array().items(Joi.string()), - }), }), savedObject: Joi.object({ all: Joi.array().items(Joi.string()).required(), diff --git a/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap b/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap index e9cd8bf48a400..afa907fe09837 100644 --- a/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap +++ b/x-pack/plugins/security/server/authorization/actions/__snapshots__/alerting.test.ts.snap @@ -12,15 +12,17 @@ exports[`#get alertType of true throws error 1`] = `"alertTypeId is required and exports[`#get alertType of undefined throws error 1`] = `"alertTypeId is required and must be a string"`; -exports[`#get consumer of "" throws error 1`] = `"consumer is optional but must be a string when specified"`; +exports[`#get consumer of "" throws error 1`] = `"consumer is required and must be a string"`; -exports[`#get consumer of {} throws error 1`] = `"consumer is optional but must be a string when specified"`; +exports[`#get consumer of {} throws error 1`] = `"consumer is required and must be a string"`; -exports[`#get consumer of 1 throws error 1`] = `"consumer is optional but must be a string when specified"`; +exports[`#get consumer of 1 throws error 1`] = `"consumer is required and must be a string"`; -exports[`#get consumer of null throws error 1`] = `"consumer is optional but must be a string when specified"`; +exports[`#get consumer of null throws error 1`] = `"consumer is required and must be a string"`; -exports[`#get consumer of true throws error 1`] = `"consumer is optional but must be a string when specified"`; +exports[`#get consumer of true throws error 1`] = `"consumer is required and must be a string"`; + +exports[`#get consumer of undefined throws error 1`] = `"consumer is required and must be a string"`; exports[`#get operation of "" throws error 1`] = `"operation is required and must be a string"`; diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index e25613fc5936f..c7bd025838864 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -30,7 +30,7 @@ export { export { AuditLogger } from './audit'; export { SecurityPluginSetup }; export { AuthenticatedUser } from '../common/model'; -export { Actions, CheckPrivilegesResponse } from './authorization'; +export { Actions } from './authorization'; export const config: PluginConfigDescriptor> = { schema: ConfigSchema, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts index 044ad8444dd05..c9155029899ad 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts @@ -35,12 +35,7 @@ export class FixturePlugin implements Plugin { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + }) + ) + .expect(200); + + const response = await supertestWithoutAuth + .delete(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password); + + 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.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'delete', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + // Ensure task still exists + await getScheduledTask(createdAlert.scheduledTaskId); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + try { + await getScheduledTask(createdAlert.scheduledTaskId); + throw new Error('Should have removed scheduled task'); + } catch (e) { + expect(e.status).to.eql(404); + } + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle delete alert request appropriately when consumer is not the producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ alertTypeId: 'test.unrestricted-noop', consumer: 'alertsFixture' }) + ) + .expect(200); + + const response = await supertestWithoutAuth + .delete(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'delete', + 'test.unrestricted-noop', + 'alertsFixture' + ), + statusCode: 403, + }); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + // Ensure task still exists + await getScheduledTask(createdAlert.scheduledTaskId); + break; + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'delete', + 'test.unrestricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + // Ensure task still exists + await getScheduledTask(createdAlert.scheduledTaskId); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + try { + await getScheduledTask(createdAlert.scheduledTaskId); + throw new Error('Should have removed scheduled task'); + } catch (e) { + expect(e.status).to.eql(404); + } + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle delete alert request appropriately when consumer is "alerts"', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.noop', + consumer: 'alerts', + }) + ) + .expect(200); + + const response = await supertestWithoutAuth + .delete(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage('delete', 'test.noop', 'alerts'), statusCode: 403, }); objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); @@ -141,7 +309,11 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('delete', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage( + 'delete', + 'test.noop', + 'alertsFixture' + ), statusCode: 403, }); objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); 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 76ebe3c8bd902..e4d8f656cd1a7 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 @@ -13,7 +13,8 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, - getUnauthorizedErrorMessage, + getConsumerUnauthorizedErrorMessage, + getProducerUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -56,7 +57,11 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('disable', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage( + 'disable', + 'test.noop', + 'alertsFixture' + ), statusCode: 403, }); // Ensure task still exists @@ -86,6 +91,166 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte } }); + it('should handle disable alert request appropriately when consumer is the same as producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + enabled: true, + }) + ) + .expect(200); + + const response = await alertUtils.getDisableRequest(createdAlert.id); + + 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.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'disable', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + // Ensure task still exists + await getScheduledTask(createdAlert.scheduledTaskId); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + try { + await getScheduledTask(createdAlert.scheduledTaskId); + throw new Error('Should have removed scheduled task'); + } catch (e) { + expect(e.status).to.eql(404); + } + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle disable alert request appropriately when consumer is not the producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.unrestricted-noop', + consumer: 'alertsFixture', + enabled: true, + }) + ) + .expect(200); + + const response = await alertUtils.getDisableRequest(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(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'disable', + 'test.unrestricted-noop', + 'alertsFixture' + ), + statusCode: 403, + }); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + // Ensure task still exists + await getScheduledTask(createdAlert.scheduledTaskId); + break; + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'disable', + 'test.unrestricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + // Ensure task still exists + await getScheduledTask(createdAlert.scheduledTaskId); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + try { + await getScheduledTask(createdAlert.scheduledTaskId); + throw new Error('Should have removed scheduled task'); + } catch (e) { + expect(e.status).to.eql(404); + } + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle disable alert request appropriately when consumer is "alerts"', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.noop', + consumer: 'alerts', + enabled: true, + }) + ) + .expect(200); + + const response = await alertUtils.getDisableRequest(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(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage('disable', 'test.noop', 'alerts'), + statusCode: 403, + }); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + // Ensure task still exists + await getScheduledTask(createdAlert.scheduledTaskId); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + try { + await getScheduledTask(createdAlert.scheduledTaskId); + throw new Error('Should have removed scheduled task'); + } catch (e) { + expect(e.status).to.eql(404); + } + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it('should still be able to disable alert when AAD is broken', async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) @@ -115,7 +280,11 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('disable', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage( + 'disable', + 'test.noop', + 'alertsFixture' + ), statusCode: 403, }); // Ensure task still exists 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 ca3947478af2f..61f886f218b1a 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 @@ -13,7 +13,8 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, - getUnauthorizedErrorMessage, + getConsumerUnauthorizedErrorMessage, + getProducerUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -56,7 +57,11 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('enable', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage( + 'enable', + 'test.noop', + 'alertsFixture' + ), statusCode: 403, }); break; @@ -91,6 +96,166 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex } }); + it('should handle enable alert request appropriately when consumer is the same as producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + enabled: false, + }) + ) + .expect(200); + + const response = await alertUtils.getEnableRequest(createdAlert.id); + + 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.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'enable', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + // Ensure task still exists + await getScheduledTask(createdAlert.scheduledTaskId); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + try { + await getScheduledTask(createdAlert.scheduledTaskId); + throw new Error('Should have removed scheduled task'); + } catch (e) { + expect(e.status).to.eql(404); + } + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle enable alert request appropriately when consumer is not the producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.unrestricted-noop', + consumer: 'alertsFixture', + enabled: false, + }) + ) + .expect(200); + + const response = await alertUtils.getEnableRequest(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(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'enable', + 'test.unrestricted-noop', + 'alertsFixture' + ), + statusCode: 403, + }); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + // Ensure task still exists + await getScheduledTask(createdAlert.scheduledTaskId); + break; + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'enable', + 'test.unrestricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + // Ensure task still exists + await getScheduledTask(createdAlert.scheduledTaskId); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + try { + await getScheduledTask(createdAlert.scheduledTaskId); + throw new Error('Should have removed scheduled task'); + } catch (e) { + expect(e.status).to.eql(404); + } + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle enable alert request appropriately when consumer is "alerts"', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.noop', + consumer: 'alerts', + enabled: false, + }) + ) + .expect(200); + + const response = await alertUtils.getEnableRequest(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(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage('enable', 'test.noop', 'alerts'), + statusCode: 403, + }); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + // Ensure task still exists + await getScheduledTask(createdAlert.scheduledTaskId); + break; + case 'superuser at space1': + case 'space_1_all at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + try { + await getScheduledTask(createdAlert.scheduledTaskId); + throw new Error('Should have removed scheduled task'); + } catch (e) { + expect(e.status).to.eql(404); + } + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it('should still be able to enable alert when AAD is broken', async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) @@ -120,7 +285,11 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('enable', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage( + 'enable', + 'test.noop', + 'alertsFixture' + ), statusCode: 403, }); break; 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 0ebb532ecb201..646e8bed577bc 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 @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { chunk } from 'lodash'; import { UserAtSpaceScenarios } from '../../scenarios'; import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -83,6 +84,90 @@ export default function createFindTests({ getService }: FtrProviderContext) { } }); + it('should filter out types that the user is not authorized to `get` retaining pagination', async () => { + async function createNoOpAlert(overrides = {}) { + const alert = getTestAlertData(overrides); + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send(alert) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + return { + id: createdAlert.id, + alertTypeId: alert.alertTypeId, + }; + } + function createRestrictedNoOpAlert() { + return createNoOpAlert({ + alertTypeId: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + }); + } + const allAlerts = []; + allAlerts.push(await createNoOpAlert()); + allAlerts.push(await createNoOpAlert()); + allAlerts.push(await createRestrictedNoOpAlert()); + allAlerts.push(await createRestrictedNoOpAlert()); + allAlerts.push(await createNoOpAlert()); + allAlerts.push(await createNoOpAlert()); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/_find?per_page=3&sort_field=createdAt`) + .auth(user.username, user.password); + + expect(response.statusCode).to.eql(200); + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + expect(response.body.page).to.equal(0); + expect(response.body.perPage).to.equal(0); + expect(response.body.total).to.equal(0); + expect(response.body.data.length).to.equal(0); + break; + case 'global_read at space1': + case 'space_1_all at space1': + expect(response.body.page).to.equal(1); + expect(response.body.perPage).to.be.equal(3); + expect(response.body.total).to.be.equal(4); + { + const [firstPage] = chunk( + allAlerts + .filter((alert) => alert.alertTypeId !== 'test.restricted-noop') + .map((alert) => alert.id), + 3 + ); + expect(response.body.data.map((alert: any) => alert.id)).to.eql(firstPage); + } + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.body.page).to.equal(1); + expect(response.body.perPage).to.be.equal(3); + expect(response.body.total).to.be.equal(6); + { + const [firstPage, secondPage] = chunk( + allAlerts.map((alert) => alert.id), + 3 + ); + expect(response.body.data.map((alert: any) => alert.id)).to.eql(firstPage); + + const secondResponse = await supertestWithoutAuth + .get( + `${getUrlPrefix( + space.id + )}/api/alerts/_find?per_page=3&sort_field=createdAt&page=2` + ) + .auth(user.username, user.password); + expect(secondResponse.body.data.map((alert: any) => alert.id)).to.eql(secondPage); + } + + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it('should handle find alert request with filter appropriately', async () => { const { body: createdAction } = await supertest .post(`${getUrlPrefix(space.id)}/api/actions/action`) 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 9cf635139bab1..475ae6712f2ac 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 @@ -10,7 +10,8 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, - getUnauthorizedErrorMessage, + getConsumerUnauthorizedErrorMessage, + getProducerUnauthorizedErrorMessage, } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -45,7 +46,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('get', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage('get', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -82,6 +83,143 @@ export default function createGetTests({ getService }: FtrProviderContext) { } }); + it('should handle get alert request appropriately when consumer is the same as producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .auth(user.username, user.password); + + 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.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'get', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle get alert request appropriately when consumer is not the producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.unrestricted-noop', + consumer: 'alertsFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'get', + 'test.unrestricted-noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'get', + 'test.unrestricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle get alert request appropriately when consumer is "alerts"', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.restricted-noop', + consumer: 'alerts', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'space_1_all at space1': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'get', + 'test.restricted-noop', + 'alerts' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it(`shouldn't get alert from another space`, async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts index 3ed32614f5f2a..6152ecb16d796 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts @@ -9,7 +9,8 @@ import { getUrlPrefix, ObjectRemover, getTestAlertData, - getUnauthorizedErrorMessage, + getConsumerUnauthorizedErrorMessage, + getProducerUnauthorizedErrorMessage, } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { UserAtSpaceScenarios } from '../../scenarios'; @@ -45,7 +46,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('get', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage('get', 'test.noop', 'alertsFixture'), statusCode: 403, }); break; @@ -61,6 +62,55 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont } }); + it('should handle getAlertState alert request appropriately', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.restricted-noop', + consumer: 'alertsFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}/state`) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage('get', 'test.noop', 'alertsFixture'), + statusCode: 403, + }); + break; + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'get', + 'test.noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'global_read at space1': + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + expect(response.body).to.key('alertInstances', 'previousStartedAt'); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); it(`shouldn't getAlertState for an alert from another space`, async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index f8feff24fc1d0..ee56f741d3e2a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -47,19 +47,6 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { }); break; case 'space_1_all_with_restricted_fixture at space1': - expect(noOpAlertType).to.eql({ - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - id: 'test.noop', - name: 'Test: Noop', - actionVariables: { - state: [], - context: [], - }, - authorizedConsumers: ['alertsRestrictedFixture', 'alertsFixture'], - producer: 'alertsFixture', - }); - break; case 'superuser at space1': const { authorizedConsumers, ...superUserFixtureAlertType } = noOpAlertType; expect(superUserFixtureAlertType).to.eql({ 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 index 9c850e08a7138..a87a96fd35a20 100644 --- 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 @@ -13,7 +13,7 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, - getUnauthorizedErrorMessage, + getConsumerUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -48,7 +48,11 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('muteAll', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage( + 'muteAll', + 'test.noop', + 'alertsFixture' + ), statusCode: 403, }); break; 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 index e6907bc3aa9d9..6d70744d1c51b 100644 --- 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 @@ -13,7 +13,7 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, - getUnauthorizedErrorMessage, + getConsumerUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -48,7 +48,11 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('muteInstance', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage( + 'muteInstance', + 'test.noop', + 'alertsFixture' + ), statusCode: 403, }); break; @@ -100,7 +104,11 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('muteInstance', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage( + 'muteInstance', + 'test.noop', + 'alertsFixture' + ), statusCode: 403, }); break; 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 index ac72d2d772516..af3ead1eda5a5 100644 --- 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 @@ -13,7 +13,7 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, - getUnauthorizedErrorMessage, + getConsumerUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -53,7 +53,11 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('unmuteAll', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage( + 'unmuteAll', + 'test.noop', + 'alertsFixture' + ), statusCode: 403, }); break; 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 index 448289af8e011..7bf3e730e97a2 100644 --- 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 @@ -13,7 +13,7 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, - getUnauthorizedErrorMessage, + getConsumerUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -55,7 +55,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage( + message: getConsumerUnauthorizedErrorMessage( 'unmuteInstance', 'test.noop', 'alertsFixture' 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 89d2b45685a90..4d885024cc7f6 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 @@ -13,7 +13,7 @@ import { getTestAlertData, ObjectRemover, ensureDatetimeIsWithinRange, - getUnauthorizedErrorMessage, + getConsumerUnauthorizedErrorMessage, } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -69,7 +69,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('update', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage( + 'update', + 'test.noop', + 'alertsFixture' + ), statusCode: 403, }); break; @@ -153,7 +157,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('update', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage( + 'update', + 'test.noop', + 'alertsFixture' + ), statusCode: 403, }); break; @@ -340,7 +348,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('update', 'test.validation', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage( + 'update', + 'test.validation', + 'alertsFixture' + ), statusCode: 403, }); break; @@ -434,7 +446,11 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('update', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage( + 'update', + 'test.noop', + 'alertsFixture' + ), statusCode: 403, }); break; 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 502ee0299f7f7..2b7f448c3e4bd 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 @@ -13,7 +13,7 @@ import { getUrlPrefix, getTestAlertData, ObjectRemover, - getUnauthorizedErrorMessage, + getConsumerUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -47,7 +47,11 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('updateApiKey', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage( + 'updateApiKey', + 'test.noop', + 'alertsFixture' + ), statusCode: 403, }); break; @@ -104,7 +108,11 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getUnauthorizedErrorMessage('updateApiKey', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage( + 'updateApiKey', + 'test.noop', + 'alertsFixture' + ), statusCode: 403, }); break; diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index 5f4afd84522cf..256394136ee69 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -28,9 +28,7 @@ export class AlertingFixturePlugin implements Plugin Date: Tue, 9 Jun 2020 16:36:19 +0100 Subject: [PATCH 023/126] expanded acceptance tests around rbac in alerts --- x-pack/plugins/alerts/server/alerts_client.ts | 20 +- .../fixtures/plugins/alerts/server/plugin.ts | 1 + .../alerts_restricted/server/plugin.ts | 2 +- .../security_and_spaces/scenarios.ts | 1 + .../tests/alerting/create.ts | 6 +- .../tests/alerting/delete.ts | 6 +- .../tests/alerting/disable.ts | 21 +- .../tests/alerting/enable.ts | 27 +- .../tests/alerting/find.ts | 2 +- .../security_and_spaces/tests/alerting/get.ts | 10 +- .../tests/alerting/get_alert_state.ts | 13 +- .../tests/alerting/list_alert_types.ts | 82 ++++-- .../tests/alerting/mute_all.ts | 177 ++++++++++++ .../tests/alerting/mute_instance.ts | 177 ++++++++++++ .../tests/alerting/unmute_all.ts | 192 +++++++++++++ .../tests/alerting/unmute_instance.ts | 198 ++++++++++++++ .../tests/alerting/update.ts | 255 ++++++++++++++++++ .../tests/alerting/update_api_key.ts | 171 ++++++++++++ 18 files changed, 1282 insertions(+), 79 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index a93dde83ebe70..47597de1f79e2 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -675,20 +675,24 @@ export class AlertsClient { producer: authorization.actions.alerting.get(alertTypeId, alertType.producer, operation), }; + // We special case the Alerts Management `consumer` as we don't want to have to + // manually authorize each alert type in the management UI + const shouldAuthorizeConsumer = consumer !== AlertsFeatureId; + const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); const { hasAllRequested, privileges } = await checkPrivileges( - consumer === AlertsFeatureId || consumer === alertType.producer + shouldAuthorizeConsumer && consumer !== alertType.producer ? [ - // skip consumer privilege checks under `alerts` as all alert types can - // be created under `alerts` if you have producer level privileges - requiredPrivilegesByScope.producer, - ] - : [ // check for access at consumer level requiredPrivilegesByScope.consumer, // check for access at producer level requiredPrivilegesByScope.producer, ] + : [ + // skip consumer privilege checks under `alerts` as all alert types can + // be created under `alerts` if you have producer level privileges + requiredPrivilegesByScope.producer, + ] ); if (!hasAllRequested) { @@ -703,7 +707,9 @@ export class AlertsClient { throw Boom.forbidden( `Unauthorized to ${operation} a "${alertTypeId}" alert ${ - unauthorizedScopes.consumer ? `for "${consumer}"` : `by "${alertType.producer}"` + shouldAuthorizeConsumer && unauthorizedScopes.consumer + ? `for "${consumer}"` + : `by "${alertType.producer}"` }` ); } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index 36147fa1f4961..8e2b1c67b0045 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -69,6 +69,7 @@ export class FixturePlugin implements Plugin alert.id)).to.eql(firstPage); } break; + case 'global_read at space1': case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.body.page).to.equal(1); 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 475ae6712f2ac..9835b18b96e3a 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 @@ -103,7 +103,6 @@ export default function createGetTests({ getService }: FtrProviderContext) { 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.statusCode).to.eql(403); expect(response.body).to.eql({ @@ -116,6 +115,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { statusCode: 403, }); break; + case 'global_read at space1': case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); @@ -145,7 +145,6 @@ export default function createGetTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - case 'global_read at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -170,6 +169,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { }); break; case 'superuser at space1': + case 'global_read at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); break; @@ -199,18 +199,18 @@ export default function createGetTests({ getService }: FtrProviderContext) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'space_1_all at space1': - case 'global_read at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage( + message: getProducerUnauthorizedErrorMessage( 'get', 'test.restricted-noop', - 'alerts' + 'alertsRestrictedFixture' ), statusCode: 403, }); break; + case 'global_read at space1': case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts index 6152ecb16d796..e188a21fd0d36 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts @@ -62,13 +62,13 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont } }); - it('should handle getAlertState alert request appropriately', async () => { + it('should handle getAlertState alert request appropriately when unauthorized', async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ - alertTypeId: 'test.restricted-noop', + alertTypeId: 'test.unrestricted-noop', consumer: 'alertsFixture', }) ) @@ -85,7 +85,11 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage('get', 'test.noop', 'alertsFixture'), + message: getConsumerUnauthorizedErrorMessage( + 'get', + 'test.unrestricted-noop', + 'alertsFixture' + ), statusCode: 403, }); break; @@ -95,7 +99,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont error: 'Forbidden', message: getProducerUnauthorizedErrorMessage( 'get', - 'test.noop', + 'test.unrestricted-noop', 'alertsRestrictedFixture' ), statusCode: 403, @@ -111,6 +115,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + it(`shouldn't getAlertState for an alert from another space`, async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index ee56f741d3e2a..c1a856ff84140 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { omit } from 'lodash'; import { UserAtSpaceScenarios } from '../../scenarios'; import { getUrlPrefix } from '../../../common/lib/space_test_utils'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -13,6 +14,30 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function listAlertTypes({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); + const expectedNoOpType = { + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + id: 'test.noop', + name: 'Test: Noop', + actionVariables: { + state: [], + context: [], + }, + producer: 'alertsFixture', + }; + + const expectedRestrictedNoOpType = { + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + id: 'test.restricted-noop', + name: 'Test: Restricted Noop', + actionVariables: { + state: [], + context: [], + }, + producer: 'alertsRestrictedFixture', + }; + describe('list_alert_types', () => { for (const scenario of UserAtSpaceScenarios) { const { user, space } = scenario; @@ -26,42 +51,45 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { const noOpAlertType = response.body.find( (alertType: any) => alertType.id === 'test.noop' ); + const restrictedNoOpAlertType = response.body.find( + (alertType: any) => alertType.id === 'test.restricted-noop' + ); switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': expect(response.body).to.eql([]); break; - case 'global_read at space1': case 'space_1_all at space1': - expect(noOpAlertType).to.eql({ - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - id: 'test.noop', - name: 'Test: Noop', - actionVariables: { - state: [], - context: [], - }, - authorizedConsumers: ['alertsFixture'], - producer: 'alertsFixture', - }); + expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType); + expect(restrictedNoOpAlertType).to.eql(undefined); + expect(noOpAlertType.authorizedConsumers).to.eql(['alertsFixture']); break; + case 'global_read at space1': case 'space_1_all_with_restricted_fixture at space1': + expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType); + expect(noOpAlertType.authorizedConsumers).to.contain('alertsFixture'); + expect(noOpAlertType.authorizedConsumers).to.contain('alertsRestrictedFixture'); + + expect(omit(restrictedNoOpAlertType, 'authorizedConsumers')).to.eql( + expectedRestrictedNoOpType + ); + expect(restrictedNoOpAlertType.authorizedConsumers).not.to.contain('alertsFixture'); + expect(restrictedNoOpAlertType.authorizedConsumers).to.contain( + 'alertsRestrictedFixture' + ); + break; case 'superuser at space1': - const { authorizedConsumers, ...superUserFixtureAlertType } = noOpAlertType; - expect(superUserFixtureAlertType).to.eql({ - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - id: 'test.noop', - name: 'Test: Noop', - actionVariables: { - state: [], - context: [], - }, - producer: 'alertsFixture', - }); - expect(authorizedConsumers).to.contain('alertsFixture'); - expect(authorizedConsumers).to.contain('alertsRestrictedFixture'); + expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType); + expect(noOpAlertType.authorizedConsumers).to.contain('alertsFixture'); + expect(noOpAlertType.authorizedConsumers).to.contain('alertsRestrictedFixture'); + + expect(omit(restrictedNoOpAlertType, 'authorizedConsumers')).to.eql( + expectedRestrictedNoOpType + ); + expect(restrictedNoOpAlertType.authorizedConsumers).to.contain('alertsFixture'); + expect(restrictedNoOpAlertType.authorizedConsumers).to.contain( + 'alertsRestrictedFixture' + ); 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_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mute_all.ts index a87a96fd35a20..21513513a8ccb 100644 --- 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 @@ -14,6 +14,7 @@ import { getTestAlertData, ObjectRemover, getConsumerUnauthorizedErrorMessage, + getProducerUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -79,6 +80,182 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it('should handle mute alert request appropriately when consumer is the same as producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + enabled: false, + alertTypeId: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + 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': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'muteAll', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.muteAll).to.eql(true); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle mute alert request appropriately when consumer is not the producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + enabled: false, + alertTypeId: 'test.unrestricted-noop', + consumer: 'alertsFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + 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(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'muteAll', + 'test.unrestricted-noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'muteAll', + 'test.unrestricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.muteAll).to.eql(true); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle mute alert request appropriately when consumer is "alerts"', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + enabled: false, + alertTypeId: 'test.restricted-noop', + consumer: 'alerts', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + 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': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'muteAll', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.muteAll).to.eql(true); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + 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 index 6d70744d1c51b..0d8630445accd 100644 --- 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 @@ -14,6 +14,7 @@ import { getTestAlertData, ObjectRemover, getConsumerUnauthorizedErrorMessage, + getProducerUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -80,6 +81,182 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider } }); + it('should handle mute alert instance request appropriately when consumer is the same as producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + enabled: false, + alertTypeId: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + 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': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'muteInstance', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.mutedInstanceIds).to.eql(['1']); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle mute alert instance request appropriately when consumer is not the producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + enabled: false, + alertTypeId: 'test.unrestricted-noop', + consumer: 'alertsFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + 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(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'muteInstance', + 'test.unrestricted-noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'muteInstance', + 'test.unrestricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.mutedInstanceIds).to.eql(['1']); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle mute alert instance request appropriately when consumer is "alerts"', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + enabled: false, + alertTypeId: 'test.restricted-noop', + consumer: 'alerts', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + 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': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'muteInstance', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.mutedInstanceIds).to.eql(['1']); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + 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/alerts/alert`) 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 index af3ead1eda5a5..9d715c9146b5e 100644 --- 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 @@ -14,6 +14,7 @@ import { getTestAlertData, ObjectRemover, getConsumerUnauthorizedErrorMessage, + getProducerUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -84,6 +85,197 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it('should handle unmute alert request appropriately when consumer is the same as producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + enabled: false, + alertTypeId: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/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': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'unmuteAll', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.muteAll).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle unmute alert request appropriately when consumer is not the producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + enabled: false, + alertTypeId: 'test.unrestricted-noop', + consumer: 'alertsFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/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(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'unmuteAll', + 'test.unrestricted-noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'unmuteAll', + 'test.unrestricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.muteAll).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle unmute alert request appropriately when consumer is "alerts"', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + enabled: false, + alertTypeId: 'test.restricted-noop', + consumer: 'alerts', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/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': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'unmuteAll', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.muteAll).to.eql(false); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + 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 index 7bf3e730e97a2..2f1f883351aee 100644 --- 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 @@ -14,6 +14,7 @@ import { getTestAlertData, ObjectRemover, getConsumerUnauthorizedErrorMessage, + getProducerUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -86,6 +87,203 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it('should handle unmute alert instance request appropriately when consumer is the same as producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + enabled: false, + alertTypeId: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + await supertest + .post( + `${getUrlPrefix(space.id)}/api/alerts/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': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'unmuteInstance', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.mutedInstanceIds).to.eql([]); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle unmute alert instance request appropriately when consumer is not the producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + enabled: false, + alertTypeId: 'test.unrestricted-noop', + consumer: 'alertsFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + await supertest + .post( + `${getUrlPrefix(space.id)}/api/alerts/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(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'unmuteInstance', + 'test.unrestricted-noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'unmuteInstance', + 'test.unrestricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.mutedInstanceIds).to.eql([]); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle unmute alert instance request appropriately when consumer is "alerts"', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + enabled: false, + alertTypeId: 'test.restricted-noop', + consumer: 'alerts', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + await supertest + .post( + `${getUrlPrefix(space.id)}/api/alerts/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': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'unmuteInstance', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.mutedInstanceIds).to.eql([]); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + 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 4d885024cc7f6..7007b4ce7e3ae 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 @@ -14,6 +14,7 @@ import { ObjectRemover, ensureDatetimeIsWithinRange, getConsumerUnauthorizedErrorMessage, + getProducerUnauthorizedErrorMessage, } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -114,6 +115,260 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { } }); + it('should handle update alert request appropriately when consumer is the same as producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + const updatedData = { + name: 'bcd', + tags: ['bar'], + params: { + foo: true, + }, + schedule: { interval: '12s' }, + actions: [], + throttle: '1m', + }; + const response = await supertestWithoutAuth + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send(updatedData); + + 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.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'update', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + ...updatedData, + id: createdAlert.id, + alertTypeId: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + createdBy: 'elastic', + enabled: true, + updatedBy: user.username, + apiKeyOwner: user.username, + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: createdAlert.scheduledTaskId, + createdAt: response.body.createdAt, + updatedAt: response.body.updatedAt, + }); + expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0); + expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0); + expect(Date.parse(response.body.updatedAt)).to.be.greaterThan( + Date.parse(response.body.createdAt) + ); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle update alert request appropriately when consumer is not the producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.unrestricted-noop', + consumer: 'alertsFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + const updatedData = { + name: 'bcd', + tags: ['bar'], + params: { + foo: true, + }, + schedule: { interval: '12s' }, + actions: [], + throttle: '1m', + }; + const response = await supertestWithoutAuth + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send(updatedData); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'update', + 'test.unrestricted-noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'update', + 'test.unrestricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + ...updatedData, + id: createdAlert.id, + alertTypeId: 'test.unrestricted-noop', + consumer: 'alertsFixture', + createdBy: 'elastic', + enabled: true, + updatedBy: user.username, + apiKeyOwner: user.username, + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: createdAlert.scheduledTaskId, + createdAt: response.body.createdAt, + updatedAt: response.body.updatedAt, + }); + expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0); + expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0); + expect(Date.parse(response.body.updatedAt)).to.be.greaterThan( + Date.parse(response.body.createdAt) + ); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle update alert request appropriately when consumer is "alerts"', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.restricted-noop', + consumer: 'alerts', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + const updatedData = { + name: 'bcd', + tags: ['bar'], + params: { + foo: true, + }, + schedule: { interval: '12s' }, + actions: [], + throttle: '1m', + }; + const response = await supertestWithoutAuth + .put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send(updatedData); + + 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.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'update', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + expect(response.body).to.eql({ + ...updatedData, + id: createdAlert.id, + alertTypeId: 'test.restricted-noop', + consumer: 'alerts', + createdBy: 'elastic', + enabled: true, + updatedBy: user.username, + apiKeyOwner: user.username, + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: createdAlert.scheduledTaskId, + createdAt: response.body.createdAt, + updatedAt: response.body.updatedAt, + }); + expect(Date.parse(response.body.createdAt)).to.be.greaterThan(0); + expect(Date.parse(response.body.updatedAt)).to.be.greaterThan(0); + expect(Date.parse(response.body.updatedAt)).to.be.greaterThan( + Date.parse(response.body.createdAt) + ); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it('should still be able to update when AAD is broken', async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) 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 2b7f448c3e4bd..903bf6b40ee7e 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 @@ -14,6 +14,7 @@ import { getTestAlertData, ObjectRemover, getConsumerUnauthorizedErrorMessage, + getProducerUnauthorizedErrorMessage, } from '../../../common/lib'; // eslint-disable-next-line import/no-default-export @@ -79,6 +80,176 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte } }); + it('should handle update alert api key request appropriately when consumer is the same as producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + const response = await alertUtils.getUpdateApiKeyRequest(createdAlert.id); + 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.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'updateApiKey', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.apiKeyOwner).to.eql(user.username); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle update alert api key request appropriately when consumer is not the producer', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.unrestricted-noop', + consumer: 'alertsFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + const response = await alertUtils.getUpdateApiKeyRequest(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(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'updateApiKey', + 'test.unrestricted-noop', + 'alertsFixture' + ), + statusCode: 403, + }); + break; + case 'space_1_all at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'updateApiKey', + 'test.unrestricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.apiKeyOwner).to.eql(user.username); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it('should handle update alert api key request appropriately when consumer is "alerts"', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.restricted-noop', + consumer: 'alerts', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + const response = await alertUtils.getUpdateApiKeyRequest(createdAlert.id); + 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.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getProducerUnauthorizedErrorMessage( + 'updateApiKey', + 'test.restricted-noop', + 'alertsRestrictedFixture' + ), + statusCode: 403, + }); + break; + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + const { body: updatedAlert } = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .expect(200); + expect(updatedAlert.apiKeyOwner).to.eql(user.username); + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: space.id, + type: 'alert', + id: createdAlert.id, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it('should still be able to update API key when AAD is broken', async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) From 69101650f8880bd7932edd8cb81d7086c2805c89 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 9 Jun 2020 18:19:21 +0100 Subject: [PATCH 024/126] fixed alerts UI tests --- .../apps/triggers_actions_ui/alerts.ts | 2 +- .../fixtures/plugins/alerts/public/plugin.ts | 6 +++--- .../test/functional_with_es_ssl/services/alerting/alerts.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 13bf47676cc09..07c3115a9b67c 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -28,7 +28,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { name: generateUniqueKey(), tags: ['foo', 'bar'], alertTypeId: 'test.noop', - consumer: 'test', + consumer: 'alerts', schedule: { interval: '1m' }, throttle: '1m', actions: [], diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts index 2bc299ede930b..503c328017a9a 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/public/plugin.ts @@ -21,7 +21,7 @@ export interface AlertingExamplePublicSetupDeps { export class AlertingFixturePlugin implements Plugin { public setup(core: CoreSetup, { alerts, triggers_actions_ui }: AlertingExamplePublicSetupDeps) { alerts.registerNavigation( - 'consumer-noop', + 'alerting_fixture', 'test.noop', (alert: SanitizedAlert, alertType: AlertType) => `/alert/${alert.id}` ); @@ -49,8 +49,8 @@ export class AlertingFixturePlugin implements Plugin Date: Tue, 9 Jun 2020 23:05:57 +0100 Subject: [PATCH 025/126] extracted auth function from alerts client --- .../server/alerts_authorization.mock.ts | 24 + .../alerts/server/alerts_authorization.ts | 150 ++ .../alerts/server/alerts_client.test.ts | 2296 +++++------------ x-pack/plugins/alerts/server/alerts_client.ts | 216 +- .../server/alerts_client_factory.test.ts | 24 +- .../alerts/server/alerts_client_factory.ts | 12 +- 6 files changed, 848 insertions(+), 1874 deletions(-) create mode 100644 x-pack/plugins/alerts/server/alerts_authorization.mock.ts create mode 100644 x-pack/plugins/alerts/server/alerts_authorization.ts diff --git a/x-pack/plugins/alerts/server/alerts_authorization.mock.ts b/x-pack/plugins/alerts/server/alerts_authorization.mock.ts new file mode 100644 index 0000000000000..58b041f8b0370 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_authorization.mock.ts @@ -0,0 +1,24 @@ +/* + * 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 { AlertsAuthorization } from './alerts_authorization'; + +type Schema = PublicMethodsOf; +export type AlertsAuthorizationMock = jest.Mocked; + +const createAlertsAuthorizationMock = () => { + const mocked: AlertsAuthorizationMock = { + ensureAuthorized: jest.fn(), + filterByAuthorized: jest.fn(), + }; + return mocked; +}; + +export const alertsAuthorizationMock: { + create: () => AlertsAuthorizationMock; +} = { + create: createAlertsAuthorizationMock, +}; diff --git a/x-pack/plugins/alerts/server/alerts_authorization.ts b/x-pack/plugins/alerts/server/alerts_authorization.ts new file mode 100644 index 0000000000000..e0469d8f49604 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_authorization.ts @@ -0,0 +1,150 @@ +/* + * 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 Boom from 'boom'; +import { pluck, mapValues } from 'lodash'; +import { KibanaRequest } from 'src/core/server'; +import { AlertsFeatureId } from '../common'; +import { AlertTypeRegistry } from './types'; +import { SecurityPluginSetup } from '../../security/server'; +import { RegistryAlertType } from './alert_type_registry'; +import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; + +export interface RegistryAlertTypeWithAuth extends RegistryAlertType { + authorizedConsumers: string[]; +} + +export interface ConstructorOptions { + alertTypeRegistry: AlertTypeRegistry; + request: KibanaRequest; + features: FeaturesPluginStart; + authorization?: SecurityPluginSetup['authz']; +} + +export class AlertsAuthorization { + private readonly alertTypeRegistry: AlertTypeRegistry; + private readonly features: FeaturesPluginStart; + private readonly request: KibanaRequest; + private readonly authorization?: SecurityPluginSetup['authz']; + + constructor({ alertTypeRegistry, request, authorization, features }: ConstructorOptions) { + this.request = request; + this.authorization = authorization; + this.features = features; + this.alertTypeRegistry = alertTypeRegistry; + } + + public async ensureAuthorized(alertTypeId: string, consumer: string, operation: string) { + const { authorization } = this; + if (authorization) { + const alertType = this.alertTypeRegistry.get(alertTypeId); + const requiredPrivilegesByScope = { + consumer: authorization.actions.alerting.get(alertTypeId, consumer, operation), + producer: authorization.actions.alerting.get(alertTypeId, alertType.producer, operation), + }; + + // We special case the Alerts Management `consumer` as we don't want to have to + // manually authorize each alert type in the management UI + const shouldAuthorizeConsumer = consumer !== AlertsFeatureId; + + const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); + const { hasAllRequested, privileges } = await checkPrivileges( + shouldAuthorizeConsumer && consumer !== alertType.producer + ? [ + // check for access at consumer level + requiredPrivilegesByScope.consumer, + // check for access at producer level + requiredPrivilegesByScope.producer, + ] + : [ + // skip consumer privilege checks under `alerts` as all alert types can + // be created under `alerts` if you have producer level privileges + requiredPrivilegesByScope.producer, + ] + ); + + if (!hasAllRequested) { + const authorizedPrivileges = pluck( + privileges.filter((privilege) => privilege.authorized), + 'privilege' + ); + const unauthorizedScopes = mapValues( + requiredPrivilegesByScope, + (privilege) => !authorizedPrivileges.includes(privilege) + ); + + throw Boom.forbidden( + `Unauthorized to ${operation} a "${alertTypeId}" alert ${ + shouldAuthorizeConsumer && unauthorizedScopes.consumer + ? `for "${consumer}"` + : `by "${alertType.producer}"` + }` + ); + } + } + } + + public async filterByAuthorized( + alertTypes: Set, + operation: string + ): Promise> { + const featuresIds = this.features.getFeatures().map((feature) => feature.id); + + if (!this.authorization) { + return this.augmentWithAuthorizedConsumers(alertTypes, featuresIds); + } else { + const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( + this.request + ); + + // add an empty `authorizedConsumers` array on each alertType + const alertTypesWithAutherization = this.augmentWithAuthorizedConsumers(alertTypes); + + // map from privilege to alertType which we can refer back to when analyzing the result + // of checkPrivileges + const privilegeToAlertType = new Map(); + // as we can't ask ES for the user's individual privileges we need to ask for each feature + // and alertType in the system whether this user has this privilege + for (const alertType of alertTypesWithAutherization) { + for (const feature of featuresIds) { + privilegeToAlertType.set( + this.authorization!.actions.alerting.get(alertType.id, feature, operation), + [alertType, feature] + ); + } + } + + const { hasAllRequested, privileges } = await checkPrivileges([ + ...privilegeToAlertType.keys(), + ]); + + return hasAllRequested + ? // has access to all features + this.augmentWithAuthorizedConsumers(alertTypes, featuresIds) + : // only has some of the required privileges + privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { + if (authorized && privilegeToAlertType.has(privilege)) { + const [alertType, consumer] = privilegeToAlertType.get(privilege)!; + alertType.authorizedConsumers.push(consumer); + authorizedAlertTypes.add(alertType); + } + return authorizedAlertTypes; + }, new Set()); + } + } + + private augmentWithAuthorizedConsumers( + alertTypes: Set, + authorizedConsumers?: string[] + ): Set { + return new Set( + Array.from(alertTypes).map((alertType) => ({ + ...alertType, + authorizedConsumers: authorizedConsumers ?? [], + })) + ); + } +} diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index f0f35717a5d22..7a610d82dd21e 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -5,15 +5,16 @@ */ import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; -import { KibanaRequest } from 'kibana/server'; import { AlertsClient, CreateOptions, + ConstructorOptions, // , UpdateOptions, FindOptions } from './alerts_client'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../src/core/server/mocks'; import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; +import { alertsAuthorizationMock } from './alerts_authorization.mock'; import { TaskStatus } from '../../task_manager/server'; import { IntervalSchedule, @@ -22,23 +23,19 @@ import { import { resolvable } from './test_utils'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { actionsClientMock } from '../../actions/server/mocks'; -import { SecurityPluginSetup } from '../../../plugins/security/server'; -import { securityMock } from '../../../plugins/security/server/mocks'; -import { PluginStartContract as FeaturesStartContract, Feature } from '../../features/server'; -import { featuresPluginMock } from '../../features/server/mocks'; +import { AlertsAuthorization } from './alerts_authorization'; const taskManager = taskManagerMock.start(); const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const features: jest.Mocked = featuresPluginMock.createStart(); +const authorization = alertsAuthorizationMock.create(); -const alertsClientParams = { +const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, - features, - request: {} as KibanaRequest, + authorization: (authorization as unknown) as AlertsAuthorization, spaceId: 'default', namespace: 'default', getUserName: jest.fn(), @@ -49,54 +46,16 @@ const alertsClientParams = { getActionsClient: jest.fn(), }; -function mockAuthorization() { - const authorization = securityMock.createSetup().authz; - // typescript is havingtrouble inferring jest's automocking - (authorization.actions.alerting.get as jest.MockedFunction< - typeof authorization.actions.alerting.get - >).mockImplementation((type, app, operation) => `${type}/${app}/${operation}`); - return authorization; -} - -function mockFeature(appName: string, typeName: string, requiredApps: string[] = []) { - return new Feature({ - id: appName, - name: appName, - app: requiredApps, - privileges: { - all: { - alerting: { - all: [typeName], - }, - savedObject: { - all: [], - read: [], - }, - ui: [], - }, - read: { - alerting: { - read: [typeName], - }, - savedObject: { - all: [], - read: [], - }, - ui: [], - }, - }, - }); -} -const alertsFeature = mockFeature('alerts', 'myBuiltInType'); -const myAppFeature = mockFeature('myApp', 'myType', ['alerts']); -const myOtherAppFeature = mockFeature('myOtherApp', 'myType', ['alerts']); - beforeEach(() => { jest.resetAllMocks(); alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false }); alertsClientParams.invalidateAPIKey.mockResolvedValue({ apiKeysEnabled: true, - result: { error_count: 0 }, + result: { + invalidated_api_keys: [], + previously_invalidated_api_keys: [], + error_count: 0, + }, }); alertsClientParams.getUserName.mockResolvedValue('elastic'); taskManager.runNow.mockResolvedValue({ id: '' }); @@ -129,26 +88,14 @@ beforeEach(() => { ]); alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); - alertTypeRegistry.get.mockImplementation((id) => - id !== 'myType' - ? { - id: '123', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - async executor() {}, - producer: 'alerts', - } - : { - id: 'myType', - name: 'My Alert Type', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - async executor() {}, - producer: 'myApp', - } - ); - features.getFeatures.mockReturnValue([alertsFeature, myAppFeature, myOtherAppFeature]); + alertTypeRegistry.get.mockImplementation((id) => ({ + id: '123', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + async executor() {}, + producer: 'alerts', + })); }); const mockedDate = new Date('2019-02-12T21:01:22.479Z'); @@ -195,22 +142,6 @@ describe('create()', () => { }); describe('authorization', () => { - let authorization: jest.Mocked; - let alertsClientWithAuthorization: AlertsClient; - let checkPrivileges: jest.MockedFunction>; - - beforeEach(() => { - authorization = mockAuthorization(); - alertsClientWithAuthorization = new AlertsClient({ - authorization, - ...alertsClientParams, - }); - checkPrivileges = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - }); - function tryToExecuteOperation(options: CreateOptions): Promise { unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ @@ -281,21 +212,10 @@ describe('create()', () => { ], }); - return alertsClientWithAuthorization.create(options); + return alertsClient.create(options); } - test('create when user is authorised to create this type of alert type for the producer', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: true, - username: '', - privileges: [ - { - privilege: 'myType/myApp/create', - authorized: true, - }, - ], - }); - + test('ensures user is authorised to create this type of alert under the consumer', async () => { const data = getMockData({ alertTypeId: 'myType', consumer: 'myApp', @@ -303,90 +223,24 @@ describe('create()', () => { await tryToExecuteOperation({ data }); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'create'); }); - test('create when user is authorised to create this type of alert type for the specified consumer and producer', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: true, - username: '', - privileges: [ - { - privilege: 'myType/myApp/create', - authorized: true, - }, - { - privilege: 'myType/myOtherApp/create', - authorized: true, - }, - ], - }); - + test('throws when user is not authorised to create this type of alert', async () => { const data = getMockData({ alertTypeId: 'myType', - consumer: 'myOtherApp', + consumer: 'myApp', }); - await tryToExecuteOperation({ data }); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myOtherApp', - 'create' + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to create a "myType" alert for "myApp"`) ); - }); - - test('throws when user is not authorised to create this type of alert at all', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/myApp/create', - authorized: false, - }, - { - privilege: 'myType/myOtherApp/create', - authorized: false, - }, - ], - }); - - const data = getMockData({ - alertTypeId: 'myType', - consumer: 'myApp', - }); await expect(tryToExecuteOperation({ data })).rejects.toMatchInlineSnapshot( `[Error: Unauthorized to create a "myType" alert for "myApp"]` ); - }); - - test('throws when user is not authorised to create this type of alert at consumer', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/myApp/create', - authorized: true, - }, - { - privilege: 'myType/myOtherApp/create', - authorized: false, - }, - ], - }); - - const data = getMockData({ - alertTypeId: 'myType', - consumer: 'myOtherApp', - }); - await expect(tryToExecuteOperation({ data })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to create a "myType" alert for "myOtherApp"]` - ); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'create'); }); }); @@ -449,6 +303,7 @@ describe('create()', () => { ], }); const result = await alertsClient.create({ data }); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('123', 'bar', 'create'); expect(result).toMatchInlineSnapshot(` Object { "actions": Array [ @@ -957,7 +812,7 @@ describe('create()', () => { const data = getMockData(); alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, - result: { id: '123', api_key: 'abc' }, + result: { id: '123', name: '123', api_key: 'abc' }, }); unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ @@ -1194,18 +1049,6 @@ describe('enable()', () => { version: '123', references: [], }; - const alertInOtherFeature = { - id: '2', - type: 'alert', - attributes: { - consumer: 'myOtherApp', - schedule: { interval: '10s' }, - alertTypeId: 'myType', - enabled: false, - }, - version: '123', - references: [], - }; beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); @@ -1230,22 +1073,7 @@ describe('enable()', () => { }); describe('authorization', () => { - let authorization: jest.Mocked; - let alertsClientWithAuthorization: AlertsClient; - let checkPrivileges: jest.MockedFunction>; - beforeEach(() => { - authorization = mockAuthorization(); - alertsClientWithAuthorization = new AlertsClient({ - authorization, - ...alertsClientParams, - }); - - checkPrivileges = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); alertsClientParams.createAPIKey.mockResolvedValue({ @@ -1266,78 +1094,22 @@ describe('enable()', () => { }); }); - test('enable when user is authorised to enable this type of alert type for the producer', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: true, - username: '', - privileges: [ - { - privilege: 'myType/myApp/enable', - authorized: true, - }, - { - privilege: 'myType/myOtherApp/enable', - authorized: true, - }, - ], - }); - - await alertsClientWithAuthorization.enable({ id: '1' }); + test('ensures user is authorised to enable this type of alert under the consumer', async () => { + await alertsClient.enable({ id: '1' }); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'enable'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'enable'); }); - test('enable when user is authorised to enable this type of alert type for producer and consumer', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(alertInOtherFeature); - unsecuredSavedObjectsClient.get.mockResolvedValue(alertInOtherFeature); - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: true, - username: '', - privileges: [ - { - privilege: 'myType/myApp/enable', - authorized: true, - }, - { - privilege: 'myType/myOtherApp/enable', - authorized: true, - }, - ], - }); - - await alertsClientWithAuthorization.enable({ id: alertInOtherFeature.id }); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'enable'); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myOtherApp', - 'enable' + test('throws when user is not authorised to enable this type of alert', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to enable a "myType" alert for "myApp"`) ); - }); - - test('throws when user is not authorised to enable this type of alert at all', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(alertInOtherFeature); - unsecuredSavedObjectsClient.get.mockResolvedValue(alertInOtherFeature); - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myType/myApp/enable', - authorized: true, - }, - { - privilege: 'myType/myOtherApp/enable', - authorized: false, - }, - ], - }); - expect( - alertsClientWithAuthorization.enable({ id: alertInOtherFeature.id }) - ).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to enable a "myType" alert for "myOtherApp"]` + await expect(alertsClient.enable({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to enable a "myType" alert for "myApp"]` ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'enable'); }); }); @@ -1419,7 +1191,7 @@ describe('enable()', () => { test('sets API key when createAPIKey returns one', async () => { alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, - result: { id: '123', api_key: 'abc' }, + result: { id: '123', name: '123', api_key: 'abc' }, }); await alertsClient.enable({ id: '1' }); @@ -1538,116 +1310,25 @@ describe('disable()', () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); }); - // describe('authorization', () => { - // let authorization: jest.Mocked; - // let alertsClientWithAuthorization: AlertsClient; - // let checkPrivileges: jest.MockedFunction>; - - // beforeEach(() => { - // authorization = mockAuthorization(); - // alertsClientWithAuthorization = new AlertsClient({ - // authorization, - // ...alertsClientParams, - // }); - - // checkPrivileges = jest.fn(); - // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - - // encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); - // unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); - // alertsClientParams.createAPIKey.mockResolvedValue({ - // apiKeysEnabled: false, - // }); - // taskManager.schedule.mockResolvedValue({ - // id: 'task-123', - // scheduledAt: new Date(), - // attempts: 0, - // status: TaskStatus.Idle, - // runAt: new Date(), - // state: {}, - // params: {}, - // taskType: '', - // startedAt: null, - // retryAt: null, - // ownerId: null, - // }); - // }); - - // test('disables when user is authorised to disable this type of alert type for the specified consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/disable', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/disable', - // authorized: true, - // }, - // ], - // }); - - // await alertsClientWithAuthorization.disable({ id: '1' }); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // undefined, - // 'disable' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'disable'); - // }); - - // test('disables when user is authorised to disable this type of alert type producer and consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/disable', - // authorized: true, - // }, - // { - // privilege: 'myType/myApp/disable', - // authorized: false, - // }, - // ], - // }); - - // await alertsClientWithAuthorization.disable({ id: '1' }); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // undefined, - // 'disable' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'disable'); - // }); - - // test('throws when user is not authorised to disable this type of alert at all', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/disable', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/disable', - // authorized: false, - // }, - // ], - // }); - - // expect(alertsClientWithAuthorization.disable({ id: '1' })).rejects.toMatchInlineSnapshot( - // `[Error: Unauthorized to disable a "myType" alert for "myApp"]` - // ); - // }); - // }); + describe('authorization', () => { + test('ensures user is authorised to disable this type of alert under the consumer', async () => { + await alertsClient.disable({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable'); + }); + + test('throws when user is not authorised to disable this type of alert', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to disable a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.disable({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to disable a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable'); + }); + }); test('disables an alert', async () => { await alertsClient.disable({ id: '1' }); @@ -1785,108 +1466,46 @@ describe('muteAll()', () => { }); }); - // describe('authorization', () => { - // let authorization: jest.Mocked; - // let alertsClientWithAuthorization: AlertsClient; - // let checkPrivileges: jest.MockedFunction>; - - // beforeEach(() => { - // authorization = mockAuthorization(); - // alertsClientWithAuthorization = new AlertsClient({ - // authorization, - // ...alertsClientParams, - // }); - - // checkPrivileges = jest.fn(); - // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - - // unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - // id: '1', - // type: 'alert', - // attributes: { - // alertTypeId: 'myType', - // consumer: 'myApp', - // muteAll: false, - // }, - // references: [], - // }); - // }); - - // test('mutes when user is authorised to muteAll this type of alert type for the specified consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/muteAll', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/muteAll', - // authorized: true, - // }, - // ], - // }); - - // await alertsClientWithAuthorization.muteAll({ id: '1' }); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // undefined, - // 'muteAll' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); - // }); - - // test('mutes when user is authorised to muteAll this type of alert type producer and consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/muteAll', - // authorized: true, - // }, - // { - // privilege: 'myType/myApp/muteAll', - // authorized: false, - // }, - // ], - // }); - - // await alertsClientWithAuthorization.muteAll({ id: '1' }); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // undefined, - // 'muteAll' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); - // }); - - // test('throws when user is not authorised to muteAll this type of alert at all', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/muteAll', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/muteAll', - // authorized: false, - // }, - // ], - // }); - - // expect(alertsClientWithAuthorization.muteAll({ id: '1' })).rejects.toMatchInlineSnapshot( - // `[Error: Unauthorized to muteAll a "myType" alert for "myApp"]` - // ); - // }); - // }); + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + apiKey: null, + apiKeyOwner: null, + enabled: false, + scheduledTaskId: null, + updatedBy: 'elastic', + muteAll: false, + }, + references: [], + }); + }); + + test('ensures user is authorised to muteAll this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.muteAll({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); + }); + + test('throws when user is not authorised to muteAll this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to muteAll a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.muteAll({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to muteAll a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); + }); + }); }); describe('unmuteAll()', () => { @@ -1909,116 +1528,46 @@ describe('unmuteAll()', () => { }); }); - // describe('authorization', () => { - // let authorization: jest.Mocked; - // let alertsClientWithAuthorization: AlertsClient; - // let checkPrivileges: jest.MockedFunction>; - - // beforeEach(() => { - // authorization = mockAuthorization(); - // alertsClientWithAuthorization = new AlertsClient({ - // authorization, - // ...alertsClientParams, - // }); - - // checkPrivileges = jest.fn(); - // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - - // unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - // id: '1', - // type: 'alert', - // attributes: { - // alertTypeId: 'myType', - // consumer: 'myApp', - // muteAll: true, - // }, - // references: [], - // }); - // }); - - // test('unmutes when user is authorised to unmuteAll this type of alert type for the specified consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/unmuteAll', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/unmuteAll', - // authorized: true, - // }, - // ], - // }); - - // await alertsClientWithAuthorization.unmuteAll({ id: '1' }); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // undefined, - // 'unmuteAll' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // 'myApp', - // 'unmuteAll' - // ); - // }); - - // test('unmutes when user is authorised to unmuteAll this type of alert type producer and consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/unmuteAll', - // authorized: true, - // }, - // { - // privilege: 'myType/myApp/unmuteAll', - // authorized: false, - // }, - // ], - // }); - - // await alertsClientWithAuthorization.unmuteAll({ id: '1' }); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // undefined, - // 'unmuteAll' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // 'myApp', - // 'unmuteAll' - // ); - // }); - - // test('throws when user is not authorised to unmuteAll this type of alert at all', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/unmuteAll', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/unmuteAll', - // authorized: false, - // }, - // ], - // }); - - // expect(alertsClientWithAuthorization.unmuteAll({ id: '1' })).rejects.toMatchInlineSnapshot( - // `[Error: Unauthorized to unmuteAll a "myType" alert for "myApp"]` - // ); - // }); - // }); + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + apiKey: null, + apiKeyOwner: null, + enabled: false, + scheduledTaskId: null, + updatedBy: 'elastic', + muteAll: false, + }, + references: [], + }); + }); + + test('ensures user is authorised to unmuteAll this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.unmuteAll({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll'); + }); + + test('throws when user is not authorised to unmuteAll this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to unmuteAll a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.unmuteAll({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to unmuteAll a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll'); + }); + }); }); describe('muteInstance()', () => { @@ -2089,118 +1638,54 @@ describe('muteInstance()', () => { expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); }); - // describe('authorization', () => { - // let authorization: jest.Mocked; - // let alertsClientWithAuthorization: AlertsClient; - // let checkPrivileges: jest.MockedFunction>; - - // beforeEach(() => { - // authorization = mockAuthorization(); - // alertsClientWithAuthorization = new AlertsClient({ - // authorization, - // ...alertsClientParams, - // }); - - // checkPrivileges = jest.fn(); - // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - - // unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - // id: '1', - // type: 'alert', - // attributes: { - // alertTypeId: 'myType', - // consumer: 'myApp', - // muteAll: true, - // }, - // references: [], - // }); - // }); - - // test('mutes instance when user is authorised to mute an instance on this type of alert type for the specified consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/muteInstance', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/muteInstance', - // authorized: true, - // }, - // ], - // }); - - // await alertsClientWithAuthorization.muteInstance({ alertId: '1', alertInstanceId: '2' }); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // undefined, - // 'muteInstance' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // 'myApp', - // 'muteInstance' - // ); - // }); - - // test('mutes instance when user is authorised to mute an instance on this type of alert type producer and consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/muteInstance', - // authorized: true, - // }, - // { - // privilege: 'myType/myApp/muteInstance', - // authorized: false, - // }, - // ], - // }); - - // await alertsClientWithAuthorization.muteInstance({ alertId: '1', alertInstanceId: '2' }); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // undefined, - // 'muteInstance' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // 'myApp', - // 'muteInstance' - // ); - // }); - - // test('throws when user is not authorised to mute an instance on this type of alert at all', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/muteInstance', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/muteInstance', - // authorized: false, - // }, - // ], - // }); - - // expect( - // alertsClientWithAuthorization.muteInstance({ alertId: '1', alertInstanceId: '2' }) - // ).rejects.toMatchInlineSnapshot( - // `[Error: Unauthorized to muteInstance a "myType" alert for "myApp"]` - // ); - // }); - // }); + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: [], + }, + version: '123', + references: [], + }); + }); + + test('ensures user is authorised to muteInstance this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'muteInstance' + ); + }); + + test('throws when user is not authorised to muteInstance this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to muteInstance a "myType" alert for "myApp"`) + ); + + await expect( + alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to muteInstance a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'muteInstance' + ); + }); + }); }); describe('unmuteInstance()', () => { @@ -2271,118 +1756,54 @@ describe('unmuteInstance()', () => { expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); }); - // describe('authorization', () => { - // let authorization: jest.Mocked; - // let alertsClientWithAuthorization: AlertsClient; - // let checkPrivileges: jest.MockedFunction>; - - // beforeEach(() => { - // authorization = mockAuthorization(); - // alertsClientWithAuthorization = new AlertsClient({ - // authorization, - // ...alertsClientParams, - // }); - - // checkPrivileges = jest.fn(); - // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - - // unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - // id: '1', - // type: 'alert', - // attributes: { - // alertTypeId: 'myType', - // consumer: 'myApp', - // muteAll: true, - // }, - // references: [], - // }); - // }); - - // test('unmutes instance when user is authorised to unmutes an instance on this type of alert type for the specified consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/unmuteInstance', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/unmuteInstance', - // authorized: true, - // }, - // ], - // }); - - // await alertsClientWithAuthorization.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // undefined, - // 'unmuteInstance' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // 'myApp', - // 'unmuteInstance' - // ); - // }); - - // test('unmutes instance when user is authorised to unmutes an instance on this type of alert type producer and consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/unmuteInstance', - // authorized: true, - // }, - // { - // privilege: 'myType/myApp/unmuteInstance', - // authorized: false, - // }, - // ], - // }); - - // await alertsClientWithAuthorization.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // undefined, - // 'unmuteInstance' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // 'myApp', - // 'unmuteInstance' - // ); - // }); - - // test('throws when user is not authorised to unmutes an instance on this type of alert at all', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/unmuteInstance', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/unmuteInstance', - // authorized: false, - // }, - // ], - // }); - - // expect( - // alertsClientWithAuthorization.unmuteInstance({ alertId: '1', alertInstanceId: '2' }) - // ).rejects.toMatchInlineSnapshot( - // `[Error: Unauthorized to unmuteInstance a "myType" alert for "myApp"]` - // ); - // }); - // }); + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: ['2'], + }, + version: '123', + references: [], + }); + }); + + test('ensures user is authorised to unmuteInstance this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'unmuteInstance' + ); + }); + + test('throws when user is not authorised to unmuteInstance this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to unmuteInstance a "myType" alert for "myApp"`) + ); + + await expect( + alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to unmuteInstance a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'unmuteInstance' + ); + }); + }); }); describe('get()', () => { @@ -2476,120 +1897,58 @@ describe('get()', () => { ); }); - // describe('authorization', () => { - // let authorization: jest.Mocked; - // let alertsClientWithAuthorization: AlertsClient; - // let checkPrivileges: jest.MockedFunction>; - - // beforeEach(() => { - // authorization = mockAuthorization(); - // alertsClientWithAuthorization = new AlertsClient({ - // authorization, - // ...alertsClientParams, - // }); - // checkPrivileges = jest.fn(); - // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - // }); - - // function tryToExecuteOperation(): Promise { - // unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - // id: '1', - // type: 'alert', - // attributes: { - // alertTypeId: 'myType', - // consumer: 'myApp', - // schedule: { interval: '10s' }, - // params: { - // bar: true, - // }, - // actions: [ - // { - // group: 'default', - // actionRef: 'action_0', - // params: { - // foo: true, - // }, - // }, - // ], - // }, - // references: [ - // { - // name: 'action_0', - // type: 'action', - // id: '1', - // }, - // ], - // }); - // return alertsClientWithAuthorization.get({ id: '1' }); - // } - - // test('gets when user is authorised to get this type of alert type for the specified consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/get', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/get', - // authorized: true, - // }, - // ], - // }); - - // await tryToExecuteOperation(); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); - // }); - - // test('gets when user is authorised to get this type of alert type producer and consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/get', - // authorized: true, - // }, - // { - // privilege: 'myType/myApp/get', - // authorized: false, - // }, - // ], - // }); - - // await tryToExecuteOperation(); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); - // }); - - // test('throws when user is not authorised to get this type of alert at all', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/get', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/get', - // authorized: false, - // }, - // ], - // }); - - // await expect(tryToExecuteOperation()).rejects.toMatchInlineSnapshot( - // `[Error: Unauthorized to get a "myType" alert for "myApp"]` - // ); - // }); - // }); + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + }); + + test('ensures user is authorised to get this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.get({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get'); + }); + + test('throws when user is not authorised to get this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.get({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get'); + }); + }); }); describe('getAlertState()', () => { @@ -2705,137 +2064,86 @@ describe('getAlertState()', () => { expect(taskManager.get).toHaveBeenCalledWith(scheduledTaskId); }); - // describe('authorization', () => { - // let authorization: jest.Mocked; - // let alertsClientWithAuthorization: AlertsClient; - // let checkPrivileges: jest.MockedFunction>; - - // beforeEach(() => { - // authorization = mockAuthorization(); - // alertsClientWithAuthorization = new AlertsClient({ - // authorization, - // ...alertsClientParams, - // }); - // checkPrivileges = jest.fn(); - // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - // }); - - // function tryToExecuteOperation(): Promise { - // unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - // id: '1', - // type: 'alert', - // attributes: { - // alertTypeId: 'myType', - // consumer: 'myApp', - // schedule: { interval: '10s' }, - // params: { - // bar: true, - // }, - // actions: [ - // { - // group: 'default', - // actionRef: 'action_0', - // params: { - // foo: true, - // }, - // }, - // ], - // }, - // references: [ - // { - // name: 'action_0', - // type: 'action', - // id: '1', - // }, - // ], - // }); - // return alertsClientWithAuthorization.getAlertState({ id: '1' }); - // } - - // test('gets AlertState when user is authorised to get this type of alert type for the specified consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/get', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/get', - // authorized: true, - // }, - // ], - // }); - - // await tryToExecuteOperation(); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); - // }); - - // test('gets AlertState when user is authorised to get this type of alert type producer and consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/get', - // authorized: true, - // }, - // { - // privilege: 'myType/myApp/get', - // authorized: false, - // }, - // ], - // }); - - // await tryToExecuteOperation(); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'get'); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'get'); - // }); - - // test('throws when user is not authorised to get this type of alert at all', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/get', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/get', - // authorized: false, - // }, - // ], - // }); - - // await expect(tryToExecuteOperation()).rejects.toMatchInlineSnapshot( - // `[Error: Unauthorized to get a "myType" alert for "myApp"]` - // ); - // }); - // }); + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + + taskManager.get.mockResolvedValueOnce({ + id: '1', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + }); + + test('ensures user is authorised to get this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.getAlertState({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get'); + }); + + test('throws when user is not authorised to get this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.getAlertState({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get'); + }); + }); }); describe('find()', () => { - test('calls saved objects client with given params', async () => { - alertTypeRegistry.list.mockReturnValue( - new Set([ - { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - id: 'myType', - name: 'myType', - producer: 'myApp', - }, - ]) - ); - const alertsClient = new AlertsClient(alertsClientParams); + const listedTypes = new Set([ + { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myType', + name: 'myType', + producer: 'myApp', + }, + ]); + beforeEach(() => { unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ total: 1, per_page: 10, @@ -2870,6 +2178,23 @@ describe('find()', () => { }, ], }); + alertTypeRegistry.list.mockReturnValue(listedTypes); + authorization.filterByAuthorized.mockResolvedValue( + new Set([ + { + id: 'myType', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + producer: 'alerts', + authorizedConsumers: ['myApp'], + }, + ]) + ); + }); + + test('calls saved objects client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); const result = await alertsClient.find({ options: {} }); expect(result).toMatchInlineSnapshot(` Object { @@ -2905,199 +2230,62 @@ describe('find()', () => { expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "filter": "((alert.attributes.alertTypeId:myType and alert.attributes.consumer:alerts) or (alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myType and alert.attributes.consumer:myOtherApp))", + "filter": "((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp))", "type": "alert", }, ] `); }); - // describe('authorization', () => { - // let authorization: jest.Mocked; - // let alertsClientWithAuthorization: AlertsClient; - // let checkPrivileges: jest.MockedFunction>; - - // function mockAlertSavedObject(alertTypeId: string) { - // return { - // id: uuid.v4(), - // type: 'alert', - // attributes: { - // alertTypeId, - // schedule: { interval: '10s' }, - // params: {}, - // actions: [], - // }, - // references: [], - // }; - // } - - // beforeEach(() => { - // authorization = mockAuthorization(); - - // alertsClientWithAuthorization = new AlertsClient({ - // authorization, - // ...alertsClientParams, - // }); - // checkPrivileges = jest.fn(); - // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - - // const myType = { - // actionGroups: [], - // actionVariables: undefined, - // defaultActionGroupId: 'default', - // id: 'myType', - // name: 'myType', - // producer: 'myApp', - // }; - // const anUnauthorizedType = { - // actionGroups: [], - // actionVariables: undefined, - // defaultActionGroupId: 'default', - // id: 'anUnauthorizedType', - // name: 'anUnauthorizedType', - // producer: 'anUnauthorizedApp', - // }; - // const setOfAlertTypes = new Set([anUnauthorizedType, myType]); - // alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - // }); - - // function tryToExecuteOperation( - // options?: FindOptions, - // savedObjects: Array> = [ - // mockAlertSavedObject('myType'), - // ] - // ): Promise { - // unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ - // total: 1, - // per_page: 10, - // page: 1, - // saved_objects: savedObjects, - // }); - // return alertsClientWithAuthorization.find({ options }); - // } - - // test('includes types that a user is authorised to find under their producer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/find', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/find', - // authorized: true, - // }, - // { - // privilege: 'anUnauthorizedType/find', - // authorized: false, - // }, - // { - // privilege: 'anUnauthorizedType/anUnauthorizedApp/find', - // authorized: false, - // }, - // ], - // }); - - // await tryToExecuteOperation(); - - // expect(unsecuredSavedObjectsClient.find.mock.calls[0][0].filter).toMatchInlineSnapshot( - // `"alert.attributes.alertTypeId:(myType)"` - // ); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'find'); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'find'); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'anUnauthorizedType', - // undefined, - // 'find' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'anUnauthorizedType', - // 'anUnauthorizedApp', - // 'find' - // ); - // }); - - // test('includes types that a user is authorised to get producer and consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/find', - // authorized: true, - // }, - // { - // privilege: 'myType/myApp/find', - // authorized: false, - // }, - // { - // privilege: 'anUnauthorizedType/find', - // authorized: false, - // }, - // { - // privilege: 'anUnauthorizedType/anUnauthorizedApp/find', - // authorized: false, - // }, - // ], - // }); - - // await tryToExecuteOperation(); - - // expect(unsecuredSavedObjectsClient.find.mock.calls[0][0].filter).toMatchInlineSnapshot( - // `"alert.attributes.alertTypeId:(myType)"` - // ); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', undefined, 'find'); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'find'); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'anUnauthorizedType', - // undefined, - // 'find' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'anUnauthorizedType', - // 'anUnauthorizedApp', - // 'find' - // ); - // }); - - // test('throws if a result contains a type the user is not authorised to find', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/find', - // authorized: true, - // }, - // { - // privilege: 'myType/myApp/find', - // authorized: true, - // }, - // { - // privilege: 'anUnauthorizedType/find', - // authorized: false, - // }, - // { - // privilege: 'anUnauthorizedType/anUnauthorizedApp/find', - // authorized: false, - // }, - // ], - // }); - - // await expect( - // tryToExecuteOperation({}, [ - // mockAlertSavedObject('myType'), - // mockAlertSavedObject('anUnauthorizedType'), - // ]) - // ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to find "anUnauthorizedType" alerts]`); - // }); - // }); + describe('authorization', () => { + test('ensures user is query filter types down to those the user is authorized to find', async () => { + authorization.filterByAuthorized.mockResolvedValue( + new Set([ + { + id: 'myType', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + producer: 'alerts', + authorizedConsumers: ['myApp'], + }, + { + id: 'myOtherType', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + producer: 'alerts', + authorizedConsumers: ['myApp', 'myOtherApp'], + }, + ]) + ); + + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.find({ options: {} }); + + const [options] = unsecuredSavedObjectsClient.find.mock.calls[0]; + expect(options.filter).toMatchInlineSnapshot( + `"((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myOtherApp))"` + ); + expect(authorization.filterByAuthorized).toHaveBeenCalledWith(listedTypes, 'find'); + }); + + test('short circuits if user is not authorized to find any types', async () => { + authorization.filterByAuthorized.mockResolvedValue(new Set([])); + + const alertsClient = new AlertsClient(alertsClientParams); + expect(await alertsClient.find({ options: {} })).toEqual({ + data: [], + page: 0, + perPage: 0, + total: 0, + }); + + expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(0); + + expect(authorization.filterByAuthorized).toHaveBeenCalledWith(listedTypes, 'find'); + }); + }); }); describe('delete()', () => { @@ -3237,96 +2425,25 @@ describe('delete()', () => { ); }); - // describe('authorization', () => { - // let authorization: jest.Mocked; - // let alertsClientWithAuthorization: AlertsClient; - // let checkPrivileges: jest.MockedFunction>; - - // beforeEach(() => { - // authorization = mockAuthorization(); - // alertsClientWithAuthorization = new AlertsClient({ - // authorization, - // ...alertsClientParams, - // }); - // checkPrivileges = jest.fn(); - // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - // }); - - // test('deletes when user is authorised to delete this type of alert type for the specified consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/delete', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/delete', - // authorized: true, - // }, - // ], - // }); - - // await alertsClientWithAuthorization.delete({ id: '1' }); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // undefined, - // 'delete' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'delete'); - // }); - - // test('deletes when user is authorised to delete this type of alert type producer and consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/delete', - // authorized: true, - // }, - // { - // privilege: 'myType/myApp/delete', - // authorized: false, - // }, - // ], - // }); - - // await alertsClientWithAuthorization.delete({ id: '1' }); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // undefined, - // 'delete' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'delete'); - // }); - - // test('throws when user is not authorised to delete this type of alert at all', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/delete', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/delete', - // authorized: false, - // }, - // ], - // }); - - // expect(alertsClientWithAuthorization.delete({ id: '1' })).rejects.toMatchInlineSnapshot( - // `[Error: Unauthorized to delete a "myType" alert for "myApp"]` - // ); - // }); - // }); + describe('authorization', () => { + test('ensures user is authorised to delete this type of alert under the consumer', async () => { + await alertsClient.delete({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'delete'); + }); + + test('throws when user is not authorised to delete this type of alert', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to delete a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.delete({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to delete a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'delete'); + }); + }); }); describe('update()', () => { @@ -3594,7 +2711,7 @@ describe('update()', () => { }); alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, - result: { id: '123', api_key: 'abc' }, + result: { id: '123', name: '123', api_key: 'abc' }, }); unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: '1', @@ -4329,205 +3446,72 @@ describe('update()', () => { }); }); - // describe('authorization', () => { - // let authorization: jest.Mocked; - // let alertsClientWithAuthorization: AlertsClient; - // let checkPrivileges: jest.MockedFunction>; - - // beforeEach(() => { - // authorization = mockAuthorization(); - // alertsClientWithAuthorization = new AlertsClient({ - // authorization, - // ...alertsClientParams, - // }); - // checkPrivileges = jest.fn(); - // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - // }); - - // function tryToExecuteOperation(options: UpdateOptions): Promise { - // unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - // saved_objects: [ - // { - // id: '1', - // type: 'action', - // attributes: { - // alertTypeId: 'myType', - // consumer: 'myApp', - // actionTypeId: 'test', - // }, - // references: [], - // }, - // { - // id: '2', - // type: 'action', - // attributes: { - // actionTypeId: 'test2', - // }, - // references: [], - // }, - // ], - // }); - // unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ - // id: '1', - // type: 'alert', - // attributes: { - // enabled: true, - // schedule: { interval: '10s' }, - // params: { - // bar: true, - // }, - // actions: [ - // { - // group: 'default', - // actionRef: 'action_0', - // actionTypeId: 'test', - // params: { - // foo: true, - // }, - // }, - // { - // group: 'default', - // actionRef: 'action_1', - // actionTypeId: 'test', - // params: { - // foo: true, - // }, - // }, - // { - // group: 'default', - // actionRef: 'action_2', - // actionTypeId: 'test2', - // params: { - // foo: true, - // }, - // }, - // ], - // scheduledTaskId: 'task-123', - // createdAt: new Date().toISOString(), - // }, - // updated_at: new Date().toISOString(), - // references: [ - // { - // name: 'action_0', - // type: 'action', - // id: '1', - // }, - // { - // name: 'action_1', - // type: 'action', - // id: '1', - // }, - // { - // name: 'action_2', - // type: 'action', - // id: '2', - // }, - // ], - // }); - // return alertsClientWithAuthorization.update(options); - // } - - // test('updates when user is authorised to update this type of alert type for the specified consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/update', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/update', - // authorized: true, - // }, - // ], - // }); - - // const data = getMockData({ - // alertTypeId: 'myType', - // consumer: 'myApp', - // }); - - // await tryToExecuteOperation({ - // id: '1', - // data, - // }); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // undefined, - // 'update' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'update'); - // }); - - // test('updates when user is authorised to update this type of alert type producer and consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/update', - // authorized: true, - // }, - // { - // privilege: 'myType/myApp/update', - // authorized: false, - // }, - // ], - // }); - - // const data = getMockData({ - // alertTypeId: 'myType', - // consumer: 'myApp', - // }); - - // await tryToExecuteOperation({ - // id: '1', - // data, - // }); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // undefined, - // 'update' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'update'); - // }); - - // test('throws when user is not authorised to update this type of alert at all', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/update', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/update', - // authorized: false, - // }, - // ], - // }); - - // const data = getMockData({ - // alertTypeId: 'myType', - // consumer: 'myApp', - // }); - - // await expect( - // tryToExecuteOperation({ - // id: '1', - // data, - // }) - // ).rejects.toMatchInlineSnapshot( - // `[Error: Unauthorized to update a "myType" alert for "myApp"]` - // ); - // }); - // }); + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [], + scheduledTaskId: 'task-123', + createdAt: new Date().toISOString(), + }, + updated_at: new Date().toISOString(), + references: [], + }); + }); + + test('ensures user is authorised to update this type of alert under the consumer', async () => { + await alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [], + }, + }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'update'); + }); + + test('throws when user is not authorised to update this type of alert', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to update a "myType" alert for "myApp"`) + ); + + await expect( + alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [], + }, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to update a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'update'); + }); + }); }); describe('updateApiKey()', () => { @@ -4558,7 +3542,7 @@ describe('updateApiKey()', () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingEncryptedAlert); alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, - result: { id: '234', api_key: 'abc' }, + result: { id: '234', name: '123', api_key: 'abc' }, }); }); @@ -4640,104 +3624,33 @@ describe('updateApiKey()', () => { expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); }); - // describe('authorization', () => { - // let authorization: jest.Mocked; - // let alertsClientWithAuthorization: AlertsClient; - // let checkPrivileges: jest.MockedFunction>; - - // beforeEach(() => { - // authorization = mockAuthorization(); - // alertsClientWithAuthorization = new AlertsClient({ - // authorization, - // ...alertsClientParams, - // }); - // checkPrivileges = jest.fn(); - // authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - // }); - - // test('updates when user is authorised to updateApiKey this type of alert type for the specified consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/updateApiKey', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/updateApiKey', - // authorized: true, - // }, - // ], - // }); - - // await alertsClientWithAuthorization.updateApiKey({ id: '1' }); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // undefined, - // 'updateApiKey' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // 'myApp', - // 'updateApiKey' - // ); - // }); - - // test('updates when user is authorised to updateApiKey this type of alert type producer and consumer', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/updateApiKey', - // authorized: true, - // }, - // { - // privilege: 'myType/myApp/updateApiKey', - // authorized: false, - // }, - // ], - // }); - - // await alertsClientWithAuthorization.updateApiKey({ id: '1' }); - - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // undefined, - // 'updateApiKey' - // ); - // expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - // 'myType', - // 'myApp', - // 'updateApiKey' - // ); - // }); - - // test('throws when user is not authorised to updateApiKey this type of alert at all', async () => { - // checkPrivileges.mockResolvedValueOnce({ - // hasAllRequested: false, - // username: '', - // privileges: [ - // { - // privilege: 'myType/updateApiKey', - // authorized: false, - // }, - // { - // privilege: 'myType/myApp/updateApiKey', - // authorized: false, - // }, - // ], - // }); - - // expect(alertsClientWithAuthorization.updateApiKey({ id: '1' })).rejects.toMatchInlineSnapshot( - // `[Error: Unauthorized to updateApiKey a "myType" alert for "myApp"]` - // ); - // }); - // }); + describe('authorization', () => { + test('ensures user is authorised to updateApiKey this type of alert under the consumer', async () => { + await alertsClient.updateApiKey({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'updateApiKey' + ); + }); + + test('throws when user is not authorised to updateApiKey this type of alert', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to updateApiKey a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to updateApiKey a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'updateApiKey' + ); + }); + }); }); describe('listAlertTypes', () => { @@ -4766,6 +3679,12 @@ describe('listAlertTypes', () => { test('should return a list of AlertTypes that exist in the registry', async () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + authorization.filterByAuthorized.mockResolvedValue( + new Set([ + { ...myAppAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, + { ...alertingAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, + ]) + ); expect(await alertsClient.listAlertTypes()).toEqual( new Set([ { ...myAppAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, @@ -4775,94 +3694,41 @@ describe('listAlertTypes', () => { }); describe('authorization', () => { - let authorization: jest.Mocked; - let alertsClientWithAuthorization: AlertsClient; - let checkPrivileges: jest.MockedFunction>; - + const listedTypes = new Set([ + { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myType', + name: 'myType', + producer: 'myApp', + }, + { + id: 'myOtherType', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + producer: 'alerts', + }, + ]); beforeEach(() => { - authorization = mockAuthorization(); - alertsClientWithAuthorization = new AlertsClient({ - authorization, - ...alertsClientParams, - }); - checkPrivileges = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + alertTypeRegistry.list.mockReturnValue(listedTypes); }); test('should return a list of AlertTypes that exist in the registry only if the user is authorised to get them', async () => { - checkPrivileges.mockResolvedValueOnce({ - hasAllRequested: false, - username: '', - privileges: [ - { - privilege: 'myAppAlertType/myApp/get', - authorized: true, - }, - { - privilege: 'myAppAlertType/myOtherApp/get', - authorized: false, - }, - { - privilege: 'myAppAlertType/alerts/get', - authorized: true, - }, - { - privilege: 'alertingAlertType/myApp/get', - authorized: true, - }, - { - privilege: 'alertingAlertType/myOtherApp/get', - authorized: true, - }, - { - privilege: 'alertingAlertType/alerts/get', - authorized: true, - }, - ], - }); - - alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - - expect(await alertsClientWithAuthorization.listAlertTypes()).toEqual( - new Set([ - { ...myAppAlertType, authorizedConsumers: ['myApp', 'alerts'] }, - { ...alertingAlertType, authorizedConsumers: ['myApp', 'myOtherApp', 'alerts'] }, - ]) - ); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myAppAlertType', - 'alerts', - 'get' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myAppAlertType', - 'myApp', - 'get' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myAppAlertType', - 'myOtherApp', - 'get' - ); + const authorizedTypes = new Set([ + { + id: 'myType', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + producer: 'alerts', + authorizedConsumers: ['myApp'], + }, + ]); + authorization.filterByAuthorized.mockResolvedValue(authorizedTypes); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'alertingAlertType', - 'alerts', - 'get' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'alertingAlertType', - 'myOtherApp', - 'get' - ); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'alertingAlertType', - 'myApp', - 'get' - ); + expect(await alertsClient.listAlertTypes()).toEqual(authorizedTypes); }); }); }); diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 47597de1f79e2..1fa8a99da1d63 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -5,17 +5,15 @@ */ import Boom from 'boom'; -import { omit, isEqual, pluck, mapValues } from 'lodash'; +import { omit, isEqual, pluck } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Logger, SavedObjectsClientContract, SavedObjectReference, SavedObject, - KibanaRequest, } from 'src/core/server'; import { ActionsClient } from '../../actions/server'; -import { AlertsFeatureId } from '../common'; import { Alert, PartialAlert, @@ -32,14 +30,13 @@ import { InvalidateAPIKeyParams, GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, - SecurityPluginSetup, } from '../../security/server'; import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../task_manager/server'; import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists'; import { RegistryAlertType } from './alert_type_registry'; -import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; +import { AlertsAuthorization } from './alerts_authorization'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; @@ -52,17 +49,15 @@ export type InvalidateAPIKeyResult = | { apiKeysEnabled: false } | { apiKeysEnabled: true; result: SecurityPluginInvalidateAPIKeyResult }; -interface ConstructorOptions { +export interface ConstructorOptions { logger: Logger; taskManager: TaskManagerStartContract; unsecuredSavedObjectsClient: SavedObjectsClientContract; - authorization?: SecurityPluginSetup['authz']; - request: KibanaRequest; + authorization: AlertsAuthorization; alertTypeRegistry: AlertTypeRegistry; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; spaceId?: string; namespace?: string; - features: FeaturesPluginStart; getUserName: () => Promise; createAPIKey: () => Promise; invalidateAPIKey: (params: InvalidateAPIKeyParams) => Promise; @@ -135,13 +130,11 @@ export interface UpdateOptions { export class AlertsClient { private readonly logger: Logger; private readonly getUserName: () => Promise; - private readonly features: FeaturesPluginStart; private readonly spaceId?: string; private readonly namespace?: string; private readonly taskManager: TaskManagerStartContract; private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; - private readonly request: KibanaRequest; - private readonly authorization?: SecurityPluginSetup['authz']; + private readonly authorization: AlertsAuthorization; private readonly alertTypeRegistry: AlertTypeRegistry; private readonly createAPIKey: () => Promise; private readonly invalidateAPIKey: ( @@ -153,7 +146,6 @@ export class AlertsClient { constructor({ alertTypeRegistry, unsecuredSavedObjectsClient, - request, authorization, taskManager, logger, @@ -164,7 +156,6 @@ export class AlertsClient { invalidateAPIKey, encryptedSavedObjectsClient, getActionsClient, - features, }: ConstructorOptions) { this.logger = logger; this.getUserName = getUserName; @@ -173,18 +164,16 @@ export class AlertsClient { this.taskManager = taskManager; this.alertTypeRegistry = alertTypeRegistry; this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; - this.request = request; this.authorization = authorization; this.createAPIKey = createAPIKey; this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; this.getActionsClient = getActionsClient; - this.features = features; } public async create({ data, options }: CreateOptions): Promise { // Throws an error if alert type isn't registered - await this.ensureAuthorized(data.alertTypeId, data.consumer, 'create'); + await this.authorization.ensureAuthorized(data.alertTypeId, data.consumer, 'create'); const alertType = this.alertTypeRegistry.get(data.alertTypeId); const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); const username = await this.getUserName(); @@ -239,7 +228,11 @@ export class AlertsClient { public async get({ id }: { id: string }): Promise { const result = await this.unsecuredSavedObjectsClient.get('alert', id); - await this.ensureAuthorized(result.attributes.alertTypeId, result.attributes.consumer, 'get'); + await this.authorization.ensureAuthorized( + result.attributes.alertTypeId, + result.attributes.consumer, + 'get' + ); return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references); } @@ -259,7 +252,7 @@ export class AlertsClient { }: { options?: FindOptions } = {}): Promise { const filters = filter ? [filter] : []; - const authorizedAlertTypes = await this.filterByAuthorized( + const authorizedAlertTypes = await this.authorization.filterByAuthorized( this.alertTypeRegistry.list(), 'find' ); @@ -276,7 +269,7 @@ export class AlertsClient { }; } - filters.push(`(${asFiltersByAlertTypeAndConsumer(authorizedAlertTypes).join(' or ')})`); + filters.push(`(${this.asFiltersByAlertTypeAndConsumer(authorizedAlertTypes).join(' or ')})`); const { page, @@ -325,7 +318,11 @@ export class AlertsClient { attributes = alert.attributes; } - await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'delete'); + await this.authorization.ensureAuthorized( + attributes.alertTypeId, + attributes.consumer, + 'delete' + ); const removeResult = await this.unsecuredSavedObjectsClient.delete('alert', id); @@ -352,7 +349,7 @@ export class AlertsClient { // Still attempt to load the object using SOC alertSavedObject = await this.unsecuredSavedObjectsClient.get('alert', id); } - await this.ensureAuthorized( + await this.authorization.ensureAuthorized( alertSavedObject.attributes.alertTypeId, alertSavedObject.attributes.consumer, 'update' @@ -458,7 +455,11 @@ export class AlertsClient { attributes = alert.attributes; version = alert.version; } - await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'updateApiKey'); + await this.authorization.ensureAuthorized( + attributes.alertTypeId, + attributes.consumer, + 'updateApiKey' + ); const username = await this.getUserName(); await this.unsecuredSavedObjectsClient.update( @@ -516,7 +517,11 @@ export class AlertsClient { version = alert.version; } - await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'enable'); + await this.authorization.ensureAuthorized( + attributes.alertTypeId, + attributes.consumer, + 'enable' + ); if (attributes.enabled === false) { const username = await this.getUserName(); @@ -564,7 +569,11 @@ export class AlertsClient { version = alert.version; } - await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'disable'); + await this.authorization.ensureAuthorized( + attributes.alertTypeId, + attributes.consumer, + 'disable' + ); if (attributes.enabled === true) { await this.unsecuredSavedObjectsClient.update( @@ -592,7 +601,11 @@ export class AlertsClient { public async muteAll({ id }: { id: string }) { const { attributes } = await this.unsecuredSavedObjectsClient.get('alert', id); - await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'muteAll'); + await this.authorization.ensureAuthorized( + attributes.alertTypeId, + attributes.consumer, + 'muteAll' + ); await this.unsecuredSavedObjectsClient.update('alert', id, { muteAll: true, @@ -603,7 +616,11 @@ export class AlertsClient { public async unmuteAll({ id }: { id: string }) { const { attributes } = await this.unsecuredSavedObjectsClient.get('alert', id); - await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'unmuteAll'); + await this.authorization.ensureAuthorized( + attributes.alertTypeId, + attributes.consumer, + 'unmuteAll' + ); await this.unsecuredSavedObjectsClient.update('alert', id, { muteAll: false, @@ -618,7 +635,11 @@ export class AlertsClient { alertId ); - await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'muteInstance'); + await this.authorization.ensureAuthorized( + attributes.alertTypeId, + attributes.consumer, + 'muteInstance' + ); const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) { @@ -646,7 +667,11 @@ export class AlertsClient { 'alert', alertId ); - await this.ensureAuthorized(attributes.alertTypeId, attributes.consumer, 'unmuteInstance'); + await this.authorization.ensureAuthorized( + attributes.alertTypeId, + attributes.consumer, + 'unmuteInstance' + ); const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { await this.unsecuredSavedObjectsClient.update( @@ -663,106 +688,7 @@ export class AlertsClient { } public async listAlertTypes() { - return await this.filterByAuthorized(this.alertTypeRegistry.list(), 'get'); - } - - private async ensureAuthorized(alertTypeId: string, consumer: string, operation: string) { - const { authorization } = this; - if (authorization) { - const alertType = this.alertTypeRegistry.get(alertTypeId); - const requiredPrivilegesByScope = { - consumer: authorization.actions.alerting.get(alertTypeId, consumer, operation), - producer: authorization.actions.alerting.get(alertTypeId, alertType.producer, operation), - }; - - // We special case the Alerts Management `consumer` as we don't want to have to - // manually authorize each alert type in the management UI - const shouldAuthorizeConsumer = consumer !== AlertsFeatureId; - - const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested, privileges } = await checkPrivileges( - shouldAuthorizeConsumer && consumer !== alertType.producer - ? [ - // check for access at consumer level - requiredPrivilegesByScope.consumer, - // check for access at producer level - requiredPrivilegesByScope.producer, - ] - : [ - // skip consumer privilege checks under `alerts` as all alert types can - // be created under `alerts` if you have producer level privileges - requiredPrivilegesByScope.producer, - ] - ); - - if (!hasAllRequested) { - const authorizedPrivileges = pluck( - privileges.filter((privilege) => privilege.authorized), - 'privilege' - ); - const unauthorizedScopes = mapValues( - requiredPrivilegesByScope, - (privilege) => !authorizedPrivileges.includes(privilege) - ); - - throw Boom.forbidden( - `Unauthorized to ${operation} a "${alertTypeId}" alert ${ - shouldAuthorizeConsumer && unauthorizedScopes.consumer - ? `for "${consumer}"` - : `by "${alertType.producer}"` - }` - ); - } - } - } - - private async filterByAuthorized( - alertTypes: Set, - operation: string - ): Promise> { - const featuresIds = this.features.getFeatures().map((feature) => feature.id); - - if (!this.authorization) { - return augmentWithAuthorizedConsumers(alertTypes, featuresIds); - } else { - const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( - this.request - ); - - // add an empty `authorizedConsumers` array on each alertType - const alertTypesWithAutherization = augmentWithAuthorizedConsumers(alertTypes); - - // map from privilege to alertType which we can refer back to when analyzing the result - // of checkPrivileges - const privilegeToAlertType = new Map(); - // as we can't ask ES for the user's individual privileges we need to ask for each feature - // and alertType in the system whether this user has this privilege - for (const alertType of alertTypesWithAutherization) { - for (const feature of featuresIds) { - privilegeToAlertType.set( - this.authorization!.actions.alerting.get(alertType.id, feature, operation), - [alertType, feature] - ); - } - } - - const { hasAllRequested, privileges } = await checkPrivileges([ - ...privilegeToAlertType.keys(), - ]); - - return hasAllRequested - ? // has access to all features - augmentWithAuthorizedConsumers(alertTypes, featuresIds) - : // only has some of the required privileges - privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { - if (authorized && privilegeToAlertType.has(privilege)) { - const [alertType, consumer] = privilegeToAlertType.get(privilege)!; - alertType.authorizedConsumers.push(consumer); - authorizedAlertTypes.add(alertType); - } - return authorizedAlertTypes; - }, new Set()); - } + return await this.authorization.filterByAuthorized(this.alertTypeRegistry.list(), 'get'); } private async scheduleAlert(id: string, alertTypeId: string) { @@ -882,27 +808,15 @@ export class AlertsClient { references, }; } -} - -function augmentWithAuthorizedConsumers( - alertTypes: Set, - authorizedConsumers?: string[] -): Set { - return new Set( - Array.from(alertTypes).map((alertType) => ({ - ...alertType, - authorizedConsumers: authorizedConsumers ?? [], - })) - ); -} -function asFiltersByAlertTypeAndConsumer(alertTypes: Set): string[] { - return Array.from(alertTypes).reduce((filters, { id, authorizedConsumers }) => { - for (const consumer of authorizedConsumers) { - filters.push( - `(alert.attributes.alertTypeId:${id} and alert.attributes.consumer:${consumer})` - ); - } - return filters; - }, []); + private asFiltersByAlertTypeAndConsumer(alertTypes: Set): string[] { + return Array.from(alertTypes).reduce((filters, { id, authorizedConsumers }) => { + for (const consumer of authorizedConsumers) { + filters.push( + `(alert.attributes.alertTypeId:${id} and alert.attributes.consumer:${consumer})` + ); + } + return filters; + }, []); + } } diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 1952aeb27d219..5253e38c20dac 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -21,6 +21,7 @@ import { actionsMock } from '../../actions/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; jest.mock('./alerts_client'); +jest.mock('./alerts_authorization'); const savedObjectsClient = savedObjectsClientMock.create(); const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(); @@ -73,10 +74,17 @@ test('creates an alerts client with proper constructor arguments when security i includedHiddenTypes: ['alert'], }); - expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ - unsecuredSavedObjectsClient: savedObjectsClient, + const { AlertsAuthorization } = jest.requireMock('./alerts_authorization'); + expect(AlertsAuthorization).toHaveBeenCalledWith({ request, authorization: securityPluginSetup.authz, + alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, + features: alertsClientFactoryParams.features, + }); + + expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ + unsecuredSavedObjectsClient: savedObjectsClient, + authorization: expect.any(AlertsAuthorization), logger: alertsClientFactoryParams.logger, taskManager: alertsClientFactoryParams.taskManager, alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, @@ -87,7 +95,6 @@ test('creates an alerts client with proper constructor arguments when security i createAPIKey: expect.any(Function), invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, - features: alertsClientFactoryParams.features, }); }); @@ -105,9 +112,17 @@ test('creates an alerts client with proper constructor arguments', async () => { includedHiddenTypes: ['alert'], }); + const { AlertsAuthorization } = jest.requireMock('./alerts_authorization'); + expect(AlertsAuthorization).toHaveBeenCalledWith({ + request, + authorization: undefined, + alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, + features: alertsClientFactoryParams.features, + }); + expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ unsecuredSavedObjectsClient: savedObjectsClient, - request, + authorization: expect.any(AlertsAuthorization), logger: alertsClientFactoryParams.logger, taskManager: alertsClientFactoryParams.taskManager, alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, @@ -117,7 +132,6 @@ test('creates an alerts client with proper constructor arguments', async () => { createAPIKey: expect.any(Function), invalidateAPIKey: expect.any(Function), encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient, - features: alertsClientFactoryParams.features, getActionsClient: expect.any(Function), }); }); diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index c84b1c1b1bd15..b3024ca03d566 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -12,6 +12,7 @@ import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../security/serv import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../task_manager/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; +import { AlertsAuthorization } from './alerts_authorization'; export interface AlertsClientFactoryOpts { logger: Logger; @@ -56,18 +57,23 @@ export class AlertsClientFactory { public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): AlertsClient { const { securityPluginSetup, actions, features } = this; const spaceId = this.getSpaceId(request); + const authorization = new AlertsAuthorization({ + authorization: securityPluginSetup?.authz, + request, + alertTypeRegistry: this.alertTypeRegistry, + features: features!, + }); + return new AlertsClient({ spaceId, logger: this.logger, - features: features!, taskManager: this.taskManager, alertTypeRegistry: this.alertTypeRegistry, unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, { excludedWrappers: ['security'], includedHiddenTypes: ['alert'], }), - authorization: this.securityPluginSetup?.authz, - request, + authorization, namespace: this.spaceIdToNamespace(spaceId), encryptedSavedObjectsClient: this.encryptedSavedObjectsClient, async getUserName() { From 0aaaef5869c479b34c6a037f4f4a49546f29496b Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 10 Jun 2020 09:57:36 +0100 Subject: [PATCH 026/126] fixed sapces only suite --- .../server/alerts_authorization.mock.ts | 3 +- .../alerts/server/alerts_authorization.ts | 42 ++++++++++- .../alerts/server/alerts_client.test.ts | 59 +++++---------- x-pack/plugins/alerts/server/alerts_client.ts | 53 ++++--------- .../tests/alerting/find.ts | 75 ++++++++++++------- .../index_threshold/alert.ts | 2 +- .../tests/alerting/list_alert_types.ts | 5 +- 7 files changed, 131 insertions(+), 108 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_authorization.mock.ts b/x-pack/plugins/alerts/server/alerts_authorization.mock.ts index 58b041f8b0370..f151a843aedeb 100644 --- a/x-pack/plugins/alerts/server/alerts_authorization.mock.ts +++ b/x-pack/plugins/alerts/server/alerts_authorization.mock.ts @@ -12,7 +12,8 @@ export type AlertsAuthorizationMock = jest.Mocked; const createAlertsAuthorizationMock = () => { const mocked: AlertsAuthorizationMock = { ensureAuthorized: jest.fn(), - filterByAuthorized: jest.fn(), + checkAlertTypeAuthorization: jest.fn(), + getFindAuthorizationFilter: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/alerts/server/alerts_authorization.ts b/x-pack/plugins/alerts/server/alerts_authorization.ts index e0469d8f49604..a992bbc1f2ad7 100644 --- a/x-pack/plugins/alerts/server/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/alerts_authorization.ts @@ -87,7 +87,36 @@ export class AlertsAuthorization { } } - public async filterByAuthorized( + public async getFindAuthorizationFilter(): Promise<{ + filter?: string; + ensureAlertTypeIsAuthorized: (alertTypeId: string) => void; + }> { + if (this.authorization) { + const authorizedAlertTypes = await this.checkAlertTypeAuthorization( + this.alertTypeRegistry.list(), + 'find' + ); + + if (!authorizedAlertTypes.size) { + throw Boom.forbidden(`Unauthorized to find a any alert types`); + } + + const authorizedAlertTypeIds = new Set(pluck([...authorizedAlertTypes], 'id')); + return { + filter: `(${this.asFiltersByAlertTypeAndConsumer(authorizedAlertTypes).join(' or ')})`, + ensureAlertTypeIsAuthorized: (alertTypeId: string) => { + if (!authorizedAlertTypeIds.has(alertTypeId)) { + throw Boom.forbidden(`Unauthorized to find "${alertTypeId}" alerts`); + } + }, + }; + } + return { + ensureAlertTypeIsAuthorized: (alertTypeId: string) => {}, + }; + } + + public async checkAlertTypeAuthorization( alertTypes: Set, operation: string ): Promise> { @@ -147,4 +176,15 @@ export class AlertsAuthorization { })) ); } + + private asFiltersByAlertTypeAndConsumer(alertTypes: Set): string[] { + return Array.from(alertTypes).reduce((filters, { id, authorizedConsumers }) => { + filters.push( + `(alert.attributes.alertTypeId:${id} and (${authorizedConsumers + .map((consumer) => `alert.attributes.consumer:${consumer}`) + .join(' or ')}))` + ); + return filters; + }, []); + } } diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 7a610d82dd21e..c9a28177dc80a 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -2144,6 +2144,9 @@ describe('find()', () => { }, ]); beforeEach(() => { + authorization.getFindAuthorizationFilter.mockResolvedValue({ + ensureAlertTypeIsAuthorized() {}, + }); unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ total: 1, per_page: 10, @@ -2179,7 +2182,7 @@ describe('find()', () => { ], }); alertTypeRegistry.list.mockReturnValue(listedTypes); - authorization.filterByAuthorized.mockResolvedValue( + authorization.checkAlertTypeAuthorization.mockResolvedValue( new Set([ { id: 'myType', @@ -2230,7 +2233,6 @@ describe('find()', () => { expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { - "filter": "((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp))", "type": "alert", }, ] @@ -2239,51 +2241,28 @@ describe('find()', () => { describe('authorization', () => { test('ensures user is query filter types down to those the user is authorized to find', async () => { - authorization.filterByAuthorized.mockResolvedValue( - new Set([ - { - id: 'myType', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - producer: 'alerts', - authorizedConsumers: ['myApp'], - }, - { - id: 'myOtherType', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - producer: 'alerts', - authorizedConsumers: ['myApp', 'myOtherApp'], - }, - ]) - ); + authorization.getFindAuthorizationFilter.mockResolvedValue({ + filter: + '((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myOtherApp))', + ensureAlertTypeIsAuthorized() {}, + }); const alertsClient = new AlertsClient(alertsClientParams); - await alertsClient.find({ options: {} }); + await alertsClient.find({ options: { filter: 'someTerm' } }); const [options] = unsecuredSavedObjectsClient.find.mock.calls[0]; expect(options.filter).toMatchInlineSnapshot( - `"((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myOtherApp))"` + `"someTerm and ((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myOtherApp))"` ); - expect(authorization.filterByAuthorized).toHaveBeenCalledWith(listedTypes, 'find'); + expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledTimes(1); }); - test('short circuits if user is not authorized to find any types', async () => { - authorization.filterByAuthorized.mockResolvedValue(new Set([])); - + test('throws if user is not authorized to find any types', async () => { const alertsClient = new AlertsClient(alertsClientParams); - expect(await alertsClient.find({ options: {} })).toEqual({ - data: [], - page: 0, - perPage: 0, - total: 0, - }); - - expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(0); - - expect(authorization.filterByAuthorized).toHaveBeenCalledWith(listedTypes, 'find'); + authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('not authorized')); + await expect(alertsClient.find({ options: {} })).rejects.toThrowErrorMatchingInlineSnapshot( + `"not authorized"` + ); }); }); }); @@ -3679,7 +3658,7 @@ describe('listAlertTypes', () => { test('should return a list of AlertTypes that exist in the registry', async () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - authorization.filterByAuthorized.mockResolvedValue( + authorization.checkAlertTypeAuthorization.mockResolvedValue( new Set([ { ...myAppAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, { ...alertingAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, @@ -3726,7 +3705,7 @@ describe('listAlertTypes', () => { authorizedConsumers: ['myApp'], }, ]); - authorization.filterByAuthorized.mockResolvedValue(authorizedTypes); + authorization.checkAlertTypeAuthorization.mockResolvedValue(authorizedTypes); expect(await alertsClient.listAlertTypes()).toEqual(authorizedTypes); }); diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 1fa8a99da1d63..45c4f5bd851c4 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -247,30 +247,18 @@ export class AlertsClient { } } - public async find({ - options: { filter, ...options } = {}, - }: { options?: FindOptions } = {}): Promise { - const filters = filter ? [filter] : []; - - const authorizedAlertTypes = await this.authorization.filterByAuthorized( - this.alertTypeRegistry.list(), - 'find' - ); - const authorizedAlertTypeIds = new Set(pluck([...authorizedAlertTypes], 'id')); - - if (!authorizedAlertTypes.size) { - // the current user isn't authorized to get any alertTypes - // we can short circuit here - return { - page: 0, - perPage: 0, - total: 0, - data: [], - }; + public async find({ options = {} }: { options?: FindOptions } = {}): Promise { + const { + filter: authorizationFilter, + ensureAlertTypeIsAuthorized, + } = await this.authorization.getFindAuthorizationFilter(); + + if (authorizationFilter) { + options.filter = options.filter + ? `${options.filter} and ${authorizationFilter}` + : authorizationFilter; } - filters.push(`(${this.asFiltersByAlertTypeAndConsumer(authorizedAlertTypes).join(' or ')})`); - const { page, per_page: perPage, @@ -278,7 +266,6 @@ export class AlertsClient { saved_objects: data, } = await this.unsecuredSavedObjectsClient.find({ ...options, - filter: filters.join(` and `), type: 'alert', }); @@ -287,9 +274,7 @@ export class AlertsClient { perPage, total, data: data.map(({ id, attributes, updated_at, references }) => { - if (!authorizedAlertTypeIds.has(attributes.alertTypeId)) { - throw Boom.forbidden(`Unauthorized to find "${attributes.alertTypeId}" alerts`); - } + ensureAlertTypeIsAuthorized(attributes.alertTypeId); return this.getAlertFromRaw(id, attributes, updated_at, references); }), }; @@ -688,7 +673,10 @@ export class AlertsClient { } public async listAlertTypes() { - return await this.authorization.filterByAuthorized(this.alertTypeRegistry.list(), 'get'); + return await this.authorization.checkAlertTypeAuthorization( + this.alertTypeRegistry.list(), + 'get' + ); } private async scheduleAlert(id: string, alertTypeId: string) { @@ -808,15 +796,4 @@ export class AlertsClient { references, }; } - - private asFiltersByAlertTypeAndConsumer(alertTypes: Set): string[] { - return Array.from(alertTypes).reduce((filters, { id, authorizedConsumers }) => { - for (const consumer of authorizedConsumers) { - filters.push( - `(alert.attributes.alertTypeId:${id} and alert.attributes.consumer:${consumer})` - ); - } - return filters; - }, []); - } } 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 50e8347b3bebe..163f831ea6c78 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 @@ -39,19 +39,21 @@ export default function createFindTests({ getService }: FtrProviderContext) { ) .auth(user.username, user.password); - expect(response.statusCode).to.eql(200); switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - expect(response.body.page).to.equal(0); - expect(response.body.perPage).to.equal(0); - expect(response.body.total).to.equal(0); - expect(response.body.data.length).to.equal(0); + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to find a any alert types`, + statusCode: 403, + }); break; case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); expect(response.body.page).to.equal(1); expect(response.body.perPage).to.be.greaterThan(0); expect(response.body.total).to.be.greaterThan(0); @@ -104,37 +106,51 @@ export default function createFindTests({ getService }: FtrProviderContext) { consumer: 'alertsRestrictedFixture', }); } + function createUnrestrictedNoOpAlert() { + return createNoOpAlert({ + alertTypeId: 'test.unrestricted-noop', + consumer: 'alertsFixture', + }); + } const allAlerts = []; allAlerts.push(await createNoOpAlert()); allAlerts.push(await createNoOpAlert()); allAlerts.push(await createRestrictedNoOpAlert()); + allAlerts.push(await createUnrestrictedNoOpAlert()); + allAlerts.push(await createUnrestrictedNoOpAlert()); allAlerts.push(await createRestrictedNoOpAlert()); allAlerts.push(await createNoOpAlert()); allAlerts.push(await createNoOpAlert()); + const perPage = 4; + const response = await supertestWithoutAuth - .get(`${getUrlPrefix(space.id)}/api/alerts/_find?per_page=3&sort_field=createdAt`) + .get( + `${getUrlPrefix(space.id)}/api/alerts/_find?per_page=${perPage}&sort_field=createdAt` + ) .auth(user.username, user.password); - expect(response.statusCode).to.eql(200); switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - expect(response.body.page).to.equal(0); - expect(response.body.perPage).to.equal(0); - expect(response.body.total).to.equal(0); - expect(response.body.data.length).to.equal(0); + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to find a any alert types`, + statusCode: 403, + }); break; case 'space_1_all at space1': + expect(response.statusCode).to.eql(200); expect(response.body.page).to.equal(1); - expect(response.body.perPage).to.be.equal(3); - expect(response.body.total).to.be.equal(4); + expect(response.body.perPage).to.be.equal(perPage); + expect(response.body.total).to.be.equal(6); { const [firstPage] = chunk( allAlerts .filter((alert) => alert.alertTypeId !== 'test.restricted-noop') .map((alert) => alert.id), - 3 + perPage ); expect(response.body.data.map((alert: any) => alert.id)).to.eql(firstPage); } @@ -142,13 +158,15 @@ export default function createFindTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); expect(response.body.page).to.equal(1); - expect(response.body.perPage).to.be.equal(3); - expect(response.body.total).to.be.equal(6); + expect(response.body.perPage).to.be.equal(perPage); + expect(response.body.total).to.be.equal(8); + { const [firstPage, secondPage] = chunk( allAlerts.map((alert) => alert.id), - 3 + perPage ); expect(response.body.data.map((alert: any) => alert.id)).to.eql(firstPage); @@ -156,9 +174,10 @@ export default function createFindTests({ getService }: FtrProviderContext) { .get( `${getUrlPrefix( space.id - )}/api/alerts/_find?per_page=3&sort_field=createdAt&page=2` + )}/api/alerts/_find?per_page=${perPage}&sort_field=createdAt&page=2` ) .auth(user.username, user.password); + expect(secondResponse.body.data.map((alert: any) => alert.id)).to.eql(secondPage); } @@ -209,10 +228,12 @@ export default function createFindTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - expect(response.body.page).to.equal(0); - expect(response.body.perPage).to.equal(0); - expect(response.body.total).to.equal(0); - expect(response.body.data.length).to.equal(0); + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to find a any alert types`, + statusCode: 403, + }); break; case 'global_read at space1': case 'superuser at space1': @@ -276,10 +297,12 @@ export default function createFindTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': - expect(response.body.page).to.equal(0); - expect(response.body.perPage).to.equal(0); - expect(response.body.total).to.equal(0); - expect(response.body.data.length).to.equal(0); + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to find a any alert types`, + statusCode: 403, + }); break; case 'global_read at space1': case 'superuser at space1': diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts index 8412c09eefcda..92db0458c0639 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts @@ -346,7 +346,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .send({ name: params.name, - consumer: 'function test', + consumer: 'alerts', enabled: true, alertTypeId: ALERT_TYPE_ID, schedule: { interval: `${ALERT_INTERVAL_SECONDS}s` }, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts index 4baae603f2960..dde75b57b6b20 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts @@ -19,7 +19,9 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { `${getUrlPrefix(Spaces.space1.id)}/api/alerts/list_alert_types` ); expect(response.status).to.eql(200); - const fixtureAlertType = response.body.find((alertType: any) => alertType.id === 'test.noop'); + const { authorizedConsumers, ...fixtureAlertType } = response.body.find( + (alertType: any) => alertType.id === 'test.noop' + ); expect(fixtureAlertType).to.eql({ actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', @@ -31,6 +33,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { }, producer: 'alertsFixture', }); + expect(authorizedConsumers).to.contain('alertsFixture'); }); it('should return actionVariables with both context and state', async () => { From ba4075753ece2360859c9fe7c9f6932d1c976fd6 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 10 Jun 2020 11:40:28 +0100 Subject: [PATCH 027/126] added more unit tests around the extracted auth code --- .../server/alerts_authorization.test.ts | 642 ++++++++++++++++++ .../alerts/server/alerts_authorization.ts | 20 +- x-pack/plugins/alerts/server/alerts_client.ts | 2 +- 3 files changed, 657 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/alerts/server/alerts_authorization.test.ts diff --git a/x-pack/plugins/alerts/server/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/alerts_authorization.test.ts new file mode 100644 index 0000000000000..3117cbd8429a1 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_authorization.test.ts @@ -0,0 +1,642 @@ +/* + * 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 { KibanaRequest } from 'kibana/server'; +import { alertTypeRegistryMock } from './alert_type_registry.mock'; +import { securityMock } from '../../../plugins/security/server/mocks'; +import { PluginStartContract as FeaturesStartContract, Feature } from '../../features/server'; +import { featuresPluginMock } from '../../features/server/mocks'; +import { AlertsAuthorization } from './alerts_authorization'; + +const alertTypeRegistry = alertTypeRegistryMock.create(); +const features: jest.Mocked = featuresPluginMock.createStart(); +const request = {} as KibanaRequest; + +const mockAuthorizationAction = (type: string, app: string, operation: string) => + `${type}/${app}/${operation}`; +function mockAuthorization() { + const authorization = securityMock.createSetup().authz; + // typescript is havingtrouble inferring jest's automocking + (authorization.actions.alerting.get as jest.MockedFunction< + typeof authorization.actions.alerting.get + >).mockImplementation(mockAuthorizationAction); + return authorization; +} + +function mockFeature(appName: string, typeName: string, requiredApps: string[] = []) { + return new Feature({ + id: appName, + name: appName, + app: requiredApps, + privileges: { + all: { + alerting: { + all: [typeName], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + read: { + alerting: { + read: [typeName], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, + }); +} +const alertsFeature = mockFeature('alerts', 'myBuiltInType'); +const myAppFeature = mockFeature('myApp', 'myType', ['alerts']); +const myOtherAppFeature = mockFeature('myOtherApp', 'myType', ['alerts']); + +beforeEach(() => { + jest.resetAllMocks(); + alertTypeRegistry.get.mockImplementation((id) => ({ + id, + name: 'My Alert Type', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + async executor() {}, + producer: 'myApp', + })); + features.getFeatures.mockReturnValue([alertsFeature, myAppFeature, myOtherAppFeature]); +}); + +describe('ensureAuthorized', () => { + test('is a no-op when there is no authorization api', async () => { + const alertAuthorization = new AlertsAuthorization({ + request, + alertTypeRegistry, + features, + }); + + await alertAuthorization.ensureAuthorized('myType', 'myApp', 'create'); + + expect(alertTypeRegistry.get).toHaveBeenCalledTimes(0); + }); + + test('ensures the user has privileges to execute the specified type, operation and consumer', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + }); + + checkPrivileges.mockResolvedValueOnce({ hasAllRequested: true, privileges: [] }); + + await alertAuthorization.ensureAuthorized('myType', 'myApp', 'create'); + + expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); + expect(checkPrivileges).toHaveBeenCalledWith([ + mockAuthorizationAction('myType', 'myApp', 'create'), + ]); + }); + + test('ensures the user has privileges to execute the specified type and operation without consumer when consumer is alerts', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + }); + + checkPrivileges.mockResolvedValueOnce({ hasAllRequested: true, privileges: [] }); + + await alertAuthorization.ensureAuthorized('myType', 'alerts', 'create'); + + expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); + expect(checkPrivileges).toHaveBeenCalledWith([ + mockAuthorizationAction('myType', 'myApp', 'create'), + ]); + }); + + test('ensures the user has privileges to execute the specified type, operation and producer when producer is different from consumer', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ hasAllRequested: true, privileges: [] }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + }); + + await alertAuthorization.ensureAuthorized('myType', 'myOtherApp', 'create'); + + expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myOtherApp', + 'create' + ); + expect(checkPrivileges).toHaveBeenCalledWith([ + mockAuthorizationAction('myType', 'myOtherApp', 'create'), + mockAuthorizationAction('myType', 'myApp', 'create'), + ]); + }); + + test('throws if user lacks the required privieleges for the consumer', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + }); + + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myType', 'myApp', 'create'), + authorized: true, + }, + ], + }); + + await expect( + alertAuthorization.ensureAuthorized('myType', 'myOtherApp', 'create') + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` + ); + }); + + test('throws if user lacks the required privieleges for the producer', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + }); + + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myType', 'myApp', 'create'), + authorized: false, + }, + ], + }); + + await expect( + alertAuthorization.ensureAuthorized('myType', 'myOtherApp', 'create') + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized to create a \\"myType\\" alert by \\"myApp\\""` + ); + }); + + test('throws if user lacks the required privieleges for both consumer and producer', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + }); + + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myType', 'myApp', 'create'), + authorized: false, + }, + ], + }); + + await expect( + alertAuthorization.ensureAuthorized('myType', 'myOtherApp', 'create') + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` + ); + }); +}); + +describe('getFindAuthorizationFilter', () => { + const alertingAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'alertingAlertType', + name: 'alertingAlertType', + producer: 'alerts', + }; + const myAppAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + }; + const setOfAlertTypes = new Set([myAppAlertType, alertingAlertType]); + + test('omits filter when there is no authorization api', async () => { + const alertAuthorization = new AlertsAuthorization({ + request, + alertTypeRegistry, + features, + }); + + const { + filter, + ensureAlertTypeIsAuthorized, + } = await alertAuthorization.getFindAuthorizationFilter(); + + expect(() => ensureAlertTypeIsAuthorized('someMadeUpType', 'myApp')).not.toThrow(); + + expect(filter).toEqual(undefined); + }); + + test('creates a filter based on the privileged types', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ hasAllRequested: true, privileges: [] }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + expect((await alertAuthorization.getFindAuthorizationFilter()).filter).toMatchInlineSnapshot( + `"((alert.attributes.alertTypeId:myAppAlertType and (alert.attributes.consumer:alerts or alert.attributes.consumer:myApp or alert.attributes.consumer:myOtherApp)) or (alert.attributes.alertTypeId:alertingAlertType and (alert.attributes.consumer:alerts or alert.attributes.consumer:myApp or alert.attributes.consumer:myOtherApp)))"` + ); + }); + + test('creates a `ensureAlertTypeIsAuthorized` function which throws if type is unauthorized', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'find'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), + authorized: false, + }, + ], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); + expect(() => { + ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); + }).toThrowErrorMatchingInlineSnapshot( + `"Unauthorized to find \\"myAppAlertType\\" alerts under myOtherApp"` + ); + }); + + test('creates a `ensureAlertTypeIsAuthorized` function which is no-op if type is authorized', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'find'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), + authorized: true, + }, + ], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); + expect(() => { + ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); + }).not.toThrow(); + }); +}); + +describe('checkAlertTypeAuthorization', () => { + const alertingAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'alertingAlertType', + name: 'alertingAlertType', + producer: 'alerts', + }; + const myAppAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + }; + const setOfAlertTypes = new Set([myAppAlertType, alertingAlertType]); + + test('augments a list of types with all features when there is no authorization api', async () => { + const alertAuthorization = new AlertsAuthorization({ + request, + alertTypeRegistry, + features, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.checkAlertTypeAuthorization( + new Set([myAppAlertType, alertingAlertType]), + 'create' + ) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Array [ + "alerts", + "myApp", + "myOtherApp", + ], + "defaultActionGroupId": "default", + "id": "myAppAlertType", + "name": "myAppAlertType", + "producer": "myApp", + }, + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Array [ + "alerts", + "myApp", + "myOtherApp", + ], + "defaultActionGroupId": "default", + "id": "alertingAlertType", + "name": "alertingAlertType", + "producer": "alerts", + }, + } + `); + }); + + test('augments a list of types with consumers under which the operation is authorized', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + authorized: true, + }, + ], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.checkAlertTypeAuthorization( + new Set([myAppAlertType, alertingAlertType]), + 'create' + ) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Array [ + "alerts", + "myApp", + ], + "defaultActionGroupId": "default", + "id": "alertingAlertType", + "name": "alertingAlertType", + "producer": "alerts", + }, + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Array [ + "alerts", + "myApp", + "myOtherApp", + ], + "defaultActionGroupId": "default", + "id": "myAppAlertType", + "name": "myAppAlertType", + "producer": "myApp", + }, + } + `); + }); + + test('omits types which have no consumers under which the operation is authorized', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + ], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.checkAlertTypeAuthorization( + new Set([myAppAlertType, alertingAlertType]), + 'create' + ) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Array [ + "alerts", + "myApp", + "myOtherApp", + ], + "defaultActionGroupId": "default", + "id": "alertingAlertType", + "name": "alertingAlertType", + "producer": "alerts", + }, + } + `); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_authorization.ts b/x-pack/plugins/alerts/server/alerts_authorization.ts index a992bbc1f2ad7..89af132ec8a22 100644 --- a/x-pack/plugins/alerts/server/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/alerts_authorization.ts @@ -89,7 +89,7 @@ export class AlertsAuthorization { public async getFindAuthorizationFilter(): Promise<{ filter?: string; - ensureAlertTypeIsAuthorized: (alertTypeId: string) => void; + ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => void; }> { if (this.authorization) { const authorizedAlertTypes = await this.checkAlertTypeAuthorization( @@ -101,18 +101,26 @@ export class AlertsAuthorization { throw Boom.forbidden(`Unauthorized to find a any alert types`); } - const authorizedAlertTypeIds = new Set(pluck([...authorizedAlertTypes], 'id')); + const authorizedAlertTypeIdsToConsumers = new Set( + [...authorizedAlertTypes].reduce((alertTypeIdConsumerPairs, alertType) => { + for (const consumer of alertType.authorizedConsumers) { + alertTypeIdConsumerPairs.push(`${alertType.id}/${consumer}`); + } + return alertTypeIdConsumerPairs; + }, []) + ); + return { filter: `(${this.asFiltersByAlertTypeAndConsumer(authorizedAlertTypes).join(' or ')})`, - ensureAlertTypeIsAuthorized: (alertTypeId: string) => { - if (!authorizedAlertTypeIds.has(alertTypeId)) { - throw Boom.forbidden(`Unauthorized to find "${alertTypeId}" alerts`); + ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => { + if (!authorizedAlertTypeIdsToConsumers.has(`${alertTypeId}/${consumer}`)) { + throw Boom.forbidden(`Unauthorized to find "${alertTypeId}" alerts under ${consumer}`); } }, }; } return { - ensureAlertTypeIsAuthorized: (alertTypeId: string) => {}, + ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => {}, }; } diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 45c4f5bd851c4..657729bf4a78f 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -274,7 +274,7 @@ export class AlertsClient { perPage, total, data: data.map(({ id, attributes, updated_at, references }) => { - ensureAlertTypeIsAuthorized(attributes.alertTypeId); + ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer); return this.getAlertFromRaw(id, attributes, updated_at, references); }), }; From 74a886af8bec00f5702107a7b1167335b94b6598 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 10 Jun 2020 14:07:13 +0100 Subject: [PATCH 028/126] fixed lintin issues --- .../alerts/server/alerts_client.test.ts | 35 ++++++++++--------- x-pack/plugins/alerts/server/alerts_client.ts | 5 +-- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index c9a28177dc80a..33c5d4a3099ee 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -5,21 +5,13 @@ */ import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; -import { - AlertsClient, - CreateOptions, - ConstructorOptions, - // , UpdateOptions, FindOptions -} from './alerts_client'; +import { AlertsClient, CreateOptions, ConstructorOptions } from './alerts_client'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../src/core/server/mocks'; import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; import { alertsAuthorizationMock } from './alerts_authorization.mock'; import { TaskStatus } from '../../task_manager/server'; -import { - IntervalSchedule, - // PartialAlert -} from './types'; +import { IntervalSchedule } from './types'; import { resolvable } from './test_utils'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { actionsClientMock } from '../../actions/server/mocks'; @@ -2114,20 +2106,31 @@ describe('getAlertState()', () => { const alertsClient = new AlertsClient(alertsClientParams); await alertsClient.getAlertState({ id: '1' }); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'getAlertState' + ); }); - test('throws when user is not authorised to get this type of alert', async () => { + test('throws when user is not authorised to getAlertState this type of alert', async () => { const alertsClient = new AlertsClient(alertsClientParams); - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to get a "myType" alert for "myApp"`) + // `get` check + authorization.ensureAuthorized.mockResolvedValueOnce(); + // `getAlertState` check + authorization.ensureAuthorized.mockRejectedValueOnce( + new Error(`Unauthorized to getAlertState a "myType" alert for "myApp"`) ); await expect(alertsClient.getAlertState({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to get a "myType" alert for "myApp"]` + `[Error: Unauthorized to getAlertState a "myType" alert for "myApp"]` ); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'getAlertState' + ); }); }); }); diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 657729bf4a78f..0d35bc7a85f5e 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -69,7 +69,7 @@ export interface MuteOptions extends IndexType { alertInstanceId: string; } -export interface FindOptions extends IndexType { +interface FindOptions extends IndexType { perPage?: number; page?: number; search?: string; @@ -115,7 +115,7 @@ export interface CreateOptions { }; } -export interface UpdateOptions { +interface UpdateOptions { id: string; data: { name: string; @@ -238,6 +238,7 @@ export class AlertsClient { public async getAlertState({ id }: { id: string }): Promise { const alert = await this.get({ id }); + await this.authorization.ensureAuthorized(alert.alertTypeId, alert.consumer, 'getAlertState'); if (alert.scheduledTaskId) { const { state } = taskInstanceToAlertTaskInstance( await this.taskManager.get(alert.scheduledTaskId), From c0552168802887ebf8330bef156cc2586a3b5301 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 10 Jun 2020 14:17:53 +0100 Subject: [PATCH 029/126] fixed producer in siem alert types --- .../notifications/rules_notification_alert_type.ts | 4 ++-- .../lib/detection_engine/signals/signal_rule_alert_type.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index 5a2a950f21bcf..953cad62402a5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -6,7 +6,7 @@ import { Logger } from 'src/core/server'; import { schema } from '@kbn/config-schema'; -import { NOTIFICATIONS_ID } from '../../../../common/constants'; +import { APP_ID, NOTIFICATIONS_ID } from '../../../../common/constants'; import { NotificationAlertTypeDefinition } from './types'; import { getSignalsCount } from './get_signals_count'; @@ -25,7 +25,7 @@ export const rulesNotificationAlertType = ({ name: 'SIEM notification', actionGroups: siemRuleActionGroups, defaultActionGroupId: 'default', - producer: 'siem', + producer: APP_ID, validate: { params: schema.object({ ruleAlertId: schema.string(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 567274be6a9f8..172d7c50d9fa3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -8,7 +8,7 @@ import { Logger, KibanaRequest } from 'src/core/server'; -import { SIGNALS_ID, DEFAULT_SEARCH_AFTER_PAGE_SIZE } from '../../../../common/constants'; +import { APP_ID, SIGNALS_ID, DEFAULT_SEARCH_AFTER_PAGE_SIZE } from '../../../../common/constants'; import { isJobStarted, isMlRule } from '../../../../common/machine_learning/helpers'; import { SetupPlugins } from '../../../plugin'; @@ -55,7 +55,7 @@ export const signalRulesAlertType = ({ validate: { params: signalParamsSchema(), }, - producer: 'siem', + producer: APP_ID, async executor({ previousStartedAt, alertId, From 399493b96c75225a4645180c7be50c1a5bbf96f2 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 10 Jun 2020 15:10:30 +0100 Subject: [PATCH 030/126] added readme --- x-pack/plugins/alerts/README.md | 103 +++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index 811478426a8d3..f77a11f9e489e 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -18,6 +18,7 @@ Table of Contents - [Methods](#methods) - [Executor](#executor) - [Example](#example) + - [Role Based Access-Control](#role-based-access-control) - [Alert Navigation](#alert-navigation) - [RESTful API](#restful-api) - [`POST /api/alerts/alert`: Create alert](#post-apialert-create-alert) @@ -58,7 +59,8 @@ A Kibana alert detects a condition and executes one or more actions when that co ## Usage 1. Develop and register an alert type (see alert types -> example). -2. Create an alert using the RESTful API (see alerts -> create). +2. Configure feature level privileges using RBAC +3. Create an alert using the RESTful API (see alerts -> create). ## Limitations @@ -293,6 +295,105 @@ server.newPlatform.setup.plugins.alerts.registerType({ }); ``` +## Role Based Access-Control +Once you have registered your AlertType, you need to grant your users privileges to use it. +When registering a feature in Kibana you can specify multiple types of privileges which are granted to users when they're assigned certain roles. + +Assuming your feature introduces its own AlertTypes, you'll want to control: +- Which roles have all/read privileges for these AlertTypes when they're inside the feature +- Which roles have all/read privileges for these AlertTypes when they're outside the feature (in another feature or in the global alerts management) + +In addition, when users are inside your feature you might want to grant them access to AlertTypes from other features, such as built-in AlertTypes or AlertTypes provided by other features. + +You can control all of these abilities by assigning privileges to the Alerting Framework from within your own feature, for example: + +``` +features.registerFeature({ + id: 'my-application-id', + name: 'My Application', + app: [], + privileges: { + all: { + alerting: { + all: [ + // grant `all` over our own types + 'my-application-id.my-alert-type', + 'my-application-id.my-restricted-alert-type', + // grant `all` over the built-in IndexThreshold + '.index-threshold', + // grant `all` over Uptime's TLS AlertType + 'xpack.uptime.alerts.actionGroups.tls'], + }, + }, + read: { + alerting: { + read: [ + // grant `read` over our own type + 'my-application-id.my-alert-type', + // grant `read` over the built-in IndexThreshold + '.index-threshold', + // grant `read` over Uptime's TLS AlertType + 'xpack.uptime.alerts.actionGroups.tls'], + }, + }, + }, + }); +``` + +In this example we can see the following: +- Our feature grants any user who's assigned the `all` role in our feature the `all` role in the Alerting framework over every alert of the `my-application-id.my-alert-type` type which is created _inside_ the feature. What that means is that this privilege will allow the user to execute any of the `all` operations (listed below) on these alerts as long as their `consumer` is `my-application-id`. Below that you'll notice we've done the same with the `read` role, which is grants the Alerting Framework's `read` role privileges over these very same alerts. +- In addition, our feature grants the same privileges over any alert of type `my-application-id.my-restricted-alert-type`, which is another hypothetical alertType registered by this feature. It's worth noting though that this type has been omitted from the `read` role. What this means is that only users with the `all` role will be able to interact with alerts of this type. +- Next, lets look at the `.index-threshold` and `xpack.uptime.alerts.actionGroups.tls` types. These have been specified in both `read` and `all`, which means that all the users in the feature will gain privileges over alerts of these types (as long as their `consumer` is `my-application-id`). The difference between these two and the previous two is that they are _produced_ by other features! `.index-threshold` is a built-in type, provided by the framework, and specifying it here is all you need in order to grant privileges to use this type. On the other hand, `xpack.uptime.alerts.actionGroups.tls` is an AlertType provided by the _Uptime_ feature. Specifying this type here tells the Alerting Framework that as far as the `my-application-id` feature is concerned, the user is privileged to use this type (with `all` and `read` applied), but that isn't enough. Using another feature's AlertType is only possible if both the producer of the AlertType, and the consumer of the AlertType, explicitly grant privileges to do so. In this case, the _Uptime_ feature would have to explicitly add these privileges to a role and this role would have to be granted to this user. + +It's important to note that any role can be granted a mix of `all` and `read` privileges accross multiple type, for example: + +``` +features.registerFeature({ + id: 'my-application-id', + name: 'My Application', + app: [], + privileges: { + all: { + alerting: { + all: ['my-application-id.my-alert-type', 'my-application-id.my-restricted-alert-type'], + }, + }, + read: { + alerting: { + all:['my-application-id.my-alert-type'] + read: ['my-application-id.my-restricted-alert-type'], + }, + }, + }, + }); +``` + +In the above example, you note that instead of denying users with the `read` role any access to the `my-application-id.my-restricted-alert-type` type, we've decided that these users _should_ be granted `read` privileges over the _resitricted_ AlertType. +As part of that same change, we also decided that not only should they be allowed to `read` the _restricted_ AlertType, but actually, despite having `read` privileges to the feature as a whole, we do actualyl want to allow them to create our basic 'my-application-id.my-alert-type' AlertType, as we consider it an extension of _reading_ data in our feature, rather than _writing_ it. + +### `read` privileges vs. `all` privileges +When a user is granted the `read` role in the Alerting Framework, they will be able to execute the following api calls: +- get +- getAlertState +- find + +When a user is granted the `all` role in the Alerting Framework, they will be able to execute al lof the `read` privileged api calls, but in addition they'll be granted the following calls: +- `create` +- `delete` +- `update` +- `enable` +- `disable` +- `updateApiKey` +- `muteAll` +- `unmuteAll` +- `muteInstance` +- `unmuteInstance` + +Finally, all users, whether they're granted any role or not, are privileged to call the following: +- `listAlertTypes`, but the output is limited to displaying the AlertTypes the user is perivileged to `get` + +Attempting to execute any operation the user isn't privileged to execute will result in an Authorization error throws by the AlertsClient. + ## Alert Navigation When registering an Alert Type, you'll likely want to provide a way of viewing alerts of that type within your own plugin, or perhaps you want to provide a view for all alerts created from within your solution within your own UI. From 8ecba6253cfe81e8de81999f8b26025820d0cfd0 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 10 Jun 2020 15:11:56 +0100 Subject: [PATCH 031/126] removed unused export --- x-pack/plugins/alerts/server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/alerts/server/index.ts b/x-pack/plugins/alerts/server/index.ts index 727e38d9ba56b..515de771e7d6b 100644 --- a/x-pack/plugins/alerts/server/index.ts +++ b/x-pack/plugins/alerts/server/index.ts @@ -21,7 +21,7 @@ export { PartialAlert, } from './types'; export { PluginSetupContract, PluginStartContract } from './plugin'; -export { FindOptions, FindResult } from './alerts_client'; +export { FindResult } from './alerts_client'; export { AlertInstance } from './alert_instance'; export { parseDuration } from './lib'; From e4e659093d65ee8ad21297568af2f3e6d1ebc281 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 11 Jun 2020 11:49:45 +0100 Subject: [PATCH 032/126] added audit logging --- .../alerts/server/alerts_client.test.ts | 4 +- x-pack/plugins/alerts/server/alerts_client.ts | 4 +- .../server/alerts_client_factory.test.ts | 21 +- .../alerts/server/alerts_client_factory.ts | 7 +- .../alerts_authorization.mock.ts | 0 .../alerts_authorization.test.ts | 159 +++++++++++- .../alerts_authorization.ts | 126 +++++++--- .../server/authorization/audit_logger.mock.ts | 23 ++ .../server/authorization/audit_logger.test.ts | 227 ++++++++++++++++++ .../server/authorization/audit_logger.ts | 94 ++++++++ x-pack/plugins/alerts/server/feature.ts | 5 +- x-pack/plugins/alerts/server/routes/find.ts | 2 +- x-pack/plugins/security/server/index.ts | 1 - 13 files changed, 617 insertions(+), 56 deletions(-) rename x-pack/plugins/alerts/server/{ => authorization}/alerts_authorization.mock.ts (100%) rename x-pack/plugins/alerts/server/{ => authorization}/alerts_authorization.test.ts (83%) rename x-pack/plugins/alerts/server/{ => authorization}/alerts_authorization.ts (63%) create mode 100644 x-pack/plugins/alerts/server/authorization/audit_logger.mock.ts create mode 100644 x-pack/plugins/alerts/server/authorization/audit_logger.test.ts create mode 100644 x-pack/plugins/alerts/server/authorization/audit_logger.ts diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 33c5d4a3099ee..63a781b82c8c8 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -9,13 +9,13 @@ import { AlertsClient, CreateOptions, ConstructorOptions } from './alerts_client import { savedObjectsClientMock, loggingServiceMock } from '../../../../src/core/server/mocks'; import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; -import { alertsAuthorizationMock } from './alerts_authorization.mock'; +import { alertsAuthorizationMock } from './authorization/alerts_authorization.mock'; import { TaskStatus } from '../../task_manager/server'; import { IntervalSchedule } from './types'; import { resolvable } from './test_utils'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { actionsClientMock } from '../../actions/server/mocks'; -import { AlertsAuthorization } from './alerts_authorization'; +import { AlertsAuthorization } from './authorization/alerts_authorization'; const taskManager = taskManagerMock.start(); const alertTypeRegistry = alertTypeRegistryMock.create(); diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 0d35bc7a85f5e..8eef472590b9b 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -36,7 +36,7 @@ import { TaskManagerStartContract } from '../../task_manager/server'; import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists'; import { RegistryAlertType } from './alert_type_registry'; -import { AlertsAuthorization } from './alerts_authorization'; +import { AlertsAuthorization } from './authorization/alerts_authorization'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; @@ -69,7 +69,7 @@ export interface MuteOptions extends IndexType { alertInstanceId: string; } -interface FindOptions extends IndexType { +export interface FindOptions extends IndexType { perPage?: number; page?: number; search?: string; diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 5253e38c20dac..9dd02f41be68e 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -19,9 +19,12 @@ import { AuthenticatedUser } from '../../security/public'; import { securityMock } from '../../security/server/mocks'; import { actionsMock } from '../../actions/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; +import { AuditLogger } from '../../security/server'; +import { AlertsFeatureId } from '../common'; jest.mock('./alerts_client'); -jest.mock('./alerts_authorization'); +jest.mock('./authorization/alerts_authorization'); +jest.mock('./authorization/audit_logger'); const savedObjectsClient = savedObjectsClientMock.create(); const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(); @@ -65,8 +68,14 @@ test('creates an alerts client with proper constructor arguments when security i factory.initialize({ securityPluginSetup, ...alertsClientFactoryParams }); const request = KibanaRequest.from(fakeRequest); + const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); savedObjectsService.getScopedClient.mockReturnValue(savedObjectsClient); + const logger = { + log: jest.fn(), + } as jest.Mocked; + securityPluginSetup.audit.getLogger.mockReturnValue(logger); + factory.create(request, savedObjectsService); expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, { @@ -74,14 +83,18 @@ test('creates an alerts client with proper constructor arguments when security i includedHiddenTypes: ['alert'], }); - const { AlertsAuthorization } = jest.requireMock('./alerts_authorization'); + const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); expect(AlertsAuthorization).toHaveBeenCalledWith({ request, authorization: securityPluginSetup.authz, alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, features: alertsClientFactoryParams.features, + auditLogger: expect.any(AlertsAuthorizationAuditLogger), }); + expect(AlertsAuthorizationAuditLogger).toHaveBeenCalledWith(logger); + expect(securityPluginSetup.audit.getLogger).toHaveBeenCalledWith(AlertsFeatureId); + expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ unsecuredSavedObjectsClient: savedObjectsClient, authorization: expect.any(AlertsAuthorization), @@ -112,12 +125,14 @@ test('creates an alerts client with proper constructor arguments', async () => { includedHiddenTypes: ['alert'], }); - const { AlertsAuthorization } = jest.requireMock('./alerts_authorization'); + const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization'); + const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); expect(AlertsAuthorization).toHaveBeenCalledWith({ request, authorization: undefined, alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, features: alertsClientFactoryParams.features, + auditLogger: expect.any(AlertsAuthorizationAuditLogger), }); expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index b3024ca03d566..86379f00b04ac 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -6,13 +6,15 @@ import { PluginStartContract as ActionsPluginStartContract } from '../../actions/server'; import { AlertsClient } from './alerts_client'; +import { AlertsFeatureId } from '../common'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; import { KibanaRequest, Logger, SavedObjectsServiceStart } from '../../../../src/core/server'; import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../security/server'; import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../task_manager/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; -import { AlertsAuthorization } from './alerts_authorization'; +import { AlertsAuthorization } from './authorization/alerts_authorization'; +import { AlertsAuthorizationAuditLogger } from './authorization/audit_logger'; export interface AlertsClientFactoryOpts { logger: Logger; @@ -62,6 +64,9 @@ export class AlertsClientFactory { request, alertTypeRegistry: this.alertTypeRegistry, features: features!, + auditLogger: new AlertsAuthorizationAuditLogger( + securityPluginSetup?.audit.getLogger(AlertsFeatureId) + ), }); return new AlertsClient({ diff --git a/x-pack/plugins/alerts/server/alerts_authorization.mock.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts similarity index 100% rename from x-pack/plugins/alerts/server/alerts_authorization.mock.ts rename to x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts diff --git a/x-pack/plugins/alerts/server/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts similarity index 83% rename from x-pack/plugins/alerts/server/alerts_authorization.test.ts rename to x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 3117cbd8429a1..7bfc61242f80c 100644 --- a/x-pack/plugins/alerts/server/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -4,16 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ import { KibanaRequest } from 'kibana/server'; -import { alertTypeRegistryMock } from './alert_type_registry.mock'; -import { securityMock } from '../../../plugins/security/server/mocks'; -import { PluginStartContract as FeaturesStartContract, Feature } from '../../features/server'; -import { featuresPluginMock } from '../../features/server/mocks'; +import { alertTypeRegistryMock } from '../alert_type_registry.mock'; +import { securityMock } from '../../../../plugins/security/server/mocks'; +import { PluginStartContract as FeaturesStartContract, Feature } from '../../../features/server'; +import { featuresPluginMock } from '../../../features/server/mocks'; import { AlertsAuthorization } from './alerts_authorization'; +import { alertsAuthorizationAuditLoggerMock } from './audit_logger.mock'; +import { AlertsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; const alertTypeRegistry = alertTypeRegistryMock.create(); const features: jest.Mocked = featuresPluginMock.createStart(); const request = {} as KibanaRequest; +const auditLogger = alertsAuthorizationAuditLoggerMock.create(); +const realAuditLogger = new AlertsAuthorizationAuditLogger(); + const mockAuthorizationAction = (type: string, app: string, operation: string) => `${type}/${app}/${operation}`; function mockAuthorization() { @@ -60,6 +65,15 @@ const myOtherAppFeature = mockFeature('myOtherApp', 'myType', ['alerts']); beforeEach(() => { jest.resetAllMocks(); + auditLogger.alertsAuthorizationFailure.mockImplementation((username, ...args) => + realAuditLogger.getAuthorizationMessage(AuthorizationResult.Unauthorized, ...args) + ); + auditLogger.alertsAuthorizationSuccess.mockImplementation((username, ...args) => + realAuditLogger.getAuthorizationMessage(AuthorizationResult.Authorized, ...args) + ); + auditLogger.alertsUnscopedAuthorizationFailure.mockImplementation( + (username, operation) => `Unauthorized ${username}/${operation}` + ); alertTypeRegistry.get.mockImplementation((id) => ({ id, name: 'My Alert Type', @@ -77,6 +91,7 @@ describe('ensureAuthorized', () => { request, alertTypeRegistry, features, + auditLogger, }); await alertAuthorization.ensureAuthorized('myType', 'myApp', 'create'); @@ -95,9 +110,14 @@ describe('ensureAuthorized', () => { authorization, alertTypeRegistry, features, + auditLogger, }); - checkPrivileges.mockResolvedValueOnce({ hasAllRequested: true, privileges: [] }); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: [], + }); await alertAuthorization.ensureAuthorized('myType', 'myApp', 'create'); @@ -107,6 +127,16 @@ describe('ensureAuthorized', () => { expect(checkPrivileges).toHaveBeenCalledWith([ mockAuthorizationAction('myType', 'myApp', 'create'), ]); + + expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myType", + 0, + "myApp", + "create", + ] + `); }); test('ensures the user has privileges to execute the specified type and operation without consumer when consumer is alerts', async () => { @@ -120,9 +150,14 @@ describe('ensureAuthorized', () => { authorization, alertTypeRegistry, features, + auditLogger, }); - checkPrivileges.mockResolvedValueOnce({ hasAllRequested: true, privileges: [] }); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: [], + }); await alertAuthorization.ensureAuthorized('myType', 'alerts', 'create'); @@ -132,6 +167,16 @@ describe('ensureAuthorized', () => { expect(checkPrivileges).toHaveBeenCalledWith([ mockAuthorizationAction('myType', 'myApp', 'create'), ]); + + expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myType", + 0, + "alerts", + "create", + ] + `); }); test('ensures the user has privileges to execute the specified type, operation and producer when producer is different from consumer', async () => { @@ -140,13 +185,18 @@ describe('ensureAuthorized', () => { typeof authorization.checkPrivilegesDynamicallyWithRequest >> = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ hasAllRequested: true, privileges: [] }); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: [], + }); const alertAuthorization = new AlertsAuthorization({ request, authorization, alertTypeRegistry, features, + auditLogger, }); await alertAuthorization.ensureAuthorized('myType', 'myOtherApp', 'create'); @@ -163,6 +213,16 @@ describe('ensureAuthorized', () => { mockAuthorizationAction('myType', 'myOtherApp', 'create'), mockAuthorizationAction('myType', 'myApp', 'create'), ]); + + expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myType", + 0, + "myOtherApp", + "create", + ] + `); }); test('throws if user lacks the required privieleges for the consumer', async () => { @@ -176,9 +236,11 @@ describe('ensureAuthorized', () => { authorization, alertTypeRegistry, features, + auditLogger, }); checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', hasAllRequested: false, privileges: [ { @@ -197,6 +259,16 @@ describe('ensureAuthorized', () => { ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` ); + + expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myType", + 0, + "myOtherApp", + "create", + ] + `); }); test('throws if user lacks the required privieleges for the producer', async () => { @@ -210,9 +282,11 @@ describe('ensureAuthorized', () => { authorization, alertTypeRegistry, features, + auditLogger, }); checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', hasAllRequested: false, privileges: [ { @@ -231,6 +305,16 @@ describe('ensureAuthorized', () => { ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unauthorized to create a \\"myType\\" alert by \\"myApp\\""` ); + + expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myType", + 1, + "myApp", + "create", + ] + `); }); test('throws if user lacks the required privieleges for both consumer and producer', async () => { @@ -244,9 +328,11 @@ describe('ensureAuthorized', () => { authorization, alertTypeRegistry, features, + auditLogger, }); checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', hasAllRequested: false, privileges: [ { @@ -265,6 +351,16 @@ describe('ensureAuthorized', () => { ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` ); + + expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myType", + 0, + "myOtherApp", + "create", + ] + `); }); }); @@ -292,6 +388,7 @@ describe('getFindAuthorizationFilter', () => { request, alertTypeRegistry, features, + auditLogger, }); const { @@ -310,28 +407,36 @@ describe('getFindAuthorizationFilter', () => { typeof authorization.checkPrivilegesDynamicallyWithRequest >> = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ hasAllRequested: true, privileges: [] }); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: [], + }); const alertAuthorization = new AlertsAuthorization({ request, authorization, alertTypeRegistry, features, + auditLogger, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); expect((await alertAuthorization.getFindAuthorizationFilter()).filter).toMatchInlineSnapshot( `"((alert.attributes.alertTypeId:myAppAlertType and (alert.attributes.consumer:alerts or alert.attributes.consumer:myApp or alert.attributes.consumer:myOtherApp)) or (alert.attributes.alertTypeId:alertingAlertType and (alert.attributes.consumer:alerts or alert.attributes.consumer:myApp or alert.attributes.consumer:myOtherApp)))"` ); + + expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); }); - test('creates a `ensureAlertTypeIsAuthorized` function which throws if type is unauthorized', async () => { + test('creates an `ensureAlertTypeIsAuthorized` function which throws if type is unauthorized', async () => { const authorization = mockAuthorization(); const checkPrivileges: jest.MockedFunction> = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', hasAllRequested: false, privileges: [ { @@ -366,24 +471,36 @@ describe('getFindAuthorizationFilter', () => { authorization, alertTypeRegistry, features, + auditLogger, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); - expect(() => { + await expect(() => { ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); }).toThrowErrorMatchingInlineSnapshot( - `"Unauthorized to find \\"myAppAlertType\\" alerts under myOtherApp"` + `"Unauthorized to find a \\"myAppAlertType\\" alert for \\"myOtherApp\\""` ); + + expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myAppAlertType", + 0, + "myOtherApp", + "find", + ] + `); }); - test('creates a `ensureAlertTypeIsAuthorized` function which is no-op if type is authorized', async () => { + test('creates an `ensureAlertTypeIsAuthorized` function which is no-op if type is authorized', async () => { const authorization = mockAuthorization(); const checkPrivileges: jest.MockedFunction> = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', hasAllRequested: false, privileges: [ { @@ -418,13 +535,24 @@ describe('getFindAuthorizationFilter', () => { authorization, alertTypeRegistry, features, + auditLogger, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); - expect(() => { + await expect(() => { ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); }).not.toThrow(); + + expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myAppAlertType", + 0, + "myOtherApp", + "find", + ] + `); }); }); @@ -452,6 +580,7 @@ describe('checkAlertTypeAuthorization', () => { request, alertTypeRegistry, features, + auditLogger, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -499,6 +628,7 @@ describe('checkAlertTypeAuthorization', () => { >> = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', hasAllRequested: false, privileges: [ { @@ -533,6 +663,7 @@ describe('checkAlertTypeAuthorization', () => { authorization, alertTypeRegistry, features, + auditLogger, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -579,6 +710,7 @@ describe('checkAlertTypeAuthorization', () => { >> = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', hasAllRequested: false, privileges: [ { @@ -613,6 +745,7 @@ describe('checkAlertTypeAuthorization', () => { authorization, alertTypeRegistry, features, + auditLogger, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); diff --git a/x-pack/plugins/alerts/server/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts similarity index 63% rename from x-pack/plugins/alerts/server/alerts_authorization.ts rename to x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 89af132ec8a22..a48a7803d1067 100644 --- a/x-pack/plugins/alerts/server/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -7,11 +7,12 @@ import Boom from 'boom'; import { pluck, mapValues } from 'lodash'; import { KibanaRequest } from 'src/core/server'; -import { AlertsFeatureId } from '../common'; -import { AlertTypeRegistry } from './types'; -import { SecurityPluginSetup } from '../../security/server'; -import { RegistryAlertType } from './alert_type_registry'; -import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; +import { AlertsFeatureId } from '../../common'; +import { AlertTypeRegistry } from '../types'; +import { SecurityPluginSetup } from '../../../security/server'; +import { RegistryAlertType } from '../alert_type_registry'; +import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; +import { AlertsAuthorizationAuditLogger, ScopeType, AuthorizationResult } from './audit_logger'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; @@ -21,6 +22,7 @@ export interface ConstructorOptions { alertTypeRegistry: AlertTypeRegistry; request: KibanaRequest; features: FeaturesPluginStart; + auditLogger: AlertsAuthorizationAuditLogger; authorization?: SecurityPluginSetup['authz']; } @@ -29,12 +31,20 @@ export class AlertsAuthorization { private readonly features: FeaturesPluginStart; private readonly request: KibanaRequest; private readonly authorization?: SecurityPluginSetup['authz']; + private readonly auditLogger: AlertsAuthorizationAuditLogger; - constructor({ alertTypeRegistry, request, authorization, features }: ConstructorOptions) { + constructor({ + alertTypeRegistry, + request, + authorization, + features, + auditLogger, + }: ConstructorOptions) { this.request = request; this.authorization = authorization; this.features = features; this.alertTypeRegistry = alertTypeRegistry; + this.auditLogger = auditLogger; } public async ensureAuthorized(alertTypeId: string, consumer: string, operation: string) { @@ -51,7 +61,7 @@ export class AlertsAuthorization { const shouldAuthorizeConsumer = consumer !== AlertsFeatureId; const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested, privileges } = await checkPrivileges( + const { hasAllRequested, username, privileges } = await checkPrivileges( shouldAuthorizeConsumer && consumer !== alertType.producer ? [ // check for access at consumer level @@ -66,7 +76,15 @@ export class AlertsAuthorization { ] ); - if (!hasAllRequested) { + if (hasAllRequested) { + this.auditLogger.alertsAuthorizationSuccess( + username, + alertTypeId, + ScopeType.Consumer, + consumer, + operation + ); + } else { const authorizedPrivileges = pluck( privileges.filter((privilege) => privilege.authorized), 'privilege' @@ -76,12 +94,19 @@ export class AlertsAuthorization { (privilege) => !authorizedPrivileges.includes(privilege) ); + const [unauthorizedScopeType, unauthorizedScope] = + shouldAuthorizeConsumer && unauthorizedScopes.consumer + ? [ScopeType.Consumer, consumer] + : [ScopeType.Producer, alertType.producer]; + throw Boom.forbidden( - `Unauthorized to ${operation} a "${alertTypeId}" alert ${ - shouldAuthorizeConsumer && unauthorizedScopes.consumer - ? `for "${consumer}"` - : `by "${alertType.producer}"` - }` + this.auditLogger.alertsAuthorizationFailure( + username, + alertTypeId, + unauthorizedScopeType, + unauthorizedScope, + operation + ) ); } } @@ -92,13 +117,15 @@ export class AlertsAuthorization { ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => void; }> { if (this.authorization) { - const authorizedAlertTypes = await this.checkAlertTypeAuthorization( + const { username, authorizedAlertTypes } = await this.augmentAlertTypesWithAuthorization( this.alertTypeRegistry.list(), 'find' ); if (!authorizedAlertTypes.size) { - throw Boom.forbidden(`Unauthorized to find a any alert types`); + throw Boom.forbidden( + this.auditLogger.alertsUnscopedAuthorizationFailure(username!, 'find') + ); } const authorizedAlertTypeIdsToConsumers = new Set( @@ -114,7 +141,23 @@ export class AlertsAuthorization { filter: `(${this.asFiltersByAlertTypeAndConsumer(authorizedAlertTypes).join(' or ')})`, ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => { if (!authorizedAlertTypeIdsToConsumers.has(`${alertTypeId}/${consumer}`)) { - throw Boom.forbidden(`Unauthorized to find "${alertTypeId}" alerts under ${consumer}`); + throw Boom.forbidden( + this.auditLogger.alertsAuthorizationFailure( + username!, + alertTypeId, + ScopeType.Consumer, + consumer, + 'find' + ) + ); + } else { + this.auditLogger.alertsAuthorizationSuccess( + username!, + alertTypeId, + ScopeType.Consumer, + consumer, + 'find' + ); } }, }; @@ -128,10 +171,27 @@ export class AlertsAuthorization { alertTypes: Set, operation: string ): Promise> { - const featuresIds = this.features.getFeatures().map((feature) => feature.id); + const { authorizedAlertTypes } = await this.augmentAlertTypesWithAuthorization( + alertTypes, + operation + ); + return authorizedAlertTypes; + } + private async augmentAlertTypesWithAuthorization( + alertTypes: Set, + operation: string + ): Promise<{ + username?: string; + hasAllRequested: boolean; + authorizedAlertTypes: Set; + }> { + const featuresIds = this.features.getFeatures().map((feature) => feature.id); if (!this.authorization) { - return this.augmentWithAuthorizedConsumers(alertTypes, featuresIds); + return { + hasAllRequested: true, + authorizedAlertTypes: this.augmentWithAuthorizedConsumers(alertTypes, featuresIds), + }; } else { const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( this.request @@ -154,22 +214,26 @@ export class AlertsAuthorization { } } - const { hasAllRequested, privileges } = await checkPrivileges([ + const { username, hasAllRequested, privileges } = await checkPrivileges([ ...privilegeToAlertType.keys(), ]); - return hasAllRequested - ? // has access to all features - this.augmentWithAuthorizedConsumers(alertTypes, featuresIds) - : // only has some of the required privileges - privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { - if (authorized && privilegeToAlertType.has(privilege)) { - const [alertType, consumer] = privilegeToAlertType.get(privilege)!; - alertType.authorizedConsumers.push(consumer); - authorizedAlertTypes.add(alertType); - } - return authorizedAlertTypes; - }, new Set()); + return { + username, + hasAllRequested, + authorizedAlertTypes: hasAllRequested + ? // has access to all features + this.augmentWithAuthorizedConsumers(alertTypes, featuresIds) + : // only has some of the required privileges + privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { + if (authorized && privilegeToAlertType.has(privilege)) { + const [alertType, consumer] = privilegeToAlertType.get(privilege)!; + alertType.authorizedConsumers.push(consumer); + authorizedAlertTypes.add(alertType); + } + return authorizedAlertTypes; + }, new Set()), + }; } } diff --git a/x-pack/plugins/alerts/server/authorization/audit_logger.mock.ts b/x-pack/plugins/alerts/server/authorization/audit_logger.mock.ts new file mode 100644 index 0000000000000..6b29eedac030b --- /dev/null +++ b/x-pack/plugins/alerts/server/authorization/audit_logger.mock.ts @@ -0,0 +1,23 @@ +/* + * 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 { AlertsAuthorizationAuditLogger } from './audit_logger'; + +const createAlertsAuthorizationAuditLoggerMock = () => { + const mocked = ({ + getAuthorizationMessage: jest.fn(), + alertsAuthorizationFailure: jest.fn(), + alertsUnscopedAuthorizationFailure: jest.fn(), + alertsAuthorizationSuccess: jest.fn(), + } as unknown) as jest.Mocked; + return mocked; +}; + +export const alertsAuthorizationAuditLoggerMock: { + create: () => jest.Mocked; +} = { + create: createAlertsAuthorizationAuditLoggerMock, +}; diff --git a/x-pack/plugins/alerts/server/authorization/audit_logger.test.ts b/x-pack/plugins/alerts/server/authorization/audit_logger.test.ts new file mode 100644 index 0000000000000..de302cb936779 --- /dev/null +++ b/x-pack/plugins/alerts/server/authorization/audit_logger.test.ts @@ -0,0 +1,227 @@ +/* + * 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 { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; + +const createMockAuditLogger = () => { + return { + log: jest.fn(), + }; +}; + +describe(`#constructor`, () => { + test('initializes a noop auditLogger if security logger is unavailable', () => { + const alertsAuditLogger = new AlertsAuthorizationAuditLogger(undefined); + + const username = 'foo-user'; + const alertTypeId = 'alert-type-id'; + const scopeType = ScopeType.Consumer; + const scope = 'myApp'; + const operation = 'create'; + expect(() => { + alertsAuditLogger.alertsAuthorizationFailure( + username, + alertTypeId, + scopeType, + scope, + operation + ); + + alertsAuditLogger.alertsAuthorizationSuccess( + username, + alertTypeId, + scopeType, + scope, + operation + ); + }).not.toThrow(); + }); +}); + +describe(`#alertsUnscopedAuthorizationFailure`, () => { + test('logs auth failure of operation', () => { + const auditLogger = createMockAuditLogger(); + const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); + const username = 'foo-user'; + const operation = 'create'; + + alertsAuditLogger.alertsUnscopedAuthorizationFailure(username, operation); + + expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alerts_unscoped_authorization_failure", + "foo-user Unauthorized to create any alert types", + Object { + "operation": "create", + "username": "foo-user", + }, + ] + `); + }); + + test('logs auth failure with producer scope', () => { + const auditLogger = createMockAuditLogger(); + const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); + const username = 'foo-user'; + const alertTypeId = 'alert-type-id'; + const scopeType = ScopeType.Producer; + const scope = 'myOtherApp'; + const operation = 'create'; + + alertsAuditLogger.alertsAuthorizationFailure( + username, + alertTypeId, + scopeType, + scope, + operation + ); + + expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alerts_authorization_failure", + "foo-user Unauthorized to create a \\"alert-type-id\\" alert by \\"myOtherApp\\"", + Object { + "alertTypeId": "alert-type-id", + "operation": "create", + "scope": "myOtherApp", + "scopeType": 1, + "username": "foo-user", + }, + ] + `); + }); +}); + +describe(`#alertsAuthorizationFailure`, () => { + test('logs auth failure with consumer scope', () => { + const auditLogger = createMockAuditLogger(); + const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); + const username = 'foo-user'; + const alertTypeId = 'alert-type-id'; + const scopeType = ScopeType.Consumer; + const scope = 'myApp'; + const operation = 'create'; + + alertsAuditLogger.alertsAuthorizationFailure( + username, + alertTypeId, + scopeType, + scope, + operation + ); + + expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alerts_authorization_failure", + "foo-user Unauthorized to create a \\"alert-type-id\\" alert for \\"myApp\\"", + Object { + "alertTypeId": "alert-type-id", + "operation": "create", + "scope": "myApp", + "scopeType": 0, + "username": "foo-user", + }, + ] + `); + }); + + test('logs auth failure with producer scope', () => { + const auditLogger = createMockAuditLogger(); + const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); + const username = 'foo-user'; + const alertTypeId = 'alert-type-id'; + const scopeType = ScopeType.Producer; + const scope = 'myOtherApp'; + const operation = 'create'; + + alertsAuditLogger.alertsAuthorizationFailure( + username, + alertTypeId, + scopeType, + scope, + operation + ); + + expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alerts_authorization_failure", + "foo-user Unauthorized to create a \\"alert-type-id\\" alert by \\"myOtherApp\\"", + Object { + "alertTypeId": "alert-type-id", + "operation": "create", + "scope": "myOtherApp", + "scopeType": 1, + "username": "foo-user", + }, + ] + `); + }); +}); + +describe(`#savedObjectsAuthorizationSuccess`, () => { + test('logs auth success with consumer scope', () => { + const auditLogger = createMockAuditLogger(); + const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); + const username = 'foo-user'; + const alertTypeId = 'alert-type-id'; + const scopeType = ScopeType.Consumer; + const scope = 'myApp'; + const operation = 'create'; + + alertsAuditLogger.alertsAuthorizationSuccess( + username, + alertTypeId, + scopeType, + scope, + operation + ); + + expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alerts_authorization_success", + "foo-user Authorized to create a \\"alert-type-id\\" alert for \\"myApp\\"", + Object { + "alertTypeId": "alert-type-id", + "operation": "create", + "scope": "myApp", + "scopeType": 0, + "username": "foo-user", + }, + ] + `); + }); + + test('logs auth success with producer scope', () => { + const auditLogger = createMockAuditLogger(); + const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); + const username = 'foo-user'; + const alertTypeId = 'alert-type-id'; + const scopeType = ScopeType.Producer; + const scope = 'myOtherApp'; + const operation = 'create'; + + alertsAuditLogger.alertsAuthorizationSuccess( + username, + alertTypeId, + scopeType, + scope, + operation + ); + + expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alerts_authorization_success", + "foo-user Authorized to create a \\"alert-type-id\\" alert by \\"myOtherApp\\"", + Object { + "alertTypeId": "alert-type-id", + "operation": "create", + "scope": "myOtherApp", + "scopeType": 1, + "username": "foo-user", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/alerts/server/authorization/audit_logger.ts b/x-pack/plugins/alerts/server/authorization/audit_logger.ts new file mode 100644 index 0000000000000..5d563bbd6db8d --- /dev/null +++ b/x-pack/plugins/alerts/server/authorization/audit_logger.ts @@ -0,0 +1,94 @@ +/* + * 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 { AuditLogger } from '../../../security/server'; + +export enum ScopeType { + Consumer, + Producer, +} + +export enum AuthorizationResult { + Unauthorized = 'Unauthorized', + Authorized = 'Authorized', +} + +export class AlertsAuthorizationAuditLogger { + private readonly auditLogger: AuditLogger; + + constructor(auditLogger: AuditLogger = { log() {} }) { + this.auditLogger = auditLogger; + } + + public getAuthorizationMessage( + authorizationResult: AuthorizationResult, + alertTypeId: string, + scopeType: ScopeType, + scope: string, + operation: string + ): string { + return `${authorizationResult} to ${operation} a "${alertTypeId}" alert ${ + scopeType === ScopeType.Consumer ? `for "${scope}"` : `by "${scope}"` + }`; + } + + public alertsAuthorizationFailure( + username: string, + alertTypeId: string, + scopeType: ScopeType, + scope: string, + operation: string + ): string { + const message = this.getAuthorizationMessage( + AuthorizationResult.Unauthorized, + alertTypeId, + scopeType, + scope, + operation + ); + this.auditLogger.log('alerts_authorization_failure', `${username} ${message}`, { + username, + alertTypeId, + scopeType, + scope, + operation, + }); + return message; + } + + public alertsUnscopedAuthorizationFailure(username: string, operation: string): string { + const message = `Unauthorized to ${operation} any alert types`; + this.auditLogger.log('alerts_unscoped_authorization_failure', `${username} ${message}`, { + username, + operation, + }); + return message; + } + + public alertsAuthorizationSuccess( + username: string, + alertTypeId: string, + scopeType: ScopeType, + scope: string, + operation: string + ): string { + const message = this.getAuthorizationMessage( + AuthorizationResult.Authorized, + alertTypeId, + scopeType, + scope, + operation + ); + this.auditLogger.log('alerts_authorization_success', `${username} ${message}`, { + username, + alertTypeId, + scopeType, + scope, + operation, + }); + return message; + } +} diff --git a/x-pack/plugins/alerts/server/feature.ts b/x-pack/plugins/alerts/server/feature.ts index 108e3e4300251..d38b0c25759f1 100644 --- a/x-pack/plugins/alerts/server/feature.ts +++ b/x-pack/plugins/alerts/server/feature.ts @@ -5,11 +5,12 @@ */ import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { IndexThresholdId } from '../../alerting_builtins/server'; +import { AlertsFeatureId } from '../common'; export function registerFeature(features: FeaturesPluginSetup) { features.registerFeature({ - id: 'alerts', - name: 'alerts', + id: AlertsFeatureId, + name: 'Alerts', app: [], privileges: { all: { diff --git a/x-pack/plugins/alerts/server/routes/find.ts b/x-pack/plugins/alerts/server/routes/find.ts index 632772eaddded..ef3b16dc9e517 100644 --- a/x-pack/plugins/alerts/server/routes/find.ts +++ b/x-pack/plugins/alerts/server/routes/find.ts @@ -16,7 +16,7 @@ import { LicenseState } from '../lib/license_state'; import { verifyApiAccess } from '../lib/license_api_access'; import { BASE_ALERT_API_PATH } from '../../common'; import { renameKeys } from './lib/rename_keys'; -import { FindOptions } from '..'; +import { FindOptions } from '../alerts_client'; // config definition const querySchema = schema.object({ diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index c7bd025838864..a0a06b537213d 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -30,7 +30,6 @@ export { export { AuditLogger } from './audit'; export { SecurityPluginSetup }; export { AuthenticatedUser } from '../common/model'; -export { Actions } from './authorization'; export const config: PluginConfigDescriptor> = { schema: ConfigSchema, From 08541d3176976d66d2c91e55cf2de1251dc6df2e Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 11 Jun 2020 12:14:41 +0100 Subject: [PATCH 033/126] added alerting to feature iterator --- .../authorization/alerts_authorization.ts | 2 +- .../feature_privilege_iterator.test.ts | 143 ++++++++++++++++++ .../feature_privilege_iterator.ts | 8 + 3 files changed, 152 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index a48a7803d1067..318be1033ff0d 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -12,7 +12,7 @@ import { AlertTypeRegistry } from '../types'; import { SecurityPluginSetup } from '../../../security/server'; import { RegistryAlertType } from '../alert_type_registry'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; -import { AlertsAuthorizationAuditLogger, ScopeType, AuthorizationResult } from './audit_logger'; +import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts index 485783253d29d..bb1f0c33fdee9 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts @@ -41,6 +41,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-type'], read: ['read-type'], }, + alerting: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, read: { @@ -54,6 +58,9 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['read-type'], }, + alerting: { + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -80,6 +87,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-type'], read: ['read-type'], }, + alerting: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -96,6 +107,9 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['read-type'], }, + alerting: { + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -118,6 +132,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-type'], read: ['read-type'], }, + alerting: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, read: { @@ -131,6 +149,9 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['read-type'], }, + alerting: { + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -158,6 +179,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-type'], read: ['read-type'], }, + alerting: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -181,6 +206,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-type'], read: ['read-type'], }, + alerting: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, read: { @@ -194,6 +223,9 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['read-type'], }, + alerting: { + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -218,6 +250,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-sub-type'], read: ['read-sub-type'], }, + alerting: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, ui: ['ui-sub-type'], }, ], @@ -247,6 +283,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-type'], read: ['read-type'], }, + alerting: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -263,6 +303,9 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['read-type'], }, + alerting: { + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -286,6 +329,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-type'], read: ['read-type'], }, + alerting: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, read: { @@ -299,6 +346,9 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['read-type'], }, + alerting: { + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -323,6 +373,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-sub-type'], read: ['read-sub-type'], }, + alerting: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, ui: ['ui-sub-type'], }, ], @@ -352,6 +406,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-type'], read: ['read-type'], }, + alerting: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -368,6 +426,9 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['read-type'], }, + alerting: { + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -391,6 +452,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-type'], read: ['read-type'], }, + alerting: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, read: { @@ -404,6 +469,9 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['read-type'], }, + alerting: { + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -429,6 +497,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-sub-type'], read: ['read-sub-type'], }, + alerting: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, ui: ['ui-sub-type'], }, ], @@ -459,6 +531,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-type', 'all-sub-type'], read: ['read-type', 'read-sub-type'], }, + alerting: { + all: ['alerting-all-type', 'alerting-all-sub-type'], + read: ['alerting-read-type', 'alerting-read-sub-type'], + }, ui: ['ui-action', 'ui-sub-type'], }, }, @@ -476,6 +552,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-sub-type'], read: ['read-type', 'read-sub-type'], }, + alerting: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-type', 'alerting-read-sub-type'], + }, ui: ['ui-action', 'ui-sub-type'], }, }, @@ -499,6 +579,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-type'], read: ['read-type'], }, + alerting: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, read: { @@ -512,6 +596,9 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['read-type'], }, + alerting: { + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -536,6 +623,9 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['read-type'], }, + alerting: { + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, ], @@ -565,6 +655,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-type'], read: ['read-type'], }, + alerting: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -581,6 +675,10 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['read-type'], }, + alerting: { + all: [], + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -604,6 +702,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-type'], read: ['read-type'], }, + alerting: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, read: { @@ -617,6 +719,9 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['read-type'], }, + alerting: { + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -642,6 +747,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-sub-type'], read: ['read-sub-type'], }, + alerting: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, ui: ['ui-sub-type'], }, ], @@ -672,6 +781,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-type', 'all-sub-type'], read: ['read-type', 'read-sub-type'], }, + alerting: { + all: ['alerting-all-type', 'alerting-all-sub-type'], + read: ['alerting-read-type', 'alerting-read-sub-type'], + }, ui: ['ui-action', 'ui-sub-type'], }, }, @@ -688,6 +801,9 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['read-type'], }, + alerting: { + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -737,6 +853,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-sub-type'], read: ['read-sub-type'], }, + alerting: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, ui: ['ui-sub-type'], }, ], @@ -767,6 +887,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-sub-type'], read: ['read-sub-type'], }, + alerting: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, ui: ['ui-sub-type'], }, }, @@ -784,6 +908,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-sub-type'], read: ['read-sub-type'], }, + alerting: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, ui: ['ui-sub-type'], }, }, @@ -807,6 +935,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-type'], read: ['read-type'], }, + alerting: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, read: { @@ -820,6 +952,9 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['read-type'], }, + alerting: { + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -867,6 +1002,10 @@ describe('featurePrivilegeIterator', () => { all: ['all-type'], read: ['read-type'], }, + alerting: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, @@ -883,6 +1022,10 @@ describe('featurePrivilegeIterator', () => { all: [], read: ['read-type'], }, + alerting: { + all: [], + read: ['alerting-read-type'], + }, ui: ['ui-action'], }, }, diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts index e239a6e280aec..afa0ffb87b6fa 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts @@ -72,6 +72,14 @@ function mergeWithSubFeatures( mergedConfig.savedObject.read, subFeaturePrivilege.savedObject.read ); + + mergedConfig.alerting = { + all: mergeArrays(mergedConfig.alerting?.all ?? [], subFeaturePrivilege.alerting?.all ?? []), + read: mergeArrays( + mergedConfig.alerting?.read ?? [], + subFeaturePrivilege.alerting?.read ?? [] + ), + }; } return mergedConfig; } From 87bd20610639f753b69c80a9b6259c2e0c2d9785 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 11 Jun 2020 12:49:15 +0100 Subject: [PATCH 034/126] added validation that alert type IDs dont contain invalid privilege charachters --- .../alerts/server/alert_type_registry.test.ts | 32 +++++++++++++++++++ .../alerts/server/alert_type_registry.ts | 17 +++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/alerts/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts index ae3633cdde62b..a1bc6c386033a 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts @@ -43,6 +43,38 @@ describe('has()', () => { }); describe('register()', () => { + test('throws if AlertType Id contains invalid characters', () => { + const alertType = { + id: 'test', + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + executor: jest.fn(), + producer: 'alerting', + }; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const registry = new AlertTypeRegistry(alertTypeRegistryParams); + + const invalidCharacters = [' ', ':', '*', '*', '/']; + for (const char of invalidCharacters) { + expect(() => registry.register({ ...alertType, id: `${alertType.id}${char}` })).toThrowError( + new Error(`expected AlertType Id not to include invalid character: ${char}`) + ); + } + + const [first, second] = invalidCharacters; + expect(() => + registry.register({ ...alertType, id: `${first}${alertType.id}${second}` }) + ).toThrowError( + new Error(`expected AlertType Id not to include invalid characters: ${first}, ${second}`) + ); + }); + test('registers the executor with the task manager', () => { const alertType = { id: 'test', diff --git a/x-pack/plugins/alerts/server/alert_type_registry.ts b/x-pack/plugins/alerts/server/alert_type_registry.ts index 300cfc5b5f549..4344ef6fc3118 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.ts @@ -6,6 +6,8 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import typeDetect from 'type-detect'; import { RunContext, TaskManagerSetupContract } from '../../task_manager/server'; import { TaskRunnerFactory } from './task_runner'; import { AlertType } from './types'; @@ -23,6 +25,19 @@ export interface RegistryAlertType id: string; } +const alertIdSchema = schema.string({ + validate(value: string): string | void { + if (typeof value !== 'string') { + return `expected AlertType Id of type [string] but got [${typeDetect(value)}]`; + } else if (!value.match(/^[a-zA-Z0-9_\-\.]*$/)) { + const invalid = value.match(/[^a-zA-Z0-9_\-\.]+/g)!; + return `expected AlertType Id not to include invalid character${ + invalid.length > 1 ? `s` : `` + }: ${invalid?.join(`, `)}`; + } + }, +}); + export class AlertTypeRegistry { private readonly taskManager: TaskManagerSetupContract; private readonly alertTypes: Map = new Map(); @@ -49,7 +64,7 @@ export class AlertTypeRegistry { ); } alertType.actionVariables = normalizedActionVariables(alertType.actionVariables); - this.alertTypes.set(alertType.id, { ...alertType }); + this.alertTypes.set(alertIdSchema.validate(alertType.id), { ...alertType }); this.taskManager.registerTaskDefinitions({ [`alerting:${alertType.id}`]: { title: alertType.name, From e1e560cfa744865a222b7df05cbdc087a862a2c3 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 11 Jun 2020 12:54:48 +0100 Subject: [PATCH 035/126] added comment around alert type ID char limitations --- x-pack/plugins/alerts/server/alert_type_registry.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/x-pack/plugins/alerts/server/alert_type_registry.ts b/x-pack/plugins/alerts/server/alert_type_registry.ts index 4344ef6fc3118..c466d0e96382c 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.ts @@ -25,6 +25,13 @@ export interface RegistryAlertType id: string; } +/** + * AlertType IDs are used as part of the authorization strings used to + * grant users privileged operations. There is a limited range of characters + * we can use in these auth strings, so we apply these same limitations to + * the AlertType Ids. + * If you wish to change this, please confer with the Kibana security team. + */ const alertIdSchema = schema.string({ validate(value: string): string | void { if (typeof value !== 'string') { From b012ae1bfd8b37c0caaf2f82ec2a90c2b8c2e855 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 11 Jun 2020 13:08:17 +0100 Subject: [PATCH 036/126] fixed tests --- .../security_and_spaces/tests/alerting/find.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 163f831ea6c78..d04f35458f30a 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 @@ -45,7 +45,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: `Unauthorized to find a any alert types`, + message: `Unauthorized to find any alert types`, statusCode: 403, }); break; @@ -136,7 +136,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: `Unauthorized to find a any alert types`, + message: `Unauthorized to find any alert types`, statusCode: 403, }); break; @@ -231,7 +231,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: `Unauthorized to find a any alert types`, + message: `Unauthorized to find any alert types`, statusCode: 403, }); break; @@ -300,7 +300,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: `Unauthorized to find a any alert types`, + message: `Unauthorized to find any alert types`, statusCode: 403, }); break; From 64e802bc2557c571bee49315619731c515247943 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 11 Jun 2020 15:07:25 +0100 Subject: [PATCH 037/126] fixed features unit tests --- .../server/__snapshots__/oss_features.test.ts.snap | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index 0999063945cb5..9c61bc0d3d943 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -55,6 +55,10 @@ exports[`buildOSSFeatures returns the dashboard feature augmented with appropria Array [ Object { "privilege": Object { + "alerting": Object { + "all": Array [], + "read": Array [], + }, "api": Array [], "app": Array [ "dashboards", @@ -179,6 +183,10 @@ exports[`buildOSSFeatures returns the discover feature augmented with appropriat Array [ Object { "privilege": Object { + "alerting": Object { + "all": Array [], + "read": Array [], + }, "api": Array [], "app": Array [ "discover", @@ -403,6 +411,10 @@ exports[`buildOSSFeatures returns the visualize feature augmented with appropria Array [ Object { "privilege": Object { + "alerting": Object { + "all": Array [], + "read": Array [], + }, "api": Array [], "app": Array [ "visualize", From f287766ea28cfa473c440d6df21f851c4776d29e Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 12 Jun 2020 10:28:13 +0100 Subject: [PATCH 038/126] added index threshold as authorized alert type in example plugin --- examples/alerting_example/server/plugin.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts index 9a93a6f8f4d6e..86772a79e669b 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/examples/alerting_example/server/plugin.ts @@ -23,6 +23,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../../x-pack/plug import { alertType as alwaysFiringAlert } from './alert_types/always_firing'; import { alertType as peopleInSpaceAlert } from './alert_types/astros'; +import { IndexThresholdId } from '../../../x-pack/plugins/alerting_builtins/server'; // this plugin's dependendencies export interface AlertingExampleDeps { @@ -42,7 +43,7 @@ export class AlertingExamplePlugin implements Plugin Date: Fri, 12 Jun 2020 11:09:09 +0100 Subject: [PATCH 039/126] fixed a bunch of styling changes and small fixes --- examples/alerting_example/server/plugin.ts | 5 +- x-pack/plugins/alerts/README.md | 83 ++++++++++--------- x-pack/plugins/alerts/common/index.ts | 2 +- .../alerts/server/alert_type_registry.test.ts | 22 +++++ .../alerts/server/alerts_client.test.ts | 6 +- x-pack/plugins/alerts/server/alerts_client.ts | 6 +- .../server/alerts_client_factory.test.ts | 4 +- .../alerts/server/alerts_client_factory.ts | 4 +- .../alerts_authorization.mock.ts | 2 +- .../alerts_authorization.test.ts | 8 +- .../authorization/alerts_authorization.ts | 6 +- x-pack/plugins/alerts/server/feature.ts | 9 +- x-pack/plugins/alerts/server/plugin.test.ts | 14 ---- .../sections/alert_form/alert_form.tsx | 4 +- .../alerts_list/components/alerts_list.tsx | 4 +- 15 files changed, 101 insertions(+), 78 deletions(-) diff --git a/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts index 86772a79e669b..2e4600bedab9e 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/examples/alerting_example/server/plugin.ts @@ -18,6 +18,7 @@ */ import { Plugin, CoreSetup } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; import { PluginSetupContract as AlertingSetup } from '../../../x-pack/plugins/alerts/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../x-pack/plugins/features/server'; @@ -38,7 +39,9 @@ export class AlertingExamplePlugin implements Plugin { ); }); + test('throws if AlertType Id isnt a string', () => { + const alertType = { + id: (123 as any) as string, + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + executor: jest.fn(), + producer: 'alerting', + }; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const registry = new AlertTypeRegistry(alertTypeRegistryParams); + + expect(() => registry.register(alertType)).toThrowError( + new Error(`expected value of type [string] but got [number]`) + ); + }); + test('registers the executor with the task manager', () => { const alertType = { id: 'test', diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 63a781b82c8c8..e6b461935a0f2 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -2185,7 +2185,7 @@ describe('find()', () => { ], }); alertTypeRegistry.list.mockReturnValue(listedTypes); - authorization.checkAlertTypeAuthorization.mockResolvedValue( + authorization.filterByAlertTypeAuthorization.mockResolvedValue( new Set([ { id: 'myType', @@ -3661,7 +3661,7 @@ describe('listAlertTypes', () => { test('should return a list of AlertTypes that exist in the registry', async () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - authorization.checkAlertTypeAuthorization.mockResolvedValue( + authorization.filterByAlertTypeAuthorization.mockResolvedValue( new Set([ { ...myAppAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, { ...alertingAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, @@ -3708,7 +3708,7 @@ describe('listAlertTypes', () => { authorizedConsumers: ['myApp'], }, ]); - authorization.checkAlertTypeAuthorization.mockResolvedValue(authorizedTypes); + authorization.filterByAlertTypeAuthorization.mockResolvedValue(authorizedTypes); expect(await alertsClient.listAlertTypes()).toEqual(authorizedTypes); }); diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 8eef472590b9b..50f9763248ced 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -172,9 +172,11 @@ export class AlertsClient { } public async create({ data, options }: CreateOptions): Promise { - // Throws an error if alert type isn't registered await this.authorization.ensureAuthorized(data.alertTypeId, data.consumer, 'create'); + + // Throws an error if alert type isn't registered const alertType = this.alertTypeRegistry.get(data.alertTypeId); + const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params); const username = await this.getUserName(); const createdAPIKey = data.enabled ? await this.createAPIKey() : null; @@ -674,7 +676,7 @@ export class AlertsClient { } public async listAlertTypes() { - return await this.authorization.checkAlertTypeAuthorization( + return await this.authorization.filterByAlertTypeAuthorization( this.alertTypeRegistry.list(), 'get' ); diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 19c91ae27da3d..bee74d4e4bec4 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -20,7 +20,7 @@ import { securityMock } from '../../security/server/mocks'; import { actionsMock } from '../../actions/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; import { AuditLogger } from '../../security/server'; -import { AlertsFeatureId } from '../common'; +import { ALERTS_FEATURE_ID } from '../common'; jest.mock('./alerts_client'); jest.mock('./authorization/alerts_authorization'); @@ -93,7 +93,7 @@ test('creates an alerts client with proper constructor arguments when security i }); expect(AlertsAuthorizationAuditLogger).toHaveBeenCalledWith(logger); - expect(securityPluginSetup.audit.getLogger).toHaveBeenCalledWith(AlertsFeatureId); + expect(securityPluginSetup.audit.getLogger).toHaveBeenCalledWith(ALERTS_FEATURE_ID); expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ unsecuredSavedObjectsClient: savedObjectsClient, diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index 86379f00b04ac..3e4133d83373d 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -6,7 +6,7 @@ import { PluginStartContract as ActionsPluginStartContract } from '../../actions/server'; import { AlertsClient } from './alerts_client'; -import { AlertsFeatureId } from '../common'; +import { ALERTS_FEATURE_ID } from '../common'; import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; import { KibanaRequest, Logger, SavedObjectsServiceStart } from '../../../../src/core/server'; import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../security/server'; @@ -65,7 +65,7 @@ export class AlertsClientFactory { alertTypeRegistry: this.alertTypeRegistry, features: features!, auditLogger: new AlertsAuthorizationAuditLogger( - securityPluginSetup?.audit.getLogger(AlertsFeatureId) + securityPluginSetup?.audit.getLogger(ALERTS_FEATURE_ID) ), }); diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts index f151a843aedeb..d7705f834ad41 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts @@ -12,7 +12,7 @@ export type AlertsAuthorizationMock = jest.Mocked; const createAlertsAuthorizationMock = () => { const mocked: AlertsAuthorizationMock = { ensureAuthorized: jest.fn(), - checkAlertTypeAuthorization: jest.fn(), + filterByAlertTypeAuthorization: jest.fn(), getFindAuthorizationFilter: jest.fn(), }; return mocked; diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 7bfc61242f80c..307490c96a0d3 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -556,7 +556,7 @@ describe('getFindAuthorizationFilter', () => { }); }); -describe('checkAlertTypeAuthorization', () => { +describe('filterByAlertTypeAuthorization', () => { const alertingAlertType = { actionGroups: [], actionVariables: undefined, @@ -585,7 +585,7 @@ describe('checkAlertTypeAuthorization', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); await expect( - alertAuthorization.checkAlertTypeAuthorization( + alertAuthorization.filterByAlertTypeAuthorization( new Set([myAppAlertType, alertingAlertType]), 'create' ) @@ -668,7 +668,7 @@ describe('checkAlertTypeAuthorization', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); await expect( - alertAuthorization.checkAlertTypeAuthorization( + alertAuthorization.filterByAlertTypeAuthorization( new Set([myAppAlertType, alertingAlertType]), 'create' ) @@ -750,7 +750,7 @@ describe('checkAlertTypeAuthorization', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); await expect( - alertAuthorization.checkAlertTypeAuthorization( + alertAuthorization.filterByAlertTypeAuthorization( new Set([myAppAlertType, alertingAlertType]), 'create' ) diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 318be1033ff0d..e9cea33ababaf 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -7,7 +7,7 @@ import Boom from 'boom'; import { pluck, mapValues } from 'lodash'; import { KibanaRequest } from 'src/core/server'; -import { AlertsFeatureId } from '../../common'; +import { ALERTS_FEATURE_ID } from '../../common'; import { AlertTypeRegistry } from '../types'; import { SecurityPluginSetup } from '../../../security/server'; import { RegistryAlertType } from '../alert_type_registry'; @@ -58,7 +58,7 @@ export class AlertsAuthorization { // We special case the Alerts Management `consumer` as we don't want to have to // manually authorize each alert type in the management UI - const shouldAuthorizeConsumer = consumer !== AlertsFeatureId; + const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID; const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); const { hasAllRequested, username, privileges } = await checkPrivileges( @@ -167,7 +167,7 @@ export class AlertsAuthorization { }; } - public async checkAlertTypeAuthorization( + public async filterByAlertTypeAuthorization( alertTypes: Set, operation: string ): Promise> { diff --git a/x-pack/plugins/alerts/server/feature.ts b/x-pack/plugins/alerts/server/feature.ts index d38b0c25759f1..e841852ecb8a2 100644 --- a/x-pack/plugins/alerts/server/feature.ts +++ b/x-pack/plugins/alerts/server/feature.ts @@ -3,14 +3,17 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { IndexThresholdId } from '../../alerting_builtins/server'; -import { AlertsFeatureId } from '../common'; +import { ALERTS_FEATURE_ID } from '../common'; export function registerFeature(features: FeaturesPluginSetup) { features.registerFeature({ - id: AlertsFeatureId, - name: 'Alerts', + id: ALERTS_FEATURE_ID, + name: i18n.translate('xpack.alerts.featureRegistry.alertsFeatureName', { + defaultMessage: 'Alerts', + }), app: [], privileges: { all: { diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index 1cea6778e7c42..56bf3a09dc06b 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -78,13 +78,6 @@ describe('Alerting Plugin', () => { ], } `); - expect(privileges?.read.alerting).toMatchInlineSnapshot(` - Object { - "read": Array [ - ".index-threshold", - ], - } - `); }); it('should grant global `read` priviliges to built in AlertTypes for anyone with `read` priviliges to alerts', async () => { @@ -114,13 +107,6 @@ describe('Alerting Plugin', () => { expect(features.registerFeature).toHaveBeenCalledTimes(1); const { privileges } = features.registerFeature.mock.calls[0][0]; - expect(privileges?.all.alerting).toMatchInlineSnapshot(` - Object { - "all": Array [ - ".index-threshold", - ], - } - `); expect(privileges?.read.alerting).toMatchInlineSnapshot(` Object { "read": Array [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 69f1b0a183766..32b1809609e43 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -38,7 +38,7 @@ import { AlertTypeModel, Alert, IErrorObject, AlertAction, AlertTypeIndex } from import { getTimeOptions } from '../../../common/lib/get_time_options'; import { useAlertsContext } from '../../context/alerts_context'; import { ActionForm } from '../action_connector_form'; -import { AlertsFeatureId } from '../../../../../alerts/common'; +import { ALERTS_FEATURE_ID } from '../../../../../alerts/common'; export function validateBaseProperties(alertObject: Alert) { const validationResult = { errors: {} }; @@ -169,7 +169,7 @@ export const AlertForm = ({ : null; const alertTypeRegistryList = - alert.consumer === AlertsFeatureId + alert.consumer === ALERTS_FEATURE_ID ? alertTypeRegistry .list() .filter( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 4c6e8d3984b01..9ce64d4796093 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -37,7 +37,7 @@ import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib import { routeToAlertDetails, DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation'; import { EmptyPrompt } from '../../../components/prompts/empty_prompt'; -import { AlertsFeatureId } from '../../../../../../alerts/common'; +import { ALERTS_FEATURE_ID } from '../../../../../../alerts/common'; const ENTER_KEY = 13; @@ -440,7 +440,7 @@ export const AlertsList: React.FunctionComponent = () => { }} > From 54ad8dd9aebdd0adb2b833e35ecaace5b6077107 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 12 Jun 2020 14:41:39 +0100 Subject: [PATCH 040/126] changed casing of const --- examples/alerting_example/server/plugin.ts | 6 +++--- x-pack/plugins/alerting_builtins/server/index.ts | 2 +- x-pack/plugins/alerts/server/feature.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts index 2e4600bedab9e..f6c0948a6c30c 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/examples/alerting_example/server/plugin.ts @@ -24,7 +24,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../../x-pack/plug import { alertType as alwaysFiringAlert } from './alert_types/always_firing'; import { alertType as peopleInSpaceAlert } from './alert_types/astros'; -import { IndexThresholdId } from '../../../x-pack/plugins/alerting_builtins/server'; +import { INDEX_THRESHOLD_ID } from '../../../x-pack/plugins/alerting_builtins/server'; // this plugin's dependendencies export interface AlertingExampleDeps { @@ -46,7 +46,7 @@ export class AlertingExamplePlugin implements Plugin new AlertingBuiltinsPlugin(ctx); diff --git a/x-pack/plugins/alerts/server/feature.ts b/x-pack/plugins/alerts/server/feature.ts index e841852ecb8a2..47c42bb9c7562 100644 --- a/x-pack/plugins/alerts/server/feature.ts +++ b/x-pack/plugins/alerts/server/feature.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import { IndexThresholdId } from '../../alerting_builtins/server'; +import { INDEX_THRESHOLD_ID } from '../../alerting_builtins/server'; import { ALERTS_FEATURE_ID } from '../common'; export function registerFeature(features: FeaturesPluginSetup) { @@ -18,7 +18,7 @@ export function registerFeature(features: FeaturesPluginSetup) { privileges: { all: { alerting: { - all: [IndexThresholdId], + all: [INDEX_THRESHOLD_ID], }, savedObject: { all: [], @@ -28,7 +28,7 @@ export function registerFeature(features: FeaturesPluginSetup) { }, read: { alerting: { - read: [IndexThresholdId], + read: [INDEX_THRESHOLD_ID], }, savedObject: { all: [], From 867b7c3bb3b1a2abf051f584f82d30af2cbb4f7a Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 15 Jun 2020 11:49:01 +0100 Subject: [PATCH 041/126] added support for fields in find --- .../alerts/server/alerts_client.test.ts | 55 +++++++++++++ x-pack/plugins/alerts/server/alerts_client.ts | 31 +++++--- .../tests/alerting/find.ts | 77 ++++++++++++++++++- 3 files changed, 153 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index e6b461935a0f2..e7ab67076f25d 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -1810,6 +1810,7 @@ describe('get()', () => { params: { bar: true, }, + createdAt: new Date().toISOString(), actions: [ { group: 'default', @@ -2164,6 +2165,7 @@ describe('find()', () => { params: { bar: true, }, + createdAt: new Date().toISOString(), actions: [ { group: 'default', @@ -2236,6 +2238,7 @@ describe('find()', () => { expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { + "fields": undefined, "type": "alert", }, ] @@ -2267,6 +2270,58 @@ describe('find()', () => { `"not authorized"` ); }); + + test('ensures authorization even when the fields required to authorize are omitted from the find', async () => { + const ensureAlertTypeIsAuthorized = jest.fn(); + authorization.getFindAuthorizationFilter.mockResolvedValue({ + filter: '', + ensureAlertTypeIsAuthorized, + }); + + unsecuredSavedObjectsClient.find.mockReset(); + unsecuredSavedObjectsClient.find.mockResolvedValue({ + total: 1, + per_page: 10, + page: 1, + saved_objects: [ + { + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + tags: ['myTag'], + }, + references: [], + }, + ], + }); + + const alertsClient = new AlertsClient(alertsClientParams); + expect(await alertsClient.find({ options: { fields: ['tags'] } })).toMatchInlineSnapshot(` + Object { + "data": Array [ + Object { + "actions": Array [], + "id": "1", + "schedule": undefined, + "tags": Array [ + "myTag", + ], + }, + ], + "page": 1, + "perPage": 10, + "total": 1, + } + `); + + expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({ + fields: ['tags', 'alertTypeId', 'consumer'], + type: 'alert', + }); + expect(ensureAlertTypeIsAuthorized).toHaveBeenCalledWith('myType', 'myApp'); + }); }); }); diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 50f9763248ced..8d1f6183f0aa0 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { omit, isEqual, pluck } from 'lodash'; +import { omit, isEqual, pluck, unique, pick } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Logger, @@ -93,7 +93,7 @@ export interface FindResult { page: number; perPage: number; total: number; - data: SanitizedAlert[]; + data: Array>; } export interface CreateOptions { @@ -250,7 +250,9 @@ export class AlertsClient { } } - public async find({ options = {} }: { options?: FindOptions } = {}): Promise { + public async find({ + options: { fields, ...options } = {}, + }: { options?: FindOptions } = {}): Promise { const { filter: authorizationFilter, ensureAlertTypeIsAuthorized, @@ -269,18 +271,25 @@ export class AlertsClient { saved_objects: data, } = await this.unsecuredSavedObjectsClient.find({ ...options, + fields: fields ? this.includeFieldsRequiredForAuthentication(fields) : fields, type: 'alert', }); - return { + const x = { page, perPage, total, data: data.map(({ id, attributes, updated_at, references }) => { ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer); - return this.getAlertFromRaw(id, attributes, updated_at, references); + return this.getPartialAlertFromRaw( + id, + fields ? pick(attributes, ...fields) : attributes, + updated_at, + references + ); }), }; + return x; } public async delete({ id }: { id: string }) { @@ -728,8 +737,8 @@ export class AlertsClient { private getPartialAlertFromRaw( id: string, - rawAlert: Partial, - updatedAt: SavedObject['updated_at'], + { createdAt, ...rawAlert }: Partial, + updatedAt: SavedObject['updated_at'] = createdAt, references: SavedObjectReference[] | undefined ): PartialAlert { return { @@ -738,11 +747,11 @@ export class AlertsClient { // we currently only support the Interval Schedule type // Once we support additional types, this type signature will likely change schedule: rawAlert.schedule as IntervalSchedule, - updatedAt: updatedAt ? new Date(updatedAt) : new Date(rawAlert.createdAt!), - createdAt: new Date(rawAlert.createdAt!), actions: rawAlert.actions ? this.injectReferencesIntoActions(rawAlert.actions, references || []) : [], + ...(updatedAt ? { updatedAt: new Date(updatedAt) } : {}), + ...(createdAt ? { createdAt: new Date(createdAt) } : {}), }; } @@ -799,4 +808,8 @@ export class AlertsClient { references, }; } + + private includeFieldsRequiredForAuthentication(fields: string[]): string[] { + return unique([...fields, 'alertTypeId', 'consumer']); + } } 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 d04f35458f30a..7160347e813ae 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 @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { chunk } from 'lodash'; +import { chunk, omit } from 'lodash'; import { UserAtSpaceScenarios } from '../../scenarios'; import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -278,6 +278,81 @@ export default function createFindTests({ getService }: FtrProviderContext) { } }); + it('should handle find alert request with fields appropriately', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + enabled: false, + tags: ['myTag'], + alertTypeId: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); + + // creat another type with same tag + const { body: createdSecondAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + tags: ['myTag'], + alertTypeId: 'test.restricted-noop', + consumer: 'alertsRestrictedFixture', + }) + ) + .expect(200); + objectRemover.add(space.id, createdSecondAlert.id, 'alert', 'alerts'); + + const response = await supertestWithoutAuth + .get( + `${getUrlPrefix( + space.id + )}/api/alerts/_find?filter=alert.attributes.alertTypeId:test.restricted-noop&fields=["tags"]` + ) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to find any alert types`, + statusCode: 403, + }); + break; + case 'space_1_all at space1': + expect(response.statusCode).to.eql(200); + expect(response.body.data).to.eql([]); + break; + case 'global_read at space1': + case 'superuser at space1': + case 'space_1_all_with_restricted_fixture at space1': + expect(response.statusCode).to.eql(200); + expect(response.body.page).to.equal(1); + expect(response.body.perPage).to.be.greaterThan(0); + expect(response.body.total).to.be.greaterThan(0); + const [matchFirst, matchSecond] = response.body.data; + expect(omit(matchFirst, 'updatedAt')).to.eql({ + id: createdAlert.id, + actions: [], + tags: ['myTag'], + }); + expect(omit(matchSecond, 'updatedAt')).to.eql({ + id: createdSecondAlert.id, + actions: [], + tags: ['myTag'], + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it(`shouldn't find alert from another space`, async () => { const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) From 289a85b7fe025250c51d806c8264ea036293cf12 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 15 Jun 2020 13:10:03 +0100 Subject: [PATCH 042/126] revert feature ID to legacy ID to prevent old alerts from breaking --- .../server/alert_types/index_threshold/alert_type.ts | 6 +++--- x-pack/plugins/alerts/common/index.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts index 1a5da8a422b9e..285abbef64f0d 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts @@ -9,11 +9,11 @@ import { AlertType, AlertExecutorOptions } from '../../types'; import { Params, ParamsSchema } from './alert_type_params'; import { BaseActionContext, addMessages } from './action_context'; import { TimeSeriesQuery } from './lib/time_series_query'; +import { Service } from '../../types'; +import { ALERTS_FEATURE_ID } from '../../../../alerts/common'; export const ID = '.index-threshold'; -import { Service } from '../../types'; - const ActionGroupId = 'threshold met'; const ComparatorFns = getComparatorFns(); export const ComparatorFnNames = new Set(ComparatorFns.keys()); @@ -85,7 +85,7 @@ export function getAlertType(service: Service): AlertType { ], }, executor, - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; async function executor(options: AlertExecutorOptions) { diff --git a/x-pack/plugins/alerts/common/index.ts b/x-pack/plugins/alerts/common/index.ts index b839c07a9db89..480cdd7027555 100644 --- a/x-pack/plugins/alerts/common/index.ts +++ b/x-pack/plugins/alerts/common/index.ts @@ -21,4 +21,4 @@ export interface AlertingFrameworkHealth { } export const BASE_ALERT_API_PATH = '/api/alerts'; -export const ALERTS_FEATURE_ID = 'alerts'; +export const ALERTS_FEATURE_ID = 'alerting'; From c9453f13e0866172df00d596b5a56e4af442220c Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 15 Jun 2020 13:28:31 +0100 Subject: [PATCH 043/126] corrected unit tests that relied on the new feature id --- .../alerts_authorization.test.ts | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 307490c96a0d3..105e44811d143 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -59,9 +59,9 @@ function mockFeature(appName: string, typeName: string, requiredApps: string[] = }, }); } -const alertsFeature = mockFeature('alerts', 'myBuiltInType'); -const myAppFeature = mockFeature('myApp', 'myType', ['alerts']); -const myOtherAppFeature = mockFeature('myOtherApp', 'myType', ['alerts']); +const alertsFeature = mockFeature('alerting', 'myBuiltInType'); +const myAppFeature = mockFeature('myApp', 'myType', ['alerting']); +const myOtherAppFeature = mockFeature('myOtherApp', 'myType', ['alerting']); beforeEach(() => { jest.resetAllMocks(); @@ -159,7 +159,7 @@ describe('ensureAuthorized', () => { privileges: [], }); - await alertAuthorization.ensureAuthorized('myType', 'alerts', 'create'); + await alertAuthorization.ensureAuthorized('myType', 'alerting', 'create'); expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); @@ -173,7 +173,7 @@ describe('ensureAuthorized', () => { "some-user", "myType", 0, - "alerts", + "alerting", "create", ] `); @@ -371,7 +371,7 @@ describe('getFindAuthorizationFilter', () => { defaultActionGroupId: 'default', id: 'alertingAlertType', name: 'alertingAlertType', - producer: 'alerts', + producer: 'alerting', }; const myAppAlertType = { actionGroups: [], @@ -423,7 +423,7 @@ describe('getFindAuthorizationFilter', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); expect((await alertAuthorization.getFindAuthorizationFilter()).filter).toMatchInlineSnapshot( - `"((alert.attributes.alertTypeId:myAppAlertType and (alert.attributes.consumer:alerts or alert.attributes.consumer:myApp or alert.attributes.consumer:myOtherApp)) or (alert.attributes.alertTypeId:alertingAlertType and (alert.attributes.consumer:alerts or alert.attributes.consumer:myApp or alert.attributes.consumer:myOtherApp)))"` + `"((alert.attributes.alertTypeId:myAppAlertType and (alert.attributes.consumer:alerting or alert.attributes.consumer:myApp or alert.attributes.consumer:myOtherApp)) or (alert.attributes.alertTypeId:alertingAlertType and (alert.attributes.consumer:alerting or alert.attributes.consumer:myApp or alert.attributes.consumer:myOtherApp)))"` ); expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -440,7 +440,7 @@ describe('getFindAuthorizationFilter', () => { hasAllRequested: false, privileges: [ { - privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'find'), + privilege: mockAuthorizationAction('alertingAlertType', 'alerting', 'find'), authorized: true, }, { @@ -452,7 +452,7 @@ describe('getFindAuthorizationFilter', () => { authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'find'), + privilege: mockAuthorizationAction('myAppAlertType', 'alerting', 'find'), authorized: true, }, { @@ -504,7 +504,7 @@ describe('getFindAuthorizationFilter', () => { hasAllRequested: false, privileges: [ { - privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'find'), + privilege: mockAuthorizationAction('alertingAlertType', 'alerting', 'find'), authorized: true, }, { @@ -516,7 +516,7 @@ describe('getFindAuthorizationFilter', () => { authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'find'), + privilege: mockAuthorizationAction('myAppAlertType', 'alerting', 'find'), authorized: true, }, { @@ -563,7 +563,7 @@ describe('filterByAlertTypeAuthorization', () => { defaultActionGroupId: 'default', id: 'alertingAlertType', name: 'alertingAlertType', - producer: 'alerts', + producer: 'alerting', }; const myAppAlertType = { actionGroups: [], @@ -595,7 +595,7 @@ describe('filterByAlertTypeAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Array [ - "alerts", + "alerting", "myApp", "myOtherApp", ], @@ -608,14 +608,14 @@ describe('filterByAlertTypeAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Array [ - "alerts", + "alerting", "myApp", "myOtherApp", ], "defaultActionGroupId": "default", "id": "alertingAlertType", "name": "alertingAlertType", - "producer": "alerts", + "producer": "alerting", }, } `); @@ -632,7 +632,7 @@ describe('filterByAlertTypeAuthorization', () => { hasAllRequested: false, privileges: [ { - privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'create'), + privilege: mockAuthorizationAction('alertingAlertType', 'alerting', 'create'), authorized: true, }, { @@ -644,7 +644,7 @@ describe('filterByAlertTypeAuthorization', () => { authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'create'), + privilege: mockAuthorizationAction('myAppAlertType', 'alerting', 'create'), authorized: true, }, { @@ -678,19 +678,19 @@ describe('filterByAlertTypeAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Array [ - "alerts", + "alerting", "myApp", ], "defaultActionGroupId": "default", "id": "alertingAlertType", "name": "alertingAlertType", - "producer": "alerts", + "producer": "alerting", }, Object { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Array [ - "alerts", + "alerting", "myApp", "myOtherApp", ], @@ -714,7 +714,7 @@ describe('filterByAlertTypeAuthorization', () => { hasAllRequested: false, privileges: [ { - privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'create'), + privilege: mockAuthorizationAction('alertingAlertType', 'alerting', 'create'), authorized: true, }, { @@ -726,7 +726,7 @@ describe('filterByAlertTypeAuthorization', () => { authorized: true, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'create'), + privilege: mockAuthorizationAction('myAppAlertType', 'alerting', 'create'), authorized: false, }, { @@ -760,14 +760,14 @@ describe('filterByAlertTypeAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Array [ - "alerts", + "alerting", "myApp", "myOtherApp", ], "defaultActionGroupId": "default", "id": "alertingAlertType", "name": "alertingAlertType", - "producer": "alerts", + "producer": "alerting", }, } `); From 244874cf58b2b5e6058833e5b68b91f46ad6ab18 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 15 Jun 2020 13:38:35 +0100 Subject: [PATCH 044/126] corrected acceptance tests that relied on the new feature id --- .../security_and_spaces/tests/alerting/create.ts | 2 +- .../security_and_spaces/tests/alerting/delete.ts | 2 +- .../security_and_spaces/tests/alerting/disable.ts | 2 +- .../security_and_spaces/tests/alerting/enable.ts | 2 +- .../security_and_spaces/tests/alerting/get.ts | 2 +- .../security_and_spaces/tests/alerting/mute_all.ts | 2 +- .../security_and_spaces/tests/alerting/mute_instance.ts | 2 +- .../security_and_spaces/tests/alerting/unmute_all.ts | 2 +- .../security_and_spaces/tests/alerting/unmute_instance.ts | 2 +- .../security_and_spaces/tests/alerting/update.ts | 4 ++-- .../security_and_spaces/tests/alerting/update_api_key.ts | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) 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 d20d939011c16..a27ca6b710f06 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 @@ -228,7 +228,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.noop', - consumer: 'alerts', + consumer: 'alerting', }) ); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts index 06c538c68d782..9905c5020779a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts @@ -205,7 +205,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.noop', - consumer: 'alerts', + consumer: 'alerting', }) ) .expect(200); 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 2531d82771cff..a3fa9586cc47b 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 @@ -205,7 +205,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte .send( getTestAlertData({ alertTypeId: 'test.noop', - consumer: 'alerts', + consumer: 'alerting', enabled: true, }) ) 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 31b71e0decdb8..f3e082d5202b7 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 @@ -204,7 +204,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex .send( getTestAlertData({ alertTypeId: 'test.noop', - consumer: 'alerts', + consumer: 'alerting', enabled: false, }) ) 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 9835b18b96e3a..ade0c706c767e 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 @@ -185,7 +185,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.restricted-noop', - consumer: 'alerts', + consumer: 'alerting', }) ) .expect(200); 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 index 21513513a8ccb..682f679e36ebc 100644 --- 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 @@ -210,7 +210,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) getTestAlertData({ enabled: false, alertTypeId: 'test.restricted-noop', - consumer: 'alerts', + consumer: 'alerting', }) ) .expect(200); 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 index 0d8630445accd..748eb837fdffb 100644 --- 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 @@ -210,7 +210,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider getTestAlertData({ enabled: false, alertTypeId: 'test.restricted-noop', - consumer: 'alerts', + consumer: 'alerting', }) ) .expect(200); 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 index 9d715c9146b5e..42c49e6103e3a 100644 --- 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 @@ -225,7 +225,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex getTestAlertData({ enabled: false, alertTypeId: 'test.restricted-noop', - consumer: 'alerts', + consumer: 'alerting', }) ) .expect(200); 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 index 2f1f883351aee..ac36aab6d1885 100644 --- 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 @@ -231,7 +231,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider getTestAlertData({ enabled: false, alertTypeId: 'test.restricted-noop', - consumer: 'alerts', + consumer: 'alerting', }) ) .expect(200); 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 7007b4ce7e3ae..582512a5f10a3 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 @@ -295,7 +295,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.restricted-noop', - consumer: 'alerts', + consumer: 'alerting', }) ) .expect(200); @@ -340,7 +340,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { ...updatedData, id: createdAlert.id, alertTypeId: 'test.restricted-noop', - consumer: 'alerts', + consumer: 'alerting', createdBy: 'elastic', enabled: true, updatedBy: user.username, 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 903bf6b40ee7e..391493b752ef8 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 @@ -204,7 +204,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte .send( getTestAlertData({ alertTypeId: 'test.restricted-noop', - consumer: 'alerts', + consumer: 'alerting', }) ) .expect(200); From 606081bdfa739f49ed9e88120e99aa94d22e2d59 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 15 Jun 2020 13:44:21 +0100 Subject: [PATCH 045/126] moved logs alert type to the correct feature --- x-pack/plugins/infra/server/features.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index 2c16493a61445..c6cb029f858f7 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -29,11 +29,7 @@ export const METRICS_FEATURE = { read: ['index-pattern'], }, alerting: { - all: [ - METRIC_THRESHOLD_ALERT_TYPE_ID, - METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, - LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, - ], + all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], }, ui: [ 'show', @@ -56,11 +52,7 @@ export const METRICS_FEATURE = { read: ['infrastructure-ui-source', 'index-pattern'], }, alerting: { - all: [ - METRIC_THRESHOLD_ALERT_TYPE_ID, - METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, - LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, - ], + all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], }, ui: [ 'show', @@ -94,12 +86,18 @@ export const LOGS_FEATURE = { all: ['infrastructure-ui-source'], read: [], }, + alerting: { + all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + }, ui: ['show', 'configureSource', 'save'], }, read: { app: ['infra', 'kibana'], catalogue: ['infralogging'], api: ['infra'], + alerting: { + all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + }, savedObject: { all: [], read: ['infrastructure-ui-source'], From 213b3301032d9248b7fb9b670f67bf9aaaebc6be Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 15 Jun 2020 15:19:13 +0100 Subject: [PATCH 046/126] reverted partial type --- x-pack/plugins/alerts/server/alerts_client.ts | 7 +++---- .../application/sections/alert_form/alert_form.test.tsx | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 8d1f6183f0aa0..d614cab6c0012 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -93,7 +93,7 @@ export interface FindResult { page: number; perPage: number; total: number; - data: Array>; + data: SanitizedAlert[]; } export interface CreateOptions { @@ -275,13 +275,13 @@ export class AlertsClient { type: 'alert', }); - const x = { + return { page, perPage, total, data: data.map(({ id, attributes, updated_at, references }) => { ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer); - return this.getPartialAlertFromRaw( + return this.getAlertFromRaw( id, fields ? pick(attributes, ...fields) : attributes, updated_at, @@ -289,7 +289,6 @@ export class AlertsClient { ); }), }; - return x; } public async delete({ id }: { id: string }) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index c9ce2848c5670..1c29bbc71154e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -302,7 +302,7 @@ describe('alert_form', () => { name: 'test', alertTypeId: alertType.id, params: {}, - consumer: 'alerts', + consumer: 'alerting', schedule: { interval: '1m', }, From 35a997114c348f800c03fc0a8d8730748b848cc4 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 15 Jun 2020 15:20:56 +0100 Subject: [PATCH 047/126] fixed another place using the alerts consumer --- .../public/application/sections/alert_form/alert_form.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index 1c29bbc71154e..ed36bc6c8d580 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -85,7 +85,7 @@ describe('alert_form', () => { const initialAlert = ({ name: 'test', params: {}, - consumer: 'alerts', + consumer: 'alerting', schedule: { interval: '1m', }, From c18ab7f40034ed0606f9e3fbe4ed33cf4b68a6da Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 15 Jun 2020 15:33:39 +0100 Subject: [PATCH 048/126] use constant to make it easier to change i nthe future --- .../application/lib/action_variables.test.ts | 3 +- .../public/application/lib/alert_api.test.ts | 3 +- .../components/alert_details.test.tsx | 32 ++++++++++--------- .../sections/alert_form/alert_add.test.tsx | 8 ++++- .../sections/alert_form/alert_form.test.tsx | 8 +++-- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts index 578c93fc4cba8..638b42d3aa7ce 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts @@ -6,6 +6,7 @@ import { AlertType, ActionVariables } from '../../types'; import { actionVariablesFromAlertType } from './action_variables'; +import { ALERTS_FEATURE_ID } from '../../../../alerts/common'; beforeEach(() => jest.resetAllMocks()); @@ -183,6 +184,6 @@ function getAlertType(actionVariables: ActionVariables): AlertType { actionVariables, actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts index 94d9166b40909..fa225de4fc9a6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts @@ -27,6 +27,7 @@ import { health, } from './alert_api'; import uuid from 'uuid'; +import { ALERTS_FEATURE_ID } from '../../../../alerts/common'; const http = httpServiceMock.createStartContract(); @@ -42,7 +43,7 @@ describe('loadAlertTypes', () => { context: [{ name: 'var1', description: 'val1' }], state: [{ name: 'var2', description: 'val2' }], }, - producer: 'alerting', + producer: ALERTS_FEATURE_ID, actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index d8f0d0b6b20a0..1a06f69580c12 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -20,6 +20,8 @@ import { i18n } from '@kbn/i18n'; import { ViewInApp } from './view_in_app'; import { PLUGIN } from '../../../constants/plugin'; import { coreMock } from 'src/core/public/mocks'; +import { ALERTS_FEATURE_ID } from '../../../../../../alerts/common'; + const mockes = coreMock.createSetup(); jest.mock('../../../app_context', () => ({ @@ -89,7 +91,7 @@ describe('alert_details', () => { actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; expect( @@ -127,7 +129,7 @@ describe('alert_details', () => { actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; expect( @@ -156,7 +158,7 @@ describe('alert_details', () => { actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; const actionTypes: ActionType[] = [ @@ -209,7 +211,7 @@ describe('alert_details', () => { actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; const actionTypes: ActionType[] = [ { @@ -267,7 +269,7 @@ describe('alert_details', () => { actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; expect( @@ -286,7 +288,7 @@ describe('alert_details', () => { actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; expect( @@ -314,7 +316,7 @@ describe('disable button', () => { actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; const enableButton = shallow( @@ -341,7 +343,7 @@ describe('disable button', () => { actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; const enableButton = shallow( @@ -368,7 +370,7 @@ describe('disable button', () => { actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; const disableAlert = jest.fn(); @@ -404,7 +406,7 @@ describe('disable button', () => { actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; const enableAlert = jest.fn(); @@ -443,7 +445,7 @@ describe('mute button', () => { actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; const enableButton = shallow( @@ -471,7 +473,7 @@ describe('mute button', () => { actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; const enableButton = shallow( @@ -499,7 +501,7 @@ describe('mute button', () => { actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; const muteAlert = jest.fn(); @@ -536,7 +538,7 @@ describe('mute button', () => { actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; const unmuteAlert = jest.fn(); @@ -573,7 +575,7 @@ describe('mute button', () => { actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }; const enableButton = shallow( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index f6e8dc49ec275..a8be282bc3b6d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -18,6 +18,8 @@ import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/ import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { ReactWrapper } from 'enzyme'; import { AppContextProvider } from '../../app_context'; +import { ALERTS_FEATURE_ID } from '../../../../../alerts/common'; + const actionTypeRegistry = actionTypeRegistryMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -120,7 +122,11 @@ describe('alert_add', () => { }, }} > - {}} /> + {}} + /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index ed36bc6c8d580..c89bb36be3cba 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -13,6 +13,8 @@ import { ValidationResult, Alert } from '../../../types'; import { AlertForm } from './alert_form'; import { AlertsContextProvider } from '../../context/alerts_context'; import { coreMock } from 'src/core/public/mocks'; +import { ALERTS_FEATURE_ID } from '../../../../../alerts/common'; + const actionTypeRegistry = actionTypeRegistryMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); jest.mock('../../lib/alert_api', () => ({ @@ -85,7 +87,7 @@ describe('alert_form', () => { const initialAlert = ({ name: 'test', params: {}, - consumer: 'alerting', + consumer: ALERTS_FEATURE_ID, schedule: { interval: '1m', }, @@ -167,7 +169,7 @@ describe('alert_form', () => { }, ], defaultActionGroupId: 'testActionGroup', - producer: 'alerting', + producer: ALERTS_FEATURE_ID, }, { id: 'same-consumer-producer-alert-type', @@ -302,7 +304,7 @@ describe('alert_form', () => { name: 'test', alertTypeId: alertType.id, params: {}, - consumer: 'alerting', + consumer: ALERTS_FEATURE_ID, schedule: { interval: '1m', }, From f2f3c2b4944e75acb9e1ab4a0c627998f690d6f6 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 15 Jun 2020 16:29:10 +0100 Subject: [PATCH 049/126] changed producer on metric alerts to match feature id --- .../register_inventory_metric_threshold_alert_type.ts | 2 +- .../metric_threshold/register_metric_threshold_alert_type.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts index e23dfed448c57..39f396b372f2a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts @@ -41,7 +41,7 @@ export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs }, defaultActionGroupId: FIRED_ACTIONS.id, actionGroups: [FIRED_ACTIONS], - producer: 'metrics', + producer: 'infrastructure', executor: curry(createInventoryMetricThresholdExecutor)(libs, uuid.v4()), actionVariables: { context: [ diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 02d9ca3e5f0c9..caa05375ec9c2 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -119,6 +119,6 @@ export function registerMetricThresholdAlertType(libs: InfraBackendLibs) { { name: 'threshold', description: thresholdActionVariableDescription }, ], }, - producer: 'metrics', + producer: 'infrastructure', }; } From ac37d1b8cd7f2d5c199dc125ae29090f5a29dc79 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 15 Jun 2020 16:39:41 +0100 Subject: [PATCH 050/126] change feature in privileges bac kto alerts --- x-pack/test/api_integration/apis/security/privileges.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 618338dc4d02f..1db45a16304a0 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -37,7 +37,7 @@ export default function ({ getService }: FtrProviderContext) { apm: ['all', 'read'], siem: ['all', 'read'], ingestManager: ['all', 'read'], - alerts: ['all', 'read'], + alerting: ['all', 'read'], }, global: ['all', 'read'], space: ['all', 'read'], From 0d2c859b699e88fa36ade2d8e1931b33ba1f6b3e Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 15 Jun 2020 16:43:15 +0100 Subject: [PATCH 051/126] change feature in privileges in basic back to alerts --- x-pack/test/api_integration/apis/security/privileges_basic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 1a139e331b889..8e64f0e5c432f 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -35,7 +35,7 @@ export default function ({ getService }: FtrProviderContext) { apm: ['all', 'read'], siem: ['all', 'read'], ingestManager: ['all', 'read'], - alerts: ['all', 'read'], + alerting: ['all', 'read'], }, global: ['all', 'read'], space: ['all', 'read'], From 80fe0fd030ccca07110004f69277525ed1a41e2c Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 15 Jun 2020 18:58:21 +0100 Subject: [PATCH 052/126] fixed consumer fields in siem --- .../detection_engine/notifications/create_notifications.ts | 4 ++-- .../server/lib/detection_engine/rules/create_rules.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.ts index a472d8a4df4a4..8f6826cec5365 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.ts @@ -5,7 +5,7 @@ */ import { Alert } from '../../../../../alerts/common'; -import { APP_ID, NOTIFICATIONS_ID } from '../../../../common/constants'; +import { SERVER_APP_ID, NOTIFICATIONS_ID } from '../../../../common/constants'; import { CreateNotificationParams } from './types'; import { addTags } from './add_tags'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; @@ -23,7 +23,7 @@ export const createNotifications = async ({ name, tags: addTags([], ruleAlertId), alertTypeId: NOTIFICATIONS_ID, - consumer: APP_ID, + consumer: SERVER_APP_ID, params: { ruleAlertId, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts index 83e9b0de16f06..4459d8078b174 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts @@ -6,7 +6,7 @@ import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { Alert } from '../../../../../alerts/common'; -import { APP_ID, SIGNALS_ID } from '../../../../common/constants'; +import { SERVER_APP_ID, SIGNALS_ID } from '../../../../common/constants'; import { CreateRulesOptions } from './types'; import { addTags } from './add_tags'; import { hasListsFeature } from '../feature_flags'; @@ -52,7 +52,7 @@ export const createRules = async ({ name, tags: addTags(tags, ruleId, immutable), alertTypeId: SIGNALS_ID, - consumer: APP_ID, + consumer: SERVER_APP_ID, params: { anomalyThreshold, description, From 270ecb15f42b36e09b14a12bf577ddc14d506bb4 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 16 Jun 2020 14:06:43 +0100 Subject: [PATCH 053/126] cleaned up some fixtures and featur eregestrations --- x-pack/plugins/apm/server/feature.ts | 4 ++-- .../common/fixtures/plugins/alerts/kibana.json | 2 +- .../common/fixtures/plugins/alerts_restricted/kibana.json | 2 +- .../fixtures/plugins/alerts_restricted/server/plugin.ts | 2 +- .../alerting_api_integration/security_and_spaces/scenarios.ts | 3 --- .../test/api_integration/apis/features/features/features.ts | 2 +- 6 files changed, 6 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index 855b0a3a0d7e3..411329a0455d0 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -24,7 +24,7 @@ export const APM_FEATURE = { api: ['apm', 'apm_write', 'actions-read', 'actions-all'], catalogue: ['apm'], savedObject: { - all: ['alert', 'action', 'action_task_params'], + all: ['action', 'action_task_params'], read: [], }, alerting: { @@ -46,7 +46,7 @@ export const APM_FEATURE = { api: ['apm', 'actions-read', 'actions-all'], catalogue: ['apm'], savedObject: { - all: ['alert', 'action', 'action_task_params'], + all: ['action', 'action_task_params'], read: [], }, alerting: { diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json index fc42e3199095d..083386480c540 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/kibana.json @@ -1,5 +1,5 @@ { - "id": "alerts-fixtures", + "id": "alertsFixtures", "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack"], diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/kibana.json b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/kibana.json index 89510d10fdc09..0e3d235293ac9 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/kibana.json +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/kibana.json @@ -1,5 +1,5 @@ { - "id": "alerts-restricted-fixtures", + "id": "alertsRestrictedFixtures", "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack"], diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts index bd12f7bd62c0d..841b3c319d1bf 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts @@ -43,7 +43,7 @@ export class FixturePlugin implements Plugin Date: Thu, 18 Jun 2020 10:37:13 +0100 Subject: [PATCH 054/126] fixed indentation --- x-pack/plugins/alerts/README.md | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index b24e603c9d268..a0849e0882485 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -315,25 +315,27 @@ features.registerFeature({ privileges: { all: { alerting: { - all: [ - // grant `all` over our own types - 'my-application-id.my-alert-type', - 'my-application-id.my-restricted-alert-type', - // grant `all` over the built-in IndexThreshold - '.index-threshold', - // grant `all` over Uptime's TLS AlertType - 'xpack.uptime.alerts.actionGroups.tls'], + all: [ + // grant `all` over our own types + 'my-application-id.my-alert-type', + 'my-application-id.my-restricted-alert-type', + // grant `all` over the built-in IndexThreshold + '.index-threshold', + // grant `all` over Uptime's TLS AlertType + 'xpack.uptime.alerts.actionGroups.tls' + ], }, }, read: { alerting: { - read: [ - // grant `read` over our own type - 'my-application-id.my-alert-type', - // grant `read` over the built-in IndexThreshold - '.index-threshold', - // grant `read` over Uptime's TLS AlertType - 'xpack.uptime.alerts.actionGroups.tls'], + read: [ + // grant `read` over our own type + 'my-application-id.my-alert-type', + // grant `read` over the built-in IndexThreshold + '.index-threshold', + // grant `read` over Uptime's TLS AlertType + 'xpack.uptime.alerts.actionGroups.tls' + ], }, }, }, From 554e7cef90739b06fd468477a9998c82850086d4 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 23 Jun 2020 10:35:34 +0100 Subject: [PATCH 055/126] removed feature registration in alerting --- x-pack/plugins/alerts/server/feature.ts | 41 ------------------------- x-pack/plugins/alerts/server/plugin.ts | 8 +---- 2 files changed, 1 insertion(+), 48 deletions(-) delete mode 100644 x-pack/plugins/alerts/server/feature.ts diff --git a/x-pack/plugins/alerts/server/feature.ts b/x-pack/plugins/alerts/server/feature.ts deleted file mode 100644 index 47c42bb9c7562..0000000000000 --- a/x-pack/plugins/alerts/server/feature.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; -import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import { INDEX_THRESHOLD_ID } from '../../alerting_builtins/server'; -import { ALERTS_FEATURE_ID } from '../common'; - -export function registerFeature(features: FeaturesPluginSetup) { - features.registerFeature({ - id: ALERTS_FEATURE_ID, - name: i18n.translate('xpack.alerts.featureRegistry.alertsFeatureName', { - defaultMessage: 'Alerts', - }), - app: [], - privileges: { - all: { - alerting: { - all: [INDEX_THRESHOLD_ID], - }, - savedObject: { - all: [], - read: [], - }, - ui: [], - }, - read: { - alerting: { - read: [INDEX_THRESHOLD_ID], - }, - savedObject: { - all: [], - read: [], - }, - ui: [], - }, - }, - }); -} diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index fb917cfc8c476..2f14919a0190f 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -58,12 +58,8 @@ import { Services } from './types'; import { registerAlertsUsageCollector } from './usage'; import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/task'; import { IEventLogger, IEventLogService } from '../../event_log/server'; -import { - PluginSetupContract as FeaturesPluginSetup, - PluginStartContract as FeaturesPluginStart, -} from '../../features/server'; +import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; import { setupSavedObjects } from './saved_objects'; -import { registerFeature } from './feature'; const EVENT_LOG_PROVIDER = 'alerting'; export const EVENT_LOG_ACTIONS = { @@ -90,7 +86,6 @@ export interface AlertingPluginsSetup { spaces?: SpacesPluginSetup; usageCollection?: UsageCollectionSetup; eventLog: IEventLogService; - features: FeaturesPluginSetup; } export interface AlertingPluginsStart { actions: ActionsPluginStartContract; @@ -143,7 +138,6 @@ export class AlertingPlugin { ); } - registerFeature(plugins.features); setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects); plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS)); From c177be09db70da022e1417027095a32f0a4d62b2 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 23 Jun 2020 11:10:15 +0100 Subject: [PATCH 056/126] ensure alerTypeId and Consumer cant be used for KQL injection --- .../alerts_authorization.test.ts | 26 ++++++++++++++++++- .../authorization/alerts_authorization.ts | 18 ++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 105e44811d143..c23f4ce2503c5 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -8,7 +8,7 @@ import { alertTypeRegistryMock } from '../alert_type_registry.mock'; import { securityMock } from '../../../../plugins/security/server/mocks'; import { PluginStartContract as FeaturesStartContract, Feature } from '../../../features/server'; import { featuresPluginMock } from '../../../features/server/mocks'; -import { AlertsAuthorization } from './alerts_authorization'; +import { AlertsAuthorization, ensureFieldIsSafeForQuery } from './alerts_authorization'; import { alertsAuthorizationAuditLoggerMock } from './audit_logger.mock'; import { AlertsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; @@ -773,3 +773,27 @@ describe('filterByAlertTypeAuthorization', () => { `); }); }); + +describe('ensureFieldIsSafeForQuery', () => { + test('throws if field contains character that isnt safe in a KQL query', () => { + expect(() => ensureFieldIsSafeForQuery('id', 'alert-*')).toThrowError( + `expected id not to include invalid character: *` + ); + + expect(() => ensureFieldIsSafeForQuery('id', '<=""')).toThrowError( + `expected id not to include invalid character: <=` + ); + + expect(() => ensureFieldIsSafeForQuery('id', '<"" or >=""')).toThrowError( + `expected id not to include invalid characters: <, >=` + ); + + expect(() => ensureFieldIsSafeForQuery('id', '1 or alertid:123')).toThrowError( + `expected id not to include invalid character: :` + ); + }); + + test('doesnt throws if field is safe as part of a KQL query', () => { + expect(() => ensureFieldIsSafeForQuery('id', '123-0456-678')).not.toThrow(); + }); +}); diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index e9cea33ababaf..6e62f12c9de5a 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -251,12 +251,28 @@ export class AlertsAuthorization { private asFiltersByAlertTypeAndConsumer(alertTypes: Set): string[] { return Array.from(alertTypes).reduce((filters, { id, authorizedConsumers }) => { + ensureFieldIsSafeForQuery('alertTypeId', id); filters.push( `(alert.attributes.alertTypeId:${id} and (${authorizedConsumers - .map((consumer) => `alert.attributes.consumer:${consumer}`) + .map((consumer) => { + ensureFieldIsSafeForQuery('alertTypeId', id); + return `alert.attributes.consumer:${consumer}`; + }) .join(' or ')}))` ); return filters; }, []); } } + +export function ensureFieldIsSafeForQuery(field: string, value: string): boolean { + const invalid = value.match(/[>=<\*:]+/g); + if (invalid) { + throw new Error( + `expected ${field} not to include invalid character${ + invalid.length > 1 ? `s` : `` + }: ${invalid?.join(`, `)}` + ); + } + return true; +} From bae77e4210b2b9a7b80f02cb43541d1c7c0205ab Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 23 Jun 2020 11:21:26 +0100 Subject: [PATCH 057/126] added some missing unit tests --- .../alerts_authorization.test.ts | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index c23f4ce2503c5..32e9e0e1184b9 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -23,7 +23,7 @@ const mockAuthorizationAction = (type: string, app: string, operation: string) = `${type}/${app}/${operation}`; function mockAuthorization() { const authorization = securityMock.createSetup().authz; - // typescript is havingtrouble inferring jest's automocking + // typescript is having trouble inferring jest's automocking (authorization.actions.alerting.get as jest.MockedFunction< typeof authorization.actions.alerting.get >).mockImplementation(mockAuthorizationAction); @@ -128,6 +128,8 @@ describe('ensureAuthorized', () => { mockAuthorizationAction('myType', 'myApp', 'create'), ]); + expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", @@ -168,6 +170,8 @@ describe('ensureAuthorized', () => { mockAuthorizationAction('myType', 'myApp', 'create'), ]); + expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", @@ -214,6 +218,8 @@ describe('ensureAuthorized', () => { mockAuthorizationAction('myType', 'myApp', 'create'), ]); + expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", @@ -260,6 +266,8 @@ describe('ensureAuthorized', () => { `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` ); + expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", @@ -306,6 +314,8 @@ describe('ensureAuthorized', () => { `"Unauthorized to create a \\"myType\\" alert by \\"myApp\\""` ); + expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", @@ -352,6 +362,8 @@ describe('ensureAuthorized', () => { `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` ); + expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", @@ -401,6 +413,22 @@ describe('getFindAuthorizationFilter', () => { expect(filter).toEqual(undefined); }); + test('ensureAlertTypeIsAuthorized is no-op when there is no authorization api', async () => { + const alertAuthorization = new AlertsAuthorization({ + request, + alertTypeRegistry, + features, + auditLogger, + }); + + const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); + + ensureAlertTypeIsAuthorized('someMadeUpType', 'myApp'); + + expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); + }); + test('creates a filter based on the privileged types', async () => { const authorization = mockAuthorization(); const checkPrivileges: jest.MockedFunction { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); - await expect(() => { + expect(() => { ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); }).toThrowErrorMatchingInlineSnapshot( `"Unauthorized to find a \\"myAppAlertType\\" alert for \\"myOtherApp\\""` ); + expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", @@ -540,10 +570,12 @@ describe('getFindAuthorizationFilter', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); - await expect(() => { + expect(() => { ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); }).not.toThrow(); + expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", From 0370e9df028340092238f74401c65467b7f32c49 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 23 Jun 2020 12:58:44 +0100 Subject: [PATCH 058/126] migrated feature to "alerts" --- x-pack/plugins/alerts/common/index.ts | 2 +- .../plugins/alerts/public/alert_api.test.ts | 8 +-- .../alert_navigation_registry.test.ts | 2 +- .../alerts/server/alert_type_registry.test.ts | 24 +++---- .../alerts_authorization.test.ts | 48 ++++++------- .../lib/validate_alert_type_params.test.ts | 6 +- x-pack/plugins/alerts/server/plugin.test.ts | 72 ------------------- .../server/routes/list_alert_types.test.ts | 4 +- .../create_execution_handler.test.ts | 2 +- .../server/task_runner/task_runner.test.ts | 2 +- .../task_runner/task_runner_factory.test.ts | 2 +- .../tests/alerting/create.ts | 2 +- .../tests/alerting/delete.ts | 2 +- .../tests/alerting/disable.ts | 2 +- .../tests/alerting/enable.ts | 2 +- .../security_and_spaces/tests/alerting/get.ts | 2 +- .../tests/alerting/mute_all.ts | 2 +- .../tests/alerting/mute_instance.ts | 2 +- .../tests/alerting/unmute_all.ts | 2 +- .../tests/alerting/unmute_instance.ts | 2 +- .../tests/alerting/update.ts | 4 +- .../tests/alerting/update_api_key.ts | 2 +- .../fixtures/plugins/alerts/server/plugin.ts | 4 +- 23 files changed, 64 insertions(+), 136 deletions(-) diff --git a/x-pack/plugins/alerts/common/index.ts b/x-pack/plugins/alerts/common/index.ts index 480cdd7027555..b839c07a9db89 100644 --- a/x-pack/plugins/alerts/common/index.ts +++ b/x-pack/plugins/alerts/common/index.ts @@ -21,4 +21,4 @@ export interface AlertingFrameworkHealth { } export const BASE_ALERT_API_PATH = '/api/alerts'; -export const ALERTS_FEATURE_ID = 'alerting'; +export const ALERTS_FEATURE_ID = 'alerts'; diff --git a/x-pack/plugins/alerts/public/alert_api.test.ts b/x-pack/plugins/alerts/public/alert_api.test.ts index 45b9f5ba8fe2e..3ee67b79b7bda 100644 --- a/x-pack/plugins/alerts/public/alert_api.test.ts +++ b/x-pack/plugins/alerts/public/alert_api.test.ts @@ -22,7 +22,7 @@ describe('loadAlertTypes', () => { actionVariables: ['var1'], actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', - producer: 'alerting', + producer: 'alerts', }, ]; http.get.mockResolvedValueOnce(resolvedValue); @@ -45,7 +45,7 @@ describe('loadAlertType', () => { actionVariables: ['var1'], actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', - producer: 'alerting', + producer: 'alerts', }; http.get.mockResolvedValueOnce([alertType]); @@ -65,7 +65,7 @@ describe('loadAlertType', () => { actionVariables: [], actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', - producer: 'alerting', + producer: 'alerts', }; http.get.mockResolvedValueOnce([alertType]); @@ -80,7 +80,7 @@ describe('loadAlertType', () => { actionVariables: [], actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', - producer: 'alerting', + producer: 'alerts', }, ]); diff --git a/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.test.ts b/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.test.ts index ff8a3a1c311c1..72c955923a0cc 100644 --- a/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.test.ts +++ b/x-pack/plugins/alerts/public/alert_navigation_registry/alert_navigation_registry.test.ts @@ -16,7 +16,7 @@ const mockAlertType = (id: string): AlertType => ({ actionGroups: [], actionVariables: [], defaultActionGroupId: 'default', - producer: 'alerting', + producer: 'alerts', }); describe('AlertNavigationRegistry', () => { diff --git a/x-pack/plugins/alerts/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts index 5a4b6294bfb13..096d064685a92 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts @@ -36,7 +36,7 @@ describe('has()', () => { ], defaultActionGroupId: 'default', executor: jest.fn(), - producer: 'alerting', + producer: 'alerts', }); expect(registry.has('foo')).toEqual(true); }); @@ -55,7 +55,7 @@ describe('register()', () => { ], defaultActionGroupId: 'default', executor: jest.fn(), - producer: 'alerting', + producer: 'alerts', }; // eslint-disable-next-line @typescript-eslint/no-var-requires const registry = new AlertTypeRegistry(alertTypeRegistryParams); @@ -87,7 +87,7 @@ describe('register()', () => { ], defaultActionGroupId: 'default', executor: jest.fn(), - producer: 'alerting', + producer: 'alerts', }; // eslint-disable-next-line @typescript-eslint/no-var-requires const registry = new AlertTypeRegistry(alertTypeRegistryParams); @@ -109,7 +109,7 @@ describe('register()', () => { ], defaultActionGroupId: 'default', executor: jest.fn(), - producer: 'alerting', + producer: 'alerts', }; // eslint-disable-next-line @typescript-eslint/no-var-requires const registry = new AlertTypeRegistry(alertTypeRegistryParams); @@ -140,7 +140,7 @@ describe('register()', () => { ], defaultActionGroupId: 'default', executor: jest.fn(), - producer: 'alerting', + producer: 'alerts', }; const registry = new AlertTypeRegistry(alertTypeRegistryParams); registry.register(alertType); @@ -161,7 +161,7 @@ describe('register()', () => { ], defaultActionGroupId: 'default', executor: jest.fn(), - producer: 'alerting', + producer: 'alerts', }); expect(() => registry.register({ @@ -175,7 +175,7 @@ describe('register()', () => { ], defaultActionGroupId: 'default', executor: jest.fn(), - producer: 'alerting', + producer: 'alerts', }) ).toThrowErrorMatchingInlineSnapshot(`"Alert type \\"test\\" is already registered."`); }); @@ -195,7 +195,7 @@ describe('get()', () => { ], defaultActionGroupId: 'default', executor: jest.fn(), - producer: 'alerting', + producer: 'alerts', }); const alertType = registry.get('test'); expect(alertType).toMatchInlineSnapshot(` @@ -214,7 +214,7 @@ describe('get()', () => { "executor": [MockFunction], "id": "test", "name": "Test", - "producer": "alerting", + "producer": "alerts", } `); }); @@ -247,7 +247,7 @@ describe('list()', () => { ], defaultActionGroupId: 'testActionGroup', executor: jest.fn(), - producer: 'alerting', + producer: 'alerts', }); const result = registry.list(); expect(result).toMatchInlineSnapshot(` @@ -266,7 +266,7 @@ describe('list()', () => { "defaultActionGroupId": "testActionGroup", "id": "test", "name": "Test", - "producer": "alerting", + "producer": "alerts", }, } `); @@ -314,7 +314,7 @@ function alertTypeWithVariables(id: string, context: string, state: string): Ale actionGroups: [], defaultActionGroupId: id, async executor() {}, - producer: 'alerting', + producer: 'alerts', }; if (!context && !state) { diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 32e9e0e1184b9..d66207441adf0 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -59,9 +59,9 @@ function mockFeature(appName: string, typeName: string, requiredApps: string[] = }, }); } -const alertsFeature = mockFeature('alerting', 'myBuiltInType'); -const myAppFeature = mockFeature('myApp', 'myType', ['alerting']); -const myOtherAppFeature = mockFeature('myOtherApp', 'myType', ['alerting']); +const alertsFeature = mockFeature('alerts', 'myBuiltInType'); +const myAppFeature = mockFeature('myApp', 'myType', ['alerts']); +const myOtherAppFeature = mockFeature('myOtherApp', 'myType', ['alerts']); beforeEach(() => { jest.resetAllMocks(); @@ -161,7 +161,7 @@ describe('ensureAuthorized', () => { privileges: [], }); - await alertAuthorization.ensureAuthorized('myType', 'alerting', 'create'); + await alertAuthorization.ensureAuthorized('myType', 'alerts', 'create'); expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); @@ -177,7 +177,7 @@ describe('ensureAuthorized', () => { "some-user", "myType", 0, - "alerting", + "alerts", "create", ] `); @@ -383,7 +383,7 @@ describe('getFindAuthorizationFilter', () => { defaultActionGroupId: 'default', id: 'alertingAlertType', name: 'alertingAlertType', - producer: 'alerting', + producer: 'alerts', }; const myAppAlertType = { actionGroups: [], @@ -451,7 +451,7 @@ describe('getFindAuthorizationFilter', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); expect((await alertAuthorization.getFindAuthorizationFilter()).filter).toMatchInlineSnapshot( - `"((alert.attributes.alertTypeId:myAppAlertType and (alert.attributes.consumer:alerting or alert.attributes.consumer:myApp or alert.attributes.consumer:myOtherApp)) or (alert.attributes.alertTypeId:alertingAlertType and (alert.attributes.consumer:alerting or alert.attributes.consumer:myApp or alert.attributes.consumer:myOtherApp)))"` + `"((alert.attributes.alertTypeId:myAppAlertType and (alert.attributes.consumer:alerts or alert.attributes.consumer:myApp or alert.attributes.consumer:myOtherApp)) or (alert.attributes.alertTypeId:alertingAlertType and (alert.attributes.consumer:alerts or alert.attributes.consumer:myApp or alert.attributes.consumer:myOtherApp)))"` ); expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -468,7 +468,7 @@ describe('getFindAuthorizationFilter', () => { hasAllRequested: false, privileges: [ { - privilege: mockAuthorizationAction('alertingAlertType', 'alerting', 'find'), + privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'find'), authorized: true, }, { @@ -480,7 +480,7 @@ describe('getFindAuthorizationFilter', () => { authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'alerting', 'find'), + privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'find'), authorized: true, }, { @@ -534,7 +534,7 @@ describe('getFindAuthorizationFilter', () => { hasAllRequested: false, privileges: [ { - privilege: mockAuthorizationAction('alertingAlertType', 'alerting', 'find'), + privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'find'), authorized: true, }, { @@ -546,7 +546,7 @@ describe('getFindAuthorizationFilter', () => { authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'alerting', 'find'), + privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'find'), authorized: true, }, { @@ -595,7 +595,7 @@ describe('filterByAlertTypeAuthorization', () => { defaultActionGroupId: 'default', id: 'alertingAlertType', name: 'alertingAlertType', - producer: 'alerting', + producer: 'alerts', }; const myAppAlertType = { actionGroups: [], @@ -627,7 +627,7 @@ describe('filterByAlertTypeAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Array [ - "alerting", + "alerts", "myApp", "myOtherApp", ], @@ -640,14 +640,14 @@ describe('filterByAlertTypeAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Array [ - "alerting", + "alerts", "myApp", "myOtherApp", ], "defaultActionGroupId": "default", "id": "alertingAlertType", "name": "alertingAlertType", - "producer": "alerting", + "producer": "alerts", }, } `); @@ -664,7 +664,7 @@ describe('filterByAlertTypeAuthorization', () => { hasAllRequested: false, privileges: [ { - privilege: mockAuthorizationAction('alertingAlertType', 'alerting', 'create'), + privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'create'), authorized: true, }, { @@ -676,7 +676,7 @@ describe('filterByAlertTypeAuthorization', () => { authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'alerting', 'create'), + privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'create'), authorized: true, }, { @@ -710,19 +710,19 @@ describe('filterByAlertTypeAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Array [ - "alerting", + "alerts", "myApp", ], "defaultActionGroupId": "default", "id": "alertingAlertType", "name": "alertingAlertType", - "producer": "alerting", + "producer": "alerts", }, Object { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Array [ - "alerting", + "alerts", "myApp", "myOtherApp", ], @@ -746,7 +746,7 @@ describe('filterByAlertTypeAuthorization', () => { hasAllRequested: false, privileges: [ { - privilege: mockAuthorizationAction('alertingAlertType', 'alerting', 'create'), + privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'create'), authorized: true, }, { @@ -758,7 +758,7 @@ describe('filterByAlertTypeAuthorization', () => { authorized: true, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'alerting', 'create'), + privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'create'), authorized: false, }, { @@ -792,14 +792,14 @@ describe('filterByAlertTypeAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Array [ - "alerting", + "alerts", "myApp", "myOtherApp", ], "defaultActionGroupId": "default", "id": "alertingAlertType", "name": "alertingAlertType", - "producer": "alerting", + "producer": "alerts", }, } `); diff --git a/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts index d31b15030fd3a..1e6c26c02e65b 100644 --- a/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts +++ b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts @@ -20,7 +20,7 @@ test('should return passed in params when validation not defined', () => { ], defaultActionGroupId: 'default', async executor() {}, - producer: 'alerting', + producer: 'alerts', }, { foo: true, @@ -48,7 +48,7 @@ test('should validate and apply defaults when params is valid', () => { }), }, async executor() {}, - producer: 'alerting', + producer: 'alerts', }, { param1: 'value' } ); @@ -77,7 +77,7 @@ test('should validate and throw error when params is invalid', () => { }), }, async executor() {}, - producer: 'alerting', + producer: 'alerts', }, {} ) diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index 56bf3a09dc06b..60cb8adee7084 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -43,78 +43,6 @@ describe('Alerting Plugin', () => { 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.' ); }); - - it('should grant global `all` priviliges to built in AlertTypes for anyone with `all` priviliges to alerts', async () => { - const context = coreMock.createPluginInitializerContext(); - const plugin = new AlertingPlugin(context); - - const coreSetup = coreMock.createSetup(); - const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); - const features = featuresPluginMock.createSetup(); - await plugin.setup( - ({ - ...coreSetup, - http: { - ...coreSetup.http, - route: jest.fn(), - }, - } as unknown) as CoreSetup, - ({ - licensing: licensingMock.createSetup(), - encryptedSavedObjects: encryptedSavedObjectsSetup, - taskManager: taskManagerMock.createSetup(), - eventLog: eventLogServiceMock.create(), - features, - } as unknown) as AlertingPluginsSetup - ); - - expect(features.registerFeature).toHaveBeenCalledTimes(1); - const { privileges } = features.registerFeature.mock.calls[0][0]; - - expect(privileges?.all.alerting).toMatchInlineSnapshot(` - Object { - "all": Array [ - ".index-threshold", - ], - } - `); - }); - - it('should grant global `read` priviliges to built in AlertTypes for anyone with `read` priviliges to alerts', async () => { - const context = coreMock.createPluginInitializerContext(); - const plugin = new AlertingPlugin(context); - - const coreSetup = coreMock.createSetup(); - const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); - const features = featuresPluginMock.createSetup(); - await plugin.setup( - ({ - ...coreSetup, - http: { - ...coreSetup.http, - route: jest.fn(), - }, - } as unknown) as CoreSetup, - ({ - licensing: licensingMock.createSetup(), - encryptedSavedObjects: encryptedSavedObjectsSetup, - taskManager: taskManagerMock.createSetup(), - eventLog: eventLogServiceMock.create(), - features, - } as unknown) as AlertingPluginsSetup - ); - - expect(features.registerFeature).toHaveBeenCalledTimes(1); - const { privileges } = features.registerFeature.mock.calls[0][0]; - - expect(privileges?.read.alerting).toMatchInlineSnapshot(` - Object { - "read": Array [ - ".index-threshold", - ], - } - `); - }); }); describe('start()', () => { diff --git a/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts index 276915973a391..6440326cc7747 100644 --- a/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts +++ b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts @@ -112,7 +112,7 @@ describe('listAlertTypesRoute', () => { context: [], state: [], }, - producer: 'alerting', + producer: 'alerts', }, ]; @@ -161,7 +161,7 @@ describe('listAlertTypesRoute', () => { context: [], state: [], }, - producer: 'alerting', + producer: 'alerts', }, ]; diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts index dd5a9f531bd58..88dab4c050a7b 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts @@ -20,7 +20,7 @@ const alertType: AlertType = { ], defaultActionGroupId: 'default', executor: jest.fn(), - producer: 'alerting', + producer: 'alerts', }; const actionsClient = actionsClientMock.create(); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 690971bc87006..28c7517872cfb 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -25,7 +25,7 @@ const alertType = { actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', executor: jest.fn(), - producer: 'alerting', + producer: 'alerts', }; let fakeTimer: sinon.SinonFakeTimers; diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts index 7d9710d8a3e08..5a9ac225c737a 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts @@ -19,7 +19,7 @@ const alertType = { actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', executor: jest.fn(), - producer: 'alerting', + producer: 'alerts', }; let fakeTimer: sinon.SinonFakeTimers; 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 a27ca6b710f06..d20d939011c16 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 @@ -228,7 +228,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.noop', - consumer: 'alerting', + consumer: 'alerts', }) ); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts index 9905c5020779a..06c538c68d782 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts @@ -205,7 +205,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.noop', - consumer: 'alerting', + consumer: 'alerts', }) ) .expect(200); 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 a3fa9586cc47b..2531d82771cff 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 @@ -205,7 +205,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte .send( getTestAlertData({ alertTypeId: 'test.noop', - consumer: 'alerting', + consumer: 'alerts', enabled: true, }) ) 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 f3e082d5202b7..31b71e0decdb8 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 @@ -204,7 +204,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex .send( getTestAlertData({ alertTypeId: 'test.noop', - consumer: 'alerting', + consumer: 'alerts', enabled: false, }) ) 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 ade0c706c767e..9835b18b96e3a 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 @@ -185,7 +185,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.restricted-noop', - consumer: 'alerting', + consumer: 'alerts', }) ) .expect(200); 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 index 682f679e36ebc..21513513a8ccb 100644 --- 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 @@ -210,7 +210,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) getTestAlertData({ enabled: false, alertTypeId: 'test.restricted-noop', - consumer: 'alerting', + consumer: 'alerts', }) ) .expect(200); 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 index 748eb837fdffb..0d8630445accd 100644 --- 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 @@ -210,7 +210,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider getTestAlertData({ enabled: false, alertTypeId: 'test.restricted-noop', - consumer: 'alerting', + consumer: 'alerts', }) ) .expect(200); 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 index 42c49e6103e3a..9d715c9146b5e 100644 --- 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 @@ -225,7 +225,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex getTestAlertData({ enabled: false, alertTypeId: 'test.restricted-noop', - consumer: 'alerting', + consumer: 'alerts', }) ) .expect(200); 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 index ac36aab6d1885..2f1f883351aee 100644 --- 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 @@ -231,7 +231,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider getTestAlertData({ enabled: false, alertTypeId: 'test.restricted-noop', - consumer: 'alerting', + consumer: 'alerts', }) ) .expect(200); 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 582512a5f10a3..7007b4ce7e3ae 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 @@ -295,7 +295,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .send( getTestAlertData({ alertTypeId: 'test.restricted-noop', - consumer: 'alerting', + consumer: 'alerts', }) ) .expect(200); @@ -340,7 +340,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { ...updatedData, id: createdAlert.id, alertTypeId: 'test.restricted-noop', - consumer: 'alerting', + consumer: 'alerts', createdBy: 'elastic', enabled: true, updatedBy: user.username, 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 391493b752ef8..903bf6b40ee7e 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 @@ -204,7 +204,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte .send( getTestAlertData({ alertTypeId: 'test.restricted-noop', - consumer: 'alerting', + consumer: 'alerts', }) ) .expect(200); diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index 256394136ee69..c750eb61fbee7 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -61,7 +61,7 @@ function createNoopAlertType(alerts: AlertingSetup) { actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', async executor() {}, - producer: 'alerting', + producer: 'alerts', }; alerts.registerType(noopAlertType); } @@ -75,7 +75,7 @@ function createAlwaysFiringAlertType(alerts: AlertingSetup) { { id: 'default', name: 'Default' }, { id: 'other', name: 'Other' }, ], - producer: 'alerting', + producer: 'alerts', async executor(alertExecutorOptions: any) { const { services, state, params } = alertExecutorOptions; From 19d38aa0a0610436e40dc02ac9794b0c7874f134 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 23 Jun 2020 14:00:37 +0100 Subject: [PATCH 059/126] support alerts consumer without a feature backing it --- .../server/authorization/alerts_authorization.test.ts | 8 ++++---- .../alerts/server/authorization/alerts_authorization.ts | 9 ++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index d66207441adf0..507fd8f0304a2 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -59,9 +59,9 @@ function mockFeature(appName: string, typeName: string, requiredApps: string[] = }, }); } -const alertsFeature = mockFeature('alerts', 'myBuiltInType'); -const myAppFeature = mockFeature('myApp', 'myType', ['alerts']); -const myOtherAppFeature = mockFeature('myOtherApp', 'myType', ['alerts']); + +const myAppFeature = mockFeature('myApp', 'myType', []); +const myOtherAppFeature = mockFeature('myOtherApp', 'myType', []); beforeEach(() => { jest.resetAllMocks(); @@ -82,7 +82,7 @@ beforeEach(() => { async executor() {}, producer: 'myApp', })); - features.getFeatures.mockReturnValue([alertsFeature, myAppFeature, myOtherAppFeature]); + features.getFeatures.mockReturnValue([myAppFeature, myOtherAppFeature]); }); describe('ensureAuthorized', () => { diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 6e62f12c9de5a..a0362b1d7a312 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -190,7 +190,10 @@ export class AlertsAuthorization { if (!this.authorization) { return { hasAllRequested: true, - authorizedAlertTypes: this.augmentWithAuthorizedConsumers(alertTypes, featuresIds), + authorizedAlertTypes: this.augmentWithAuthorizedConsumers(alertTypes, [ + ALERTS_FEATURE_ID, + ...featuresIds, + ]), }; } else { const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( @@ -223,7 +226,7 @@ export class AlertsAuthorization { hasAllRequested, authorizedAlertTypes: hasAllRequested ? // has access to all features - this.augmentWithAuthorizedConsumers(alertTypes, featuresIds) + this.augmentWithAuthorizedConsumers(alertTypes, [ALERTS_FEATURE_ID, ...featuresIds]) : // only has some of the required privileges privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { if (authorized && privilegeToAlertType.has(privilege)) { @@ -244,7 +247,7 @@ export class AlertsAuthorization { return new Set( Array.from(alertTypes).map((alertType) => ({ ...alertType, - authorizedConsumers: authorizedConsumers ?? [], + authorizedConsumers: authorizedConsumers ?? [ALERTS_FEATURE_ID], })) ); } From 3c66ba0d351b4bc53aca4706b2bbdc60a14c9cb7 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 23 Jun 2020 16:04:27 +0100 Subject: [PATCH 060/126] bump timeout on delete all test as the rbac work has made it a little slower --- .../functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 07c3115a9b67c..17f4b2c4309de 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -362,7 +362,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('deleteAll'); await testSubjects.existOrFail('deleteIdsConfirmation'); await testSubjects.click('deleteIdsConfirmation > confirmModalConfirmButton'); - await testSubjects.missingOrFail('deleteIdsConfirmation'); + await testSubjects.missingOrFail('deleteIdsConfirmation', { timeout: 5000 }); await pageObjects.common.closeToast(); From 8f82baf5db12dd6db8e0e85b8730207f7e6b79c0 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 23 Jun 2020 16:12:12 +0100 Subject: [PATCH 061/126] removed alerting feature from privileges --- x-pack/test/api_integration/apis/security/privileges.ts | 1 - x-pack/test/api_integration/apis/security/privileges_basic.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 1db45a16304a0..de0abe2350eb5 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -37,7 +37,6 @@ export default function ({ getService }: FtrProviderContext) { apm: ['all', 'read'], siem: ['all', 'read'], ingestManager: ['all', 'read'], - alerting: ['all', 'read'], }, global: ['all', 'read'], space: ['all', 'read'], diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 8e64f0e5c432f..00bfcdc119e47 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -35,7 +35,6 @@ export default function ({ getService }: FtrProviderContext) { apm: ['all', 'read'], siem: ['all', 'read'], ingestManager: ['all', 'read'], - alerting: ['all', 'read'], }, global: ['all', 'read'], space: ['all', 'read'], From 04cd6f51c5e56b4247e66008da30f2f1e6cefebe Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 23 Jun 2020 17:11:45 +0100 Subject: [PATCH 062/126] include alerts in auth consumers for all types --- x-pack/plugins/alerts/server/alerts_client.test.ts | 1 + .../security_and_spaces/tests/alerting/list_alert_types.ts | 2 +- x-pack/test/api_integration/apis/features/features/features.ts | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 12baa7ffd5ced..ce0ecbc29c8e6 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -2293,6 +2293,7 @@ describe('find()', () => { consumer: 'myApp', tags: ['myTag'], }, + score: 1, references: [], }, ], diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index c1a856ff84140..b83a81badc14a 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -62,7 +62,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { case 'space_1_all at space1': expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType); expect(restrictedNoOpAlertType).to.eql(undefined); - expect(noOpAlertType.authorizedConsumers).to.eql(['alertsFixture']); + expect(noOpAlertType.authorizedConsumers).to.eql(['alerts', 'alertsFixture']); break; case 'global_read at space1': case 'space_1_all_with_restricted_fixture at space1': diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index 93137cdb97b5d..11fb9b2de7199 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -112,7 +112,6 @@ export default function ({ getService }: FtrProviderContext) { 'uptime', 'siem', 'ingestManager', - 'alerting', ].sort() ); }); From cc06e671adc2f7ed7b8f5bce4f48b218aa56edf5 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 23 Jun 2020 19:21:52 +0100 Subject: [PATCH 063/126] ensure test tag is isolated from other tests --- .../security_and_spaces/tests/alerting/find.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 7160347e813ae..ece2ee8e54788 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 @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; import { chunk, omit } from 'lodash'; +import uuid from 'uuid'; import { UserAtSpaceScenarios } from '../../scenarios'; import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -279,13 +280,14 @@ export default function createFindTests({ getService }: FtrProviderContext) { }); it('should handle find alert request with fields appropriately', async () => { + const myTag = uuid.v4(); const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ enabled: false, - tags: ['myTag'], + tags: [myTag], alertTypeId: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', }) @@ -293,13 +295,13 @@ export default function createFindTests({ getService }: FtrProviderContext) { .expect(200); objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); - // creat another type with same tag + // create another type with same tag const { body: createdSecondAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') .send( getTestAlertData({ - tags: ['myTag'], + tags: [myTag], alertTypeId: 'test.restricted-noop', consumer: 'alertsRestrictedFixture', }) @@ -311,7 +313,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { .get( `${getUrlPrefix( space.id - )}/api/alerts/_find?filter=alert.attributes.alertTypeId:test.restricted-noop&fields=["tags"]` + )}/api/alerts/_find?filter=alert.attributes.alertTypeId:test.restricted-noop&fields=["tags"]&sort_field=createdAt` ) .auth(user.username, user.password); @@ -340,12 +342,12 @@ export default function createFindTests({ getService }: FtrProviderContext) { expect(omit(matchFirst, 'updatedAt')).to.eql({ id: createdAlert.id, actions: [], - tags: ['myTag'], + tags: [myTag], }); expect(omit(matchSecond, 'updatedAt')).to.eql({ id: createdSecondAlert.id, actions: [], - tags: ['myTag'], + tags: [myTag], }); break; default: From 3ccb14fc51c93a749f5a6d4e06ec99047832c443 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 24 Jun 2020 10:26:48 +0100 Subject: [PATCH 064/126] prevent parens and whitespace in consumer or alerttypeid --- .../authorization/alerts_authorization.test.ts | 14 +++++++++++--- .../authorization/alerts_authorization.ts | 18 +++++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 507fd8f0304a2..c0c4c9caa0844 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -816,12 +816,20 @@ describe('ensureFieldIsSafeForQuery', () => { `expected id not to include invalid character: <=` ); - expect(() => ensureFieldIsSafeForQuery('id', '<"" or >=""')).toThrowError( - `expected id not to include invalid characters: <, >=` + expect(() => ensureFieldIsSafeForQuery('id', '>=""')).toThrowError( + `expected id not to include invalid character: >=` ); expect(() => ensureFieldIsSafeForQuery('id', '1 or alertid:123')).toThrowError( - `expected id not to include invalid character: :` + `expected id not to include whitespace and invalid character: :` + ); + + expect(() => ensureFieldIsSafeForQuery('id', ') or alertid:123')).toThrowError( + `expected id not to include whitespace and invalid characters: ), :` + ); + + expect(() => ensureFieldIsSafeForQuery('id', 'some space')).toThrowError( + `expected id not to include whitespace` ); }); diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index a0362b1d7a312..5a43e3050ae93 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { pluck, mapValues } from 'lodash'; +import { pluck, mapValues, remove } from 'lodash'; import { KibanaRequest } from 'src/core/server'; import { ALERTS_FEATURE_ID } from '../../common'; import { AlertTypeRegistry } from '../types'; @@ -269,13 +269,17 @@ export class AlertsAuthorization { } export function ensureFieldIsSafeForQuery(field: string, value: string): boolean { - const invalid = value.match(/[>=<\*:]+/g); + const invalid = value.match(/([>=<\*:()]+|\s+)/g); if (invalid) { - throw new Error( - `expected ${field} not to include invalid character${ - invalid.length > 1 ? `s` : `` - }: ${invalid?.join(`, `)}` - ); + const whitespace = remove(invalid, (chars) => chars.trim().length === 0); + const errors = []; + if (whitespace.length) { + errors.push(`whitespace`); + } + if (invalid.length) { + errors.push(`invalid character${invalid.length > 1 ? `s` : ``}: ${invalid?.join(`, `)}`); + } + throw new Error(`expected ${field} not to include ${errors.join(' and ')}`); } return true; } From e00ffe47c17e770362cb8e617ec4f3ac22ad8c6b Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 24 Jun 2020 13:02:13 +0100 Subject: [PATCH 065/126] improved perf of "find" api by improving the filter --- .../alerts/server/authorization/alerts_authorization.test.ts | 2 +- .../alerts/server/authorization/alerts_authorization.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index c0c4c9caa0844..60b4735e6f801 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -451,7 +451,7 @@ describe('getFindAuthorizationFilter', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); expect((await alertAuthorization.getFindAuthorizationFilter()).filter).toMatchInlineSnapshot( - `"((alert.attributes.alertTypeId:myAppAlertType and (alert.attributes.consumer:alerts or alert.attributes.consumer:myApp or alert.attributes.consumer:myOtherApp)) or (alert.attributes.alertTypeId:alertingAlertType and (alert.attributes.consumer:alerts or alert.attributes.consumer:myApp or alert.attributes.consumer:myOtherApp)))"` + `"((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp)) or (alert.attributes.alertTypeId:alertingAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp)))"` ); expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 5a43e3050ae93..d174fefeaf486 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -256,10 +256,10 @@ export class AlertsAuthorization { return Array.from(alertTypes).reduce((filters, { id, authorizedConsumers }) => { ensureFieldIsSafeForQuery('alertTypeId', id); filters.push( - `(alert.attributes.alertTypeId:${id} and (${authorizedConsumers + `(alert.attributes.alertTypeId:${id} and alert.attributes.consumer:(${authorizedConsumers .map((consumer) => { ensureFieldIsSafeForQuery('alertTypeId', id); - return `alert.attributes.consumer:${consumer}`; + return consumer; }) .join(' or ')}))` ); From adefb2f0b5d32ae92e5a790399b49c2910d9969e Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 24 Jun 2020 13:07:40 +0100 Subject: [PATCH 066/126] fixed tests broken by merge conflict --- x-pack/plugins/alerts/server/alerts_client.test.ts | 2 +- x-pack/plugins/alerts/server/alerts_client_factory.test.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 28e263a283ed3..3cdbece390d3b 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -6,7 +6,7 @@ import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; import { AlertsClient, CreateOptions, ConstructorOptions } from './alerts_client'; -import { savedObjectsClientMock, loggingServiceMock } from '../../../../src/core/server/mocks'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../src/core/server/mocks'; import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; import { alertsAuthorizationMock } from './authorization/alerts_authorization.mock'; diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 4c06dde020185..ae828ed0c1e35 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -9,7 +9,11 @@ import { AlertsClientFactory, AlertsClientFactoryOpts } from './alerts_client_fa import { alertTypeRegistryMock } from './alert_type_registry.mock'; import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { KibanaRequest } from '../../../../src/core/server'; -import { savedObjectsClientMock, savedObjectsServiceMock } from '../../../../src/core/server/mocks'; +import { + savedObjectsClientMock, + savedObjectsServiceMock, + loggingSystemMock, +} from '../../../../src/core/server/mocks'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { AuthenticatedUser } from '../../../plugins/security/common/model'; import { securityMock } from '../../security/server/mocks'; From 8b2a4232f5586e832a57157c2184d54c64f23853 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 25 Jun 2020 11:03:56 +0100 Subject: [PATCH 067/126] reduce features included in auth to those who grant alerting privileges --- .../alerts_authorization.test.ts | 31 ++++++++++++------- .../authorization/alerts_authorization.ts | 13 +++++++- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 60b4735e6f801..32e5e36f4db10 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -30,16 +30,20 @@ function mockAuthorization() { return authorization; } -function mockFeature(appName: string, typeName: string, requiredApps: string[] = []) { +function mockFeature(appName: string, typeName?: string) { return new Feature({ id: appName, name: appName, - app: requiredApps, + app: [], privileges: { all: { - alerting: { - all: [typeName], - }, + ...(typeName + ? { + alerting: { + all: [typeName], + }, + } + : {}), savedObject: { all: [], read: [], @@ -47,9 +51,13 @@ function mockFeature(appName: string, typeName: string, requiredApps: string[] = ui: [], }, read: { - alerting: { - read: [typeName], - }, + ...(typeName + ? { + alerting: { + read: [typeName], + }, + } + : {}), savedObject: { all: [], read: [], @@ -60,8 +68,9 @@ function mockFeature(appName: string, typeName: string, requiredApps: string[] = }); } -const myAppFeature = mockFeature('myApp', 'myType', []); -const myOtherAppFeature = mockFeature('myOtherApp', 'myType', []); +const myAppFeature = mockFeature('myApp', 'myType'); +const myOtherAppFeature = mockFeature('myOtherApp', 'myType'); +const myFeatureWithoutAlerting = mockFeature('myOtherApp'); beforeEach(() => { jest.resetAllMocks(); @@ -82,7 +91,7 @@ beforeEach(() => { async executor() {}, producer: 'myApp', })); - features.getFeatures.mockReturnValue([myAppFeature, myOtherAppFeature]); + features.getFeatures.mockReturnValue([myAppFeature, myOtherAppFeature, myFeatureWithoutAlerting]); }); describe('ensureAuthorized', () => { diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index d174fefeaf486..3b70905d24c5a 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -186,7 +186,18 @@ export class AlertsAuthorization { hasAllRequested: boolean; authorizedAlertTypes: Set; }> { - const featuresIds = this.features.getFeatures().map((feature) => feature.id); + const featuresIds = this.features + .getFeatures() + // ignore features which don't grant privileges to alerting + .filter(({ privileges }) => { + return ( + (privileges?.all.alerting?.all?.length ?? 0 > 0) || + (privileges?.all.alerting?.read?.length ?? 0 > 0) || + (privileges?.read.alerting?.all?.length ?? 0 > 0) || + (privileges?.read.alerting?.read?.length ?? 0 > 0) + ); + }) + .map((feature) => feature.id); if (!this.authorization) { return { hasAllRequested: true, From 44a0c4ebebce2906a3529ea415fd94e59dcb32db Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 25 Jun 2020 15:03:11 +0100 Subject: [PATCH 068/126] incluyde sub features in privilege check --- .../alerts_authorization.test.ts | 74 ++++++++++++++++++- .../authorization/alerts_authorization.ts | 24 ++++-- 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 32e5e36f4db10..280798e002822 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -68,8 +68,71 @@ function mockFeature(appName: string, typeName?: string) { }); } +function mockFeatureWithSubFeature(appName: string, typeName: string) { + return new Feature({ + id: appName, + name: appName, + app: [], + privileges: { + all: { + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + read: { + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, + subFeatures: [ + { + name: appName, + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'doSomethingAlertRelated', + name: 'sub feature alert', + includeIn: 'all', + alerting: { + all: [typeName], + }, + savedObject: { + all: [], + read: [], + }, + ui: ['doSomethingAlertRelated'], + }, + { + id: 'doSomethingAlertRelated', + name: 'sub feature alert', + includeIn: 'read', + alerting: { + read: [typeName], + }, + savedObject: { + all: [], + read: [], + }, + ui: ['doSomethingAlertRelated'], + }, + ], + }, + ], + }, + ], + }); +} + const myAppFeature = mockFeature('myApp', 'myType'); const myOtherAppFeature = mockFeature('myOtherApp', 'myType'); +const myAppWithSubFeature = mockFeatureWithSubFeature('myAppWithSubFeature', 'myType'); const myFeatureWithoutAlerting = mockFeature('myOtherApp'); beforeEach(() => { @@ -91,7 +154,12 @@ beforeEach(() => { async executor() {}, producer: 'myApp', })); - features.getFeatures.mockReturnValue([myAppFeature, myOtherAppFeature, myFeatureWithoutAlerting]); + features.getFeatures.mockReturnValue([ + myAppFeature, + myOtherAppFeature, + myAppWithSubFeature, + myFeatureWithoutAlerting, + ]); }); describe('ensureAuthorized', () => { @@ -460,7 +528,7 @@ describe('getFindAuthorizationFilter', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); expect((await alertAuthorization.getFindAuthorizationFilter()).filter).toMatchInlineSnapshot( - `"((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp)) or (alert.attributes.alertTypeId:alertingAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp)))"` + `"((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:alertingAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)))"` ); expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -639,6 +707,7 @@ describe('filterByAlertTypeAuthorization', () => { "alerts", "myApp", "myOtherApp", + "myAppWithSubFeature", ], "defaultActionGroupId": "default", "id": "myAppAlertType", @@ -652,6 +721,7 @@ describe('filterByAlertTypeAuthorization', () => { "alerts", "myApp", "myOtherApp", + "myAppWithSubFeature", ], "defaultActionGroupId": "default", "id": "alertingAlertType", diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 3b70905d24c5a..ddb7bbc404169 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -11,6 +11,7 @@ import { ALERTS_FEATURE_ID } from '../../common'; import { AlertTypeRegistry } from '../types'; import { SecurityPluginSetup } from '../../../security/server'; import { RegistryAlertType } from '../alert_type_registry'; +import { FeatureKibanaPrivileges, SubFeaturePrivilegeConfig } from '../../../features/common'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; @@ -189,12 +190,17 @@ export class AlertsAuthorization { const featuresIds = this.features .getFeatures() // ignore features which don't grant privileges to alerting - .filter(({ privileges }) => { + .filter(({ privileges, subFeatures }) => { return ( - (privileges?.all.alerting?.all?.length ?? 0 > 0) || - (privileges?.all.alerting?.read?.length ?? 0 > 0) || - (privileges?.read.alerting?.all?.length ?? 0 > 0) || - (privileges?.read.alerting?.read?.length ?? 0 > 0) + hasAnyAlertingPrivileges(privileges?.all) || + hasAnyAlertingPrivileges(privileges?.read) || + subFeatures.some((subFeature) => + subFeature.privilegeGroups.some((privilegeGroup) => + privilegeGroup.privileges.some((subPrivileges) => + hasAnyAlertingPrivileges(subPrivileges) + ) + ) + ) ); }) .map((feature) => feature.id); @@ -294,3 +300,11 @@ export function ensureFieldIsSafeForQuery(field: string, value: string): boolean } return true; } + +function hasAnyAlertingPrivileges( + privileges?: FeatureKibanaPrivileges | SubFeaturePrivilegeConfig +): boolean { + return ( + ((privileges?.alerting?.all?.length ?? 0) || (privileges?.alerting?.read?.length ?? 0)) > 0 + ); +} From b0949104db063f6543c85e9727ad5be3d744b032 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 26 Jun 2020 14:39:58 +0100 Subject: [PATCH 069/126] fixed broken index threshold in non metrics users --- .../authorization/alerts_authorization.ts | 134 ++++++++++-------- .../application/lib/action_variables.test.ts | 1 + .../public/application/lib/alert_api.test.ts | 1 + .../components/alert_details.test.tsx | 15 ++ .../sections/alert_form/alert_add.test.tsx | 25 ++++ .../sections/alert_form/alert_form.test.tsx | 23 +++ .../sections/alert_form/alert_form.tsx | 48 ++++--- .../alerts_list/components/alerts_list.tsx | 8 +- .../triggers_actions_ui/public/types.ts | 3 +- .../tests/alerting/find.ts | 55 ++++--- .../tests/alerting/list_alert_types.ts | 7 +- 11 files changed, 201 insertions(+), 119 deletions(-) diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index ddb7bbc404169..1260deb64e6f1 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { pluck, mapValues, remove } from 'lodash'; +import { pluck, mapValues, remove, omit, isUndefined } from 'lodash'; import { KibanaRequest } from 'src/core/server'; import { ALERTS_FEATURE_ID } from '../../common'; import { AlertTypeRegistry } from '../types'; @@ -52,63 +52,67 @@ export class AlertsAuthorization { const { authorization } = this; if (authorization) { const alertType = this.alertTypeRegistry.get(alertTypeId); - const requiredPrivilegesByScope = { - consumer: authorization.actions.alerting.get(alertTypeId, consumer, operation), - producer: authorization.actions.alerting.get(alertTypeId, alertType.producer, operation), - }; // We special case the Alerts Management `consumer` as we don't want to have to // manually authorize each alert type in the management UI const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID; + // We special case the Alerts Management `prodcuer` as all users are authorized + // to use built-in alert types by definition + const shouldAuthorizeProducer = + alertType.producer !== ALERTS_FEATURE_ID && alertType.producer !== consumer; - const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested, username, privileges } = await checkPrivileges( - shouldAuthorizeConsumer && consumer !== alertType.producer - ? [ - // check for access at consumer level - requiredPrivilegesByScope.consumer, - // check for access at producer level - requiredPrivilegesByScope.producer, - ] - : [ - // skip consumer privilege checks under `alerts` as all alert types can - // be created under `alerts` if you have producer level privileges - requiredPrivilegesByScope.producer, - ] - ); - - if (hasAllRequested) { - this.auditLogger.alertsAuthorizationSuccess( - username, - alertTypeId, - ScopeType.Consumer, - consumer, - operation - ); - } else { - const authorizedPrivileges = pluck( - privileges.filter((privilege) => privilege.authorized), - 'privilege' - ); - const unauthorizedScopes = mapValues( - requiredPrivilegesByScope, - (privilege) => !authorizedPrivileges.includes(privilege) + if (shouldAuthorizeConsumer || shouldAuthorizeProducer) { + const requiredPrivilegesByScope = omit( + { + consumer: shouldAuthorizeConsumer + ? authorization.actions.alerting.get(alertTypeId, consumer, operation) + : undefined, + producer: shouldAuthorizeProducer + ? authorization.actions.alerting.get(alertTypeId, alertType.producer, operation) + : undefined, + }, + isUndefined ); - const [unauthorizedScopeType, unauthorizedScope] = - shouldAuthorizeConsumer && unauthorizedScopes.consumer - ? [ScopeType.Consumer, consumer] - : [ScopeType.Producer, alertType.producer]; + const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); + const { hasAllRequested, username, privileges } = await checkPrivileges( + Object.values(requiredPrivilegesByScope) + ); - throw Boom.forbidden( - this.auditLogger.alertsAuthorizationFailure( + if (hasAllRequested) { + this.auditLogger.alertsAuthorizationSuccess( username, alertTypeId, - unauthorizedScopeType, - unauthorizedScope, + ScopeType.Consumer, + consumer, operation - ) - ); + ); + } else { + const authorizedPrivileges = pluck( + privileges.filter((privilege) => privilege.authorized), + 'privilege' + ); + + const unauthorizedScopes = mapValues( + requiredPrivilegesByScope, + (privilege) => !authorizedPrivileges.includes(privilege) + ); + + const [unauthorizedScopeType, unauthorizedScope] = + shouldAuthorizeConsumer && unauthorizedScopes.consumer + ? [ScopeType.Consumer, consumer] + : [ScopeType.Producer, alertType.producer]; + + throw Boom.forbidden( + this.auditLogger.alertsAuthorizationFailure( + username, + alertTypeId, + unauthorizedScopeType, + unauthorizedScope, + operation + ) + ); + } } } } @@ -204,13 +208,13 @@ export class AlertsAuthorization { ); }) .map((feature) => feature.id); + + const allPossibleConsumers = [ALERTS_FEATURE_ID, ...featuresIds]; + if (!this.authorization) { return { hasAllRequested: true, - authorizedAlertTypes: this.augmentWithAuthorizedConsumers(alertTypes, [ - ALERTS_FEATURE_ID, - ...featuresIds, - ]), + authorizedAlertTypes: this.augmentWithAuthorizedConsumers(alertTypes, allPossibleConsumers), }; } else { const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( @@ -218,18 +222,28 @@ export class AlertsAuthorization { ); // add an empty `authorizedConsumers` array on each alertType - const alertTypesWithAutherization = this.augmentWithAuthorizedConsumers(alertTypes); + const alertTypesWithAutherization = this.augmentWithAuthorizedConsumers(alertTypes, []); + const preAuthorizedAlertTypes = new Set(); // map from privilege to alertType which we can refer back to when analyzing the result // of checkPrivileges - const privilegeToAlertType = new Map(); + const privilegeToAlertType = new Map(); // as we can't ask ES for the user's individual privileges we need to ask for each feature // and alertType in the system whether this user has this privilege for (const alertType of alertTypesWithAutherization) { + if (alertType.producer === ALERTS_FEATURE_ID) { + alertType.authorizedConsumers.push(ALERTS_FEATURE_ID); + preAuthorizedAlertTypes.add(alertType); + } + for (const feature of featuresIds) { privilegeToAlertType.set( this.authorization!.actions.alerting.get(alertType.id, feature, operation), - [alertType, feature] + [ + alertType, + // granting privileges under the producer automatically authorized the Alerts Management UI as well + alertType.producer === feature ? [ALERTS_FEATURE_ID, feature] : [feature], + ] ); } } @@ -243,28 +257,28 @@ export class AlertsAuthorization { hasAllRequested, authorizedAlertTypes: hasAllRequested ? // has access to all features - this.augmentWithAuthorizedConsumers(alertTypes, [ALERTS_FEATURE_ID, ...featuresIds]) + this.augmentWithAuthorizedConsumers(alertTypes, allPossibleConsumers) : // only has some of the required privileges privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { if (authorized && privilegeToAlertType.has(privilege)) { - const [alertType, consumer] = privilegeToAlertType.get(privilege)!; - alertType.authorizedConsumers.push(consumer); + const [alertType, consumers] = privilegeToAlertType.get(privilege)!; + alertType.authorizedConsumers.push(...consumers); authorizedAlertTypes.add(alertType); } return authorizedAlertTypes; - }, new Set()), + }, preAuthorizedAlertTypes), }; } } private augmentWithAuthorizedConsumers( alertTypes: Set, - authorizedConsumers?: string[] + authorizedConsumers: string[] ): Set { return new Set( Array.from(alertTypes).map((alertType) => ({ ...alertType, - authorizedConsumers: authorizedConsumers ?? [ALERTS_FEATURE_ID], + authorizedConsumers: [...authorizedConsumers], })) ); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts index 638b42d3aa7ce..ef7a044bc4799 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts @@ -184,6 +184,7 @@ function getAlertType(actionVariables: ActionVariables): AlertType { actionVariables, actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + authorizedConsumers: [], producer: ALERTS_FEATURE_ID, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts index fa225de4fc9a6..f444e0c3ba732 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts @@ -46,6 +46,7 @@ describe('loadAlertTypes', () => { producer: ALERTS_FEATURE_ID, actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', + authorizedConsumers: [], }, ]; http.get.mockResolvedValueOnce(resolvedValue); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 1a06f69580c12..5340835461ba4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -92,6 +92,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID], }; expect( @@ -130,6 +131,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID], }; expect( @@ -159,6 +161,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID], }; const actionTypes: ActionType[] = [ @@ -212,6 +215,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID], }; const actionTypes: ActionType[] = [ { @@ -270,6 +274,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID], }; expect( @@ -289,6 +294,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID], }; expect( @@ -317,6 +323,7 @@ describe('disable button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID], }; const enableButton = shallow( @@ -344,6 +351,7 @@ describe('disable button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID], }; const enableButton = shallow( @@ -371,6 +379,7 @@ describe('disable button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID], }; const disableAlert = jest.fn(); @@ -407,6 +416,7 @@ describe('disable button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID], }; const enableAlert = jest.fn(); @@ -446,6 +456,7 @@ describe('mute button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID], }; const enableButton = shallow( @@ -474,6 +485,7 @@ describe('mute button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID], }; const enableButton = shallow( @@ -502,6 +514,7 @@ describe('mute button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID], }; const muteAlert = jest.fn(); @@ -539,6 +552,7 @@ describe('mute button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID], }; const unmuteAlert = jest.fn(); @@ -576,6 +590,7 @@ describe('mute button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID], }; const enableButton = shallow( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index a8be282bc3b6d..90a57eafd66d1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -19,6 +19,10 @@ import { dataPluginMock } from '../../../../../../../src/plugins/data/public/moc import { ReactWrapper } from 'enzyme'; import { AppContextProvider } from '../../app_context'; import { ALERTS_FEATURE_ID } from '../../../../../alerts/common'; +jest.mock('../../lib/alert_api', () => ({ + loadAlertTypes: jest.fn(), + health: jest.fn((async) => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true })), +})); const actionTypeRegistry = actionTypeRegistryMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); @@ -44,6 +48,27 @@ describe('alert_add', () => { async function setup() { const mocks = coreMock.createSetup(); + const { loadAlertTypes } = jest.requireMock('../../lib/alert_api'); + const alertTypes = [ + { + id: 'my-alert-type', + name: 'Test', + actionGroups: [ + { + id: 'testActionGroup', + name: 'Test Action Group', + }, + ], + defaultActionGroupId: 'testActionGroup', + producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID, 'test'], + actionVariables: { + context: [], + state: [], + }, + }, + ]; + loadAlertTypes.mockResolvedValue(alertTypes); const [ { application: { capabilities }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index c89bb36be3cba..66883d468312b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -22,6 +22,10 @@ jest.mock('../../lib/alert_api', () => ({ })); describe('alert_form', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + let deps: any; const alertType = { id: 'my-alert-type', @@ -65,6 +69,23 @@ describe('alert_form', () => { async function setup() { const mocks = coreMock.createSetup(); + const { loadAlertTypes } = jest.requireMock('../../lib/alert_api'); + const alertTypes = [ + { + id: 'my-alert-type', + name: 'Test', + actionGroups: [ + { + id: 'testActionGroup', + name: 'Test Action Group', + }, + ], + defaultActionGroupId: 'testActionGroup', + producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID, 'test'], + }, + ]; + loadAlertTypes.mockResolvedValue(alertTypes); const [ { application: { capabilities }, @@ -170,6 +191,7 @@ describe('alert_form', () => { ], defaultActionGroupId: 'testActionGroup', producer: ALERTS_FEATURE_ID, + authorizedConsumers: [ALERTS_FEATURE_ID, 'test'], }, { id: 'same-consumer-producer-alert-type', @@ -182,6 +204,7 @@ describe('alert_form', () => { ], defaultActionGroupId: 'testActionGroup', producer: 'test', + authorizedConsumers: [ALERTS_FEATURE_ID, 'test'], }, ]); const mocks = coreMock.createSetup(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 32b1809609e43..c22029e2f70cd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -122,12 +122,12 @@ export const AlertForm = ({ (async () => { try { const alertTypes = await loadAlertTypes({ http }); - const index: AlertTypeIndex = {}; + const index: AlertTypeIndex = new Map(); for (const alertTypeItem of alertTypes) { - index[alertTypeItem.id] = alertTypeItem; + index.set(alertTypeItem.id, alertTypeItem); } - if (alert.alertTypeId && index[alert.alertTypeId]) { - setDefaultActionGroupId(index[alert.alertTypeId].defaultActionGroupId); + if (alert.alertTypeId && index.has(alert.alertTypeId)) { + setDefaultActionGroupId(index.get(alert.alertTypeId)!.defaultActionGroupId); } setAlertTypesIndex(index); } catch (e) { @@ -168,21 +168,23 @@ export const AlertForm = ({ ? alertTypeModel.alertParamsExpression : null; - const alertTypeRegistryList = - alert.consumer === ALERTS_FEATURE_ID - ? alertTypeRegistry - .list() - .filter( - (alertTypeRegistryItem: AlertTypeModel) => !alertTypeRegistryItem.requiresAppContext - ) - : alertTypeRegistry - .list() - .filter( - (alertTypeRegistryItem: AlertTypeModel) => - alertTypesIndex && - alertTypesIndex[alertTypeRegistryItem.id] && - alertTypesIndex[alertTypeRegistryItem.id].producer === alert.consumer - ); + const alertTypeRegistryList = alertTypesIndex + ? alertTypeRegistry + .list() + .filter( + (alertTypeRegistryItem: AlertTypeModel) => + alertTypesIndex.has(alertTypeRegistryItem.id) && + alertTypesIndex + .get(alertTypeRegistryItem.id)! + .authorizedConsumers.includes(alert.consumer) + ) + .filter((alertTypeRegistryItem: AlertTypeModel) => + alert.consumer === ALERTS_FEATURE_ID + ? !alertTypeRegistryItem.requiresAppContext + : alertTypesIndex.get(alertTypeRegistryItem.id)!.producer === alert.consumer + ) + : []; + const alertTypeNodes = alertTypeRegistryList.map(function (item, index) { return ( @@ -263,8 +265,8 @@ export const AlertForm = ({ actions={alert.actions} setHasActionsDisabled={setHasActionsDisabled} messageVariables={ - alertTypesIndex && alertTypesIndex[alert.alertTypeId] - ? actionVariablesFromAlertType(alertTypesIndex[alert.alertTypeId]).map( + alertTypesIndex && alertTypesIndex.has(alert.alertTypeId) + ? actionVariablesFromAlertType(alertTypesIndex.get(alert.alertTypeId)!).map( (av) => av.name ) : undefined diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 9ce64d4796093..a237e9b3fba7f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -80,7 +80,7 @@ export const AlertsList: React.FunctionComponent = () => { const [alertTypesState, setAlertTypesState] = useState({ isLoading: false, isInitialized: false, - data: {}, + data: new Map(), }); const [alertsState, setAlertsState] = useState({ isLoading: false, @@ -99,9 +99,9 @@ export const AlertsList: React.FunctionComponent = () => { try { setAlertTypesState({ ...alertTypesState, isLoading: true }); const alertTypes = await loadAlertTypes({ http }); - const index: AlertTypeIndex = {}; + const index: AlertTypeIndex = new Map(); for (const alertType of alertTypes) { - index[alertType.id] = alertType; + index.set(alertType.id, alertType); } setAlertTypesState({ isLoading: false, data: index, isInitialized: true }); } catch (e) { @@ -458,6 +458,6 @@ function convertAlertsToTableItems(alerts: Alert[], alertTypesIndex: AlertTypeIn ...alert, actionsText: alert.actions.length, tagsText: alert.tags.join(', '), - alertType: alertTypesIndex[alert.alertTypeId]?.name ?? alert.alertTypeId, + alertType: alertTypesIndex.get(alert.alertTypeId)?.name ?? alert.alertTypeId, })); } diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 52179dd35767c..6eaae4c1b91a3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -19,7 +19,7 @@ export { Alert, AlertAction, AlertTaskState, RawAlertInstance, AlertingFramework export { ActionType }; export type ActionTypeIndex = Record; -export type AlertTypeIndex = Record; +export type AlertTypeIndex = Map; export type ActionTypeRegistryContract = PublicMethodsOf< TypeRegistry> >; @@ -99,6 +99,7 @@ export interface AlertType { actionGroups: ActionGroup[]; actionVariables: ActionVariables; defaultActionGroupId: ActionGroup['id']; + authorizedConsumers: string[]; producer: string; } 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 ece2ee8e54788..221b685395ef7 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 @@ -43,12 +43,11 @@ export default function createFindTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: `Unauthorized to find any alert types`, - statusCode: 403, - }); + expect(response.statusCode).to.eql(200); + expect(response.body.page).to.equal(1); + expect(response.body.perPage).to.be.greaterThan(0); + expect(response.body.total).to.equal(0); + expect(response.body.data).to.eql([]); break; case 'global_read at space1': case 'superuser at space1': @@ -134,12 +133,11 @@ export default function createFindTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: `Unauthorized to find any alert types`, - statusCode: 403, - }); + expect(response.statusCode).to.eql(200); + expect(response.body.page).to.equal(1); + expect(response.body.perPage).to.be.greaterThan(0); + expect(response.body.total).to.equal(0); + expect(response.body.data).to.eql([]); break; case 'space_1_all at space1': expect(response.statusCode).to.eql(200); @@ -229,12 +227,11 @@ export default function createFindTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: `Unauthorized to find any alert types`, - statusCode: 403, - }); + expect(response.statusCode).to.eql(200); + expect(response.body.page).to.equal(1); + expect(response.body.perPage).to.be.greaterThan(0); + expect(response.body.total).to.equal(0); + expect(response.body.data).to.eql([]); break; case 'global_read at space1': case 'superuser at space1': @@ -320,12 +317,11 @@ export default function createFindTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: `Unauthorized to find any alert types`, - statusCode: 403, - }); + expect(response.statusCode).to.eql(200); + expect(response.body.page).to.equal(1); + expect(response.body.perPage).to.be.greaterThan(0); + expect(response.body.total).to.equal(0); + expect(response.body.data).to.eql([]); break; case 'space_1_all at space1': expect(response.statusCode).to.eql(200); @@ -374,12 +370,11 @@ export default function createFindTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: `Unauthorized to find any alert types`, - statusCode: 403, - }); + expect(response.statusCode).to.eql(200); + expect(response.body.page).to.equal(1); + expect(response.body.perPage).to.be.greaterThan(0); + expect(response.body.total).to.equal(0); + expect(response.body.data).to.eql([]); break; case 'global_read at space1': case 'superuser at space1': diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index b83a81badc14a..0b2377c537f93 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -9,6 +9,7 @@ import { omit } from 'lodash'; import { UserAtSpaceScenarios } from '../../scenarios'; import { getUrlPrefix } from '../../../common/lib/space_test_utils'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { ALERTS_FEATURE_ID, AlertType } from '../../../../../plugins/alerts/common'; // eslint-disable-next-line import/no-default-export export default function listAlertTypes({ getService }: FtrProviderContext) { @@ -57,7 +58,11 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - expect(response.body).to.eql([]); + // users with no privileges should only have access to + // built-in types + response.body.forEach((alertType: AlertType) => { + expect(alertType.producer).to.equal(ALERTS_FEATURE_ID); + }); break; case 'space_1_all at space1': expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType); From 611061e56bec1afbe9713ee8d12fa1df0e858b10 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 26 Jun 2020 14:56:49 +0100 Subject: [PATCH 070/126] migrate alerts with consumer "metrics" to be "infrastructure" --- .../alerts/server/saved_objects/migrations.ts | 1 + .../spaces_only/tests/alerting/migrations.ts | 9 ++++ .../functional/es_archives/alerts/data.json | 42 +++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index 142102dd711c7..79413aff907c4 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -28,6 +28,7 @@ function changeAlertingConsumer( ): SavedObjectMigrationFn { const consumerMigration = new Map(); consumerMigration.set('alerting', 'alerts'); + consumerMigration.set('metrics', 'infrastructure'); return encryptedSavedObjects.createMigration( function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts index fc61f59d129d7..e2c9879790fec 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -30,5 +30,14 @@ export default function createGetTests({ getService }: FtrProviderContext) { expect(response.status).to.eql(200); expect(response.body.consumer).to.equal('alerts'); }); + + it('7.9.0 migrates the `metrics` consumer to be the `infrastructure`', async () => { + const response = await supertest.get( + `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-fdf248d5f2a4` + ); + + expect(response.status).to.eql(200); + expect(response.body.consumer).to.equal('infrastructure'); + }); }); } diff --git a/x-pack/test/functional/es_archives/alerts/data.json b/x-pack/test/functional/es_archives/alerts/data.json index 3703473606ea2..cc246b0fe44da 100644 --- a/x-pack/test/functional/es_archives/alerts/data.json +++ b/x-pack/test/functional/es_archives/alerts/data.json @@ -38,4 +38,46 @@ "updated_at": "2020-06-17T15:35:39.839Z" } } +} + +{ + "type": "doc", + "value": { + "id": "alert:74f3e6d7-b7bb-477d-ac28-fdf248d5f2a4", + "index": ".kibana_1", + "source": { + "alert": { + "actions": [ + ], + "alertTypeId": "example.always-firing", + "apiKey": "XHcE1hfSJJCvu2oJrKErgbIbR7iu3XAX+c1kki8jESzWZNyBlD4+6yHhCDEx7rNLlP/Hvbut/V8N1BaQkaSpVpiNsW/UxshiCouqJ+cmQ9LbaYnca9eTTVUuPhbHwwsDjfYkakDPqW3gB8sonwZl6rpzZVacfp4=", + "apiKeyOwner": "elastic", + "consumer": "metrics", + "createdAt": "2020-06-17T15:35:38.497Z", + "createdBy": "elastic", + "enabled": true, + "muteAll": false, + "mutedInstanceIds": [ + ], + "name": "always-firing-alert", + "params": { + }, + "schedule": { + "interval": "1m" + }, + "scheduledTaskId": "329798f0-b0b0-11ea-9510-fdf248d5f2a4", + "tags": [ + ], + "throttle": null, + "updatedBy": "elastic" + }, + "migrationVersion": { + "alert": "7.8.0" + }, + "references": [ + ], + "type": "alert", + "updated_at": "2020-06-17T15:35:39.839Z" + } + } } \ No newline at end of file From 1a208488905d72dd040683dd883476abfedd666a Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 29 Jun 2020 14:55:24 +0100 Subject: [PATCH 071/126] fixed consumer in metrics alert types --- .../infra/public/alerting/inventory/components/alert_flyout.tsx | 2 +- .../alerting/metric_threshold/components/alert_flyout.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx index 7e85a2bdf7e9b..804ff9602c81c 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_flyout.tsx @@ -44,7 +44,7 @@ export const AlertFlyout = (props: Props) => { setAddFlyoutVisibility={props.setVisible} alertTypeId={METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID} canChangeTrigger={false} - consumer={'metrics'} + consumer={'infrastructure'} /> )} diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx index b0c8cdb9d4195..b19a399b0e50d 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx @@ -46,7 +46,7 @@ export const AlertFlyout = (props: Props) => { setAddFlyoutVisibility={props.setVisible} alertTypeId={METRIC_THRESHOLD_ALERT_TYPE_ID} canChangeTrigger={false} - consumer={'metrics'} + consumer={'infrastructure'} /> )} From 025ed9ed88d837ee0c08409636151781df89005e Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 30 Jun 2020 10:32:19 +0100 Subject: [PATCH 072/126] use feature based RBAC for actions instead of api privileges --- x-pack/plugins/actions/kibana.json | 4 +- .../actions/server/actions_client.mock.ts | 1 + .../actions/server/actions_client.test.ts | 459 ++++++++++++++++++ .../plugins/actions/server/actions_client.ts | 36 +- .../actions_authorization.mock.ts | 22 + .../actions_authorization.test.ts | 127 +++++ .../authorization/actions_authorization.ts | 54 +++ .../server/authorization/audit_logger.mock.ts | 22 + .../server/authorization/audit_logger.test.ts | 121 +++++ .../server/authorization/audit_logger.ts | 66 +++ x-pack/plugins/actions/server/feature.ts | 38 ++ x-pack/plugins/actions/server/plugin.test.ts | 3 + x-pack/plugins/actions/server/plugin.ts | 26 + .../actions/server/routes/create.test.ts | 7 - .../plugins/actions/server/routes/create.ts | 3 - .../actions/server/routes/delete.test.ts | 7 - .../plugins/actions/server/routes/delete.ts | 3 - .../actions/server/routes/execute.test.ts | 7 - .../plugins/actions/server/routes/execute.ts | 3 - .../plugins/actions/server/routes/get.test.ts | 7 - x-pack/plugins/actions/server/routes/get.ts | 3 - .../actions/server/routes/get_all.test.ts | 21 - .../plugins/actions/server/routes/get_all.ts | 3 - .../server/routes/list_action_types.test.ts | 38 +- .../server/routes/list_action_types.ts | 6 +- .../actions/server/routes/update.test.ts | 7 - .../plugins/actions/server/routes/update.ts | 3 - .../actions/server/saved_objects/index.ts | 6 +- x-pack/plugins/apm/server/feature.ts | 17 +- x-pack/plugins/infra/server/features.ts | 4 +- .../security_solution/server/plugin.ts | 4 +- x-pack/plugins/uptime/server/kibana.index.ts | 11 +- .../actions_simulators/server/plugin.ts | 8 +- .../tests/actions/create.ts | 39 +- .../tests/actions/delete.ts | 33 +- .../tests/actions/execute.ts | 56 +-- .../security_and_spaces/tests/actions/get.ts | 25 +- .../tests/actions/get_all.ts | 24 +- .../tests/actions/list_action_types.ts | 9 +- .../tests/actions/update.ts | 55 +-- 40 files changed, 1111 insertions(+), 277 deletions(-) create mode 100644 x-pack/plugins/actions/server/authorization/actions_authorization.mock.ts create mode 100644 x-pack/plugins/actions/server/authorization/actions_authorization.test.ts create mode 100644 x-pack/plugins/actions/server/authorization/actions_authorization.ts create mode 100644 x-pack/plugins/actions/server/authorization/audit_logger.mock.ts create mode 100644 x-pack/plugins/actions/server/authorization/audit_logger.test.ts create mode 100644 x-pack/plugins/actions/server/authorization/audit_logger.ts create mode 100644 x-pack/plugins/actions/server/feature.ts diff --git a/x-pack/plugins/actions/kibana.json b/x-pack/plugins/actions/kibana.json index 14ddb8257ff37..ef604a9cf6417 100644 --- a/x-pack/plugins/actions/kibana.json +++ b/x-pack/plugins/actions/kibana.json @@ -4,7 +4,7 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "actions"], - "requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "eventLog"], - "optionalPlugins": ["usageCollection", "spaces"], + "requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "eventLog", "features"], + "optionalPlugins": ["usageCollection", "spaces", "security"], "ui": false } diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index efd044c7e2493..48122a5ce4e0f 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -19,6 +19,7 @@ const createActionsClientMock = () => { getBulk: jest.fn(), execute: jest.fn(), enqueueExecution: jest.fn(), + listTypes: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 69fab828e63de..1b40e4458b77f 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -22,11 +22,14 @@ import { import { actionExecutorMock } from './lib/action_executor.mock'; import uuid from 'uuid'; import { KibanaRequest } from 'kibana/server'; +import { ActionsAuthorization } from './authorization/actions_authorization'; +import { actionsAuthorizationMock } from './authorization/actions_authorization.mock'; const defaultKibanaIndex = '.kibana'; const savedObjectsClient = savedObjectsClientMock.create(); const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); const actionExecutor = actionExecutorMock.create(); +const authorization = actionsAuthorizationMock.create(); const executionEnqueuer = jest.fn(); const request = {} as KibanaRequest; @@ -62,10 +65,81 @@ beforeEach(() => { actionExecutor, executionEnqueuer, request, + authorization: (authorization as unknown) as ActionsAuthorization, }); }); describe('create()', () => { + describe('authorization', () => { + test('ensures user is authorised to create this type of action', async () => { + const savedObjectCreateResult = { + id: '1', + type: 'action', + attributes: { + name: 'my name', + actionTypeId: 'my-action-type', + config: {}, + }, + references: [], + }; + actionTypeRegistry.register({ + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'basic', + executor, + }); + savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); + + await actionsClient.create({ + action: { + name: 'my name', + actionTypeId: 'my-action-type', + config: {}, + secrets: {}, + }, + }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('create', 'my-action-type'); + }); + + test('throws when user is not authorised to create this type of action', async () => { + const savedObjectCreateResult = { + id: '1', + type: 'action', + attributes: { + name: 'my name', + actionTypeId: 'my-action-type', + config: {}, + }, + references: [], + }; + actionTypeRegistry.register({ + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'basic', + executor, + }); + savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); + + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to create a "my-action-type" action`) + ); + + await expect( + actionsClient.create({ + action: { + name: 'my name', + actionTypeId: 'my-action-type', + config: {}, + secrets: {}, + }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to create a "my-action-type" action]`); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('create', 'my-action-type'); + }); + }); + test('creates an action with all given properties', async () => { const savedObjectCreateResult = { id: '1', @@ -244,6 +318,7 @@ describe('create()', () => { actionExecutor, executionEnqueuer, request, + authorization: (authorization as unknown) as ActionsAuthorization, }); const savedObjectCreateResult = { @@ -313,6 +388,116 @@ describe('create()', () => { }); describe('get()', () => { + describe('authorization', () => { + test('ensures user is authorised to get the type of action', async () => { + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'type', + attributes: { + name: 'my name', + actionTypeId: 'my-action-type', + config: {}, + }, + references: [], + }); + + await actionsClient.get({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get'); + }); + + test('ensures user is authorised to get preconfigured type of action', async () => { + actionsClient = new ActionsClient({ + actionTypeRegistry, + savedObjectsClient, + scopedClusterClient, + defaultKibanaIndex, + actionExecutor, + executionEnqueuer, + request, + authorization: (authorization as unknown) as ActionsAuthorization, + preconfiguredActions: [ + { + id: 'testPreconfigured', + actionTypeId: 'my-action-type', + secrets: { + test: 'test1', + }, + isPreconfigured: true, + name: 'test', + config: { + foo: 'bar', + }, + }, + ], + }); + + await actionsClient.get({ id: 'testPreconfigured' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get'); + }); + + test('throws when user is not authorised to create the type of action', async () => { + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'type', + attributes: { + name: 'my name', + actionTypeId: 'my-action-type', + config: {}, + }, + references: [], + }); + + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get a "my-action-type" action`) + ); + + await expect(actionsClient.get({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "my-action-type" action]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get'); + }); + + test('throws when user is not authorised to create preconfigured of action', async () => { + actionsClient = new ActionsClient({ + actionTypeRegistry, + savedObjectsClient, + scopedClusterClient, + defaultKibanaIndex, + actionExecutor, + executionEnqueuer, + request, + authorization: (authorization as unknown) as ActionsAuthorization, + preconfiguredActions: [ + { + id: 'testPreconfigured', + actionTypeId: 'my-action-type', + secrets: { + test: 'test1', + }, + isPreconfigured: true, + name: 'test', + config: { + foo: 'bar', + }, + }, + ], + }); + + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get a "my-action-type" action`) + ); + + await expect(actionsClient.get({ id: 'testPreconfigured' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "my-action-type" action]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get'); + }); + }); + test('calls savedObjectsClient with id', async () => { savedObjectsClient.get.mockResolvedValueOnce({ id: '1', @@ -343,6 +528,7 @@ describe('get()', () => { actionExecutor, executionEnqueuer, request, + authorization: (authorization as unknown) as ActionsAuthorization, preconfiguredActions: [ { id: 'testPreconfigured', @@ -371,6 +557,78 @@ describe('get()', () => { }); describe('getAll()', () => { + describe('authorization', () => { + function getAllOperation(): ReturnType { + const expectedResult = { + total: 1, + per_page: 10, + page: 1, + saved_objects: [ + { + id: '1', + type: 'type', + attributes: { + name: 'test', + config: { + foo: 'bar', + }, + }, + score: 1, + references: [], + }, + ], + }; + savedObjectsClient.find.mockResolvedValueOnce(expectedResult); + scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({ + aggregations: { + '1': { doc_count: 6 }, + testPreconfigured: { doc_count: 2 }, + }, + }); + + actionsClient = new ActionsClient({ + actionTypeRegistry, + savedObjectsClient, + scopedClusterClient, + defaultKibanaIndex, + actionExecutor, + executionEnqueuer, + request, + authorization: (authorization as unknown) as ActionsAuthorization, + preconfiguredActions: [ + { + id: 'testPreconfigured', + actionTypeId: '.slack', + secrets: {}, + isPreconfigured: true, + name: 'test', + config: { + foo: 'bar', + }, + }, + ], + }); + return actionsClient.getAll(); + } + + test('ensures user is authorised to get the type of action', async () => { + await getAllOperation(); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get'); + }); + + test('throws when user is not authorised to create the type of action', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get all actions`) + ); + + await expect(getAllOperation()).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get all actions]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get'); + }); + }); + test('calls savedObjectsClient with parameters', async () => { const expectedResult = { total: 1, @@ -407,6 +665,7 @@ describe('getAll()', () => { actionExecutor, executionEnqueuer, request, + authorization: (authorization as unknown) as ActionsAuthorization, preconfiguredActions: [ { id: 'testPreconfigured', @@ -443,6 +702,74 @@ describe('getAll()', () => { }); describe('getBulk()', () => { + describe('authorization', () => { + function getBulkOperation(): ReturnType { + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + name: 'test', + config: { + foo: 'bar', + }, + }, + references: [], + }, + ], + }); + scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({ + aggregations: { + '1': { doc_count: 6 }, + testPreconfigured: { doc_count: 2 }, + }, + }); + + actionsClient = new ActionsClient({ + actionTypeRegistry, + savedObjectsClient, + scopedClusterClient, + defaultKibanaIndex, + actionExecutor, + executionEnqueuer, + request, + authorization: (authorization as unknown) as ActionsAuthorization, + preconfiguredActions: [ + { + id: 'testPreconfigured', + actionTypeId: '.slack', + secrets: {}, + isPreconfigured: true, + name: 'test', + config: { + foo: 'bar', + }, + }, + ], + }); + return actionsClient.getBulk(['1', 'testPreconfigured']); + } + + test('ensures user is authorised to get the type of action', async () => { + await getBulkOperation(); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get'); + }); + + test('throws when user is not authorised to create the type of action', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get all actions`) + ); + + await expect(getBulkOperation()).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get all actions]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('get'); + }); + }); + test('calls getBulk savedObjectsClient with parameters', async () => { savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ @@ -475,6 +802,7 @@ describe('getBulk()', () => { actionExecutor, executionEnqueuer, request, + authorization: (authorization as unknown) as ActionsAuthorization, preconfiguredActions: [ { id: 'testPreconfigured', @@ -514,6 +842,25 @@ describe('getBulk()', () => { }); describe('delete()', () => { + describe('authorization', () => { + test('ensures user is authorised to delete actions', async () => { + await actionsClient.delete({ id: '1' }); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('delete'); + }); + + test('throws when user is not authorised to create the type of action', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to delete all actions`) + ); + + await expect(actionsClient.delete({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to delete all actions]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('delete'); + }); + }); + test('calls savedObjectsClient with id', async () => { const expectedResult = Symbol(); savedObjectsClient.delete.mockResolvedValueOnce(expectedResult); @@ -530,6 +877,60 @@ describe('delete()', () => { }); describe('update()', () => { + describe('authorization', () => { + function updateOperation(): ReturnType { + actionTypeRegistry.register({ + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'basic', + executor, + }); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'action', + attributes: { + actionTypeId: 'my-action-type', + }, + references: [], + }); + savedObjectsClient.update.mockResolvedValueOnce({ + id: 'my-action', + type: 'action', + attributes: { + actionTypeId: 'my-action-type', + name: 'my name', + config: {}, + secrets: {}, + }, + references: [], + }); + return actionsClient.update({ + id: 'my-action', + action: { + name: 'my name', + config: {}, + secrets: {}, + }, + }); + } + test('ensures user is authorised to update actions', async () => { + await updateOperation(); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update'); + }); + + test('throws when user is not authorised to create the type of action', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to update all actions`) + ); + + await expect(updateOperation()).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to update all actions]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update'); + }); + }); + test('updates an action with all given properties', async () => { actionTypeRegistry.register({ id: 'my-action-type', @@ -742,6 +1143,35 @@ describe('update()', () => { }); describe('execute()', () => { + describe('authorization', () => { + test('ensures user is authorised to excecute actions', async () => { + await actionsClient.execute({ + actionId: 'action-id', + params: { + name: 'my name', + }, + }); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute'); + }); + + test('throws when user is not authorised to create the type of action', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to execute all actions`) + ); + + await expect( + actionsClient.execute({ + actionId: 'action-id', + params: { + name: 'my name', + }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to execute all actions]`); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute'); + }); + }); + test('calls the actionExecutor with the appropriate parameters', async () => { const actionId = uuid.v4(); actionExecutor.execute.mockResolvedValue({ status: 'ok', actionId }); @@ -765,6 +1195,35 @@ describe('execute()', () => { }); describe('enqueueExecution()', () => { + describe('authorization', () => { + test('ensures user is authorised to excecute actions', async () => { + await actionsClient.enqueueExecution({ + id: uuid.v4(), + params: {}, + spaceId: 'default', + apiKey: null, + }); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute'); + }); + + test('throws when user is not authorised to create the type of action', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to execute all actions`) + ); + + await expect( + actionsClient.enqueueExecution({ + id: uuid.v4(), + params: {}, + spaceId: 'default', + apiKey: null, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to execute all actions]`); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('execute'); + }); + }); + test('calls the executionEnqueuer with the appropriate parameters', async () => { const opts = { id: uuid.v4(), diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index a2feac83cba9f..ca7b98f3cfc02 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -28,6 +28,8 @@ import { ExecutionEnqueuer, ExecuteOptions as EnqueueExecutionOptions, } from './create_execute_function'; +import { ActionsAuthorization } from './authorization/actions_authorization'; +import { ActionType } from '../common'; // We are assuming there won't be many actions. This is why we will load // all the actions in advance and assume the total count to not go over 10000. @@ -57,6 +59,7 @@ interface ConstructorOptions { actionExecutor: ActionExecutorContract; executionEnqueuer: ExecutionEnqueuer; request: KibanaRequest; + authorization: ActionsAuthorization; } interface UpdateOptions { @@ -72,6 +75,7 @@ export class ActionsClient { private readonly preconfiguredActions: PreConfiguredAction[]; private readonly actionExecutor: ActionExecutorContract; private readonly request: KibanaRequest; + private readonly authorization: ActionsAuthorization; private readonly executionEnqueuer: ExecutionEnqueuer; constructor({ @@ -83,6 +87,7 @@ export class ActionsClient { actionExecutor, executionEnqueuer, request, + authorization, }: ConstructorOptions) { this.actionTypeRegistry = actionTypeRegistry; this.savedObjectsClient = savedObjectsClient; @@ -92,13 +97,17 @@ export class ActionsClient { this.actionExecutor = actionExecutor; this.executionEnqueuer = executionEnqueuer; this.request = request; + this.authorization = authorization; } /** * Create an action */ - public async create({ action }: CreateOptions): Promise { - const { actionTypeId, name, config, secrets } = action; + public async create({ + action: { actionTypeId, name, config, secrets }, + }: CreateOptions): Promise { + await this.authorization.ensureAuthorized('create', actionTypeId); + const actionType = this.actionTypeRegistry.get(actionTypeId); const validatedActionTypeConfig = validateConfig(actionType, config); const validatedActionTypeSecrets = validateSecrets(actionType, secrets); @@ -125,6 +134,8 @@ export class ActionsClient { * Update action */ public async update({ id, action }: UpdateOptions): Promise { + await this.authorization.ensureAuthorized('update'); + if ( this.preconfiguredActions.find((preconfiguredAction) => preconfiguredAction.id === id) !== undefined @@ -168,6 +179,8 @@ export class ActionsClient { * Get an action */ public async get({ id }: { id: string }): Promise { + await this.authorization.ensureAuthorized('get'); + const preconfiguredActionsList = this.preconfiguredActions.find( (preconfiguredAction) => preconfiguredAction.id === id ); @@ -194,6 +207,8 @@ export class ActionsClient { * Get all actions with preconfigured list */ public async getAll(): Promise { + await this.authorization.ensureAuthorized('get'); + const savedObjectsActions = ( await this.savedObjectsClient.find({ perPage: MAX_ACTIONS_RETURNED, @@ -221,6 +236,8 @@ export class ActionsClient { * Get bulk actions with preconfigured list */ public async getBulk(ids: string[]): Promise { + await this.authorization.ensureAuthorized('get'); + const actionResults = new Array(); for (const actionId of ids) { const action = this.preconfiguredActions.find( @@ -259,6 +276,8 @@ export class ActionsClient { * Delete action */ public async delete({ id }: { id: string }) { + await this.authorization.ensureAuthorized('delete'); + if ( this.preconfiguredActions.find((preconfiguredAction) => preconfiguredAction.id === id) !== undefined @@ -280,12 +299,25 @@ export class ActionsClient { actionId, params, }: Omit): Promise { + await this.authorization.ensureAuthorized('execute'); return this.actionExecutor.execute({ actionId, params, request: this.request }); } public async enqueueExecution(options: EnqueueExecutionOptions): Promise { + await this.authorization.ensureAuthorized('execute'); return this.executionEnqueuer(this.savedObjectsClient, options); } + + public async listTypes(): Promise { + try { + await this.authorization.ensureAuthorized('list'); + return this.actionTypeRegistry.list(); + } catch { + // auditing will log this unauthorized attempt, so we'll return + // an empty list to align with the behaviour in the AlertsClient + return []; + } + } } function actionFromSavedObject(savedObject: SavedObject): ActionResult { diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.mock.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.mock.ts new file mode 100644 index 0000000000000..6b55c18241c55 --- /dev/null +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.mock.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 { ActionsAuthorization } from './actions_authorization'; + +export type ActionsAuthorizationMock = jest.Mocked>; + +const createActionsAuthorizationMock = () => { + const mocked: ActionsAuthorizationMock = { + ensureAuthorized: jest.fn(), + }; + return mocked; +}; + +export const actionsAuthorizationMock: { + create: () => ActionsAuthorizationMock; +} = { + create: createActionsAuthorizationMock, +}; diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts new file mode 100644 index 0000000000000..a876483f025a2 --- /dev/null +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts @@ -0,0 +1,127 @@ +/* + * 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 { KibanaRequest } from 'kibana/server'; +import { securityMock } from '../../../../plugins/security/server/mocks'; +import { ActionsAuthorization } from './actions_authorization'; +import { actionsAuthorizationAuditLoggerMock } from './audit_logger.mock'; +import { ActionsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; + +const request = {} as KibanaRequest; + +const auditLogger = actionsAuthorizationAuditLoggerMock.create(); +const realAuditLogger = new ActionsAuthorizationAuditLogger(); + +const mockAuthorizationAction = (type: string, operation: string) => `${type}/${operation}`; +function mockAuthorization() { + const authorization = securityMock.createSetup().authz; + // typescript is having trouble inferring jest's automocking + (authorization.actions.savedObject.get as jest.MockedFunction< + typeof authorization.actions.savedObject.get + >).mockImplementation(mockAuthorizationAction); + return authorization; +} + +beforeEach(() => { + jest.resetAllMocks(); + auditLogger.actionsAuthorizationFailure.mockImplementation((username, ...args) => + realAuditLogger.getAuthorizationMessage(AuthorizationResult.Unauthorized, ...args) + ); + auditLogger.actionsAuthorizationSuccess.mockImplementation((username, ...args) => + realAuditLogger.getAuthorizationMessage(AuthorizationResult.Authorized, ...args) + ); +}); + +describe('ensureAuthorized', () => { + test('is a no-op when there is no authorization api', async () => { + const actionsAuthorization = new ActionsAuthorization({ + request, + auditLogger, + }); + + await actionsAuthorization.ensureAuthorized('create', 'myType'); + }); + + test('ensures the user has privileges to execute the operation on the Actions Saved Object type', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const actionsAuthorization = new ActionsAuthorization({ + request, + authorization, + auditLogger, + }); + + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: [ + { + privilege: mockAuthorizationAction('myType', 'create'), + authorized: true, + }, + ], + }); + + await actionsAuthorization.ensureAuthorized('create', 'myType'); + + expect(authorization.actions.savedObject.get).toHaveBeenCalledWith('action', 'create'); + expect(checkPrivileges).toHaveBeenCalledWith(mockAuthorizationAction('action', 'create')); + + expect(auditLogger.actionsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.actionsAuthorizationFailure).not.toHaveBeenCalled(); + expect(auditLogger.actionsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "create", + "myType", + ] + `); + }); + + test('throws if user lacks the required privieleges', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const actionsAuthorization = new ActionsAuthorization({ + request, + authorization, + auditLogger, + }); + + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('myType', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myOtherType', 'create'), + authorized: true, + }, + ], + }); + + await expect( + actionsAuthorization.ensureAuthorized('create', 'myType') + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Unauthorized to create a \\"myType\\" action"`); + + expect(auditLogger.actionsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.actionsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(auditLogger.actionsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "create", + "myType", + ] + `); + }); +}); diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.ts new file mode 100644 index 0000000000000..e2aa80e3cbdd0 --- /dev/null +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.ts @@ -0,0 +1,54 @@ +/* + * 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 Boom from 'boom'; +import { KibanaRequest } from 'src/core/server'; +import { SecurityPluginSetup } from '../../../security/server'; +import { ActionsAuthorizationAuditLogger } from './audit_logger'; +import { ACTION_SAVED_OBJECT_TYPE } from '../saved_objects'; + +export interface ConstructorOptions { + request: KibanaRequest; + auditLogger: ActionsAuthorizationAuditLogger; + authorization?: SecurityPluginSetup['authz']; +} + +const operationAlias: Record = { + execute: 'get', + list: 'get', +}; + +export class ActionsAuthorization { + private readonly request: KibanaRequest; + private readonly authorization?: SecurityPluginSetup['authz']; + private readonly auditLogger: ActionsAuthorizationAuditLogger; + + constructor({ request, authorization, auditLogger }: ConstructorOptions) { + this.request = request; + this.authorization = authorization; + this.auditLogger = auditLogger; + } + + public async ensureAuthorized(operation: string, actionTypeId?: string) { + const { authorization } = this; + if (authorization) { + const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); + const { hasAllRequested, username } = await checkPrivileges( + authorization.actions.savedObject.get( + ACTION_SAVED_OBJECT_TYPE, + operationAlias[operation] ?? operation + ) + ); + if (hasAllRequested) { + this.auditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId); + } else { + throw Boom.forbidden( + this.auditLogger.actionsAuthorizationFailure(username, operation, actionTypeId) + ); + } + } + } +} diff --git a/x-pack/plugins/actions/server/authorization/audit_logger.mock.ts b/x-pack/plugins/actions/server/authorization/audit_logger.mock.ts new file mode 100644 index 0000000000000..95d4f4ebcd3aa --- /dev/null +++ b/x-pack/plugins/actions/server/authorization/audit_logger.mock.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 { ActionsAuthorizationAuditLogger } from './audit_logger'; + +const createActionsAuthorizationAuditLoggerMock = () => { + const mocked = ({ + getAuthorizationMessage: jest.fn(), + actionsAuthorizationFailure: jest.fn(), + actionsAuthorizationSuccess: jest.fn(), + } as unknown) as jest.Mocked; + return mocked; +}; + +export const actionsAuthorizationAuditLoggerMock: { + create: () => jest.Mocked; +} = { + create: createActionsAuthorizationAuditLoggerMock, +}; diff --git a/x-pack/plugins/actions/server/authorization/audit_logger.test.ts b/x-pack/plugins/actions/server/authorization/audit_logger.test.ts new file mode 100644 index 0000000000000..6d3e69b822c96 --- /dev/null +++ b/x-pack/plugins/actions/server/authorization/audit_logger.test.ts @@ -0,0 +1,121 @@ +/* + * 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 { ActionsAuthorizationAuditLogger } from './audit_logger'; + +const createMockAuditLogger = () => { + return { + log: jest.fn(), + }; +}; + +describe(`#constructor`, () => { + test('initializes a noop auditLogger if security logger is unavailable', () => { + const actionsAuditLogger = new ActionsAuthorizationAuditLogger(undefined); + + const username = 'foo-user'; + const actionTypeId = 'action-type-id'; + const operation = 'create'; + expect(() => { + actionsAuditLogger.actionsAuthorizationFailure(username, operation, actionTypeId); + actionsAuditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId); + }).not.toThrow(); + }); +}); + +describe(`#actionsAuthorizationFailure`, () => { + test('logs auth failure with consumer scope', () => { + const auditLogger = createMockAuditLogger(); + const actionsAuditLogger = new ActionsAuthorizationAuditLogger(auditLogger); + const username = 'foo-user'; + const actionTypeId = 'action-type-id'; + const operation = 'create'; + + actionsAuditLogger.actionsAuthorizationFailure(username, operation, actionTypeId); + + expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "actions_authorization_failure", + "foo-user Unauthorized to create a \\"action-type-id\\" action", + Object { + "actionTypeId": "action-type-id", + "operation": "create", + "username": "foo-user", + }, + ] + `); + }); + + test('logs auth failure with producer scope', () => { + const auditLogger = createMockAuditLogger(); + const actionsAuditLogger = new ActionsAuthorizationAuditLogger(auditLogger); + const username = 'foo-user'; + const actionTypeId = 'action-type-id'; + + const operation = 'create'; + + actionsAuditLogger.actionsAuthorizationFailure(username, operation, actionTypeId); + + expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "actions_authorization_failure", + "foo-user Unauthorized to create a \\"action-type-id\\" action", + Object { + "actionTypeId": "action-type-id", + "operation": "create", + "username": "foo-user", + }, + ] + `); + }); +}); + +describe(`#savedObjectsAuthorizationSuccess`, () => { + test('logs auth success with consumer scope', () => { + const auditLogger = createMockAuditLogger(); + const actionsAuditLogger = new ActionsAuthorizationAuditLogger(auditLogger); + const username = 'foo-user'; + const actionTypeId = 'action-type-id'; + + const operation = 'create'; + + actionsAuditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId); + + expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "actions_authorization_success", + "foo-user Authorized to create a \\"action-type-id\\" action", + Object { + "actionTypeId": "action-type-id", + "operation": "create", + "username": "foo-user", + }, + ] + `); + }); + + test('logs auth success with producer scope', () => { + const auditLogger = createMockAuditLogger(); + const actionsAuditLogger = new ActionsAuthorizationAuditLogger(auditLogger); + const username = 'foo-user'; + const actionTypeId = 'action-type-id'; + + const operation = 'create'; + + actionsAuditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId); + + expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "actions_authorization_success", + "foo-user Authorized to create a \\"action-type-id\\" action", + Object { + "actionTypeId": "action-type-id", + "operation": "create", + "username": "foo-user", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/actions/server/authorization/audit_logger.ts b/x-pack/plugins/actions/server/authorization/audit_logger.ts new file mode 100644 index 0000000000000..7e0adc9206656 --- /dev/null +++ b/x-pack/plugins/actions/server/authorization/audit_logger.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 { AuditLogger } from '../../../security/server'; + +export enum AuthorizationResult { + Unauthorized = 'Unauthorized', + Authorized = 'Authorized', +} + +export class ActionsAuthorizationAuditLogger { + private readonly auditLogger: AuditLogger; + + constructor(auditLogger: AuditLogger = { log() {} }) { + this.auditLogger = auditLogger; + } + + public getAuthorizationMessage( + authorizationResult: AuthorizationResult, + operation: string, + actionTypeId?: string + ): string { + return `${authorizationResult} to ${operation} ${ + actionTypeId ? `a "${actionTypeId}" action` : `actions` + }`; + } + + public actionsAuthorizationFailure( + username: string, + operation: string, + actionTypeId?: string + ): string { + const message = this.getAuthorizationMessage( + AuthorizationResult.Unauthorized, + operation, + actionTypeId + ); + this.auditLogger.log('actions_authorization_failure', `${username} ${message}`, { + username, + actionTypeId, + operation, + }); + return message; + } + + public actionsAuthorizationSuccess( + username: string, + operation: string, + actionTypeId?: string + ): string { + const message = this.getAuthorizationMessage( + AuthorizationResult.Authorized, + operation, + actionTypeId + ); + this.auditLogger.log('actions_authorization_success', `${username} ${message}`, { + username, + actionTypeId, + operation, + }); + return message; + } +} diff --git a/x-pack/plugins/actions/server/feature.ts b/x-pack/plugins/actions/server/feature.ts new file mode 100644 index 0000000000000..5fb87b5bd73f2 --- /dev/null +++ b/x-pack/plugins/actions/server/feature.ts @@ -0,0 +1,38 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ACTIONS_FEATURE = { + id: 'actions', + name: i18n.translate('xpack.actions.featureRegistry.actionsFeatureName', { + defaultMessage: 'Actions', + }), + navLinkId: 'actions', + app: [], + privileges: { + all: { + app: [], + api: [], + catalogue: [], + savedObject: { + all: ['action'], + read: [], + }, + ui: [], + }, + read: { + app: [], + api: [], + catalogue: [], + savedObject: { + all: [], + read: ['action'], + }, + ui: [], + }, + }, +}; diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 1602b26559bed..ac4b332e7fd7a 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -8,6 +8,7 @@ import { PluginInitializerContext, RequestHandlerContext } from '../../../../src import { coreMock, httpServerMock } from '../../../../src/core/server/mocks'; import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; +import { featuresPluginMock } from '../../features/server/mocks'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { taskManagerMock } from '../../task_manager/server/mocks'; import { eventLogMock } from '../../event_log/server/mocks'; @@ -43,6 +44,7 @@ describe('Actions Plugin', () => { licensing: licensingMock.createSetup(), eventLog: eventLogMock.createSetup(), usageCollection: usageCollectionPluginMock.createSetupContract(), + features: featuresPluginMock.createSetup(), }; }); @@ -200,6 +202,7 @@ describe('Actions Plugin', () => { licensing: licensingMock.createSetup(), eventLog: eventLogMock.createSetup(), usageCollection: usageCollectionPluginMock.createSetupContract(), + features: featuresPluginMock.createSetup(), }; pluginsStart = { taskManager: taskManagerMock.createStart(), diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index ad3fa97ee0c36..1d0d49cd4c3fd 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -29,6 +29,8 @@ import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_m import { LicensingPluginSetup } from '../../licensing/server'; import { LICENSE_TYPE } from '../../licensing/common/types'; import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { SecurityPluginSetup } from '../../security/server'; import { ActionsConfig } from './config'; import { Services, ActionType, PreConfiguredAction } from './types'; @@ -53,6 +55,9 @@ import { import { IEventLogger, IEventLogService } from '../../event_log/server'; import { initializeActionsTelemetry, scheduleActionsTelemetry } from './usage/task'; import { setupSavedObjects } from './saved_objects'; +import { ACTIONS_FEATURE } from './feature'; +import { ActionsAuthorization } from './authorization/actions_authorization'; +import { ActionsAuthorizationAuditLogger } from './authorization/audit_logger'; const EVENT_LOG_PROVIDER = 'actions'; export const EVENT_LOG_ACTIONS = { @@ -78,6 +83,8 @@ export interface ActionsPluginsSetup { spaces?: SpacesPluginSetup; eventLog: IEventLogService; usageCollection?: UsageCollectionSetup; + security?: SecurityPluginSetup; + features: FeaturesPluginSetup; } export interface ActionsPluginsStart { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; @@ -97,6 +104,7 @@ export class ActionsPlugin implements Plugin, Plugi private actionExecutor?: ActionExecutor; private licenseState: ILicenseState | null = null; private spaces?: SpacesServiceSetup; + private security?: SecurityPluginSetup; private eventLogger?: IEventLogger; private isESOUsingEphemeralEncryptionKey?: boolean; private readonly telemetryLogger: Logger; @@ -131,6 +139,7 @@ export class ActionsPlugin implements Plugin, Plugi ); } + plugins.features.registerFeature(ACTIONS_FEATURE); setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects); plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS)); @@ -167,6 +176,7 @@ export class ActionsPlugin implements Plugin, Plugi this.serverBasePath = core.http.basePath.serverBasePath; this.actionExecutor = actionExecutor; this.spaces = plugins.spaces?.spacesService; + this.security = plugins.security; registerBuiltInActionTypes({ logger: this.logger, @@ -227,6 +237,7 @@ export class ActionsPlugin implements Plugin, Plugi kibanaIndex, isESOUsingEphemeralEncryptionKey, preconfiguredActions, + security, } = this; const encryptedSavedObjectsClient = plugins.encryptedSavedObjects.getClient({ @@ -287,6 +298,13 @@ export class ActionsPlugin implements Plugin, Plugi scopedClusterClient: core.elasticsearch.legacy.client.asScoped(request), preconfiguredActions, request, + authorization: new ActionsAuthorization({ + request, + authorization: security?.authz, + auditLogger: new ActionsAuthorizationAuditLogger( + security?.audit.getLogger(ACTIONS_FEATURE.id) + ), + }), actionExecutor: actionExecutor!, executionEnqueuer: createExecutionEnqueuerFunction({ taskManager: plugins.taskManager, @@ -322,6 +340,7 @@ export class ActionsPlugin implements Plugin, Plugi isESOUsingEphemeralEncryptionKey, preconfiguredActions, actionExecutor, + security, } = this; return async function actionsRouteHandlerContext(context, request) { @@ -340,6 +359,13 @@ export class ActionsPlugin implements Plugin, Plugi scopedClusterClient: context.core.elasticsearch.legacy.client, preconfiguredActions, request, + authorization: new ActionsAuthorization({ + request, + authorization: security?.authz, + auditLogger: new ActionsAuthorizationAuditLogger( + security?.audit.getLogger(ACTIONS_FEATURE.id) + ), + }), actionExecutor: actionExecutor!, executionEnqueuer: createExecutionEnqueuerFunction({ taskManager, diff --git a/x-pack/plugins/actions/server/routes/create.test.ts b/x-pack/plugins/actions/server/routes/create.test.ts index 940b8ecc61f4e..76f2a79c9f3ee 100644 --- a/x-pack/plugins/actions/server/routes/create.test.ts +++ b/x-pack/plugins/actions/server/routes/create.test.ts @@ -28,13 +28,6 @@ describe('createActionRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/actions/action"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:actions-all", - ], - } - `); const createResult = { id: '1', diff --git a/x-pack/plugins/actions/server/routes/create.ts b/x-pack/plugins/actions/server/routes/create.ts index 8135567157583..462d3f42b506c 100644 --- a/x-pack/plugins/actions/server/routes/create.ts +++ b/x-pack/plugins/actions/server/routes/create.ts @@ -30,9 +30,6 @@ export const createActionRoute = (router: IRouter, licenseState: ILicenseState) validate: { body: bodySchema, }, - options: { - tags: ['access:actions-all'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, diff --git a/x-pack/plugins/actions/server/routes/delete.test.ts b/x-pack/plugins/actions/server/routes/delete.test.ts index 8d759f1a7565e..3bd2d93f255df 100644 --- a/x-pack/plugins/actions/server/routes/delete.test.ts +++ b/x-pack/plugins/actions/server/routes/delete.test.ts @@ -28,13 +28,6 @@ describe('deleteActionRoute', () => { const [config, handler] = router.delete.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:actions-all", - ], - } - `); const actionsClient = actionsClientMock.create(); actionsClient.delete.mockResolvedValueOnce({}); diff --git a/x-pack/plugins/actions/server/routes/delete.ts b/x-pack/plugins/actions/server/routes/delete.ts index 9d4fa4019744c..a7303247e95b0 100644 --- a/x-pack/plugins/actions/server/routes/delete.ts +++ b/x-pack/plugins/actions/server/routes/delete.ts @@ -31,9 +31,6 @@ export const deleteActionRoute = (router: IRouter, licenseState: ILicenseState) validate: { params: paramSchema, }, - options: { - tags: ['access:actions-all'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts index 6e8ebbf6f91cd..38fca656bef5a 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/execute.test.ts @@ -53,13 +53,6 @@ describe('executeActionRoute', () => { const [config, handler] = router.post.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}/_execute"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:actions-read", - ], - } - `); expect(await handler(context, req, res)).toEqual({ body: executeResult }); diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts index 28e6a54f5e92d..0d49d9a3a256e 100644 --- a/x-pack/plugins/actions/server/routes/execute.ts +++ b/x-pack/plugins/actions/server/routes/execute.ts @@ -32,9 +32,6 @@ export const executeActionRoute = (router: IRouter, licenseState: ILicenseState) body: bodySchema, params: paramSchema, }, - options: { - tags: ['access:actions-read'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, diff --git a/x-pack/plugins/actions/server/routes/get.test.ts b/x-pack/plugins/actions/server/routes/get.test.ts index ee2586851366c..434bd6a9bc224 100644 --- a/x-pack/plugins/actions/server/routes/get.test.ts +++ b/x-pack/plugins/actions/server/routes/get.test.ts @@ -29,13 +29,6 @@ describe('getActionRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:actions-read", - ], - } - `); const getResult = { id: '1', diff --git a/x-pack/plugins/actions/server/routes/get.ts b/x-pack/plugins/actions/server/routes/get.ts index 224de241c7374..33577fad87c04 100644 --- a/x-pack/plugins/actions/server/routes/get.ts +++ b/x-pack/plugins/actions/server/routes/get.ts @@ -26,9 +26,6 @@ export const getActionRoute = (router: IRouter, licenseState: ILicenseState) => validate: { params: paramSchema, }, - options: { - tags: ['access:actions-read'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, diff --git a/x-pack/plugins/actions/server/routes/get_all.test.ts b/x-pack/plugins/actions/server/routes/get_all.test.ts index 6550921278aa5..35db22d2da486 100644 --- a/x-pack/plugins/actions/server/routes/get_all.test.ts +++ b/x-pack/plugins/actions/server/routes/get_all.test.ts @@ -29,13 +29,6 @@ describe('getAllActionRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/actions"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:actions-read", - ], - } - `); const actionsClient = actionsClientMock.create(); actionsClient.getAll.mockResolvedValueOnce([]); @@ -64,13 +57,6 @@ describe('getAllActionRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/actions"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:actions-read", - ], - } - `); const actionsClient = actionsClientMock.create(); actionsClient.getAll.mockResolvedValueOnce([]); @@ -95,13 +81,6 @@ describe('getAllActionRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/actions"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:actions-read", - ], - } - `); const actionsClient = actionsClientMock.create(); actionsClient.getAll.mockResolvedValueOnce([]); diff --git a/x-pack/plugins/actions/server/routes/get_all.ts b/x-pack/plugins/actions/server/routes/get_all.ts index 03a4a97855b6b..1b57f31d14a0d 100644 --- a/x-pack/plugins/actions/server/routes/get_all.ts +++ b/x-pack/plugins/actions/server/routes/get_all.ts @@ -19,9 +19,6 @@ export const getAllActionRoute = (router: IRouter, licenseState: ILicenseState) { path: `${BASE_ACTION_API_PATH}`, validate: {}, - options: { - tags: ['access:actions-read'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, diff --git a/x-pack/plugins/actions/server/routes/list_action_types.test.ts b/x-pack/plugins/actions/server/routes/list_action_types.test.ts index f231efe1a07f3..982b64c339a5f 100644 --- a/x-pack/plugins/actions/server/routes/list_action_types.test.ts +++ b/x-pack/plugins/actions/server/routes/list_action_types.test.ts @@ -10,6 +10,7 @@ import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { LicenseType } from '../../../../plugins/licensing/server'; +import { actionsClientMock } from '../mocks'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -29,13 +30,6 @@ describe('listActionTypesRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/actions/list_action_types"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:actions-read", - ], - } - `); const listTypes = [ { @@ -48,7 +42,9 @@ describe('listActionTypesRoute', () => { }, ]; - const [context, req, res] = mockHandlerArguments({ listTypes }, {}, ['ok']); + const actionsClient = actionsClientMock.create(); + actionsClient.listTypes.mockResolvedValueOnce(listTypes); + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); expect(await handler(context, req, res)).toMatchInlineSnapshot(` Object { @@ -65,8 +61,6 @@ describe('listActionTypesRoute', () => { } `); - expect(context.actions!.listTypes).toHaveBeenCalledTimes(1); - expect(res.ok).toHaveBeenCalledWith({ body: listTypes, }); @@ -81,13 +75,6 @@ describe('listActionTypesRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/actions/list_action_types"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:actions-read", - ], - } - `); const listTypes = [ { @@ -100,8 +87,11 @@ describe('listActionTypesRoute', () => { }, ]; + const actionsClient = actionsClientMock.create(); + actionsClient.listTypes.mockResolvedValueOnce(listTypes); + const [context, req, res] = mockHandlerArguments( - { listTypes }, + { actionsClient }, { params: { id: '1' }, }, @@ -126,13 +116,6 @@ describe('listActionTypesRoute', () => { const [config, handler] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/actions/list_action_types"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:actions-read", - ], - } - `); const listTypes = [ { @@ -145,8 +128,11 @@ describe('listActionTypesRoute', () => { }, ]; + const actionsClient = actionsClientMock.create(); + actionsClient.listTypes.mockResolvedValueOnce(listTypes); + const [context, req, res] = mockHandlerArguments( - { listTypes }, + { actionsClient }, { params: { id: '1' }, }, diff --git a/x-pack/plugins/actions/server/routes/list_action_types.ts b/x-pack/plugins/actions/server/routes/list_action_types.ts index bfb5fabe127f3..c960a6bac6de0 100644 --- a/x-pack/plugins/actions/server/routes/list_action_types.ts +++ b/x-pack/plugins/actions/server/routes/list_action_types.ts @@ -19,9 +19,6 @@ export const listActionTypesRoute = (router: IRouter, licenseState: ILicenseStat { path: `${BASE_ACTION_API_PATH}/list_action_types`, validate: {}, - options: { - tags: ['access:actions-read'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, @@ -32,8 +29,9 @@ export const listActionTypesRoute = (router: IRouter, licenseState: ILicenseStat if (!context.actions) { return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); } + const actionsClient = context.actions.getActionsClient(); return res.ok({ - body: context.actions.listTypes(), + body: await actionsClient.listTypes(), }); }) ); diff --git a/x-pack/plugins/actions/server/routes/update.test.ts b/x-pack/plugins/actions/server/routes/update.test.ts index 323a52f2fc6e2..6d5b78650ba2a 100644 --- a/x-pack/plugins/actions/server/routes/update.test.ts +++ b/x-pack/plugins/actions/server/routes/update.test.ts @@ -28,13 +28,6 @@ describe('updateActionRoute', () => { const [config, handler] = router.put.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/actions/action/{id}"`); - expect(config.options).toMatchInlineSnapshot(` - Object { - "tags": Array [ - "access:actions-all", - ], - } - `); const updateResult = { id: '1', diff --git a/x-pack/plugins/actions/server/routes/update.ts b/x-pack/plugins/actions/server/routes/update.ts index 1e107a4d6edb4..328ce74ef0b08 100644 --- a/x-pack/plugins/actions/server/routes/update.ts +++ b/x-pack/plugins/actions/server/routes/update.ts @@ -33,9 +33,6 @@ export const updateActionRoute = (router: IRouter, licenseState: ILicenseState) body: bodySchema, params: paramSchema, }, - options: { - tags: ['access:actions-all'], - }, }, router.handleLegacyErrors(async function ( context: RequestHandlerContext, diff --git a/x-pack/plugins/actions/server/saved_objects/index.ts b/x-pack/plugins/actions/server/saved_objects/index.ts index d68c96a5e9270..f5052d2e7f6ab 100644 --- a/x-pack/plugins/actions/server/saved_objects/index.ts +++ b/x-pack/plugins/actions/server/saved_objects/index.ts @@ -8,12 +8,14 @@ import { SavedObjectsServiceSetup } from 'kibana/server'; import mappings from './mappings.json'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; +export const ACTION_SAVED_OBJECT_TYPE = 'action'; + export function setupSavedObjects( savedObjects: SavedObjectsServiceSetup, encryptedSavedObjects: EncryptedSavedObjectsPluginSetup ) { savedObjects.registerType({ - name: 'action', + name: ACTION_SAVED_OBJECT_TYPE, hidden: true, namespaceType: 'single', mappings: mappings.action, @@ -24,7 +26,7 @@ export function setupSavedObjects( // - `config` will be included in AAD // - everything else excluded from AAD encryptedSavedObjects.registerType({ - type: 'action', + type: ACTION_SAVED_OBJECT_TYPE, attributesToEncrypt: new Set(['secrets']), attributesToExcludeFromAAD: new Set(['name']), }); diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index 80f722bae0868..bc05235257eed 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -20,14 +20,7 @@ export const APM_FEATURE = { privileges: { all: { app: ['apm', 'kibana'], - api: [ - 'apm', - 'apm_write', - 'actions-read', - 'actions-all', - 'alerting-read', - 'alerting-all', - ], + api: ['apm', 'apm_write', 'alerting-read', 'alerting-all'], catalogue: ['apm'], savedObject: { all: ['alert', 'action', 'action_task_params'], @@ -46,13 +39,7 @@ export const APM_FEATURE = { }, read: { app: ['apm', 'kibana'], - api: [ - 'apm', - 'actions-read', - 'actions-all', - 'alerting-read', - 'alerting-all', - ], + api: ['apm', 'alerting-read', 'alerting-all'], catalogue: ['apm'], savedObject: { all: ['alert', 'action', 'action_task_params'], diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index fa228e03194a9..0efa512ca9f1a 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -20,7 +20,7 @@ export const METRICS_FEATURE = { all: { app: ['infra', 'kibana'], catalogue: ['infraops'], - api: ['infra', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'], + api: ['infra', 'alerting-read', 'alerting-all'], savedObject: { all: ['infrastructure-ui-source', 'alert', 'action', 'action_task_params'], read: ['index-pattern'], @@ -40,7 +40,7 @@ export const METRICS_FEATURE = { read: { app: ['infra', 'kibana'], catalogue: ['infraops'], - api: ['infra', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'], + api: ['infra', 'alerting-read', 'alerting-all'], savedObject: { all: ['alert', 'action', 'action_task_params'], read: ['infrastructure-ui-source', 'index-pattern'], diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 879c132ddec54..616895bca84eb 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -137,7 +137,7 @@ export class Plugin implements IPlugin Date: Tue, 30 Jun 2020 10:33:15 +0100 Subject: [PATCH 073/126] temporary security changes until alerting rbac branch is merged --- .../authorization/actions/actions.mock.ts | 32 +++++++++++++++++++ .../server/authorization/index.mock.ts | 5 ++- x-pack/plugins/security/server/mocks.ts | 1 + x-pack/plugins/security/server/plugin.ts | 6 +++- 4 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/security/server/authorization/actions/actions.mock.ts diff --git a/x-pack/plugins/security/server/authorization/actions/actions.mock.ts b/x-pack/plugins/security/server/authorization/actions/actions.mock.ts new file mode 100644 index 0000000000000..3a6038b3f55c3 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/actions/actions.mock.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 { ApiActions } from './api'; +import { AppActions } from './app'; +import { SavedObjectActions } from './saved_object'; +import { SpaceActions } from './space'; +import { UIActions } from './ui'; +import { Actions } from './actions'; + +jest.mock('./api'); +jest.mock('./app'); +jest.mock('./saved_object'); +jest.mock('./space'); +jest.mock('./ui'); + +const create = (versionNumber: string) => { + const t = ({ + api: new ApiActions(versionNumber), + app: new AppActions(versionNumber), + login: 'login:', + savedObject: new SavedObjectActions(versionNumber), + space: new SpaceActions(versionNumber), + ui: new UIActions(versionNumber), + version: `version:${versionNumber}`, + } as unknown) as jest.Mocked; + return t; +}; + +export const actionsMock = { create }; diff --git a/x-pack/plugins/security/server/authorization/index.mock.ts b/x-pack/plugins/security/server/authorization/index.mock.ts index 930ede4157723..62b254d132d9e 100644 --- a/x-pack/plugins/security/server/authorization/index.mock.ts +++ b/x-pack/plugins/security/server/authorization/index.mock.ts @@ -3,16 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { Actions } from '.'; import { AuthorizationMode } from './mode'; +import { actionsMock } from './actions/actions.mock'; export const authorizationMock = { create: ({ version = 'mock-version', applicationName = 'mock-application', }: { version?: string; applicationName?: string } = {}) => ({ - actions: new Actions(version), + actions: actionsMock.create(version), checkPrivilegesWithRequest: jest.fn(), checkPrivilegesDynamicallyWithRequest: jest.fn(), checkSavedObjectsPrivilegesWithRequest: jest.fn(), diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index c2d99433b0346..4ce0ec6e3c10e 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -17,6 +17,7 @@ function createSetupMock() { authz: { actions: mockAuthz.actions, checkPrivilegesWithRequest: mockAuthz.checkPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: mockAuthz.checkPrivilegesDynamicallyWithRequest, mode: mockAuthz.mode, }, registerSpacesService: jest.fn(), diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index a14617c8489cc..8a15106a86872 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -51,7 +51,10 @@ export interface SecurityPluginSetup { | 'grantAPIKeyAsInternalUser' | 'invalidateAPIKeyAsInternalUser' >; - authz: Pick; + authz: Pick< + AuthorizationServiceSetup, + 'actions' | 'checkPrivilegesDynamicallyWithRequest' | 'checkPrivilegesWithRequest' | 'mode' + >; license: SecurityLicense; audit: Pick; @@ -206,6 +209,7 @@ export class Plugin { authz: { actions: authz.actions, checkPrivilegesWithRequest: authz.checkPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: authz.checkPrivilegesDynamicallyWithRequest, mode: authz.mode, }, From 29c9cc7567027aecdf7e3dc68c59cec05538204f Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 30 Jun 2020 11:27:32 +0100 Subject: [PATCH 074/126] base execution privileges on access to action_task_params type --- .../actions_authorization.test.ts | 52 ++++++++++++++++++- .../authorization/actions_authorization.ts | 21 +++++--- .../actions/server/create_execute_function.ts | 14 +++-- x-pack/plugins/actions/server/feature.ts | 8 +-- .../actions/server/lib/task_runner_factory.ts | 7 +-- x-pack/plugins/actions/server/plugin.ts | 8 ++- .../actions/server/saved_objects/index.ts | 5 +- x-pack/plugins/apm/server/feature.ts | 4 +- x-pack/plugins/infra/server/features.ts | 4 +- .../security_solution/server/plugin.ts | 4 +- x-pack/plugins/uptime/server/kibana.index.ts | 4 +- .../actions_simulators/server/plugin.ts | 6 +-- .../security_and_spaces/scenarios.ts | 2 + 13 files changed, 103 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts index a876483f025a2..d7d646ca4dd54 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts @@ -8,6 +8,7 @@ import { securityMock } from '../../../../plugins/security/server/mocks'; import { ActionsAuthorization } from './actions_authorization'; import { actionsAuthorizationAuditLoggerMock } from './audit_logger.mock'; import { ActionsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; +import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects'; const request = {} as KibanaRequest; @@ -44,7 +45,7 @@ describe('ensureAuthorized', () => { await actionsAuthorization.ensureAuthorized('create', 'myType'); }); - test('ensures the user has privileges to execute the operation on the Actions Saved Object type', async () => { + test('ensures the user has privileges to use the operation on the Actions Saved Object type', async () => { const authorization = mockAuthorization(); const checkPrivileges: jest.MockedFunction { `); }); + test('ensures the user has privileges to execute an Actions Saved Object type', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const actionsAuthorization = new ActionsAuthorization({ + request, + authorization, + auditLogger, + }); + + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: [ + { + privilege: mockAuthorizationAction('myType', 'execute'), + authorized: true, + }, + ], + }); + + await actionsAuthorization.ensureAuthorized('execute', 'myType'); + + expect(authorization.actions.savedObject.get).toHaveBeenCalledWith( + ACTION_SAVED_OBJECT_TYPE, + 'get' + ); + expect(authorization.actions.savedObject.get).toHaveBeenCalledWith( + ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + 'create' + ); + expect(checkPrivileges).toHaveBeenCalledWith([ + mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'), + mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'), + ]); + + expect(auditLogger.actionsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.actionsAuthorizationFailure).not.toHaveBeenCalled(); + expect(auditLogger.actionsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "execute", + "myType", + ] + `); + }); + test('throws if user lacks the required privieleges', async () => { const authorization = mockAuthorization(); const checkPrivileges: jest.MockedFunction = { - execute: 'get', - list: 'get', +const operationAlias: Record< + string, + (authorization: SecurityPluginSetup['authz']) => string | string[] +> = { + execute: (authorization) => [ + authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, 'get'), + authorization.actions.savedObject.get(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'), + ], + list: (authorization) => authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, 'get'), }; export class ActionsAuthorization { @@ -37,10 +43,9 @@ export class ActionsAuthorization { if (authorization) { const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); const { hasAllRequested, username } = await checkPrivileges( - authorization.actions.savedObject.get( - ACTION_SAVED_OBJECT_TYPE, - operationAlias[operation] ?? operation - ) + operationAlias[operation] + ? operationAlias[operation](authorization) + : authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation) ); if (hasAllRequested) { this.auditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId); diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 2bad33d56f228..85052eef93e05 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -7,6 +7,7 @@ import { SavedObjectsClientContract } from '../../../../src/core/server'; import { TaskManagerStartContract } from '../../task_manager/server'; import { RawAction, ActionTypeRegistryContract, PreConfiguredAction } from './types'; +import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './saved_objects'; interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; @@ -49,11 +50,14 @@ export function createExecutionEnqueuerFunction({ actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); } - const actionTaskParamsRecord = await savedObjectsClient.create('action_task_params', { - actionId: id, - params, - apiKey, - }); + const actionTaskParamsRecord = await savedObjectsClient.create( + ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, + { + actionId: id, + params, + apiKey, + } + ); await taskManager.schedule({ taskType: `actions:${actionTypeId}`, diff --git a/x-pack/plugins/actions/server/feature.ts b/x-pack/plugins/actions/server/feature.ts index 5fb87b5bd73f2..93f94337dff60 100644 --- a/x-pack/plugins/actions/server/feature.ts +++ b/x-pack/plugins/actions/server/feature.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './saved_objects'; export const ACTIONS_FEATURE = { id: 'actions', @@ -19,7 +20,7 @@ export const ACTIONS_FEATURE = { api: [], catalogue: [], savedObject: { - all: ['action'], + all: [ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE], read: [], }, ui: [], @@ -29,8 +30,9 @@ export const ACTIONS_FEATURE = { api: [], catalogue: [], savedObject: { - all: [], - read: ['action'], + // action execution requires 'read' over `actions`, but 'all' over `action_task_params` + all: [ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE], + read: [ACTION_SAVED_OBJECT_TYPE], }, ui: [], }, diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index a962497f906a9..9204c41b9288c 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -17,6 +17,7 @@ import { SpaceIdToNamespaceFunction, ActionTypeExecutorResult, } from '../types'; +import { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects'; export interface TaskRunnerContext { logger: Logger; @@ -66,7 +67,7 @@ export class TaskRunnerFactory { const { attributes: { actionId, params, apiKey }, } = await encryptedSavedObjectsClient.getDecryptedAsInternalUser( - 'action_task_params', + ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, actionTaskParamsId, { namespace } ); @@ -121,11 +122,11 @@ export class TaskRunnerFactory { // Cleanup action_task_params object now that we're done with it try { const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest); - await savedObjectsClient.delete('action_task_params', actionTaskParamsId); + await savedObjectsClient.delete(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, actionTaskParamsId); } catch (e) { // Log error only, we shouldn't fail the task because of an error here (if ever there's retry logic) logger.error( - `Failed to cleanup action_task_params object [id="${actionTaskParamsId}"]: ${e.message}` + `Failed to cleanup ${ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE} object [id="${actionTaskParamsId}"]: ${e.message}` ); } }, diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 1d0d49cd4c3fd..cf022bc90b43a 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -54,7 +54,11 @@ import { } from './routes'; import { IEventLogger, IEventLogService } from '../../event_log/server'; import { initializeActionsTelemetry, scheduleActionsTelemetry } from './usage/task'; -import { setupSavedObjects } from './saved_objects'; +import { + setupSavedObjects, + ACTION_SAVED_OBJECT_TYPE, + ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, +} from './saved_objects'; import { ACTIONS_FEATURE } from './feature'; import { ActionsAuthorization } from './authorization/actions_authorization'; import { ActionsAuthorizationAuditLogger } from './authorization/audit_logger'; @@ -91,7 +95,7 @@ export interface ActionsPluginsStart { taskManager: TaskManagerStartContract; } -const includedHiddenTypes = ['action', 'action_task_params']; +const includedHiddenTypes = [ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE]; export class ActionsPlugin implements Plugin, PluginStartContract> { private readonly kibanaIndex: Promise; diff --git a/x-pack/plugins/actions/server/saved_objects/index.ts b/x-pack/plugins/actions/server/saved_objects/index.ts index f5052d2e7f6ab..54f186acc1ba5 100644 --- a/x-pack/plugins/actions/server/saved_objects/index.ts +++ b/x-pack/plugins/actions/server/saved_objects/index.ts @@ -9,6 +9,7 @@ import mappings from './mappings.json'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; export const ACTION_SAVED_OBJECT_TYPE = 'action'; +export const ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE = 'action_task_params'; export function setupSavedObjects( savedObjects: SavedObjectsServiceSetup, @@ -32,13 +33,13 @@ export function setupSavedObjects( }); savedObjects.registerType({ - name: 'action_task_params', + name: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, hidden: true, namespaceType: 'single', mappings: mappings.action_task_params, }); encryptedSavedObjects.registerType({ - type: 'action_task_params', + type: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, attributesToEncrypt: new Set(['apiKey']), }); } diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index bc05235257eed..c02b894ff1f3a 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -23,7 +23,7 @@ export const APM_FEATURE = { api: ['apm', 'apm_write', 'alerting-read', 'alerting-all'], catalogue: ['apm'], savedObject: { - all: ['alert', 'action', 'action_task_params'], + all: ['alert'], read: [], }, ui: [ @@ -42,7 +42,7 @@ export const APM_FEATURE = { api: ['apm', 'alerting-read', 'alerting-all'], catalogue: ['apm'], savedObject: { - all: ['alert', 'action', 'action_task_params'], + all: ['alert'], read: [], }, ui: [ diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index 0efa512ca9f1a..65524dd5e4df7 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -22,7 +22,7 @@ export const METRICS_FEATURE = { catalogue: ['infraops'], api: ['infra', 'alerting-read', 'alerting-all'], savedObject: { - all: ['infrastructure-ui-source', 'alert', 'action', 'action_task_params'], + all: ['infrastructure-ui-source', 'alert'], read: ['index-pattern'], }, ui: [ @@ -42,7 +42,7 @@ export const METRICS_FEATURE = { catalogue: ['infraops'], api: ['infra', 'alerting-read', 'alerting-all'], savedObject: { - all: ['alert', 'action', 'action_task_params'], + all: ['alert'], read: ['infrastructure-ui-source', 'index-pattern'], }, ui: [ diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 616895bca84eb..9cf431611d7c4 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -141,8 +141,6 @@ export class Plugin implements IPlugin Date: Tue, 30 Jun 2020 11:42:27 +0100 Subject: [PATCH 075/126] fixed linting --- x-pack/plugins/alerts/server/alert_type_registry.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/alerts/server/alert_type_registry.test.ts b/x-pack/plugins/alerts/server/alert_type_registry.test.ts index 096d064685a92..c740390713715 100644 --- a/x-pack/plugins/alerts/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerts/server/alert_type_registry.test.ts @@ -77,7 +77,7 @@ describe('register()', () => { test('throws if AlertType Id isnt a string', () => { const alertType = { - id: (123 as any) as string, + id: (123 as unknown) as string, name: 'Test', actionGroups: [ { From 036a0829854383fa76dd474ff68b112132201da7 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 1 Jul 2020 09:59:05 +0100 Subject: [PATCH 076/126] fixed security typing --- .../alerts/server/authorization/alerts_authorization.ts | 5 ++++- x-pack/plugins/features/common/feature_kibana_privileges.ts | 4 ++-- .../privileges/feature_privilege_builder/alerting.ts | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 1260deb64e6f1..5ad34b69272f7 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -7,6 +7,7 @@ import Boom from 'boom'; import { pluck, mapValues, remove, omit, isUndefined } from 'lodash'; import { KibanaRequest } from 'src/core/server'; +import { RecursiveReadonly } from '@kbn/utility-types'; import { ALERTS_FEATURE_ID } from '../../common'; import { AlertTypeRegistry } from '../types'; import { SecurityPluginSetup } from '../../../security/server'; @@ -316,7 +317,9 @@ export function ensureFieldIsSafeForQuery(field: string, value: string): boolean } function hasAnyAlertingPrivileges( - privileges?: FeatureKibanaPrivileges | SubFeaturePrivilegeConfig + privileges?: + | RecursiveReadonly + | RecursiveReadonly ): boolean { return ( ((privileges?.alerting?.all?.length ?? 0) || (privileges?.alerting?.read?.length ?? 0)) > 0 diff --git a/x-pack/plugins/features/common/feature_kibana_privileges.ts b/x-pack/plugins/features/common/feature_kibana_privileges.ts index 19433193010e1..c8faf75b348fd 100644 --- a/x-pack/plugins/features/common/feature_kibana_privileges.ts +++ b/x-pack/plugins/features/common/feature_kibana_privileges.ts @@ -90,7 +90,7 @@ export interface FeatureKibanaPrivileges { * } * ``` */ - all?: string[]; + all?: readonly string[]; /** * List of alert types which users should have read-only access to when granted this privilege. @@ -101,7 +101,7 @@ export interface FeatureKibanaPrivileges { * } * ``` */ - read?: string[]; + read?: readonly string[]; }; /** * If your feature requires access to specific saved objects, then specify your access needs here. diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts index d697884e25104..42dd7794ba184 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -27,7 +27,7 @@ export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { const getAlertingPrivilege = ( operations: string[], - privilegedTypes: string[], + privilegedTypes: readonly string[], consumer: string ) => privilegedTypes.flatMap((type) => From 33ef0b0f588b11e0fca12f217722f497ef522f23 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 1 Jul 2020 16:18:30 +0100 Subject: [PATCH 077/126] ensure save/edit buttons in triggers UI is based on RBAC auth --- examples/alerting_example/server/plugin.ts | 3 +- .../alerts/server/alerts_client.test.ts | 22 +- x-pack/plugins/alerts/server/alerts_client.ts | 46 ++- .../alerts_authorization.test.ts | 318 ++++++++++++++---- .../authorization/alerts_authorization.ts | 138 ++++++-- .../server/routes/list_alert_types.test.ts | 8 +- .../application/lib/action_variables.test.ts | 2 +- .../public/application/lib/alert_api.test.ts | 2 +- .../public/application/lib/capabilities.ts | 12 +- .../components/alert_details.test.tsx | 40 +-- .../components/alert_details.tsx | 4 +- .../sections/alert_form/alert_add.test.tsx | 5 +- .../sections/alert_form/alert_form.test.tsx | 15 +- .../sections/alert_form/alert_form.tsx | 6 +- .../components/alerts_list.test.tsx | 27 +- .../alerts_list/components/alerts_list.tsx | 37 +- .../components/collapsed_item_actions.tsx | 15 +- .../triggers_actions_ui/public/types.ts | 3 +- .../tests/alerting/list_alert_types.ts | 67 +++- 19 files changed, 566 insertions(+), 204 deletions(-) diff --git a/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts index f6c0948a6c30c..2bd742fc58bcc 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/examples/alerting_example/server/plugin.ts @@ -25,6 +25,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../../x-pack/plug import { alertType as alwaysFiringAlert } from './alert_types/always_firing'; import { alertType as peopleInSpaceAlert } from './alert_types/astros'; import { INDEX_THRESHOLD_ID } from '../../../x-pack/plugins/alerting_builtins/server'; +import { ALERTING_EXAMPLE_APP_ID } from '../common/constants'; // this plugin's dependendencies export interface AlertingExampleDeps { @@ -38,7 +39,7 @@ export class AlertingExamplePlugin implements Plugin { actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', producer: 'alerts', - authorizedConsumers: ['myApp'], + authorizedConsumers: { + myApp: { read: true, all: true }, + }, }, ]) ); @@ -3712,6 +3714,12 @@ describe('listAlertTypes', () => { }; const setOfAlertTypes = new Set([myAppAlertType, alertingAlertType]); + const authorizedConsumers = { + alerts: { read: true, all: true }, + myApp: { read: true, all: true }, + myOtherApp: { read: true, all: true }, + }; + beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); }); @@ -3720,14 +3728,14 @@ describe('listAlertTypes', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); authorization.filterByAlertTypeAuthorization.mockResolvedValue( new Set([ - { ...myAppAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, - { ...alertingAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, + { ...myAppAlertType, authorizedConsumers }, + { ...alertingAlertType, authorizedConsumers }, ]) ); expect(await alertsClient.listAlertTypes()).toEqual( new Set([ - { ...myAppAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, - { ...alertingAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, + { ...myAppAlertType, authorizedConsumers }, + { ...alertingAlertType, authorizedConsumers }, ]) ); }); @@ -3762,7 +3770,9 @@ describe('listAlertTypes', () => { actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', producer: 'alerts', - authorizedConsumers: ['myApp'], + authorizedConsumers: { + myApp: { read: true, all: true }, + }, }, ]); authorization.filterByAlertTypeAuthorization.mockResolvedValue(authorizedTypes); diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index d614cab6c0012..0be60673881fb 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -36,7 +36,11 @@ import { TaskManagerStartContract } from '../../task_manager/server'; import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists'; import { RegistryAlertType } from './alert_type_registry'; -import { AlertsAuthorization } from './authorization/alerts_authorization'; +import { + AlertsAuthorization, + WriteOperations, + ReadOperations, +} from './authorization/alerts_authorization'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; @@ -172,7 +176,11 @@ export class AlertsClient { } public async create({ data, options }: CreateOptions): Promise { - await this.authorization.ensureAuthorized(data.alertTypeId, data.consumer, 'create'); + await this.authorization.ensureAuthorized( + data.alertTypeId, + data.consumer, + WriteOperations.Create + ); // Throws an error if alert type isn't registered const alertType = this.alertTypeRegistry.get(data.alertTypeId); @@ -233,14 +241,18 @@ export class AlertsClient { await this.authorization.ensureAuthorized( result.attributes.alertTypeId, result.attributes.consumer, - 'get' + ReadOperations.Get ); return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references); } public async getAlertState({ id }: { id: string }): Promise { const alert = await this.get({ id }); - await this.authorization.ensureAuthorized(alert.alertTypeId, alert.consumer, 'getAlertState'); + await this.authorization.ensureAuthorized( + alert.alertTypeId, + alert.consumer, + ReadOperations.GetAlertState + ); if (alert.scheduledTaskId) { const { state } = taskInstanceToAlertTaskInstance( await this.taskManager.get(alert.scheduledTaskId), @@ -317,7 +329,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, - 'delete' + WriteOperations.Delete ); const removeResult = await this.unsecuredSavedObjectsClient.delete('alert', id); @@ -348,7 +360,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( alertSavedObject.attributes.alertTypeId, alertSavedObject.attributes.consumer, - 'update' + WriteOperations.Update ); const updateResult = await this.updateAlert({ id, data }, alertSavedObject); @@ -454,7 +466,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, - 'updateApiKey' + WriteOperations.UpdateApiKey ); const username = await this.getUserName(); @@ -516,7 +528,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, - 'enable' + WriteOperations.Enable ); if (attributes.enabled === false) { @@ -568,7 +580,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, - 'disable' + WriteOperations.Disable ); if (attributes.enabled === true) { @@ -600,7 +612,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, - 'muteAll' + WriteOperations.MuteAll ); await this.unsecuredSavedObjectsClient.update('alert', id, { @@ -615,7 +627,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, - 'unmuteAll' + WriteOperations.UnmuteAll ); await this.unsecuredSavedObjectsClient.update('alert', id, { @@ -634,7 +646,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, - 'muteInstance' + WriteOperations.MuteInstance ); const mutedInstanceIds = attributes.mutedInstanceIds || []; @@ -666,7 +678,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, - 'unmuteInstance' + WriteOperations.UnmuteInstance ); const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { @@ -684,10 +696,10 @@ export class AlertsClient { } public async listAlertTypes() { - return await this.authorization.filterByAlertTypeAuthorization( - this.alertTypeRegistry.list(), - 'get' - ); + return await this.authorization.filterByAlertTypeAuthorization(this.alertTypeRegistry.list(), [ + ReadOperations.Get, + WriteOperations.Create, + ]); } private async scheduleAlert(id: string, alertTypeId: string) { diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 280798e002822..42c244108b6a4 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -8,7 +8,12 @@ import { alertTypeRegistryMock } from '../alert_type_registry.mock'; import { securityMock } from '../../../../plugins/security/server/mocks'; import { PluginStartContract as FeaturesStartContract, Feature } from '../../../features/server'; import { featuresPluginMock } from '../../../features/server/mocks'; -import { AlertsAuthorization, ensureFieldIsSafeForQuery } from './alerts_authorization'; +import { + AlertsAuthorization, + ensureFieldIsSafeForQuery, + WriteOperations, + ReadOperations, +} from './alerts_authorization'; import { alertsAuthorizationAuditLoggerMock } from './audit_logger.mock'; import { AlertsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; @@ -171,7 +176,7 @@ describe('ensureAuthorized', () => { auditLogger, }); - await alertAuthorization.ensureAuthorized('myType', 'myApp', 'create'); + await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); expect(alertTypeRegistry.get).toHaveBeenCalledTimes(0); }); @@ -196,7 +201,7 @@ describe('ensureAuthorized', () => { privileges: [], }); - await alertAuthorization.ensureAuthorized('myType', 'myApp', 'create'); + await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); @@ -238,7 +243,7 @@ describe('ensureAuthorized', () => { privileges: [], }); - await alertAuthorization.ensureAuthorized('myType', 'alerts', 'create'); + await alertAuthorization.ensureAuthorized('myType', 'alerts', WriteOperations.Create); expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); @@ -280,7 +285,7 @@ describe('ensureAuthorized', () => { auditLogger, }); - await alertAuthorization.ensureAuthorized('myType', 'myOtherApp', 'create'); + await alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create); expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); @@ -338,7 +343,7 @@ describe('ensureAuthorized', () => { }); await expect( - alertAuthorization.ensureAuthorized('myType', 'myOtherApp', 'create') + alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` ); @@ -386,7 +391,7 @@ describe('ensureAuthorized', () => { }); await expect( - alertAuthorization.ensureAuthorized('myType', 'myOtherApp', 'create') + alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unauthorized to create a \\"myType\\" alert by \\"myApp\\""` ); @@ -434,7 +439,7 @@ describe('ensureAuthorized', () => { }); await expect( - alertAuthorization.ensureAuthorized('myType', 'myOtherApp', 'create') + alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` ); @@ -544,10 +549,6 @@ describe('getFindAuthorizationFilter', () => { username: 'some-user', hasAllRequested: false, privileges: [ - { - privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'find'), - authorized: true, - }, { privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'find'), authorized: true, @@ -556,10 +557,6 @@ describe('getFindAuthorizationFilter', () => { privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'find'), authorized: false, }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'find'), - authorized: true, - }, { privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), authorized: true, @@ -610,10 +607,6 @@ describe('getFindAuthorizationFilter', () => { username: 'some-user', hasAllRequested: false, privileges: [ - { - privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'find'), - authorized: true, - }, { privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'find'), authorized: true, @@ -622,10 +615,6 @@ describe('getFindAuthorizationFilter', () => { privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'find'), authorized: false, }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'find'), - authorized: true, - }, { privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), authorized: true, @@ -696,19 +685,31 @@ describe('filterByAlertTypeAuthorization', () => { await expect( alertAuthorization.filterByAlertTypeAuthorization( new Set([myAppAlertType, alertingAlertType]), - 'create' + [WriteOperations.Create] ) ).resolves.toMatchInlineSnapshot(` Set { Object { "actionGroups": Array [], "actionVariables": undefined, - "authorizedConsumers": Array [ - "alerts", - "myApp", - "myOtherApp", - "myAppWithSubFeature", - ], + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + "myAppWithSubFeature": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, + }, "defaultActionGroupId": "default", "id": "myAppAlertType", "name": "myAppAlertType", @@ -717,12 +718,24 @@ describe('filterByAlertTypeAuthorization', () => { Object { "actionGroups": Array [], "actionVariables": undefined, - "authorizedConsumers": Array [ - "alerts", - "myApp", - "myOtherApp", - "myAppWithSubFeature", - ], + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + "myAppWithSubFeature": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, + }, "defaultActionGroupId": "default", "id": "alertingAlertType", "name": "alertingAlertType", @@ -742,10 +755,6 @@ describe('filterByAlertTypeAuthorization', () => { username: 'some-user', hasAllRequested: false, privileges: [ - { - privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'create'), - authorized: true, - }, { privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'create'), authorized: true, @@ -754,10 +763,6 @@ describe('filterByAlertTypeAuthorization', () => { privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'create'), authorized: false, }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'create'), - authorized: true, - }, { privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), authorized: true, @@ -781,17 +786,23 @@ describe('filterByAlertTypeAuthorization', () => { await expect( alertAuthorization.filterByAlertTypeAuthorization( new Set([myAppAlertType, alertingAlertType]), - 'create' + [WriteOperations.Create] ) ).resolves.toMatchInlineSnapshot(` Set { Object { "actionGroups": Array [], "actionVariables": undefined, - "authorizedConsumers": Array [ - "alerts", - "myApp", - ], + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + }, "defaultActionGroupId": "default", "id": "alertingAlertType", "name": "alertingAlertType", @@ -800,11 +811,20 @@ describe('filterByAlertTypeAuthorization', () => { Object { "actionGroups": Array [], "actionVariables": undefined, - "authorizedConsumers": Array [ - "alerts", - "myApp", - "myOtherApp", - ], + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, + }, "defaultActionGroupId": "default", "id": "myAppAlertType", "name": "myAppAlertType", @@ -814,7 +834,7 @@ describe('filterByAlertTypeAuthorization', () => { `); }); - test('omits types which have no consumers under which the operation is authorized', async () => { + test('authorizes user under the Alerts consumer when they are authorized by the producer', async () => { const authorization = mockAuthorization(); const checkPrivileges: jest.MockedFunction { hasAllRequested: false, privileges: [ { - privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'create'), + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), authorized: true, }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + ], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + auditLogger, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.filterByAlertTypeAuthorization(new Set([myAppAlertType]), [ + WriteOperations.Create, + ]) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + }, + "defaultActionGroupId": "default", + "id": "myAppAlertType", + "name": "myAppAlertType", + "producer": "myApp", + }, + } + `); + }); + + test('augments a list of types with consumers under which multiple operations are authorized', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: [ { privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'create'), authorized: true, }, { privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'create'), - authorized: true, + authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'create'), + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), authorized: false, }, + { + privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'get'), + authorized: true, + }, + ], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + auditLogger, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.filterByAlertTypeAuthorization( + new Set([myAppAlertType, alertingAlertType]), + [WriteOperations.Create, ReadOperations.Get] + ) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": false, + "read": true, + }, + }, + "defaultActionGroupId": "default", + "id": "alertingAlertType", + "name": "alertingAlertType", + "producer": "alerts", + }, + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "alerts": Object { + "all": false, + "read": true, + }, + "myApp": Object { + "all": false, + "read": true, + }, + "myOtherApp": Object { + "all": false, + "read": true, + }, + }, + "defaultActionGroupId": "default", + "id": "myAppAlertType", + "name": "myAppAlertType", + "producer": "myApp", + }, + } + `); + }); + + test('omits types which have no consumers under which the operation is authorized', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'create'), + authorized: true, + }, { privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), authorized: false, @@ -863,18 +1042,27 @@ describe('filterByAlertTypeAuthorization', () => { await expect( alertAuthorization.filterByAlertTypeAuthorization( new Set([myAppAlertType, alertingAlertType]), - 'create' + [WriteOperations.Create] ) ).resolves.toMatchInlineSnapshot(` Set { Object { "actionGroups": Array [], "actionVariables": undefined, - "authorizedConsumers": Array [ - "alerts", - "myApp", - "myOtherApp", - ], + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, + }, "defaultActionGroupId": "default", "id": "alertingAlertType", "name": "alertingAlertType", diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 5ad34b69272f7..22532ca030419 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { pluck, mapValues, remove, omit, isUndefined } from 'lodash'; +import { pluck, mapValues, remove, omit, isUndefined, zipObject } from 'lodash'; import { KibanaRequest } from 'src/core/server'; import { RecursiveReadonly } from '@kbn/utility-types'; import { ALERTS_FEATURE_ID } from '../../common'; @@ -16,10 +16,36 @@ import { FeatureKibanaPrivileges, SubFeaturePrivilegeConfig } from '../../../fea import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; +export enum ReadOperations { + Get = 'get', + GetAlertState = 'getAlertState', + Find = 'find', +} + +export enum WriteOperations { + Create = 'create', + Delete = 'delete', + Update = 'update', + UpdateApiKey = 'updateApiKey', + Enable = 'enable', + Disable = 'disable', + MuteAll = 'muteAll', + UnmuteAll = 'unmuteAll', + MuteInstance = 'muteInstance', + UnmuteInstance = 'unmuteInstance', +} + +interface HasPrivileges { + read: boolean; + all: boolean; +} +type AuthorizedConsumers = Record; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { - authorizedConsumers: string[]; + authorizedConsumers: AuthorizedConsumers; } +type IsAuthorizedAtProducerLevel = boolean; + export interface ConstructorOptions { alertTypeRegistry: AlertTypeRegistry; request: KibanaRequest; @@ -49,7 +75,11 @@ export class AlertsAuthorization { this.auditLogger = auditLogger; } - public async ensureAuthorized(alertTypeId: string, consumer: string, operation: string) { + public async ensureAuthorized( + alertTypeId: string, + consumer: string, + operation: ReadOperations | WriteOperations + ) { const { authorization } = this; if (authorization) { const alertType = this.alertTypeRegistry.get(alertTypeId); @@ -123,10 +153,12 @@ export class AlertsAuthorization { ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => void; }> { if (this.authorization) { - const { username, authorizedAlertTypes } = await this.augmentAlertTypesWithAuthorization( - this.alertTypeRegistry.list(), - 'find' - ); + const { + username, + authorizedAlertTypes, + } = await this.augmentAlertTypesWithAuthorization(this.alertTypeRegistry.list(), [ + ReadOperations.Find, + ]); if (!authorizedAlertTypes.size) { throw Boom.forbidden( @@ -136,7 +168,7 @@ export class AlertsAuthorization { const authorizedAlertTypeIdsToConsumers = new Set( [...authorizedAlertTypes].reduce((alertTypeIdConsumerPairs, alertType) => { - for (const consumer of alertType.authorizedConsumers) { + for (const consumer of Object.keys(alertType.authorizedConsumers)) { alertTypeIdConsumerPairs.push(`${alertType.id}/${consumer}`); } return alertTypeIdConsumerPairs; @@ -175,18 +207,18 @@ export class AlertsAuthorization { public async filterByAlertTypeAuthorization( alertTypes: Set, - operation: string + operations: Array ): Promise> { const { authorizedAlertTypes } = await this.augmentAlertTypesWithAuthorization( alertTypes, - operation + operations ); return authorizedAlertTypes; } private async augmentAlertTypesWithAuthorization( alertTypes: Set, - operation: string + operations: Array ): Promise<{ username?: string; hasAllRequested: boolean; @@ -210,7 +242,10 @@ export class AlertsAuthorization { }) .map((feature) => feature.id); - const allPossibleConsumers = [ALERTS_FEATURE_ID, ...featuresIds]; + const allPossibleConsumers: AuthorizedConsumers = asAuthorizedConsumers( + [ALERTS_FEATURE_ID, ...featuresIds], + { read: true, all: true } + ); if (!this.authorization) { return { @@ -223,29 +258,35 @@ export class AlertsAuthorization { ); // add an empty `authorizedConsumers` array on each alertType - const alertTypesWithAutherization = this.augmentWithAuthorizedConsumers(alertTypes, []); + const alertTypesWithAutherization = this.augmentWithAuthorizedConsumers(alertTypes, {}); const preAuthorizedAlertTypes = new Set(); // map from privilege to alertType which we can refer back to when analyzing the result // of checkPrivileges - const privilegeToAlertType = new Map(); + const privilegeToAlertType = new Map< + string, + [RegistryAlertTypeWithAuth, string, HasPrivileges, IsAuthorizedAtProducerLevel] + >(); // as we can't ask ES for the user's individual privileges we need to ask for each feature // and alertType in the system whether this user has this privilege for (const alertType of alertTypesWithAutherization) { if (alertType.producer === ALERTS_FEATURE_ID) { - alertType.authorizedConsumers.push(ALERTS_FEATURE_ID); + alertType.authorizedConsumers[ALERTS_FEATURE_ID] = { read: true, all: true }; preAuthorizedAlertTypes.add(alertType); } for (const feature of featuresIds) { - privilegeToAlertType.set( - this.authorization!.actions.alerting.get(alertType.id, feature, operation), - [ - alertType, - // granting privileges under the producer automatically authorized the Alerts Management UI as well - alertType.producer === feature ? [ALERTS_FEATURE_ID, feature] : [feature], - ] - ); + for (const operation of operations) { + privilegeToAlertType.set( + this.authorization!.actions.alerting.get(alertType.id, feature, operation), + [ + alertType, + feature, + hasPrivilegeByOperation(operation), + alertType.producer === feature, + ] + ); + } } } @@ -262,8 +303,24 @@ export class AlertsAuthorization { : // only has some of the required privileges privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { if (authorized && privilegeToAlertType.has(privilege)) { - const [alertType, consumers] = privilegeToAlertType.get(privilege)!; - alertType.authorizedConsumers.push(...consumers); + const [ + alertType, + feature, + hasPrivileges, + isAuthorizedAtProducerLevel, + ] = privilegeToAlertType.get(privilege)!; + alertType.authorizedConsumers[feature] = mergeHasPrivileges( + hasPrivileges, + alertType.authorizedConsumers[feature] + ); + + if (isAuthorizedAtProducerLevel) { + // granting privileges under the producer automatically authorized the Alerts Management UI as well + alertType.authorizedConsumers[ALERTS_FEATURE_ID] = mergeHasPrivileges( + hasPrivileges, + alertType.authorizedConsumers[ALERTS_FEATURE_ID] + ); + } authorizedAlertTypes.add(alertType); } return authorizedAlertTypes; @@ -274,12 +331,12 @@ export class AlertsAuthorization { private augmentWithAuthorizedConsumers( alertTypes: Set, - authorizedConsumers: string[] + authorizedConsumers: AuthorizedConsumers ): Set { return new Set( Array.from(alertTypes).map((alertType) => ({ ...alertType, - authorizedConsumers: [...authorizedConsumers], + authorizedConsumers: { ...authorizedConsumers }, })) ); } @@ -288,7 +345,9 @@ export class AlertsAuthorization { return Array.from(alertTypes).reduce((filters, { id, authorizedConsumers }) => { ensureFieldIsSafeForQuery('alertTypeId', id); filters.push( - `(alert.attributes.alertTypeId:${id} and alert.attributes.consumer:(${authorizedConsumers + `(alert.attributes.alertTypeId:${id} and alert.attributes.consumer:(${Object.keys( + authorizedConsumers + ) .map((consumer) => { ensureFieldIsSafeForQuery('alertTypeId', id); return consumer; @@ -325,3 +384,26 @@ function hasAnyAlertingPrivileges( ((privileges?.alerting?.all?.length ?? 0) || (privileges?.alerting?.read?.length ?? 0)) > 0 ); } + +function mergeHasPrivileges(left: HasPrivileges, right?: HasPrivileges): HasPrivileges { + return { + read: (left.read || right?.read) ?? false, + all: (left.all || right?.all) ?? false, + }; +} + +function hasPrivilegeByOperation(operation: ReadOperations | WriteOperations): HasPrivileges { + const read = Object.values(ReadOperations).includes((operation as unknown) as ReadOperations); + const all = Object.values(WriteOperations).includes((operation as unknown) as WriteOperations); + return { + read: read || all, + all, + }; +} + +function asAuthorizedConsumers( + consumers: string[], + hasPrivileges: HasPrivileges +): AuthorizedConsumers { + return zipObject(consumers.map((feature) => [feature, hasPrivileges])); +} diff --git a/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts index 6440326cc7747..af20dd6e202ba 100644 --- a/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts +++ b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts @@ -43,7 +43,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', - authorizedConsumers: [], + authorizedConsumers: {}, actionVariables: { context: [], state: [], @@ -69,7 +69,7 @@ describe('listAlertTypesRoute', () => { "context": Array [], "state": Array [], }, - "authorizedConsumers": Array [], + "authorizedConsumers": Object {}, "defaultActionGroupId": "default", "id": "1", "name": "name", @@ -107,7 +107,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', - authorizedConsumers: [], + authorizedConsumers: {}, actionVariables: { context: [], state: [], @@ -156,7 +156,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', - authorizedConsumers: [], + authorizedConsumers: {}, actionVariables: { context: [], state: [], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts index ef7a044bc4799..ddd03df8bee6b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts @@ -184,7 +184,7 @@ function getAlertType(actionVariables: ActionVariables): AlertType { actionVariables, actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', - authorizedConsumers: [], + authorizedConsumers: {}, producer: ALERTS_FEATURE_ID, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts index f444e0c3ba732..23caf2cfb31a8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts @@ -46,7 +46,7 @@ describe('loadAlertTypes', () => { producer: ALERTS_FEATURE_ID, actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', - authorizedConsumers: [], + authorizedConsumers: {}, }, ]; http.get.mockResolvedValueOnce(resolvedValue); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts index 82d03be41e1aa..135721e1856f6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Alert, AlertType } from '../../types'; + /** * NOTE: Applications that want to show the alerting UIs will need to add * check against their features here until we have a better solution. This @@ -23,8 +25,14 @@ function createCapabilityCheck(capability: string) { } export const hasShowAlertsCapability = createCapabilityCheck('alerting:show'); + export const hasShowActionsCapability = createCapabilityCheck('actions:show'); -export const hasSaveAlertsCapability = createCapabilityCheck('alerting:save'); export const hasSaveActionsCapability = createCapabilityCheck('actions:save'); -export const hasDeleteAlertsCapability = createCapabilityCheck('alerting:delete'); export const hasDeleteActionsCapability = createCapabilityCheck('actions:delete'); + +export function hasAllPrivilege(alert: Alert, alertType?: AlertType): boolean { + return alertType?.authorizedConsumers[alert.consumer]?.all ?? false; +} +export function hasReadPrivilege(alert: Alert, alertType?: AlertType): boolean { + return alertType?.authorizedConsumers[alert.consumer]?.read ?? false; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 5340835461ba4..c0c7991a65a00 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -31,8 +31,6 @@ jest.mock('../../../app_context', () => ({ get: jest.fn(() => ({})), securitySolution: { 'alerting:show': true, - 'alerting:save': true, - 'alerting:delete': true, }, }, actionTypeRegistry: jest.fn(), @@ -68,7 +66,7 @@ jest.mock('react-router-dom', () => ({ })); jest.mock('../../../lib/capabilities', () => ({ - hasSaveAlertsCapability: jest.fn(() => true), + hasAllPrivilege: jest.fn(() => true), })); const mockAlertApis = { @@ -79,6 +77,10 @@ const mockAlertApis = { requestRefresh: jest.fn(), }; +const authorizedConsumers = { + [ALERTS_FEATURE_ID]: { read: true, all: true }, +}; + // const AlertDetails = withBulkAlertOperations(RawAlertDetails); describe('alert_details', () => { // mock Api handlers @@ -92,7 +94,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; expect( @@ -131,7 +133,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; expect( @@ -161,7 +163,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const actionTypes: ActionType[] = [ @@ -215,7 +217,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const actionTypes: ActionType[] = [ { @@ -274,7 +276,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; expect( @@ -294,7 +296,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; expect( @@ -323,7 +325,7 @@ describe('disable button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const enableButton = shallow( @@ -351,7 +353,7 @@ describe('disable button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const enableButton = shallow( @@ -379,7 +381,7 @@ describe('disable button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const disableAlert = jest.fn(); @@ -416,7 +418,7 @@ describe('disable button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const enableAlert = jest.fn(); @@ -456,7 +458,7 @@ describe('mute button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const enableButton = shallow( @@ -485,7 +487,7 @@ describe('mute button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const enableButton = shallow( @@ -514,7 +516,7 @@ describe('mute button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const muteAlert = jest.fn(); @@ -552,7 +554,7 @@ describe('mute button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const unmuteAlert = jest.fn(); @@ -590,7 +592,7 @@ describe('mute button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const enableButton = shallow( @@ -614,7 +616,7 @@ function mockAlert(overloads: Partial = {}): Alert { name: `alert-${uuid.v4()}`, tags: [], alertTypeId: '.noop', - consumer: 'consumer', + consumer: ALERTS_FEATURE_ID, schedule: { interval: '1m' }, actions: [], params: {}, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 3a25417f7db4c..e87b621e44210 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -28,7 +28,6 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useAppDependencies } from '../../../app_context'; -import { hasSaveAlertsCapability } from '../../../lib/capabilities'; import { Alert, AlertType, ActionType } from '../../../../types'; import { ComponentOpts as BulkOperationsComponentOpts, @@ -40,6 +39,7 @@ import { PLUGIN } from '../../../constants/plugin'; import { AlertEdit } from '../../alert_form'; import { AlertsContextProvider } from '../../../context/alerts_context'; import { routeToAlertDetails } from '../../../constants'; +import { hasAllPrivilege } from '../../../lib/capabilities'; type AlertDetailsProps = { alert: Alert; @@ -71,7 +71,7 @@ export const AlertDetails: React.FunctionComponent = ({ dataPlugin, } = useAppDependencies(); - const canSave = hasSaveAlertsCapability(capabilities); + const canSave = hasAllPrivilege(alert, alertType); const actionTypesByTypeId = indexBy(actionTypes, 'id'); const hasEditButton = canSave && alertTypeRegistry.has(alert.alertTypeId) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 90a57eafd66d1..e241367070610 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -61,7 +61,10 @@ describe('alert_add', () => { ], defaultActionGroupId: 'testActionGroup', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID, 'test'], + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + test: { read: true, all: true }, + }, actionVariables: { context: [], state: [], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index 66883d468312b..76b447cde6837 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -82,7 +82,10 @@ describe('alert_form', () => { ], defaultActionGroupId: 'testActionGroup', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID, 'test'], + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + test: { read: true, all: true }, + }, }, ]; loadAlertTypes.mockResolvedValue(alertTypes); @@ -191,7 +194,10 @@ describe('alert_form', () => { ], defaultActionGroupId: 'testActionGroup', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID, 'test'], + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + test: { read: true, all: true }, + }, }, { id: 'same-consumer-producer-alert-type', @@ -204,7 +210,10 @@ describe('alert_form', () => { ], defaultActionGroupId: 'testActionGroup', producer: 'test', - authorizedConsumers: [ALERTS_FEATURE_ID, 'test'], + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + test: { read: true, all: true }, + }, }, ]); const mocks = coreMock.createSetup(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index c22029e2f70cd..83deabef473f3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -174,9 +174,9 @@ export const AlertForm = ({ .filter( (alertTypeRegistryItem: AlertTypeModel) => alertTypesIndex.has(alertTypeRegistryItem.id) && - alertTypesIndex - .get(alertTypeRegistryItem.id)! - .authorizedConsumers.includes(alert.consumer) + (alertTypesIndex.get(alertTypeRegistryItem.id)?.authorizedConsumers[alert.consumer] + ?.all ?? + false) ) .filter((alertTypeRegistryItem: AlertTypeModel) => alert.consumer === ALERTS_FEATURE_ID diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index dc2c1f972a5db..7aa45d2d55701 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -18,6 +18,7 @@ import { AppContextProvider } from '../../../app_context'; import { chartPluginMock } from '../../../../../../../../src/plugins/charts/public/mocks'; import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; import { alertingPluginMock } from '../../../../../../alerts/public/mocks'; +import { ALERTS_FEATURE_ID } from '../../../../../../alerts/common'; jest.mock('../../../lib/action_connector_api', () => ({ loadActionTypes: jest.fn(), @@ -48,6 +49,17 @@ const alertType = { alertParamsExpression: () => null, requiresAppContext: false, }; +const alertTypeFromApi = { + id: 'test_alert_type', + name: 'some alert type', + actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, + defaultActionGroupId: 'default', + producer: ALERTS_FEATURE_ID, + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + }, +}; alertTypeRegistry.list.mockReturnValue([alertType]); actionTypeRegistry.list.mockReturnValue([]); @@ -74,7 +86,7 @@ describe('alerts_list component empty', () => { name: 'Test2', }, ]); - loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]); + loadAlertTypes.mockResolvedValue([alertTypeFromApi]); loadAllActions.mockResolvedValue([]); const mockes = coreMock.createSetup(); @@ -99,8 +111,6 @@ describe('alerts_list component empty', () => { ...capabilities, securitySolution: { 'alerting:show': true, - 'alerting:save': true, - 'alerting:delete': true, }, }, history: (scopedHistoryMock.create() as unknown) as ScopedHistory, @@ -194,7 +204,7 @@ describe('alerts_list component with items', () => { name: 'Test2', }, ]); - loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]); + loadAlertTypes.mockResolvedValue([alertTypeFromApi]); loadAllActions.mockResolvedValue([]); const mockes = coreMock.createSetup(); const [ @@ -218,8 +228,6 @@ describe('alerts_list component with items', () => { ...capabilities, securitySolution: { 'alerting:show': true, - 'alerting:save': true, - 'alerting:delete': true, }, }, history: (scopedHistoryMock.create() as unknown) as ScopedHistory, @@ -300,8 +308,6 @@ describe('alerts_list component empty with show only capability', () => { ...capabilities, securitySolution: { 'alerting:show': true, - 'alerting:save': false, - 'alerting:delete': false, }, }, history: (scopedHistoryMock.create() as unknown) as ScopedHistory, @@ -391,7 +397,8 @@ describe('alerts_list with show only capability', () => { name: 'Test2', }, ]); - loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]); + + loadAlertTypes.mockResolvedValue([alertTypeFromApi]); loadAllActions.mockResolvedValue([]); const mockes = coreMock.createSetup(); const [ @@ -415,8 +422,6 @@ describe('alerts_list with show only capability', () => { ...capabilities, securitySolution: { 'alerting:show': true, - 'alerting:save': false, - 'alerting:delete': false, }, }, history: (scopedHistoryMock.create() as unknown) as ScopedHistory, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index a237e9b3fba7f..b5f386b1e633f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -33,11 +33,11 @@ import { TypeFilter } from './type_filter'; import { ActionTypeFilter } from './action_type_filter'; import { loadAlerts, loadAlertTypes, deleteAlerts } from '../../../lib/alert_api'; import { loadActionTypes } from '../../../lib/action_connector_api'; -import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities'; import { routeToAlertDetails, DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation'; import { EmptyPrompt } from '../../../components/prompts/empty_prompt'; import { ALERTS_FEATURE_ID } from '../../../../../../alerts/common'; +import { hasAllPrivilege } from '../../../lib/capabilities'; const ENTER_KEY = 13; @@ -65,9 +65,6 @@ export const AlertsList: React.FunctionComponent = () => { charts, dataPlugin, } = useAppDependencies(); - const canDelete = hasDeleteAlertsCapability(capabilities); - const canSave = hasSaveAlertsCapability(capabilities); - const [actionTypes, setActionTypes] = useState([]); const [selectedIds, setSelectedIds] = useState([]); const [isPerformingAction, setIsPerformingAction] = useState(false); @@ -246,11 +243,13 @@ export const AlertsList: React.FunctionComponent = () => { }, ]; + const authorizedAlertTypes = [...alertTypesState.data.values()]; + const toolsRight = [ setTypesFilter(types)} - options={Object.values(alertTypesState.data) + options={authorizedAlertTypes .map((alertType) => ({ value: alertType.id, name: alertType.name, @@ -264,7 +263,9 @@ export const AlertsList: React.FunctionComponent = () => { />, ]; - if (canSave) { + if ( + authorizedAlertTypes.some((alertType) => alertType.authorizedConsumers[ALERTS_FEATURE_ID]?.all) + ) { toolsRight.push( { ); } + const authorizedToModifySelectedAlerts = selectedIds.length + ? filterAlertsById(alertsState.data, selectedIds).every((selectedAlert) => + hasAllPrivilege(selectedAlert, alertTypesState.data.get(selectedAlert.alertTypeId)) + ) + : false; + const table = ( - {selectedIds.length > 0 && canDelete && ( + {selectedIds.length > 0 && authorizedToModifySelectedAlerts && ( { /* Don't display alert count until we have the alert types initialized */ totalItemCount: alertTypesState.isInitialized === false ? 0 : alertsState.totalItemCount, }} - selection={ - canDelete - ? { - onSelectionChange(updatedSelectedItemsList: AlertTableItem[]) { - setSelectedIds(updatedSelectedItemsList.map((item) => item.id)); - }, - } - : undefined - } + selection={{ + selectable: (alert: AlertTableItem) => alert.isEditable, + onSelectionChange(updatedSelectedItemsList: AlertTableItem[]) { + setSelectedIds(updatedSelectedItemsList.map((item) => item.id)); + }, + }} onChange={({ page: changedPage }: { page: Pagination }) => { setPage(changedPage); }} @@ -459,5 +463,6 @@ function convertAlertsToTableItems(alerts: Alert[], alertTypesIndex: AlertTypeIn actionsText: alert.actions.length, tagsText: alert.tags.join(', '), alertType: alertTypesIndex.get(alert.alertTypeId)?.name ?? alert.alertTypeId, + isEditable: hasAllPrivilege(alert, alertTypesIndex.get(alert.alertTypeId)), })); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx index 2b746e5dea457..9279f8a1745fc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx @@ -20,8 +20,6 @@ import { } from '@elastic/eui'; import { AlertTableItem } from '../../../../types'; -import { useAppDependencies } from '../../../app_context'; -import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities'; import { ComponentOpts as BulkOperationsComponentOpts, withBulkAlertOperations, @@ -43,16 +41,11 @@ export const CollapsedItemActions: React.FunctionComponent = ({ muteAlert, setAlertsToDelete, }: ComponentOpts) => { - const { capabilities } = useAppDependencies(); - - const canDelete = hasDeleteAlertsCapability(capabilities); - const canSave = hasSaveAlertsCapability(capabilities); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); const button = ( setIsPopoverOpen(!isPopoverOpen)} aria-label={i18n.translate( @@ -75,7 +68,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({
= ({ { @@ -134,7 +127,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({
setAlertsToDelete([item.id])} > diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 6eaae4c1b91a3..32eb3ff9c5364 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -99,7 +99,7 @@ export interface AlertType { actionGroups: ActionGroup[]; actionVariables: ActionVariables; defaultActionGroupId: ActionGroup['id']; - authorizedConsumers: string[]; + authorizedConsumers: Record; producer: string; } @@ -110,6 +110,7 @@ export type AlertWithoutId = Omit; export interface AlertTableItem extends Alert { alertType: AlertType['name']; tagsText: string; + isEditable: boolean; } export interface AlertTypeParamsExpressionProps< diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index 0b2377c537f93..023506776ab33 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -67,34 +67,77 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { case 'space_1_all at space1': expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType); expect(restrictedNoOpAlertType).to.eql(undefined); - expect(noOpAlertType.authorizedConsumers).to.eql(['alerts', 'alertsFixture']); + expect(noOpAlertType.authorizedConsumers).to.eql({ + alerts: { read: true, all: true }, + alertsFixture: { read: true, all: true }, + }); break; case 'global_read at space1': + expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType); + expect(noOpAlertType.authorizedConsumers.alertsFixture).to.eql({ + read: true, + all: false, + }); + expect(noOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({ + read: true, + all: false, + }); + + expect(omit(restrictedNoOpAlertType, 'authorizedConsumers')).to.eql( + expectedRestrictedNoOpType + ); + expect(Object.keys(restrictedNoOpAlertType.authorizedConsumers)).not.to.contain( + 'alertsFixture' + ); + expect(restrictedNoOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({ + read: true, + all: false, + }); + break; case 'space_1_all_with_restricted_fixture at space1': expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType); - expect(noOpAlertType.authorizedConsumers).to.contain('alertsFixture'); - expect(noOpAlertType.authorizedConsumers).to.contain('alertsRestrictedFixture'); + expect(noOpAlertType.authorizedConsumers.alertsFixture).to.eql({ + read: true, + all: true, + }); + expect(noOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({ + read: true, + all: true, + }); expect(omit(restrictedNoOpAlertType, 'authorizedConsumers')).to.eql( expectedRestrictedNoOpType ); - expect(restrictedNoOpAlertType.authorizedConsumers).not.to.contain('alertsFixture'); - expect(restrictedNoOpAlertType.authorizedConsumers).to.contain( - 'alertsRestrictedFixture' + expect(Object.keys(restrictedNoOpAlertType.authorizedConsumers)).not.to.contain( + 'alertsFixture' ); + expect(restrictedNoOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({ + read: true, + all: true, + }); break; case 'superuser at space1': expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType); - expect(noOpAlertType.authorizedConsumers).to.contain('alertsFixture'); - expect(noOpAlertType.authorizedConsumers).to.contain('alertsRestrictedFixture'); + expect(noOpAlertType.authorizedConsumers.alertsFixture).to.eql({ + read: true, + all: true, + }); + expect(noOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({ + read: true, + all: true, + }); expect(omit(restrictedNoOpAlertType, 'authorizedConsumers')).to.eql( expectedRestrictedNoOpType ); - expect(restrictedNoOpAlertType.authorizedConsumers).to.contain('alertsFixture'); - expect(restrictedNoOpAlertType.authorizedConsumers).to.contain( - 'alertsRestrictedFixture' - ); + expect(noOpAlertType.authorizedConsumers.alertsFixture).to.eql({ + read: true, + all: true, + }); + expect(noOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({ + read: true, + all: true, + }); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); From 353dd2510a18e5243d89c95ed1074a57c28cc1be Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 2 Jul 2020 11:35:22 +0100 Subject: [PATCH 078/126] introduces a feature for built-in alert types --- .../plugins/alerting_builtins/common/index.ts | 7 ++ x-pack/plugins/alerting_builtins/kibana.json | 2 +- .../alert_types/index_threshold/alert_type.ts | 4 +- .../alerting_builtins/server/feature.ts | 49 ++++++++ .../alerting_builtins/server/plugin.ts | 8 +- .../plugins/alerting_builtins/server/types.ts | 2 + .../authorization/alerts_authorization.ts | 108 ++++++++---------- .../alerts/server/saved_objects/migrations.ts | 1 + .../public/application/lib/capabilities.ts | 3 +- .../tests/alerting/find.ts | 55 +++++---- .../tests/alerting/list_alert_types.ts | 6 +- 11 files changed, 151 insertions(+), 94 deletions(-) create mode 100644 x-pack/plugins/alerting_builtins/common/index.ts create mode 100644 x-pack/plugins/alerting_builtins/server/feature.ts diff --git a/x-pack/plugins/alerting_builtins/common/index.ts b/x-pack/plugins/alerting_builtins/common/index.ts new file mode 100644 index 0000000000000..4f2c166669355 --- /dev/null +++ b/x-pack/plugins/alerting_builtins/common/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export const BUILT_IN_ALERTS_FEATURE_ID = 'builtInAlerts'; diff --git a/x-pack/plugins/alerting_builtins/kibana.json b/x-pack/plugins/alerting_builtins/kibana.json index cc613d5247ef4..dd70e53604f16 100644 --- a/x-pack/plugins/alerting_builtins/kibana.json +++ b/x-pack/plugins/alerting_builtins/kibana.json @@ -3,7 +3,7 @@ "server": true, "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["alerts"], + "requiredPlugins": ["alerts", "features"], "configPath": ["xpack", "alerting_builtins"], "ui": false } diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts index 285abbef64f0d..153334cb64047 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts @@ -10,7 +10,7 @@ import { Params, ParamsSchema } from './alert_type_params'; import { BaseActionContext, addMessages } from './action_context'; import { TimeSeriesQuery } from './lib/time_series_query'; import { Service } from '../../types'; -import { ALERTS_FEATURE_ID } from '../../../../alerts/common'; +import { BUILT_IN_ALERTS_FEATURE_ID } from '../../../common'; export const ID = '.index-threshold'; @@ -85,7 +85,7 @@ export function getAlertType(service: Service): AlertType { ], }, executor, - producer: ALERTS_FEATURE_ID, + producer: BUILT_IN_ALERTS_FEATURE_ID, }; async function executor(options: AlertExecutorOptions) { diff --git a/x-pack/plugins/alerting_builtins/server/feature.ts b/x-pack/plugins/alerting_builtins/server/feature.ts new file mode 100644 index 0000000000000..fcaec214d49d9 --- /dev/null +++ b/x-pack/plugins/alerting_builtins/server/feature.ts @@ -0,0 +1,49 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { ID as IndexThreshold } from './alert_types/index_threshold/alert_type'; +import { BUILT_IN_ALERTS_FEATURE_ID } from '../common'; + +export const BUILT_IN_ALERTS_FEATURE = { + id: BUILT_IN_ALERTS_FEATURE_ID, + name: i18n.translate('xpack.builtInAlerts.featureRegistry.actionsFeatureName', { + defaultMessage: 'Built-In Alerts', + }), + icon: 'bell', + navLinkId: 'builtInAlerts', + app: [], + privileges: { + all: { + app: [], + api: [], + catalogue: [], + alerting: { + all: [IndexThreshold], + read: [], + }, + savedObject: { + all: [], + read: [], + }, + ui: ['alerting:show'], + }, + read: { + app: [], + api: [], + catalogue: [], + alerting: { + all: [], + read: [IndexThreshold], + }, + savedObject: { + all: [], + read: [], + }, + ui: ['alerting:show'], + }, + }, +}; diff --git a/x-pack/plugins/alerting_builtins/server/plugin.ts b/x-pack/plugins/alerting_builtins/server/plugin.ts index 12d1b080c7c63..41871c01bfb50 100644 --- a/x-pack/plugins/alerting_builtins/server/plugin.ts +++ b/x-pack/plugins/alerting_builtins/server/plugin.ts @@ -9,6 +9,7 @@ import { Plugin, Logger, CoreSetup, CoreStart, PluginInitializerContext } from ' import { Service, IService, AlertingBuiltinsDeps } from './types'; import { getService as getServiceIndexThreshold } from './alert_types/index_threshold'; import { registerBuiltInAlertTypes } from './alert_types'; +import { BUILT_IN_ALERTS_FEATURE } from './feature'; export class AlertingBuiltinsPlugin implements Plugin { private readonly logger: Logger; @@ -22,7 +23,12 @@ export class AlertingBuiltinsPlugin implements Plugin { }; } - public async setup(core: CoreSetup, { alerts }: AlertingBuiltinsDeps): Promise { + public async setup( + core: CoreSetup, + { alerts, features }: AlertingBuiltinsDeps + ): Promise { + features.registerFeature(BUILT_IN_ALERTS_FEATURE); + registerBuiltInAlertTypes({ service: this.service, router: core.http.createRouter(), diff --git a/x-pack/plugins/alerting_builtins/server/types.ts b/x-pack/plugins/alerting_builtins/server/types.ts index 1fb5314ca4fd9..f3abc26be8dab 100644 --- a/x-pack/plugins/alerting_builtins/server/types.ts +++ b/x-pack/plugins/alerting_builtins/server/types.ts @@ -15,10 +15,12 @@ export { AlertType, AlertExecutorOptions, } from '../../alerts/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; // this plugin's dependendencies export interface AlertingBuiltinsDeps { alerts: AlertingSetup; + features: FeaturesPluginSetup; } // external service exposed through plugin setup/start diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 22532ca030419..46d6407ae51f8 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { pluck, mapValues, remove, omit, isUndefined, zipObject } from 'lodash'; +import { pluck, mapValues, remove, zipObject } from 'lodash'; import { KibanaRequest } from 'src/core/server'; import { RecursiveReadonly } from '@kbn/utility-types'; import { ALERTS_FEATURE_ID } from '../../common'; @@ -83,67 +83,63 @@ export class AlertsAuthorization { const { authorization } = this; if (authorization) { const alertType = this.alertTypeRegistry.get(alertTypeId); + const requiredPrivilegesByScope = { + consumer: authorization.actions.alerting.get(alertTypeId, consumer, operation), + producer: authorization.actions.alerting.get(alertTypeId, alertType.producer, operation), + }; // We special case the Alerts Management `consumer` as we don't want to have to // manually authorize each alert type in the management UI const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID; - // We special case the Alerts Management `prodcuer` as all users are authorized - // to use built-in alert types by definition - const shouldAuthorizeProducer = - alertType.producer !== ALERTS_FEATURE_ID && alertType.producer !== consumer; - - if (shouldAuthorizeConsumer || shouldAuthorizeProducer) { - const requiredPrivilegesByScope = omit( - { - consumer: shouldAuthorizeConsumer - ? authorization.actions.alerting.get(alertTypeId, consumer, operation) - : undefined, - producer: shouldAuthorizeProducer - ? authorization.actions.alerting.get(alertTypeId, alertType.producer, operation) - : undefined, - }, - isUndefined - ); - const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested, username, privileges } = await checkPrivileges( - Object.values(requiredPrivilegesByScope) + const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); + const { hasAllRequested, username, privileges } = await checkPrivileges( + shouldAuthorizeConsumer && consumer !== alertType.producer + ? [ + // check for access at consumer level + requiredPrivilegesByScope.consumer, + // check for access at producer level + requiredPrivilegesByScope.producer, + ] + : [ + // skip consumer privilege checks under `alerts` as all alert types can + // be created under `alerts` if you have producer level privileges + requiredPrivilegesByScope.producer, + ] + ); + + if (hasAllRequested) { + this.auditLogger.alertsAuthorizationSuccess( + username, + alertTypeId, + ScopeType.Consumer, + consumer, + operation + ); + } else { + const authorizedPrivileges = pluck( + privileges.filter((privilege) => privilege.authorized), + 'privilege' + ); + const unauthorizedScopes = mapValues( + requiredPrivilegesByScope, + (privilege) => !authorizedPrivileges.includes(privilege) ); - if (hasAllRequested) { - this.auditLogger.alertsAuthorizationSuccess( + const [unauthorizedScopeType, unauthorizedScope] = + shouldAuthorizeConsumer && unauthorizedScopes.consumer + ? [ScopeType.Consumer, consumer] + : [ScopeType.Producer, alertType.producer]; + + throw Boom.forbidden( + this.auditLogger.alertsAuthorizationFailure( username, alertTypeId, - ScopeType.Consumer, - consumer, + unauthorizedScopeType, + unauthorizedScope, operation - ); - } else { - const authorizedPrivileges = pluck( - privileges.filter((privilege) => privilege.authorized), - 'privilege' - ); - - const unauthorizedScopes = mapValues( - requiredPrivilegesByScope, - (privilege) => !authorizedPrivileges.includes(privilege) - ); - - const [unauthorizedScopeType, unauthorizedScope] = - shouldAuthorizeConsumer && unauthorizedScopes.consumer - ? [ScopeType.Consumer, consumer] - : [ScopeType.Producer, alertType.producer]; - - throw Boom.forbidden( - this.auditLogger.alertsAuthorizationFailure( - username, - alertTypeId, - unauthorizedScopeType, - unauthorizedScope, - operation - ) - ); - } + ) + ); } } } @@ -259,7 +255,6 @@ export class AlertsAuthorization { // add an empty `authorizedConsumers` array on each alertType const alertTypesWithAutherization = this.augmentWithAuthorizedConsumers(alertTypes, {}); - const preAuthorizedAlertTypes = new Set(); // map from privilege to alertType which we can refer back to when analyzing the result // of checkPrivileges @@ -270,11 +265,6 @@ export class AlertsAuthorization { // as we can't ask ES for the user's individual privileges we need to ask for each feature // and alertType in the system whether this user has this privilege for (const alertType of alertTypesWithAutherization) { - if (alertType.producer === ALERTS_FEATURE_ID) { - alertType.authorizedConsumers[ALERTS_FEATURE_ID] = { read: true, all: true }; - preAuthorizedAlertTypes.add(alertType); - } - for (const feature of featuresIds) { for (const operation of operations) { privilegeToAlertType.set( @@ -324,7 +314,7 @@ export class AlertsAuthorization { authorizedAlertTypes.add(alertType); } return authorizedAlertTypes; - }, preAuthorizedAlertTypes), + }, new Set()), }; } } diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index 79413aff907c4..806d3deb44c05 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { BUILT_IN_ALERTS_FEATURE_ID } from '../../../alerting_builtins/common'; import { SavedObjectMigrationMap, SavedObjectUnsanitizedDoc, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts index 135721e1856f6..b31eab7f18ad4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { BUILT_IN_ALERTS_FEATURE_ID } from '../../../../alerting_builtins/common'; import { Alert, AlertType } from '../../types'; /** @@ -14,7 +15,7 @@ import { Alert, AlertType } from '../../types'; type Capabilities = Record; -const apps = ['apm', 'siem', 'uptime', 'infrastructure']; +const apps = ['apm', 'siem', 'uptime', 'infrastructure', BUILT_IN_ALERTS_FEATURE_ID]; function hasCapability(capabilities: Capabilities, capability: string) { return apps.some((app) => capabilities[app]?.[capability]); 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 221b685395ef7..ece2ee8e54788 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 @@ -43,11 +43,12 @@ export default function createFindTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - expect(response.statusCode).to.eql(200); - expect(response.body.page).to.equal(1); - expect(response.body.perPage).to.be.greaterThan(0); - expect(response.body.total).to.equal(0); - expect(response.body.data).to.eql([]); + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to find any alert types`, + statusCode: 403, + }); break; case 'global_read at space1': case 'superuser at space1': @@ -133,11 +134,12 @@ export default function createFindTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - expect(response.statusCode).to.eql(200); - expect(response.body.page).to.equal(1); - expect(response.body.perPage).to.be.greaterThan(0); - expect(response.body.total).to.equal(0); - expect(response.body.data).to.eql([]); + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to find any alert types`, + statusCode: 403, + }); break; case 'space_1_all at space1': expect(response.statusCode).to.eql(200); @@ -227,11 +229,12 @@ export default function createFindTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - expect(response.statusCode).to.eql(200); - expect(response.body.page).to.equal(1); - expect(response.body.perPage).to.be.greaterThan(0); - expect(response.body.total).to.equal(0); - expect(response.body.data).to.eql([]); + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to find any alert types`, + statusCode: 403, + }); break; case 'global_read at space1': case 'superuser at space1': @@ -317,11 +320,12 @@ export default function createFindTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - expect(response.statusCode).to.eql(200); - expect(response.body.page).to.equal(1); - expect(response.body.perPage).to.be.greaterThan(0); - expect(response.body.total).to.equal(0); - expect(response.body.data).to.eql([]); + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to find any alert types`, + statusCode: 403, + }); break; case 'space_1_all at space1': expect(response.statusCode).to.eql(200); @@ -370,11 +374,12 @@ export default function createFindTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': - expect(response.statusCode).to.eql(200); - expect(response.body.page).to.equal(1); - expect(response.body.perPage).to.be.greaterThan(0); - expect(response.body.total).to.equal(0); - expect(response.body.data).to.eql([]); + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to find any alert types`, + statusCode: 403, + }); break; case 'global_read at space1': case 'superuser at space1': diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index 023506776ab33..a5430e6ea2a1e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -58,11 +58,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - // users with no privileges should only have access to - // built-in types - response.body.forEach((alertType: AlertType) => { - expect(alertType.producer).to.equal(ALERTS_FEATURE_ID); - }); + expect(response.body).to.eql([]); break; case 'space_1_all at space1': expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType); From c0d09cc62d6ee98298e1d49409cb6e14a3723fff Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 2 Jul 2020 11:36:14 +0100 Subject: [PATCH 079/126] introduces a feature for built-in alert types mend --- x-pack/plugins/alerts/server/saved_objects/migrations.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index 806d3deb44c05..79413aff907c4 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { BUILT_IN_ALERTS_FEATURE_ID } from '../../../alerting_builtins/common'; import { SavedObjectMigrationMap, SavedObjectUnsanitizedDoc, From ee05baa5c403374d9b8e729da12d1527794c9a06 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 2 Jul 2020 12:50:00 +0100 Subject: [PATCH 080/126] show prompt if user has no privileges in flyout --- .../public/components/view_astros_alert.tsx | 4 +- .../alerting_builtins/server/feature.ts | 12 ++--- .../sections/alert_form/alert_add.tsx | 3 ++ .../sections/alert_form/alert_edit.tsx | 3 ++ .../sections/alert_form/alert_form.tsx | 44 ++++++++++++++++--- 5 files changed, 53 insertions(+), 13 deletions(-) diff --git a/examples/alerting_example/public/components/view_astros_alert.tsx b/examples/alerting_example/public/components/view_astros_alert.tsx index 19f235a3f3e4e..b2d3cec269b72 100644 --- a/examples/alerting_example/public/components/view_astros_alert.tsx +++ b/examples/alerting_example/public/components/view_astros_alert.tsx @@ -55,10 +55,10 @@ export const ViewPeopleInSpaceAlertPage = withRouter(({ http, id }: Props) => { useEffect(() => { if (!alert) { - http.get(`${BASE_ALERT_API_PATH}/${id}`).then(setAlert); + http.get(`${BASE_ALERT_API_PATH}/alert/${id}`).then(setAlert); } if (!alertState) { - http.get(`${BASE_ALERT_API_PATH}/${id}/state`).then(setAlertState); + http.get(`${BASE_ALERT_API_PATH}/alert/${id}/state`).then(setAlertState); } }, [alert, alertState, http, id]); diff --git a/x-pack/plugins/alerting_builtins/server/feature.ts b/x-pack/plugins/alerting_builtins/server/feature.ts index fcaec214d49d9..34b004c19e583 100644 --- a/x-pack/plugins/alerting_builtins/server/feature.ts +++ b/x-pack/plugins/alerting_builtins/server/feature.ts @@ -19,21 +19,20 @@ export const BUILT_IN_ALERTS_FEATURE = { privileges: { all: { app: [], - api: [], catalogue: [], alerting: { all: [IndexThreshold], read: [], }, savedObject: { - all: [], + all: ['action'], read: [], }, - ui: ['alerting:show'], + api: ['actions-read'], + ui: ['alerting:show', 'actions:show', 'actions:save', 'actions:delete'], }, read: { app: [], - api: [], catalogue: [], alerting: { all: [], @@ -41,9 +40,10 @@ export const BUILT_IN_ALERTS_FEATURE = { }, savedObject: { all: [], - read: [], + read: ['action'], }, - ui: ['alerting:show'], + api: ['actions-read'], + ui: ['alerting:show', 'actions:show'], }, }, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 52c281761f2c1..20cbd42e34b67 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -168,6 +168,9 @@ export const AlertAdd = ({ dispatch={dispatch} errors={errors} canChangeTrigger={canChangeTrigger} + operation={i18n.translate('xpack.triggersActionsUI.sections.alertAdd.operationName', { + defaultMessage: 'create', + })} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 076f4b69fb496..f991cea9c009c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -156,6 +156,9 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { errors={errors} canChangeTrigger={false} setHasActionsDisabled={setHasActionsDisabled} + operation="i18n.translate('xpack.triggersActionsUI.sections.alertEdit.operationName', { + defaultMessage: 'edit', + })" /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 83deabef473f3..6c73ca1a2e45b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -24,6 +24,7 @@ import { EuiButtonIcon, EuiHorizontalRule, EuiLoadingSpinner, + EuiEmptyPrompt, } from '@elastic/eui'; import { some, filter, map, fold } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -39,6 +40,7 @@ import { getTimeOptions } from '../../../common/lib/get_time_options'; import { useAlertsContext } from '../../context/alerts_context'; import { ActionForm } from '../action_connector_form'; import { ALERTS_FEATURE_ID } from '../../../../../alerts/common'; +import { hasAllPrivilege } from '../../lib/capabilities'; export function validateBaseProperties(alertObject: Alert) { const validationResult = { errors: {} }; @@ -79,6 +81,7 @@ interface AlertFormProps { errors: IErrorObject; canChangeTrigger?: boolean; // to hide Change trigger button setHasActionsDisabled?: (value: boolean) => void; + operation: string; } export const AlertForm = ({ @@ -87,6 +90,7 @@ export const AlertForm = ({ dispatch, errors, setHasActionsDisabled, + operation, }: AlertFormProps) => { const alertsContext = useAlertsContext(); const { @@ -174,9 +178,7 @@ export const AlertForm = ({ .filter( (alertTypeRegistryItem: AlertTypeModel) => alertTypesIndex.has(alertTypeRegistryItem.id) && - (alertTypesIndex.get(alertTypeRegistryItem.id)?.authorizedConsumers[alert.consumer] - ?.all ?? - false) + hasAllPrivilege(alert, alertTypesIndex.get(alertTypeRegistryItem.id)) ) .filter((alertTypeRegistryItem: AlertTypeModel) => alert.consumer === ALERTS_FEATURE_ID @@ -322,7 +324,9 @@ export const AlertForm = ({ ); - return ( + return !(alertTypeModel || alertTypeNodes.length) ? ( + + ) : ( @@ -490,7 +494,7 @@ export const AlertForm = ({ {alertTypeModel ? ( {alertTypeDetails} - ) : ( + ) : alertTypeNodes.length ? ( @@ -506,7 +510,37 @@ export const AlertForm = ({ {alertTypeNodes}
+ ) : ( + )} ); }; + +const NoAuthorizedAlertTypes = ({ operation }: { operation: string }) => ( + + + + } + body={ +
+

+ +

+
+ } + /> +); From 6c42c925da54508484ad9b5c9014e27a44109d14 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 2 Jul 2020 13:02:59 +0100 Subject: [PATCH 081/126] fixed list types test --- .../spaces_only/tests/alerting/list_alert_types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts index dde75b57b6b20..dd09a14b4cb81 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts @@ -33,7 +33,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { }, producer: 'alertsFixture', }); - expect(authorizedConsumers).to.contain('alertsFixture'); + expect(Object.keys(authorizedConsumers)).to.contain('alertsFixture'); }); it('should return actionVariables with both context and state', async () => { From a7d36e40bd7e1ac5f5c4cfcd6b2f6169a890ed11 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 2 Jul 2020 13:56:07 +0100 Subject: [PATCH 082/126] fixed unit tests in trigegrs UI --- .../alerting_builtins/server/plugin.test.ts | 12 +++++++++-- .../sections/alert_form/alert_add.test.tsx | 4 ++++ .../sections/alert_form/alert_form.test.tsx | 21 ++++++++++++++++--- .../sections/alert_form/alert_form.tsx | 4 +--- 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/alerting_builtins/server/plugin.test.ts b/x-pack/plugins/alerting_builtins/server/plugin.test.ts index 71a904dcbab3d..15ad066523502 100644 --- a/x-pack/plugins/alerting_builtins/server/plugin.test.ts +++ b/x-pack/plugins/alerting_builtins/server/plugin.test.ts @@ -7,6 +7,8 @@ import { AlertingBuiltinsPlugin } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { alertsMock } from '../../alerts/server/mocks'; +import { featuresPluginMock } from '../../features/server/mocks'; +import { BUILT_IN_ALERTS_FEATURE } from './feature'; describe('AlertingBuiltins Plugin', () => { describe('setup()', () => { @@ -22,7 +24,8 @@ describe('AlertingBuiltins Plugin', () => { it('should register built-in alert types', async () => { const alertingSetup = alertsMock.createSetup(); - await plugin.setup(coreSetup, { alerts: alertingSetup }); + const featuresSetup = featuresPluginMock.createSetup(); + await plugin.setup(coreSetup, { alerts: alertingSetup, features: featuresSetup }); expect(alertingSetup.registerType).toHaveBeenCalledTimes(1); @@ -40,11 +43,16 @@ describe('AlertingBuiltins Plugin', () => { "name": "Index threshold", } `); + expect(featuresSetup.registerFeature).toHaveBeenCalledWith(BUILT_IN_ALERTS_FEATURE); }); it('should return a service in the expected shape', async () => { const alertingSetup = alertsMock.createSetup(); - const service = await plugin.setup(coreSetup, { alerts: alertingSetup }); + const featuresSetup = featuresPluginMock.createSetup(); + const service = await plugin.setup(coreSetup, { + alerts: alertingSetup, + features: featuresSetup, + }); expect(typeof service.indexThreshold.timeSeriesQuery).toBe('function'); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index e241367070610..10efabd70aded 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -169,6 +169,10 @@ describe('alert_add', () => { it('renders alert add flyout', async () => { await setup(); + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + expect(wrapper.find('[data-test-subj="addAlertFlyoutTitle"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="saveAlertButton"]').exists()).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index 76b447cde6837..6091519f5851e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -137,7 +137,12 @@ describe('alert_form', () => { capabilities: deps!.capabilities, }} > - {}} errors={{ name: [], interval: [] }} /> + {}} + errors={{ name: [], interval: [] }} + operation="create" + /> ); @@ -284,7 +289,12 @@ describe('alert_form', () => { capabilities: deps!.capabilities, }} > - {}} errors={{ name: [], interval: [] }} /> + {}} + errors={{ name: [], interval: [] }} + operation="create" + /> ); @@ -362,7 +372,12 @@ describe('alert_form', () => { capabilities: deps!.capabilities, }} > - {}} errors={{ name: [], interval: [] }} /> + {}} + errors={{ name: [], interval: [] }} + operation="create" + /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 6c73ca1a2e45b..ceef73ea7924c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -324,9 +324,7 @@ export const AlertForm = ({ ); - return !(alertTypeModel || alertTypeNodes.length) ? ( - - ) : ( + return ( From ae38572945acda30c3a552cadf07394594e34908 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 2 Jul 2020 16:18:35 +0100 Subject: [PATCH 083/126] fix test broken by addition of built-in types feature --- .../alerts_authorization.test.ts | 82 +++++++++---------- .../tests/alerting/list_alert_types.ts | 1 - .../apis/security/privileges.ts | 1 + .../apis/security/privileges_basic.ts | 1 + 4 files changed, 41 insertions(+), 44 deletions(-) diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 42c244108b6a4..4dfca13f87be3 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -459,12 +459,12 @@ describe('ensureAuthorized', () => { }); describe('getFindAuthorizationFilter', () => { - const alertingAlertType = { + const myOtherAppAlertType = { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', - id: 'alertingAlertType', - name: 'alertingAlertType', + id: 'myOtherAppAlertType', + name: 'myOtherAppAlertType', producer: 'alerts', }; const myAppAlertType = { @@ -475,7 +475,7 @@ describe('getFindAuthorizationFilter', () => { name: 'myAppAlertType', producer: 'myApp', }; - const setOfAlertTypes = new Set([myAppAlertType, alertingAlertType]); + const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType]); test('omits filter when there is no authorization api', async () => { const alertAuthorization = new AlertsAuthorization({ @@ -533,7 +533,7 @@ describe('getFindAuthorizationFilter', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); expect((await alertAuthorization.getFindAuthorizationFilter()).filter).toMatchInlineSnapshot( - `"((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:alertingAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)))"` + `"((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:myOtherAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)))"` ); expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -550,11 +550,11 @@ describe('getFindAuthorizationFilter', () => { hasAllRequested: false, privileges: [ { - privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'find'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), authorized: true, }, { - privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'find'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), authorized: false, }, { @@ -608,11 +608,11 @@ describe('getFindAuthorizationFilter', () => { hasAllRequested: false, privileges: [ { - privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'find'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), authorized: true, }, { - privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'find'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), authorized: false, }, { @@ -655,13 +655,13 @@ describe('getFindAuthorizationFilter', () => { }); describe('filterByAlertTypeAuthorization', () => { - const alertingAlertType = { + const myOtherAppAlertType = { actionGroups: [], actionVariables: undefined, defaultActionGroupId: 'default', - id: 'alertingAlertType', - name: 'alertingAlertType', - producer: 'alerts', + id: 'myOtherAppAlertType', + name: 'myOtherAppAlertType', + producer: 'myOtherApp', }; const myAppAlertType = { actionGroups: [], @@ -671,7 +671,7 @@ describe('filterByAlertTypeAuthorization', () => { name: 'myAppAlertType', producer: 'myApp', }; - const setOfAlertTypes = new Set([myAppAlertType, alertingAlertType]); + const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType]); test('augments a list of types with all features when there is no authorization api', async () => { const alertAuthorization = new AlertsAuthorization({ @@ -684,7 +684,7 @@ describe('filterByAlertTypeAuthorization', () => { await expect( alertAuthorization.filterByAlertTypeAuthorization( - new Set([myAppAlertType, alertingAlertType]), + new Set([myAppAlertType, myOtherAppAlertType]), [WriteOperations.Create] ) ).resolves.toMatchInlineSnapshot(` @@ -737,9 +737,9 @@ describe('filterByAlertTypeAuthorization', () => { }, }, "defaultActionGroupId": "default", - "id": "alertingAlertType", - "name": "alertingAlertType", - "producer": "alerts", + "id": "myOtherAppAlertType", + "name": "myOtherAppAlertType", + "producer": "myOtherApp", }, } `); @@ -756,11 +756,11 @@ describe('filterByAlertTypeAuthorization', () => { hasAllRequested: false, privileges: [ { - privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'create'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), authorized: true, }, { - privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'create'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), authorized: false, }, { @@ -785,7 +785,7 @@ describe('filterByAlertTypeAuthorization', () => { await expect( alertAuthorization.filterByAlertTypeAuthorization( - new Set([myAppAlertType, alertingAlertType]), + new Set([myAppAlertType, myOtherAppAlertType]), [WriteOperations.Create] ) ).resolves.toMatchInlineSnapshot(` @@ -794,19 +794,15 @@ describe('filterByAlertTypeAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "alerts": Object { - "all": true, - "read": true, - }, "myApp": Object { "all": true, "read": true, }, }, "defaultActionGroupId": "default", - "id": "alertingAlertType", - "name": "alertingAlertType", - "producer": "alerts", + "id": "myOtherAppAlertType", + "name": "myOtherAppAlertType", + "producer": "myOtherApp", }, Object { "actionGroups": Array [], @@ -903,11 +899,11 @@ describe('filterByAlertTypeAuthorization', () => { hasAllRequested: false, privileges: [ { - privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'create'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), authorized: true, }, { - privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'create'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), authorized: false, }, { @@ -919,11 +915,11 @@ describe('filterByAlertTypeAuthorization', () => { authorized: false, }, { - privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'get'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'get'), authorized: true, }, { - privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'get'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'get'), authorized: true, }, { @@ -948,7 +944,7 @@ describe('filterByAlertTypeAuthorization', () => { await expect( alertAuthorization.filterByAlertTypeAuthorization( - new Set([myAppAlertType, alertingAlertType]), + new Set([myAppAlertType, myOtherAppAlertType]), [WriteOperations.Create, ReadOperations.Get] ) ).resolves.toMatchInlineSnapshot(` @@ -958,7 +954,7 @@ describe('filterByAlertTypeAuthorization', () => { "actionVariables": undefined, "authorizedConsumers": Object { "alerts": Object { - "all": true, + "all": false, "read": true, }, "myApp": Object { @@ -971,9 +967,9 @@ describe('filterByAlertTypeAuthorization', () => { }, }, "defaultActionGroupId": "default", - "id": "alertingAlertType", - "name": "alertingAlertType", - "producer": "alerts", + "id": "myOtherAppAlertType", + "name": "myOtherAppAlertType", + "producer": "myOtherApp", }, Object { "actionGroups": Array [], @@ -1012,11 +1008,11 @@ describe('filterByAlertTypeAuthorization', () => { hasAllRequested: false, privileges: [ { - privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'create'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), authorized: true, }, { - privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'create'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), authorized: true, }, { @@ -1041,7 +1037,7 @@ describe('filterByAlertTypeAuthorization', () => { await expect( alertAuthorization.filterByAlertTypeAuthorization( - new Set([myAppAlertType, alertingAlertType]), + new Set([myAppAlertType, myOtherAppAlertType]), [WriteOperations.Create] ) ).resolves.toMatchInlineSnapshot(` @@ -1064,9 +1060,9 @@ describe('filterByAlertTypeAuthorization', () => { }, }, "defaultActionGroupId": "default", - "id": "alertingAlertType", - "name": "alertingAlertType", - "producer": "alerts", + "id": "myOtherAppAlertType", + "name": "myOtherAppAlertType", + "producer": "myOtherApp", }, } `); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index a5430e6ea2a1e..8ff97fba65cc1 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -9,7 +9,6 @@ import { omit } from 'lodash'; import { UserAtSpaceScenarios } from '../../scenarios'; import { getUrlPrefix } from '../../../common/lib/space_test_utils'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { ALERTS_FEATURE_ID, AlertType } from '../../../../../plugins/alerts/common'; // eslint-disable-next-line import/no-default-export export default function listAlertTypes({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index de0abe2350eb5..d084c3a47e116 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -37,6 +37,7 @@ export default function ({ getService }: FtrProviderContext) { apm: ['all', 'read'], siem: ['all', 'read'], ingestManager: ['all', 'read'], + builtInAlerts: ['all', 'read'], }, global: ['all', 'read'], space: ['all', 'read'], diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 00bfcdc119e47..7bf2793dbca25 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -35,6 +35,7 @@ export default function ({ getService }: FtrProviderContext) { apm: ['all', 'read'], siem: ['all', 'read'], ingestManager: ['all', 'read'], + builtInAlerts: ['all', 'read'], }, global: ['all', 'read'], space: ['all', 'read'], From 8f30d0fe39744a4b130e65a8f3243f9f0b4d3185 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 2 Jul 2020 17:40:26 +0100 Subject: [PATCH 084/126] updated readme and i18n usage --- x-pack/plugins/alerting_builtins/server/feature.ts | 2 +- x-pack/plugins/alerts/README.md | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/alerting_builtins/server/feature.ts b/x-pack/plugins/alerting_builtins/server/feature.ts index 34b004c19e583..3b0a98d3e0637 100644 --- a/x-pack/plugins/alerting_builtins/server/feature.ts +++ b/x-pack/plugins/alerting_builtins/server/feature.ts @@ -10,7 +10,7 @@ import { BUILT_IN_ALERTS_FEATURE_ID } from '../common'; export const BUILT_IN_ALERTS_FEATURE = { id: BUILT_IN_ALERTS_FEATURE_ID, - name: i18n.translate('xpack.builtInAlerts.featureRegistry.actionsFeatureName', { + name: i18n.translate('xpack.alertingBuiltins.featureRegistry.actionsFeatureName', { defaultMessage: 'Built-In Alerts', }), icon: 'bell', diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index a0849e0882485..ee6141bec65ca 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -299,10 +299,7 @@ server.newPlatform.setup.plugins.alerts.registerType({ Once you have registered your AlertType, you need to grant your users privileges to use it. When registering a feature in Kibana you can specify multiple types of privileges which are granted to users when they're assigned certain roles. -Assuming your feature introduces its own AlertTypes, you'll want to control: -- Which roles have all/read privileges for these AlertTypes when they're inside the feature -- Which roles have all/read privileges for these AlertTypes when they're outside the feature (in another feature or in the global alerts management) - +Assuming your feature introduces its own AlertTypes, you'll want to control which roles have all/read privileges for these AlertTypes when they're inside the feature. In addition, when users are inside your feature you might want to grant them access to AlertTypes from other features, such as built-in AlertTypes or AlertTypes provided by other features. You can control all of these abilities by assigning privileges to the Alerting Framework from within your own feature, for example: @@ -345,7 +342,7 @@ features.registerFeature({ In this example we can see the following: - Our feature grants any user who's assigned the `all` role in our feature the `all` role in the Alerting framework over every alert of the `my-application-id.my-alert-type` type which is created _inside_ the feature. What that means is that this privilege will allow the user to execute any of the `all` operations (listed below) on these alerts as long as their `consumer` is `my-application-id`. Below that you'll notice we've done the same with the `read` role, which is grants the Alerting Framework's `read` role privileges over these very same alerts. - In addition, our feature grants the same privileges over any alert of type `my-application-id.my-restricted-alert-type`, which is another hypothetical alertType registered by this feature. It's worth noting though that this type has been omitted from the `read` role. What this means is that only users with the `all` role will be able to interact with alerts of this type. -- Next, lets look at the `.index-threshold` and `xpack.uptime.alerts.actionGroups.tls` types. These have been specified in both `read` and `all`, which means that all the users in the feature will gain privileges over alerts of these types (as long as their `consumer` is `my-application-id`). The difference between these two and the previous two is that they are _produced_ by other features! `.index-threshold` is a built-in type, provided by the framework, and specifying it here is all you need in order to grant privileges to use this type. On the other hand, `xpack.uptime.alerts.actionGroups.tls` is an AlertType provided by the _Uptime_ feature. Specifying this type here tells the Alerting Framework that as far as the `my-application-id` feature is concerned, the user is privileged to use this type (with `all` and `read` applied), but that isn't enough. Using another feature's AlertType is only possible if both the producer of the AlertType, and the consumer of the AlertType, explicitly grant privileges to do so. In this case, the _Uptime_ feature would have to explicitly add these privileges to a role and this role would have to be granted to this user. +- Next, lets look at the `.index-threshold` and `xpack.uptime.alerts.actionGroups.tls` types. These have been specified in both `read` and `all`, which means that all the users in the feature will gain privileges over alerts of these types (as long as their `consumer` is `my-application-id`). The difference between these two and the previous two is that they are _produced_ by other features! `.index-threshold` is a built-in type, provided by the _Built-In Alerts_ feature, and `xpack.uptime.alerts.actionGroups.tls` is an AlertType provided by the _Uptime_ feature. Specifying these type here tells the Alerting Framework that as far as the `my-application-id` feature is concerned, the user is privileged to use them (with `all` and `read` applied), but that isn't enough. Using another feature's AlertType is only possible if both the producer of the AlertType, and the consumer of the AlertType, explicitly grant privileges to do so. In this case, the _Built-In Alerts_ & _Uptime_ features would have to explicitly add these privileges to a role and this role would have to be granted to this user. It's important to note that any role can be granted a mix of `all` and `read` privileges accross multiple type, for example: @@ -382,9 +379,9 @@ As part of that same change, we also decided that not only should they be allowe ### `read` privileges vs. `all` privileges When a user is granted the `read` role in the Alerting Framework, they will be able to execute the following api calls: -- get -- getAlertState -- find +- `get` +- `getAlertState` +- `find` When a user is granted the `all` role in the Alerting Framework, they will be able to execute all of the `read` privileged api calls, but in addition they'll be granted the following calls: - `create` From e454e59de42d08927590fabbe7d15c97d047fafb Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 2 Jul 2020 17:41:42 +0100 Subject: [PATCH 085/126] added builtInAlerts to feature set test --- x-pack/test/api_integration/apis/features/features/features.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index 11fb9b2de7199..fbbad8a765f5e 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -105,6 +105,7 @@ export default function ({ getService }: FtrProviderContext) { 'savedObjectsManagement', 'ml', 'apm', + 'builtInAlerts', 'canvas', 'infrastructure', 'logs', From b3ed8324f1c305bddd86d052ae683bef1a691ba7 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 2 Jul 2020 21:46:44 +0100 Subject: [PATCH 086/126] use alertsclient in task runner --- .../alerts/server/alerts_client.test.ts | 2 +- x-pack/plugins/alerts/server/alerts_client.ts | 5 +- x-pack/plugins/alerts/server/plugin.ts | 20 ++-- .../server/task_runner/task_runner.test.ts | 112 +++++++----------- .../alerts/server/task_runner/task_runner.ts | 83 +++++-------- .../task_runner/task_runner_factory.test.ts | 4 +- .../server/task_runner/task_runner_factory.ts | 4 +- 7 files changed, 90 insertions(+), 140 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index dbf9258d3ba79..5194d3b6b1fb8 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -1886,7 +1886,7 @@ describe('get()', () => { references: [], }); await expect(alertsClient.get({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Reference action_0 not found"` + `"Action reference \\"action_0\\" not found in alert id: 1"` ); }); diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 0be60673881fb..9fb302193d602 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -719,13 +719,14 @@ export class AlertsClient { } private injectReferencesIntoActions( + alertId: string, actions: RawAlert['actions'], references: SavedObjectReference[] ) { return actions.map((action) => { const reference = references.find((ref) => ref.name === action.actionRef); if (!reference) { - throw new Error(`Reference ${action.actionRef} not found`); + throw new Error(`Action reference "${action.actionRef}" not found in alert id: ${alertId}`); } return { ...omit(action, 'actionRef'), @@ -759,7 +760,7 @@ export class AlertsClient { // Once we support additional types, this type signature will likely change schedule: rawAlert.schedule as IntervalSchedule, actions: rawAlert.actions - ? this.injectReferencesIntoActions(rawAlert.actions, references || []) + ? this.injectReferencesIntoActions(id, rawAlert.actions, references || []) : [], ...(updatedAt ? { updatedAt: new Date(updatedAt) } : {}), ...(createdAt ? { createdAt: new Date(createdAt) } : {}), diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 182bde560700d..6ca65ac152ee3 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -222,9 +222,19 @@ export class AlertingPlugin { features: plugins.features, }); + const getAlertsClientWithRequest = (request: KibanaRequest) => { + if (isESOUsingEphemeralEncryptionKey === true) { + throw new Error( + `Unable to create alerts client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` + ); + } + return alertsClientFactory!.create(request, core.savedObjects); + }; + taskRunnerFactory.initialize({ logger, getServices: this.getServicesFactory(core.savedObjects, core.elasticsearch), + getAlertsClientWithRequest, spaceIdToNamespace: this.spaceIdToNamespace, actionsPlugin: plugins.actions, encryptedSavedObjectsClient, @@ -236,15 +246,7 @@ export class AlertingPlugin { return { listTypes: alertTypeRegistry!.list.bind(this.alertTypeRegistry!), - // Ability to get an alerts client from legacy code - getAlertsClientWithRequest: (request: KibanaRequest) => { - if (isESOUsingEphemeralEncryptionKey === true) { - throw new Error( - `Unable to create alerts client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` - ); - } - return alertsClientFactory!.create(request, core.savedObjects); - }, + getAlertsClientWithRequest, }; } diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 2373ae264c492..4abe58de5a904 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -14,7 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv import { loggingSystemMock } from '../../../../../src/core/server/mocks'; import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; import { actionsMock, actionsClientMock } from '../../../actions/server/mocks'; -import { alertsMock } from '../mocks'; +import { alertsMock, alertsClientMock } from '../mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { IEventLogger } from '../../../event_log/server'; import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; @@ -56,8 +56,8 @@ describe('Task Runner', () => { const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient(); const services = alertsMock.createAlertServices(); - const savedObjectsClient = services.savedObjectsClient; const actionsClient = actionsClientMock.create(); + const alertsClient = alertsClientMock.create(); const taskRunnerFactoryInitializerParams: jest.Mocked & { actionsPlugin: jest.Mocked; @@ -65,6 +65,7 @@ describe('Task Runner', () => { } = { getServices: jest.fn().mockReturnValue(services), actionsPlugin: actionsMock.createStart(), + getAlertsClientWithRequest: jest.fn().mockReturnValue(alertsClient), encryptedSavedObjectsClient, logger: loggingSystemMock.create().get(), spaceIdToNamespace: jest.fn().mockReturnValue(undefined), @@ -74,34 +75,31 @@ describe('Task Runner', () => { const mockedAlertTypeSavedObject = { id: '1', - type: 'alert', - attributes: { - enabled: true, - alertTypeId: '123', - schedule: { interval: '10s' }, - name: 'alert-name', - tags: ['alert-', '-tags'], - createdBy: 'alert-creator', - updatedBy: 'alert-updater', - mutedInstanceIds: [], - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], + consumer: 'bar', + createdAt: new Date('2019-02-12T21:01:22.479Z'), + updatedAt: new Date('2019-02-12T21:01:22.479Z'), + throttle: null, + muteAll: false, + enabled: true, + alertTypeId: '123', + apiKeyOwner: 'elastic', + schedule: { interval: '10s' }, + name: 'alert-name', + tags: ['alert-', '-tags'], + createdBy: 'alert-creator', + updatedBy: 'alert-updater', + mutedInstanceIds: [], + params: { + bar: true, }, - references: [ + actions: [ { - name: 'action_0', - type: 'action', + group: 'default', id: '1', + actionTypeId: 'action', + params: { + foo: true, + }, }, ], }; @@ -109,6 +107,7 @@ describe('Task Runner', () => { beforeEach(() => { jest.resetAllMocks(); taskRunnerFactoryInitializerParams.getServices.mockReturnValue(services); + taskRunnerFactoryInitializerParams.getAlertsClientWithRequest.mockReturnValue(alertsClient); taskRunnerFactoryInitializerParams.actionsPlugin.getActionsClientWithRequest.mockResolvedValue( actionsClient ); @@ -126,7 +125,7 @@ describe('Task Runner', () => { }, taskRunnerFactoryInitializerParams ); - savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); + alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -200,7 +199,7 @@ describe('Task Runner', () => { mockedTaskInstance, taskRunnerFactoryInitializerParams ); - savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); + alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -285,7 +284,7 @@ describe('Task Runner', () => { ], }, message: - "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: undefined:1", + "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", }); }); @@ -302,7 +301,7 @@ describe('Task Runner', () => { mockedTaskInstance, taskRunnerFactoryInitializerParams ); - savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); + alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -412,7 +411,7 @@ describe('Task Runner', () => { }, ], }, - "message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: undefined:1", + "message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", }, ], ] @@ -439,7 +438,7 @@ describe('Task Runner', () => { }, taskRunnerFactoryInitializerParams ); - savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); + alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -526,7 +525,7 @@ describe('Task Runner', () => { mockedTaskInstance, taskRunnerFactoryInitializerParams ); - savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); + alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -548,44 +547,13 @@ describe('Task Runner', () => { ); }); - test('throws error if reference not found', async () => { - const taskRunner = new TaskRunner( - alertType, - mockedTaskInstance, - taskRunnerFactoryInitializerParams - ); - savedObjectsClient.get.mockResolvedValueOnce({ - ...mockedAlertTypeSavedObject, - references: [], - }); - encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - apiKey: Buffer.from('123:abc').toString('base64'), - }, - references: [], - }); - expect(await taskRunner.run()).toMatchInlineSnapshot(` - Object { - "runAt": 1970-01-01T00:00:10.000Z, - "state": Object { - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, - } - `); - expect(taskRunnerFactoryInitializerParams.logger.error).toHaveBeenCalledWith( - `Executing Alert \"1\" has resulted in Error: Action reference \"action_0\" not found in alert id: 1` - ); - }); - test('uses API key when provided', async () => { const taskRunner = new TaskRunner( alertType, mockedTaskInstance, taskRunnerFactoryInitializerParams ); - savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); + alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -621,7 +589,7 @@ describe('Task Runner', () => { mockedTaskInstance, taskRunnerFactoryInitializerParams ); - savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); + alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -660,7 +628,7 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams ); - savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); + alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -722,7 +690,7 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams ); - savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); + alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); const runnerResult = await taskRunner.run(); @@ -747,7 +715,7 @@ describe('Task Runner', () => { taskRunnerFactoryInitializerParams ); - savedObjectsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); + alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -770,7 +738,7 @@ describe('Task Runner', () => { }); test('recovers gracefully when the Alert Task Runner throws an exception when fetching attributes', async () => { - savedObjectsClient.get.mockImplementation(() => { + alertsClient.get.mockImplementation(() => { throw new Error('OMG'); }); @@ -802,7 +770,7 @@ describe('Task Runner', () => { }); test('avoids rescheduling a failed Alert Task Runner when it throws due to failing to fetch the alert', async () => { - savedObjectsClient.get.mockImplementation(() => { + alertsClient.get.mockImplementation(() => { throw SavedObjectsErrorHelpers.createGenericNotFoundError('task', '1'); }); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 3512ab16a3712..90a5bc413d273 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -5,7 +5,7 @@ */ import { pick, mapValues, omit, without } from 'lodash'; -import { Logger, SavedObject, KibanaRequest } from '../../../../../src/core/server'; +import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance } from '../../../task_manager/server'; import { createExecutionHandler } from './create_execution_handler'; @@ -17,9 +17,11 @@ import { RawAlert, IntervalSchedule, Services, - AlertInfoParams, RawAlertInstance, AlertTaskState, + Alert, + AlertExecutorOptions, + SanitizedAlert, } from '../types'; import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type'; import { taskInstanceToAlertTaskInstance } from './alert_task_instance'; @@ -27,6 +29,7 @@ import { AlertInstances } from '../alert_instance/alert_instance'; import { EVENT_LOG_ACTIONS } from '../plugin'; import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error'; +import { AlertsClient } from '../alerts_client'; const FALLBACK_RETRY_INTERVAL: IntervalSchedule = { interval: '5m' }; @@ -94,8 +97,12 @@ export class TaskRunner { } as unknown) as KibanaRequest; } - async getServicesWithSpaceLevelPermissions(spaceId: string, apiKey: string | null) { - return this.context.getServices(this.getFakeKibanaRequest(spaceId, apiKey)); + private getServicesWithSpaceLevelPermissions( + spaceId: string, + apiKey: string | null + ): [Services, PublicMethodsOf] { + const request = this.getFakeKibanaRequest(spaceId, apiKey); + return [this.context.getServices(request), this.context.getAlertsClientWithRequest(request)]; } private getExecutionHandler( @@ -104,21 +111,8 @@ export class TaskRunner { tags: string[] | undefined, spaceId: string, apiKey: string | null, - actions: RawAlert['actions'], - references: SavedObject['references'] + actions: Alert['actions'] ) { - // Inject ids into actions - const actionsWithIds = actions.map((action) => { - const actionReference = references.find((obj) => obj.name === action.actionRef); - if (!actionReference) { - throw new Error(`Action reference "${action.actionRef}" not found in alert id: ${alertId}`); - } - return { - ...action, - id: actionReference.id, - }; - }); - return createExecutionHandler({ alertId, alertName, @@ -126,7 +120,7 @@ export class TaskRunner { logger: this.logger, actionsPlugin: this.context.actionsPlugin, apiKey, - actions: actionsWithIds, + actions, spaceId, alertType: this.alertType, eventLogger: this.context.eventLogger, @@ -147,20 +141,12 @@ export class TaskRunner { async executeAlertInstances( services: Services, - alertInfoParams: AlertInfoParams, + alert: SanitizedAlert, + params: AlertExecutorOptions['params'], executionHandler: ReturnType, spaceId: string ): Promise { - const { - params, - throttle, - muteAll, - mutedInstanceIds, - name, - tags, - createdBy, - updatedBy, - } = alertInfoParams; + const { throttle, muteAll, mutedInstanceIds, name, tags, createdBy, updatedBy } = alert; const { params: { alertId }, state: { alertInstances: alertRawInstances = {}, alertTypeState = {}, previousStartedAt }, @@ -267,33 +253,22 @@ export class TaskRunner { }; } - async validateAndExecuteAlert( - services: Services, - apiKey: string | null, - attributes: RawAlert, - references: SavedObject['references'] - ) { + async validateAndExecuteAlert(services: Services, apiKey: string | null, alert: SanitizedAlert) { const { params: { alertId, spaceId }, } = this.taskInstance; // Validate - const params = validateAlertTypeParams(this.alertType, attributes.params); + const validatedParams = validateAlertTypeParams(this.alertType, alert.params); const executionHandler = this.getExecutionHandler( alertId, - attributes.name, - attributes.tags, + alert.name, + alert.tags, spaceId, apiKey, - attributes.actions, - references - ); - return this.executeAlertInstances( - services, - { ...attributes, params }, - executionHandler, - spaceId + alert.actions ); + return this.executeAlertInstances(services, alert, validatedParams, executionHandler, spaceId); } async loadAlertAttributesAndRun(): Promise> { @@ -302,17 +277,17 @@ export class TaskRunner { } = this.taskInstance; const apiKey = await this.getApiKeyForAlertPermissions(alertId, spaceId); - const services = await this.getServicesWithSpaceLevelPermissions(spaceId, apiKey); + const [services, alertsClient] = await this.getServicesWithSpaceLevelPermissions( + spaceId, + apiKey + ); // Ensure API key is still valid and user has access - const { attributes, references } = await services.savedObjectsClient.get( - 'alert', - alertId - ); + const alert = await alertsClient.get({ id: alertId }); return { state: await promiseResult( - this.validateAndExecuteAlert(services, apiKey, attributes, references) + this.validateAndExecuteAlert(services, apiKey, alert) ), runAt: asOk( getNextRunAt( @@ -320,7 +295,7 @@ export class TaskRunner { // we do not currently have a good way of returning the type // from SavedObjectsClient, and as we currenrtly require a schedule // and we only support `interval`, we can cast this safely - attributes.schedule as IntervalSchedule + alert.schedule ) ), }; diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts index ba151c2356191..9af7d9ddc44eb 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts @@ -10,7 +10,7 @@ import { TaskRunnerContext, TaskRunnerFactory } from './task_runner_factory'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; import { loggingSystemMock } from '../../../../../src/core/server/mocks'; import { actionsMock } from '../../../actions/server/mocks'; -import { alertsMock } from '../mocks'; +import { alertsMock, alertsClientMock } from '../mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; const alertType = { @@ -52,9 +52,11 @@ describe('Task Runner Factory', () => { const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.createStart(); const services = alertsMock.createAlertServices(); + const alertsClient = alertsClientMock.create(); const taskRunnerFactoryInitializerParams: jest.Mocked = { getServices: jest.fn().mockReturnValue(services), + getAlertsClientWithRequest: jest.fn().mockReturnValue(alertsClient), actionsPlugin: actionsMock.createStart(), encryptedSavedObjectsClient: encryptedSavedObjectsPlugin.getClient(), logger: loggingSystemMock.create().get(), diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts index ca762cf2b2105..6f83e34cdbe03 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Logger } from '../../../../../src/core/server'; +import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { RunContext } from '../../../task_manager/server'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; import { PluginStartContract as ActionsPluginStartContract } from '../../../actions/server'; @@ -15,10 +15,12 @@ import { } from '../types'; import { TaskRunner } from './task_runner'; import { IEventLogger } from '../../../event_log/server'; +import { AlertsClient } from '../alerts_client'; export interface TaskRunnerContext { logger: Logger; getServices: GetServicesFunction; + getAlertsClientWithRequest(request: KibanaRequest): PublicMethodsOf; actionsPlugin: ActionsPluginStartContract; eventLogger: IEventLogger; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; From f0f82f3d3d7360a0c12afcfe341ae6dde8a8dca7 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 3 Jul 2020 10:37:47 +0100 Subject: [PATCH 087/126] fixed lodash usage broken by upgrade to lodash 4 --- .../alerts/server/authorization/alerts_authorization.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 46d6407ae51f8..539e0b0d44a8c 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { pluck, mapValues, remove, zipObject } from 'lodash'; +import { map, mapValues, remove, fromPairs } from 'lodash'; import { KibanaRequest } from 'src/core/server'; import { RecursiveReadonly } from '@kbn/utility-types'; import { ALERTS_FEATURE_ID } from '../../common'; @@ -117,7 +117,7 @@ export class AlertsAuthorization { operation ); } else { - const authorizedPrivileges = pluck( + const authorizedPrivileges = map( privileges.filter((privilege) => privilege.authorized), 'privilege' ); @@ -395,5 +395,5 @@ function asAuthorizedConsumers( consumers: string[], hasPrivileges: HasPrivileges ): AuthorizedConsumers { - return zipObject(consumers.map((feature) => [feature, hasPrivileges])); + return fromPairs(consumers.map((feature) => [feature, hasPrivileges])); } From 541cdfd3b367f95fb50144634dabe49a3101b704 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 3 Jul 2020 13:30:15 +0100 Subject: [PATCH 088/126] prevent rendering alert editing when there are no privileges to edit their actions --- .../plugins/actions/server/actions_client.ts | 9 +- x-pack/plugins/actions/server/feature.ts | 5 +- x-pack/plugins/alerts/server/alerts_client.ts | 53 ++++---- x-pack/plugins/apm/server/feature.ts | 21 +-- x-pack/plugins/infra/server/features.ts | 22 +-- .../security_solution/server/plugin.ts | 21 +-- .../public/application/lib/capabilities.ts | 12 +- .../connector_add_modal.test.tsx | 8 +- .../actions_connectors_list.test.tsx | 40 +++--- .../components/alert_details.test.tsx | 128 ++++++++++++++++++ .../components/alert_details.tsx | 19 ++- .../sections/alert_form/alert_form.tsx | 4 +- .../alerts_list/components/alerts_list.tsx | 54 ++++++-- .../triggers_actions_ui/public/types.ts | 1 + x-pack/plugins/uptime/server/kibana.index.ts | 13 +- 15 files changed, 256 insertions(+), 154 deletions(-) diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index ca7b98f3cfc02..fcd201231e4f3 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -309,14 +309,7 @@ export class ActionsClient { } public async listTypes(): Promise { - try { - await this.authorization.ensureAuthorized('list'); - return this.actionTypeRegistry.list(); - } catch { - // auditing will log this unauthorized attempt, so we'll return - // an empty list to align with the behaviour in the AlertsClient - return []; - } + return this.actionTypeRegistry.list(); } } diff --git a/x-pack/plugins/actions/server/feature.ts b/x-pack/plugins/actions/server/feature.ts index 93f94337dff60..c06acb6761454 100644 --- a/x-pack/plugins/actions/server/feature.ts +++ b/x-pack/plugins/actions/server/feature.ts @@ -12,6 +12,7 @@ export const ACTIONS_FEATURE = { name: i18n.translate('xpack.actions.featureRegistry.actionsFeatureName', { defaultMessage: 'Actions', }), + icon: 'bell', navLinkId: 'actions', app: [], privileges: { @@ -23,7 +24,7 @@ export const ACTIONS_FEATURE = { all: [ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE], read: [], }, - ui: [], + ui: ['show', 'execute', 'save', 'delete'], }, read: { app: [], @@ -34,7 +35,7 @@ export const ACTIONS_FEATURE = { all: [ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE], read: [ACTION_SAVED_OBJECT_TYPE], }, - ui: [], + ui: ['show', 'execute'], }, }, }; diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 6b091a5a4503b..1b1524f00dc73 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -666,32 +666,35 @@ export class AlertsClient { private async denormalizeActions( alertActions: NormalizedAlertAction[] ): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> { - const actionsClient = await this.getActionsClient(); - const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))]; - const actionResults = await actionsClient.getBulk(actionIds); const references: SavedObjectReference[] = []; - const actions = alertActions.map(({ id, ...alertAction }, i) => { - const actionResultValue = actionResults.find((action) => action.id === id); - if (actionResultValue) { - const actionRef = `action_${i}`; - references.push({ - id, - name: actionRef, - type: 'action', - }); - return { - ...alertAction, - actionRef, - actionTypeId: actionResultValue.actionTypeId, - }; - } else { - return { - ...alertAction, - actionRef: '', - actionTypeId: '', - }; - } - }); + const actions: RawAlert['actions'] = []; + if (alertActions.length) { + const actionsClient = await this.getActionsClient(); + const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))]; + const actionResults = await actionsClient.getBulk(actionIds); + alertActions.forEach(({ id, ...alertAction }, i) => { + const actionResultValue = actionResults.find((action) => action.id === id); + if (actionResultValue) { + const actionRef = `action_${i}`; + references.push({ + id, + name: actionRef, + type: 'action', + }); + actions.push({ + ...alertAction, + actionRef, + actionTypeId: actionResultValue.actionTypeId, + }); + } else { + actions.push({ + ...alertAction, + actionRef: '', + actionTypeId: '', + }); + } + }); + } return { actions, references, diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index c02b894ff1f3a..4c0159e7da93d 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -26,16 +26,7 @@ export const APM_FEATURE = { all: ['alert'], read: [], }, - ui: [ - 'show', - 'save', - 'alerting:show', - 'actions:show', - 'alerting:save', - 'actions:save', - 'alerting:delete', - 'actions:delete', - ], + ui: ['show', 'save', 'alerting:show', 'alerting:save', 'alerting:delete'], }, read: { app: ['apm', 'kibana'], @@ -45,15 +36,7 @@ export const APM_FEATURE = { all: ['alert'], read: [], }, - ui: [ - 'show', - 'alerting:show', - 'actions:show', - 'alerting:save', - 'actions:save', - 'alerting:delete', - 'actions:delete', - ], + ui: ['show', 'alerting:show', 'alerting:save', 'alerting:delete'], }, }, }; diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index 65524dd5e4df7..4914ca3c2b07f 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -25,17 +25,7 @@ export const METRICS_FEATURE = { all: ['infrastructure-ui-source', 'alert'], read: ['index-pattern'], }, - ui: [ - 'show', - 'configureSource', - 'save', - 'alerting:show', - 'actions:show', - 'alerting:save', - 'actions:save', - 'alerting:delete', - 'actions:delete', - ], + ui: ['show', 'configureSource', 'save', 'alerting:show', 'alerting:save', 'alerting:delete'], }, read: { app: ['infra', 'kibana'], @@ -45,15 +35,7 @@ export const METRICS_FEATURE = { all: ['alert'], read: ['infrastructure-ui-source', 'index-pattern'], }, - ui: [ - 'show', - 'alerting:show', - 'actions:show', - 'alerting:save', - 'actions:save', - 'alerting:delete', - 'actions:delete', - ], + ui: ['show', 'alerting:show', 'alerting:save', 'alerting:delete'], }, }, }; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 9cf431611d7c4..21fc16a080e65 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -149,16 +149,7 @@ export class Plugin implements IPlugin; -const apps = ['apm', 'siem', 'uptime', 'infrastructure']; +const apps = ['apm', 'siem', 'uptime', 'infrastructure', 'actions']; function hasCapability(capabilities: Capabilities, capability: string) { return apps.some((app) => capabilities[app]?.[capability]); @@ -23,8 +23,12 @@ function createCapabilityCheck(capability: string) { } export const hasShowAlertsCapability = createCapabilityCheck('alerting:show'); -export const hasShowActionsCapability = createCapabilityCheck('actions:show'); export const hasSaveAlertsCapability = createCapabilityCheck('alerting:save'); -export const hasSaveActionsCapability = createCapabilityCheck('actions:save'); export const hasDeleteAlertsCapability = createCapabilityCheck('alerting:delete'); -export const hasDeleteActionsCapability = createCapabilityCheck('actions:delete'); + +export const hasShowActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.show; +export const hasSaveActionsCapability = (capabilities: Capabilities) => capabilities?.actions?.save; +export const hasExecuteActionsCapability = (capabilities: Capabilities) => + capabilities?.actions?.execute; +export const hasDeleteActionsCapability = (capabilities: Capabilities) => + capabilities?.actions?.delete; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx index 1b35b5636872d..3d621367fc40a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx @@ -26,10 +26,10 @@ describe('connector_add_modal', () => { http: mocks.http, capabilities: { ...capabilities, - siem: { - 'actions:show': true, - 'actions:save': true, - 'actions:delete': true, + actions: { + show: true, + save: true, + delete: true, }, }, actionTypeRegistry: actionTypeRegistry as any, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 09d94e2418cb8..44ea9624692ce 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -62,10 +62,10 @@ describe('actions_connectors_list component empty', () => { navigateToApp, capabilities: { ...capabilities, - siem: { - 'actions:show': true, - 'actions:save': true, - 'actions:delete': true, + actions: { + show: true, + save: true, + delete: true, }, }, history: (scopedHistoryMock.create() as unknown) as ScopedHistory, @@ -165,10 +165,10 @@ describe('actions_connectors_list component with items', () => { navigateToApp, capabilities: { ...capabilities, - securitySolution: { - 'actions:show': true, - 'actions:save': true, - 'actions:delete': true, + actions: { + show: true, + save: true, + delete: true, }, }, history: (scopedHistoryMock.create() as unknown) as ScopedHistory, @@ -248,10 +248,10 @@ describe('actions_connectors_list component empty with show only capability', () navigateToApp, capabilities: { ...capabilities, - securitySolution: { - 'actions:show': true, - 'actions:save': false, - 'actions:delete': false, + actions: { + show: true, + save: false, + delete: false, }, }, history: (scopedHistoryMock.create() as unknown) as ScopedHistory, @@ -334,10 +334,10 @@ describe('actions_connectors_list with show only capability', () => { navigateToApp, capabilities: { ...capabilities, - securitySolution: { - 'actions:show': true, - 'actions:save': false, - 'actions:delete': false, + actions: { + show: true, + save: false, + delete: false, }, }, history: (scopedHistoryMock.create() as unknown) as ScopedHistory, @@ -432,10 +432,10 @@ describe('actions_connectors_list component with disabled items', () => { navigateToApp, capabilities: { ...capabilities, - securitySolution: { - 'actions:show': true, - 'actions:save': true, - 'actions:delete': true, + actions: { + show: true, + save: true, + delete: true, }, }, history: (scopedHistoryMock.create() as unknown) as ScopedHistory, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index d8f0d0b6b20a0..1a07ffd81b176 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -67,6 +67,7 @@ jest.mock('react-router-dom', () => ({ jest.mock('../../../lib/capabilities', () => ({ hasSaveAlertsCapability: jest.fn(() => true), + hasExecuteActionsCapability: jest.fn(() => true), })); const mockAlertApis = { @@ -590,6 +591,133 @@ describe('mute button', () => { }); }); +describe('edit button', () => { + const actionTypes: ActionType[] = [ + { + id: '.server-log', + name: 'Server log', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, + ]; + + it('should render an edit button when alert and actions are editable', () => { + const alert = mockAlert({ + enabled: true, + muteAll: false, + actions: [ + { + group: 'default', + id: uuid.v4(), + params: {}, + actionTypeId: '.server-log', + }, + ], + }); + + const alertType = { + id: '.noop', + name: 'No Op', + actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, + defaultActionGroupId: 'default', + producer: 'alerting', + }; + + expect( + shallow( + + ) + .find(EuiButtonEmpty) + .find('[name="edit"]') + .first() + .exists() + ).toBeTruthy(); + }); + + it('should not render an edit button when alert editable but actions arent', () => { + const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities'); + hasExecuteActionsCapability.mockReturnValue(false); + const alert = mockAlert({ + enabled: true, + muteAll: false, + actions: [ + { + group: 'default', + id: uuid.v4(), + params: {}, + actionTypeId: '.server-log', + }, + ], + }); + + const alertType = { + id: '.noop', + name: 'No Op', + actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, + defaultActionGroupId: 'default', + producer: 'alerting', + }; + + expect( + shallow( + + ) + .find(EuiButtonEmpty) + .find('[name="edit"]') + .first() + .exists() + ).toBeFalsy(); + }); + + it('should render an edit button when alert editable but actions arent when there are no actions on the alert', () => { + const { hasExecuteActionsCapability } = jest.requireMock('../../../lib/capabilities'); + hasExecuteActionsCapability.mockReturnValue(false); + const alert = mockAlert({ + enabled: true, + muteAll: false, + actions: [], + }); + + const alertType = { + id: '.noop', + name: 'No Op', + actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, + defaultActionGroupId: 'default', + producer: 'alerting', + }; + + expect( + shallow( + + ) + .find(EuiButtonEmpty) + .find('[name="edit"]') + .first() + .exists() + ).toBeTruthy(); + }); +}); + function mockAlert(overloads: Partial = {}): Alert { return { id: uuid.v4(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 3a25417f7db4c..4424f0a3d10fb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -28,7 +28,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useAppDependencies } from '../../../app_context'; -import { hasSaveAlertsCapability } from '../../../lib/capabilities'; +import { hasSaveAlertsCapability, hasExecuteActionsCapability } from '../../../lib/capabilities'; import { Alert, AlertType, ActionType } from '../../../../types'; import { ComponentOpts as BulkOperationsComponentOpts, @@ -71,12 +71,18 @@ export const AlertDetails: React.FunctionComponent = ({ dataPlugin, } = useAppDependencies(); - const canSave = hasSaveAlertsCapability(capabilities); + const canSaveAlert = hasSaveAlertsCapability(capabilities); + const canExecuteActions = hasExecuteActionsCapability(capabilities); const actionTypesByTypeId = indexBy(actionTypes, 'id'); const hasEditButton = - canSave && alertTypeRegistry.has(alert.alertTypeId) + // can the user save the alert + canSaveAlert && + // if the alert has actions, can the user save the alert's action params + (canExecuteActions || (!canExecuteActions && alert.actions.length === 0)) && + // is this alert type editable from within Alerts Management + (alertTypeRegistry.has(alert.alertTypeId) ? !alertTypeRegistry.get(alert.alertTypeId).requiresAppContext - : false; + : false); const alertActions = alert.actions; const uniqueActions = Array.from(new Set(alertActions.map((item: any) => item.actionTypeId))); @@ -124,6 +130,7 @@ export const AlertDetails: React.FunctionComponent = ({ data-test-subj="openEditAlertFlyoutButton" iconType="pencil" onClick={() => setEditFlyoutVisibility(true)} + name="edit" > = ({ { @@ -229,7 +236,7 @@ export const AlertDetails: React.FunctionComponent = ({ { if (isMuted) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 874091b2bb7a8..79cc965d71d7a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -38,6 +38,7 @@ import { AlertTypeModel, Alert, IErrorObject, AlertAction, AlertTypeIndex } from import { getTimeOptions } from '../../../common/lib/get_time_options'; import { useAlertsContext } from '../../context/alerts_context'; import { ActionForm } from '../action_connector_form'; +import { hasShowActionsCapability } from '../../lib/capabilities'; export function validateBaseProperties(alertObject: Alert) { const validationResult = { errors: {} }; @@ -96,6 +97,7 @@ export const AlertForm = ({ docLinks, capabilities, } = alertsContext; + const canShowActions = hasShowActionsCapability(capabilities); const [alertTypeModel, setAlertTypeModel] = useState( alert.alertTypeId ? alertTypeRegistry.get(alert.alertTypeId) : null @@ -257,7 +259,7 @@ export const AlertForm = ({ /> ) : null} - {defaultActionGroupId ? ( + {canShowActions && defaultActionGroupId ? ( { } = useAppDependencies(); const canDelete = hasDeleteAlertsCapability(capabilities); const canSave = hasSaveAlertsCapability(capabilities); + const canExecuteActions = hasExecuteActionsCapability(capabilities); const [actionTypes, setActionTypes] = useState([]); const [selectedIds, setSelectedIds] = useState([]); @@ -288,7 +293,8 @@ export const AlertsList: React.FunctionComponent = () => { setIsPerformingAction(true)} onActionPerformed={() => { @@ -337,7 +343,11 @@ export const AlertsList: React.FunctionComponent = () => { items={ alertTypesState.isInitialized === false ? [] - : convertAlertsToTableItems(alertsState.data, alertTypesState.data) + : convertAlertsToTableItems(alertsState.data, alertTypesState.data, { + canDelete, + canSave, + canExecuteActions, + }) } itemId="id" columns={alertsTableColumns} @@ -354,15 +364,12 @@ export const AlertsList: React.FunctionComponent = () => { /* Don't display alert count until we have the alert types initialized */ totalItemCount: alertTypesState.isInitialized === false ? 0 : alertsState.totalItemCount, }} - selection={ - canDelete - ? { - onSelectionChange(updatedSelectedItemsList: AlertTableItem[]) { - setSelectedIds(updatedSelectedItemsList.map((item) => item.id)); - }, - } - : undefined - } + selection={{ + selectable: (alert: AlertTableItem) => alert.isEditable, + onSelectionChange(updatedSelectedItemsList: AlertTableItem[]) { + setSelectedIds(updatedSelectedItemsList.map((item) => item.id)); + }, + }} onChange={({ page: changedPage }: { page: Pagination }) => { setPage(changedPage); }} @@ -370,7 +377,11 @@ export const AlertsList: React.FunctionComponent = () => { ); - const loadedItems = convertAlertsToTableItems(alertsState.data, alertTypesState.data); + const loadedItems = convertAlertsToTableItems(alertsState.data, alertTypesState.data, { + canDelete, + canSave, + canExecuteActions, + }); const isFilterApplied = !( isEmpty(searchText) && @@ -452,11 +463,26 @@ function filterAlertsById(alerts: Alert[], ids: string[]): Alert[] { return alerts.filter((alert) => ids.includes(alert.id)); } -function convertAlertsToTableItems(alerts: Alert[], alertTypesIndex: AlertTypeIndex) { +interface Capabilities { + canDelete: boolean; + canSave: boolean; + canExecuteActions: boolean; +} + +function convertAlertsToTableItems( + alerts: Alert[], + alertTypesIndex: AlertTypeIndex, + capabilities: Capabilities +) { return alerts.map((alert) => ({ ...alert, actionsText: alert.actions.length, tagsText: alert.tags.join(', '), alertType: alertTypesIndex[alert.alertTypeId]?.name ?? alert.alertTypeId, + isEditable: + capabilities.canDelete && + capabilities.canSave && + (capabilities.canExecuteActions || + (!capabilities.canExecuteActions && !alert.actions.length)), })); } diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 52179dd35767c..314219de4048d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -109,6 +109,7 @@ export type AlertWithoutId = Omit; export interface AlertTableItem extends Alert { alertType: AlertType['name']; tagsText: string; + isEditable: boolean; } export interface AlertTypeParamsExpressionProps< diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index 4f17f0ae9d2b3..90e01857abdbe 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -49,11 +49,8 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor 'configureSettings', 'show', 'alerting:show', - 'actions:show', 'alerting:save', - 'actions:save', 'alerting:delete', - 'actions:delete', ], }, read: { @@ -64,15 +61,7 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor all: ['alert'], read: [umDynamicSettings.name], }, - ui: [ - 'show', - 'alerting:show', - 'actions:show', - 'alerting:save', - 'actions:save', - 'alerting:delete', - 'actions:delete', - ], + ui: ['show', 'alerting:show', 'alerting:save', 'alerting:delete'], }, }, }); From 169789adee7f32796ac8073851aa0e493ee84dec Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 3 Jul 2020 15:27:48 +0100 Subject: [PATCH 089/126] allow all to see list of action types by default (for now) --- .../security_and_spaces/tests/actions/list_action_types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts index 60bcdc39358cc..8c9f134878e86 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts @@ -32,8 +32,6 @@ export default function listActionTypesTests({ getService }: FtrProviderContext) switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': - expect(response.body).to.eql([]); - break; case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': From 49b40be836c087feb8b322abbe6c8fc922c47f89 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 3 Jul 2020 17:49:16 +0100 Subject: [PATCH 090/126] fixec privileges feature tests --- x-pack/test/api_integration/apis/security/privileges.ts | 1 + x-pack/test/api_integration/apis/security/privileges_basic.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 78915f6580299..357da5203e336 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -37,6 +37,7 @@ export default function ({ getService }: FtrProviderContext) { apm: ['all', 'read'], siem: ['all', 'read'], ingestManager: ['all', 'read'], + actions: ['all', 'read'], }, global: ['all', 'read'], space: ['all', 'read'], diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index d2bfdbe4dc967..da960e565ae83 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -35,6 +35,7 @@ export default function ({ getService }: FtrProviderContext) { apm: ['all', 'read'], siem: ['all', 'read'], ingestManager: ['all', 'read'], + actions: ['all', 'read'], }, global: ['all', 'read'], space: ['all', 'read'], From 53916fd47987ddbc58eca77d751386b2b2a7ab4d Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 3 Jul 2020 17:55:40 +0100 Subject: [PATCH 091/126] fixed security test --- .../server/authorization/disable_ui_capabilities.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts index a1bedea9f7deb..9f21117d3296e 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts @@ -20,6 +20,9 @@ const mockRequest = httpServerMock.createKibanaRequest(); const createMockAuthz = (options: MockAuthzOptions) => { const mock = authorizationMock.create({ version: '1.0.0-zeta1' }); + // plug actual ui actions into mock Actions with + mock.actions = actions; + mock.checkPrivilegesDynamicallyWithRequest.mockImplementation((request) => { expect(request).toBe(mockRequest); From e6025ba4e870f1a4841485e783adb879571bdbec Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 3 Jul 2020 19:37:47 +0100 Subject: [PATCH 092/126] disble connector fields when user is read only --- .../email/email_connector.tsx | 8 +++- .../es_index/es_index_connector.tsx | 5 ++- .../pagerduty/pagerduty_connectors.tsx | 4 +- .../slack/slack_connectors.tsx | 3 +- .../webhook/webhook_connectors.tsx | 9 +++- .../action_connector_form.tsx | 9 +++- .../action_connector_form/action_form.tsx | 42 +++++++++++-------- .../connector_add_flyout.tsx | 1 + .../connector_add_modal.tsx | 1 + .../connector_edit_flyout.tsx | 1 + .../components/actions_connectors_list.tsx | 28 +++++++------ .../triggers_actions_ui/public/types.ts | 1 + 12 files changed, 76 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx index 734ffc49649de..015dcb5783215 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx @@ -19,7 +19,7 @@ import { EmailActionConnector } from '../types'; export const EmailActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => { +>> = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { const { from, host, port, secure } = action.config; const { user, password } = action.secrets; @@ -41,6 +41,7 @@ export const EmailActionConnectorFields: React.FunctionComponent 0 && from !== undefined} name="from" value={from || ''} @@ -73,6 +74,7 @@ export const EmailActionConnectorFields: React.FunctionComponent 0 && host !== undefined} name="host" value={host || ''} @@ -108,6 +110,7 @@ export const EmailActionConnectorFields: React.FunctionComponent 0 && port !== undefined} fullWidth + readOnly={readOnly} name="port" value={port || ''} data-test-subj="emailPortInput" @@ -132,6 +135,7 @@ export const EmailActionConnectorFields: React.FunctionComponent { editActionConfig('secure', e.target.checked); @@ -161,6 +165,7 @@ export const EmailActionConnectorFields: React.FunctionComponent 0} name="user" + readOnly={readOnly} value={user || ''} data-test-subj="emailUserInput" onChange={(e) => { @@ -184,6 +189,7 @@ export const EmailActionConnectorFields: React.FunctionComponent 0} name="password" value={password || ''} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx index b5aa42cfd539a..35fa1c42eae5a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx @@ -28,7 +28,7 @@ import { const IndexActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, errors, http }) => { +>> = ({ action, editActionConfig, errors, http, readOnly }) => { const { index, refresh, executionTimeField } = action.config; const [hasTimeFieldCheckbox, setTimeFieldCheckboxState] = useState( executionTimeField != null @@ -102,6 +102,7 @@ const IndexActionConnectorFields: React.FunctionComponent { editActionConfig('index', selected.length > 0 ? selected[0].value : ''); const indices = selected.map((s) => s.value as string); @@ -132,6 +133,7 @@ const IndexActionConnectorFields: React.FunctionComponent { editActionConfig('refresh', e.target.checked); }} @@ -159,6 +161,7 @@ const IndexActionConnectorFields: React.FunctionComponent { setTimeFieldCheckboxState(!hasTimeFieldCheckbox); // if changing from checked to not checked (hasTimeField === true), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx index 48da3f1778b48..6399e1f80984c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx @@ -12,7 +12,7 @@ import { PagerDutyActionConnector } from '.././types'; const PagerDutyActionConnectorFields: React.FunctionComponent> = ({ errors, action, editActionConfig, editActionSecrets, docLinks }) => { +>> = ({ errors, action, editActionConfig, editActionSecrets, docLinks, readOnly }) => { const { apiUrl } = action.config; const { routingKey } = action.secrets; return ( @@ -31,6 +31,7 @@ const PagerDutyActionConnectorFields: React.FunctionComponent) => { editActionConfig('apiUrl', e.target.value); @@ -69,6 +70,7 @@ const PagerDutyActionConnectorFields: React.FunctionComponent 0 && routingKey !== undefined} name="routingKey" + readOnly={readOnly} value={routingKey || ''} data-test-subj="pagerdutyRoutingKeyInput" onChange={(e: React.ChangeEvent) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx index 11934d3af3ceb..e9e8724272719 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx @@ -12,7 +12,7 @@ import { SlackActionConnector } from '../types'; const SlackActionFields: React.FunctionComponent> = ({ action, editActionSecrets, errors, docLinks }) => { +>> = ({ action, editActionSecrets, errors, docLinks, readOnly }) => { const { webhookUrl } = action.secrets; return ( @@ -44,6 +44,7 @@ const SlackActionFields: React.FunctionComponent 0 && webhookUrl !== undefined} name="webhookUrl" + readOnly={readOnly} placeholder="Example: https://hooks.slack.com/services" value={webhookUrl || ''} data-test-subj="slackWebhookUrlInput" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx index 5cea7d087f33a..1b0211dc57f12 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx @@ -30,7 +30,7 @@ const HTTP_VERBS = ['post', 'put']; const WebhookActionConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => { +>> = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { const [httpHeaderKey, setHttpHeaderKey] = useState(''); const [httpHeaderValue, setHttpHeaderValue] = useState(''); const [hasHeaders, setHasHeaders] = useState(false); @@ -126,6 +126,7 @@ const WebhookActionConnectorFields: React.FunctionComponent { @@ -151,6 +152,7 @@ const WebhookActionConnectorFields: React.FunctionComponent { @@ -220,6 +222,7 @@ const WebhookActionConnectorFields: React.FunctionComponent ({ text: verb.toUpperCase(), value: verb }))} onChange={(e) => { @@ -245,6 +248,7 @@ const WebhookActionConnectorFields: React.FunctionComponent 0 && url !== undefined} fullWidth + readOnly={readOnly} value={url || ''} data-test-subj="webhookUrlText" onChange={(e) => { @@ -277,6 +281,7 @@ const WebhookActionConnectorFields: React.FunctionComponent 0 && user !== undefined} name="user" + readOnly={readOnly} value={user || ''} data-test-subj="webhookUserInput" onChange={(e) => { @@ -306,6 +311,7 @@ const WebhookActionConnectorFields: React.FunctionComponent 0 && password !== undefined} value={password || ''} data-test-subj="webhookPasswordInput" @@ -325,6 +331,7 @@ const WebhookActionConnectorFields: React.FunctionComponent ; docLinks: DocLinksStart; + capabilities: ApplicationStart['capabilities']; } export const ActionConnectorForm = ({ @@ -64,7 +66,10 @@ export const ActionConnectorForm = ({ http, actionTypeRegistry, docLinks, + capabilities, }: ActionConnectorProps) => { + const canSave = hasSaveActionsCapability(capabilities); + const setActionProperty = (key: string, value: any) => { dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); }; @@ -137,6 +142,7 @@ export const ActionConnectorForm = ({ 0 && connector.name !== undefined} name="name" placeholder="Untitled" @@ -166,6 +172,7 @@ export const ActionConnectorForm = ({ { + const canSave = hasSaveActionsCapability(capabilities); + const [addModalVisible, setAddModalVisibility] = useState(false); const [activeActionItem, setActiveActionItem] = useState( undefined @@ -254,6 +257,7 @@ export const ActionForm = ({ /> } labelAppend={ + canSave && actionTypesIndex && actionTypesIndex[actionConnector.actionTypeId].enabledInConfig ? ( ) } - actions={[ - { - setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index }); - setAddModalVisibility(true); - }} - > - - , - ]} + actions={ + canSave + ? [ + { + setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index }); + setAddModalVisibility(true); + }} + > + + , + ] + : [] + } /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 2dd1f83749372..861400a3d968d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -117,6 +117,7 @@ export const ConnectorAddFlyout = ({ actionTypeRegistry={actionTypeRegistry} http={http} docLinks={docLinks} + capabilities={capabilities} /> ); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index 1d19f436950c7..2a149df95ad67 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -164,6 +164,7 @@ export const ConnectorAddModal = ({ actionTypeRegistry={actionTypeRegistry} docLinks={docLinks} http={http} + capabilities={capabilities} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index cbbbbfaea7ea3..bc2812d7a0699 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -185,6 +185,7 @@ export const ConnectorEditFlyout = ({ actionTypeRegistry={actionTypeRegistry} http={http} docLinks={docLinks} + capabilities={capabilities} /> ) : ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index 5d52896cc628f..f92d0d4642b3e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -324,19 +324,21 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { />
, ], - toolsRight: [ - setAddFlyoutVisibility(true)} - > - - , - ], + toolsRight: canSave + ? [ + setAddFlyoutVisibility(true)} + > + + , + ] + : [], }} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 314219de4048d..52010df1bc35b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -32,6 +32,7 @@ export interface ActionConnectorFieldsProps { errors: IErrorObject; docLinks: DocLinksStart; http?: HttpSetup; + readOnly: boolean; } export interface ActionParamsProps { From d7f0b27a2ca8f25fb43d96b8871b7cdbed305147 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 3 Jul 2020 19:39:34 +0100 Subject: [PATCH 093/126] correct capabilities check --- .../sections/alert_details/components/alert_details.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index d6a6af146730a..5d619f728a191 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -28,7 +28,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useAppDependencies } from '../../../app_context'; -import { hasSaveAlertsCapability, hasExecuteActionsCapability } from '../../../lib/capabilities'; +import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; import { Alert, AlertType, ActionType } from '../../../../types'; import { ComponentOpts as BulkOperationsComponentOpts, @@ -40,7 +40,6 @@ import { PLUGIN } from '../../../constants/plugin'; import { AlertEdit } from '../../alert_form'; import { AlertsContextProvider } from '../../../context/alerts_context'; import { routeToAlertDetails } from '../../../constants'; -import { hasAllPrivilege } from '../../../lib/capabilities'; type AlertDetailsProps = { alert: Alert; @@ -72,7 +71,7 @@ export const AlertDetails: React.FunctionComponent = ({ dataPlugin, } = useAppDependencies(); - const canSaveAlert = hasSaveAlertsCapability(capabilities); + const canSaveAlert = hasAllPrivilege(alert, alertType); const canExecuteActions = hasExecuteActionsCapability(capabilities); const actionTypesByTypeId = keyBy(actionTypes, 'id'); const hasEditButton = From d78b9187dbc20674c29f5bbc81d11d4a850fc157 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 6 Jul 2020 09:44:59 +0100 Subject: [PATCH 094/126] fixed type errors --- .../builtin_action_types/email/email_connector.test.tsx | 1 + .../es_index/es_index_connector.test.tsx | 1 + .../pagerduty/pagerduty_connectors.test.tsx | 1 + .../builtin_action_types/slack/slack_connectors.test.tsx | 1 + .../webhook/webhook_connectors.test.tsx | 1 + .../action_connector_form/action_connector_form.test.tsx | 7 +++++++ 6 files changed, 12 insertions(+) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx index 8ee953c00795e..6856e553ab400 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx @@ -30,6 +30,7 @@ describe('EmailActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + readOnly={false} /> ); expect(wrapper.find('[data-test-subj="emailFromInput"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx index 4cb397927b53e..f5f14cb041335 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx @@ -88,6 +88,7 @@ describe('IndexActionConnectorFields renders', () => { editActionSecrets={() => {}} http={deps!.http} docLinks={deps!.docLinks} + readOnly={false} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx index 86730c0ab4ac7..53e68e6453690 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx @@ -34,6 +34,7 @@ describe('PagerDutyActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} docLinks={deps!.docLinks} + readOnly={false} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx index bd905c1c95650..5bc778830b6e6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx @@ -31,6 +31,7 @@ describe('SlackActionFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} docLinks={deps!.docLinks} + readOnly={false} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx index 3b7865e59b9e6..4b0465743fbd4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx @@ -33,6 +33,7 @@ describe('WebhookActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + readOnly={false} /> ); expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx index 17a1d929a0def..b7c9865cbd9d0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx @@ -15,10 +15,16 @@ describe('action_connector_form', () => { let deps: any; beforeAll(async () => { const mocks = coreMock.createSetup(); + const [ + { + application: { capabilities }, + }, + ] = await mocks.getStartServices(); deps = { http: mocks.http, actionTypeRegistry: actionTypeRegistry as any, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, + capabilities, }; }); @@ -56,6 +62,7 @@ describe('action_connector_form', () => { http={deps!.http} actionTypeRegistry={deps!.actionTypeRegistry} docLinks={deps!.docLinks} + capabilities={deps!.capabilities} /> ); } From a4f1a7d5d6ca6763795a9d3bcca6574631c40f5d Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 6 Jul 2020 10:38:03 +0100 Subject: [PATCH 095/126] fixed security unit test --- x-pack/plugins/security/server/plugin.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 64af6fc857273..ddedccf0578a2 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -88,6 +88,7 @@ describe('Security Plugin', () => { "version": "version:version", "versionNumber": "version", }, + "checkPrivilegesDynamicallyWithRequest": [Function], "checkPrivilegesWithRequest": [Function], "mode": Object { "useRbacForRequest": [Function], From 3cc2cb57f37413a3613526de55cfd652185de3c6 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 6 Jul 2020 10:54:18 +0100 Subject: [PATCH 096/126] fixed some missing typing --- x-pack/plugins/apm/server/feature.ts | 5 ----- .../alert_details/components/alert_details.test.tsx | 3 +++ .../sections/alerts_list/components/alerts_list.tsx | 6 +----- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index e65002780d0b4..e6e7ef5f25e43 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -61,11 +61,6 @@ export const APM_FEATURE = { 'alerting:delete', 'actions:delete', ], - catalogue: ['apm'], - savedObject: { - all: ['alert'], - read: [], - }, }, }, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 4c940f1d8d2e1..ccaa180de0edc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -644,6 +644,7 @@ describe('edit button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: 'alerting', + authorizedConsumers, }; expect( @@ -685,6 +686,7 @@ describe('edit button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: 'alerting', + authorizedConsumers, }; expect( @@ -719,6 +721,7 @@ describe('edit button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: 'alerting', + authorizedConsumers, }; expect( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 9a87c6e5055bc..4056cdaa02352 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -33,11 +33,7 @@ import { TypeFilter } from './type_filter'; import { ActionTypeFilter } from './action_type_filter'; import { loadAlerts, loadAlertTypes, deleteAlerts } from '../../../lib/alert_api'; import { loadActionTypes } from '../../../lib/action_connector_api'; -import { - hasDeleteAlertsCapability, - hasSaveAlertsCapability, - hasExecuteActionsCapability, -} from '../../../lib/capabilities'; +import { hasExecuteActionsCapability } from '../../../lib/capabilities'; import { routeToAlertDetails, DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation'; import { EmptyPrompt } from '../../../components/prompts/empty_prompt'; From 7f5099ca920d93552c876ac244adba6298589437 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 6 Jul 2020 12:36:37 +0100 Subject: [PATCH 097/126] show prompt if user has no privileges in actions form --- .../action_connector_form/action_form.tsx | 92 +++++++++++-------- 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 0be3e41696610..2af5506436e20 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -407,6 +407,16 @@ export const ActionForm = ({ : actionItem.actionTypeId; const actionTypeRegistered = actionTypeRegistry.get(actionItem.actionTypeId); if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null; + + const noConnectorsLabel = ( + + ); return ( - actionItem.id === emptyId) ? ( + {canSave ? ( + actionItem.id === emptyId) ? ( + noConnectorsLabel + ) : ( + + ) + } + actions={[ + { + setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index }); + setAddModalVisibility(true); + }} + > + + , + ]} + /> + ) : ( + +

- ) : ( - - ) - } - actions={ - canSave - ? [ - { - setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index }); - setAddModalVisibility(true); - }} - > - - , - ] - : [] - } - /> +

+
+ )}
From 76d28183a0b6bcb3f0b71df86296032c56965988 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 6 Jul 2020 12:39:38 +0100 Subject: [PATCH 098/126] added actions feature to features test --- x-pack/test/api_integration/apis/features/features/features.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index 11fb9b2de7199..117152e2d1872 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -97,6 +97,7 @@ export default function ({ getService }: FtrProviderContext) { 'visualize', 'dashboard', 'dev_tools', + 'actions', 'advancedSettings', 'indexPatterns', 'timelion', From 3fd2309c86892322be255539e4b978cb56bcdd52 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 6 Jul 2020 14:56:03 +0100 Subject: [PATCH 099/126] added missing SO privileges --- x-pack/plugins/alerting_builtins/server/feature.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/alerting_builtins/server/feature.ts b/x-pack/plugins/alerting_builtins/server/feature.ts index 3b0a98d3e0637..dd2fe2552ee55 100644 --- a/x-pack/plugins/alerting_builtins/server/feature.ts +++ b/x-pack/plugins/alerting_builtins/server/feature.ts @@ -25,10 +25,10 @@ export const BUILT_IN_ALERTS_FEATURE = { read: [], }, savedObject: { - all: ['action'], + all: ['action', 'action_task_params'], read: [], }, - api: ['actions-read'], + api: ['actions-read', 'actions-all'], ui: ['alerting:show', 'actions:show', 'actions:save', 'actions:delete'], }, read: { @@ -39,7 +39,7 @@ export const BUILT_IN_ALERTS_FEATURE = { read: [IndexThreshold], }, savedObject: { - all: [], + all: ['action_task_params'], read: ['action'], }, api: ['actions-read'], From da1f944077121a18259bd8ec299156943e78a685 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 7 Jul 2020 09:12:24 +0100 Subject: [PATCH 100/126] improved copy --- .../sections/action_connector_form/action_form.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 2af5506436e20..d2003d982e0b6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -411,7 +411,7 @@ export const ActionForm = ({ const noConnectorsLabel = ( Date: Tue, 7 Jul 2020 11:19:20 +0100 Subject: [PATCH 101/126] added readonly support to servicenow connector --- .../servicenow/servicenow_connectors.test.tsx | 2 ++ .../servicenow/servicenow_connectors.tsx | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx index 452d9c288926e..3727d80eb2d1f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx @@ -34,6 +34,7 @@ describe('ServiceNowActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} docLinks={deps!.docLinks} + readOnly={false} /> ); expect( @@ -72,6 +73,7 @@ describe('ServiceNowActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} docLinks={deps!.docLinks} + readOnly={false} /> ); expect(wrapper.find('[data-test-subj="case-servicenow-mappings"]').length > 0).toBeTruthy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx index a5c4849cb63d9..0b377d55f9681 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -23,7 +23,7 @@ import { FieldMapping } from './case_mappings/field_mapping'; const ServiceNowConnectorFields: React.FC> = ({ action, editActionSecrets, editActionConfig, errors, consumer }) => { +>> = ({ action, editActionSecrets, editActionConfig, errors, consumer, readOnly }) => { // TODO: remove incidentConfiguration later, when Case ServiceNow will move their fields to the level of action execution const { apiUrl, incidentConfiguration, isCaseOwned } = action.config; const mapping = incidentConfiguration ? incidentConfiguration.mapping : []; @@ -84,6 +84,7 @@ const ServiceNowConnectorFields: React.FC Date: Tue, 7 Jul 2020 12:40:35 +0100 Subject: [PATCH 102/126] removed unused variable in i18n --- .../application/sections/action_connector_form/action_form.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 651228e4deed1..a1c02e196fbe4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -520,9 +520,6 @@ export const ActionForm = ({

From 5e2f0ddf85c1630d7e71f7e7930e092335ffe9e3 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 8 Jul 2020 09:59:15 +0100 Subject: [PATCH 103/126] added bulk audit log api for alerts --- examples/alerting_example/server/plugin.ts | 11 ++- .../alerting_builtins/server/feature.ts | 1 - .../alerts_authorization.test.ts | 97 +++++++++++++++++-- .../authorization/alerts_authorization.ts | 88 ++++++++++------- .../server/authorization/audit_logger.mock.ts | 1 + .../server/authorization/audit_logger.test.ts | 84 ++++++++++++++++ .../server/authorization/audit_logger.ts | 23 +++++ 7 files changed, 262 insertions(+), 43 deletions(-) diff --git a/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts index 2bd742fc58bcc..b1842b190a5a6 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/examples/alerting_example/server/plugin.ts @@ -53,7 +53,14 @@ export class AlertingExamplePlugin implements Plugin { name: 'myAppAlertType', producer: 'myApp', }; - const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType]); + const mySecondAppAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'mySecondAppAlertType', + name: 'mySecondAppAlertType', + producer: 'myApp', + }; + const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]); test('omits filter when there is no authorization api', async () => { const alertAuthorization = new AlertsAuthorization({ @@ -533,7 +541,7 @@ describe('getFindAuthorizationFilter', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); expect((await alertAuthorization.getFindAuthorizationFilter()).filter).toMatchInlineSnapshot( - `"((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:myOtherAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)))"` + `"((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:myOtherAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:mySecondAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)))"` ); expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); @@ -635,19 +643,94 @@ describe('getFindAuthorizationFilter', () => { }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); + const { + ensureAlertTypeIsAuthorized, + logSuccessfulAuthorization, + } = await alertAuthorization.getFindAuthorizationFilter(); expect(() => { ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); }).not.toThrow(); - expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + }); + + test('creates an `logSuccessfulAuthorization` function which logs every authorized type', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('mySecondAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('mySecondAppAlertType', 'myOtherApp', 'find'), + authorized: true, + }, + ], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + auditLogger, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + const { + ensureAlertTypeIsAuthorized, + logSuccessfulAuthorization, + } = await alertAuthorization.getFindAuthorizationFilter(); + expect(() => { + ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); + ensureAlertTypeIsAuthorized('mySecondAppAlertType', 'myOtherApp'); + ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); + }).not.toThrow(); + + expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); + + logSuccessfulAuthorization(); + + expect(auditLogger.alertsBulkAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.alertsBulkAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ "some-user", - "myAppAlertType", + Array [ + Array [ + "myAppAlertType", + "myOtherApp", + ], + Array [ + "mySecondAppAlertType", + "myOtherApp", + ], + ], 0, - "myOtherApp", "find", ] `); diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 539e0b0d44a8c..6736c27e687e2 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -56,10 +56,11 @@ export interface ConstructorOptions { export class AlertsAuthorization { private readonly alertTypeRegistry: AlertTypeRegistry; - private readonly features: FeaturesPluginStart; private readonly request: KibanaRequest; private readonly authorization?: SecurityPluginSetup['authz']; private readonly auditLogger: AlertsAuthorizationAuditLogger; + private readonly featuresIds: string[]; + private readonly allPossibleConsumers: AuthorizedConsumers; constructor({ alertTypeRegistry, @@ -70,9 +71,31 @@ export class AlertsAuthorization { }: ConstructorOptions) { this.request = request; this.authorization = authorization; - this.features = features; this.alertTypeRegistry = alertTypeRegistry; this.auditLogger = auditLogger; + + this.featuresIds = features + .getFeatures() + // ignore features which don't grant privileges to alerting + .filter(({ privileges, subFeatures }) => { + return ( + hasAnyAlertingPrivileges(privileges?.all) || + hasAnyAlertingPrivileges(privileges?.read) || + subFeatures.some((subFeature) => + subFeature.privilegeGroups.some((privilegeGroup) => + privilegeGroup.privileges.some((subPrivileges) => + hasAnyAlertingPrivileges(subPrivileges) + ) + ) + ) + ); + }) + .map((feature) => feature.id); + + this.allPossibleConsumers = asAuthorizedConsumers([ALERTS_FEATURE_ID, ...this.featuresIds], { + read: true, + all: true, + }); } public async ensureAuthorized( @@ -147,6 +170,7 @@ export class AlertsAuthorization { public async getFindAuthorizationFilter(): Promise<{ filter?: string; ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => void; + logSuccessfulAuthorization: () => void; }> { if (this.authorization) { const { @@ -171,6 +195,7 @@ export class AlertsAuthorization { }, []) ); + const authorizedEntries: Map> = new Map(); return { filter: `(${this.asFiltersByAlertTypeAndConsumer(authorizedAlertTypes).join(' or ')})`, ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => { @@ -185,11 +210,27 @@ export class AlertsAuthorization { ) ); } else { - this.auditLogger.alertsAuthorizationSuccess( + if (authorizedEntries.has(alertTypeId)) { + authorizedEntries.get(alertTypeId).add(consumer); + } else { + authorizedEntries.set(alertTypeId, new Set([consumer])); + } + } + }, + logSuccessfulAuthorization: () => { + if (authorizedEntries.size) { + this.auditLogger.alertsBulkAuthorizationSuccess( username!, - alertTypeId, + [...authorizedEntries.entries()].reduce( + (authorizedPairs, [alertTypeId, consumers]) => { + for (const consumer of consumers) { + authorizedPairs.push([alertTypeId, consumer]); + } + return authorizedPairs; + }, + [] + ), ScopeType.Consumer, - consumer, 'find' ); } @@ -198,6 +239,7 @@ export class AlertsAuthorization { } return { ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => {}, + logSuccessfulAuthorization: () => {}, }; } @@ -220,33 +262,13 @@ export class AlertsAuthorization { hasAllRequested: boolean; authorizedAlertTypes: Set; }> { - const featuresIds = this.features - .getFeatures() - // ignore features which don't grant privileges to alerting - .filter(({ privileges, subFeatures }) => { - return ( - hasAnyAlertingPrivileges(privileges?.all) || - hasAnyAlertingPrivileges(privileges?.read) || - subFeatures.some((subFeature) => - subFeature.privilegeGroups.some((privilegeGroup) => - privilegeGroup.privileges.some((subPrivileges) => - hasAnyAlertingPrivileges(subPrivileges) - ) - ) - ) - ); - }) - .map((feature) => feature.id); - - const allPossibleConsumers: AuthorizedConsumers = asAuthorizedConsumers( - [ALERTS_FEATURE_ID, ...featuresIds], - { read: true, all: true } - ); - if (!this.authorization) { return { hasAllRequested: true, - authorizedAlertTypes: this.augmentWithAuthorizedConsumers(alertTypes, allPossibleConsumers), + authorizedAlertTypes: this.augmentWithAuthorizedConsumers( + alertTypes, + this.allPossibleConsumers + ), }; } else { const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( @@ -254,7 +276,7 @@ export class AlertsAuthorization { ); // add an empty `authorizedConsumers` array on each alertType - const alertTypesWithAutherization = this.augmentWithAuthorizedConsumers(alertTypes, {}); + const alertTypesWithAuthorization = this.augmentWithAuthorizedConsumers(alertTypes, {}); // map from privilege to alertType which we can refer back to when analyzing the result // of checkPrivileges @@ -264,8 +286,8 @@ export class AlertsAuthorization { >(); // as we can't ask ES for the user's individual privileges we need to ask for each feature // and alertType in the system whether this user has this privilege - for (const alertType of alertTypesWithAutherization) { - for (const feature of featuresIds) { + for (const alertType of alertTypesWithAuthorization) { + for (const feature of this.featuresIds) { for (const operation of operations) { privilegeToAlertType.set( this.authorization!.actions.alerting.get(alertType.id, feature, operation), @@ -289,7 +311,7 @@ export class AlertsAuthorization { hasAllRequested, authorizedAlertTypes: hasAllRequested ? // has access to all features - this.augmentWithAuthorizedConsumers(alertTypes, allPossibleConsumers) + this.augmentWithAuthorizedConsumers(alertTypes, this.allPossibleConsumers) : // only has some of the required privileges privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { if (authorized && privilegeToAlertType.has(privilege)) { diff --git a/x-pack/plugins/alerts/server/authorization/audit_logger.mock.ts b/x-pack/plugins/alerts/server/authorization/audit_logger.mock.ts index 6b29eedac030b..ca6a35b24bcac 100644 --- a/x-pack/plugins/alerts/server/authorization/audit_logger.mock.ts +++ b/x-pack/plugins/alerts/server/authorization/audit_logger.mock.ts @@ -12,6 +12,7 @@ const createAlertsAuthorizationAuditLoggerMock = () => { alertsAuthorizationFailure: jest.fn(), alertsUnscopedAuthorizationFailure: jest.fn(), alertsAuthorizationSuccess: jest.fn(), + alertsBulkAuthorizationSuccess: jest.fn(), } as unknown) as jest.Mocked; return mocked; }; diff --git a/x-pack/plugins/alerts/server/authorization/audit_logger.test.ts b/x-pack/plugins/alerts/server/authorization/audit_logger.test.ts index de302cb936779..367bf7a2d2c95 100644 --- a/x-pack/plugins/alerts/server/authorization/audit_logger.test.ts +++ b/x-pack/plugins/alerts/server/authorization/audit_logger.test.ts @@ -160,6 +160,90 @@ describe(`#alertsAuthorizationFailure`, () => { }); }); +describe(`#alertsBulkAuthorizationSuccess`, () => { + test('logs auth success with consumer scope', () => { + const auditLogger = createMockAuditLogger(); + const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); + const username = 'foo-user'; + const scopeType = ScopeType.Consumer; + const authorizedEntries = [ + ['alert-type-id', 'myApp'], + ['other-alert-type-id', 'myOtherApp'], + ]; + const operation = 'create'; + + alertsAuditLogger.alertsBulkAuthorizationSuccess( + username, + authorizedEntries, + scopeType, + operation + ); + + expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alerts_authorization_success", + "foo-user Authorized to create: \\"alert-type-id\\" alert for \\"myApp\\", \\"other-alert-type-id\\" alert for \\"myOtherApp\\"", + Object { + "authorizedEntries": Array [ + Array [ + "alert-type-id", + "myApp", + ], + Array [ + "other-alert-type-id", + "myOtherApp", + ], + ], + "operation": "create", + "scopeType": 0, + "username": "foo-user", + }, + ] + `); + }); + + test('logs auth success with producer scope', () => { + const auditLogger = createMockAuditLogger(); + const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); + const username = 'foo-user'; + const scopeType = ScopeType.Producer; + const authorizedEntries = [ + ['alert-type-id', 'myApp'], + ['other-alert-type-id', 'myOtherApp'], + ]; + const operation = 'create'; + + alertsAuditLogger.alertsBulkAuthorizationSuccess( + username, + authorizedEntries, + scopeType, + operation + ); + + expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alerts_authorization_success", + "foo-user Authorized to create: \\"alert-type-id\\" alert by \\"myApp\\", \\"other-alert-type-id\\" alert by \\"myOtherApp\\"", + Object { + "authorizedEntries": Array [ + Array [ + "alert-type-id", + "myApp", + ], + Array [ + "other-alert-type-id", + "myOtherApp", + ], + ], + "operation": "create", + "scopeType": 1, + "username": "foo-user", + }, + ] + `); + }); +}); + describe(`#savedObjectsAuthorizationSuccess`, () => { test('logs auth success with consumer scope', () => { const auditLogger = createMockAuditLogger(); diff --git a/x-pack/plugins/alerts/server/authorization/audit_logger.ts b/x-pack/plugins/alerts/server/authorization/audit_logger.ts index 5d563bbd6db8d..f930da2ce428c 100644 --- a/x-pack/plugins/alerts/server/authorization/audit_logger.ts +++ b/x-pack/plugins/alerts/server/authorization/audit_logger.ts @@ -91,4 +91,27 @@ export class AlertsAuthorizationAuditLogger { }); return message; } + + public alertsBulkAuthorizationSuccess( + username: string, + authorizedEntries: Array<[string, string]>, + scopeType: ScopeType, + operation: string + ): string { + const message = `${AuthorizationResult.Authorized} to ${operation}: ${authorizedEntries + .map( + ([alertTypeId, scope]) => + `"${alertTypeId}" alert ${ + scopeType === ScopeType.Consumer ? `for "${scope}"` : `by "${scope}"` + }` + ) + .join(', ')}`; + this.auditLogger.log('alerts_authorization_success', `${username} ${message}`, { + username, + scopeType, + authorizedEntries, + operation, + }); + return message; + } } From abbb2c06e9269a7466110786b7cf3ef686b3b64f Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 8 Jul 2020 10:05:02 +0100 Subject: [PATCH 104/126] fixed bulk audit log api for alerts --- .../alerts/server/alerts_client.test.ts | 5 +++ x-pack/plugins/alerts/server/alerts_client.ts | 23 ++++++++----- .../alerts_authorization.test.ts | 5 +-- .../authorization/alerts_authorization.ts | 4 +-- .../server/authorization/audit_logger.test.ts | 4 +-- x-pack/plugins/alerts/server/plugin.test.ts | 34 +++++++++++++++++-- 6 files changed, 55 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 5194d3b6b1fb8..95ee3680de143 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -2150,6 +2150,7 @@ describe('find()', () => { beforeEach(() => { authorization.getFindAuthorizationFilter.mockResolvedValue({ ensureAlertTypeIsAuthorized() {}, + logSuccessfulAuthorization() {}, }); unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ total: 1, @@ -2254,6 +2255,7 @@ describe('find()', () => { filter: '((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myOtherApp))', ensureAlertTypeIsAuthorized() {}, + logSuccessfulAuthorization() {}, }); const alertsClient = new AlertsClient(alertsClientParams); @@ -2276,9 +2278,11 @@ describe('find()', () => { test('ensures authorization even when the fields required to authorize are omitted from the find', async () => { const ensureAlertTypeIsAuthorized = jest.fn(); + const logSuccessfulAuthorization = jest.fn(); authorization.getFindAuthorizationFilter.mockResolvedValue({ filter: '', ensureAlertTypeIsAuthorized, + logSuccessfulAuthorization, }); unsecuredSavedObjectsClient.find.mockReset(); @@ -2325,6 +2329,7 @@ describe('find()', () => { type: 'alert', }); expect(ensureAlertTypeIsAuthorized).toHaveBeenCalledWith('myType', 'myApp'); + expect(logSuccessfulAuthorization).toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index cfa3fe6be2bcb..06af01e30aff1 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -268,6 +268,7 @@ export class AlertsClient { const { filter: authorizationFilter, ensureAlertTypeIsAuthorized, + logSuccessfulAuthorization, } = await this.authorization.getFindAuthorizationFilter(); if (authorizationFilter) { @@ -287,19 +288,23 @@ export class AlertsClient { type: 'alert', }); + const authorizedData = data.map(({ id, attributes, updated_at, references }) => { + ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer); + return this.getAlertFromRaw( + id, + fields ? (pick(attributes, fields) as RawAlert) : attributes, + updated_at, + references + ); + }); + + logSuccessfulAuthorization(); + return { page, perPage, total, - data: data.map(({ id, attributes, updated_at, references }) => { - ensureAlertTypeIsAuthorized(attributes.alertTypeId, attributes.consumer); - return this.getAlertFromRaw( - id, - fields ? (pick(attributes, fields) as RawAlert) : attributes, - updated_at, - references - ); - }), + data: authorizedData, }; } diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 44d9351657a42..442ee215a304b 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -643,10 +643,7 @@ describe('getFindAuthorizationFilter', () => { }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - const { - ensureAlertTypeIsAuthorized, - logSuccessfulAuthorization, - } = await alertAuthorization.getFindAuthorizationFilter(); + const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); expect(() => { ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); }).not.toThrow(); diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 6736c27e687e2..98cbed061513c 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -211,7 +211,7 @@ export class AlertsAuthorization { ); } else { if (authorizedEntries.has(alertTypeId)) { - authorizedEntries.get(alertTypeId).add(consumer); + authorizedEntries.get(alertTypeId)!.add(consumer); } else { authorizedEntries.set(alertTypeId, new Set([consumer])); } @@ -221,7 +221,7 @@ export class AlertsAuthorization { if (authorizedEntries.size) { this.auditLogger.alertsBulkAuthorizationSuccess( username!, - [...authorizedEntries.entries()].reduce( + [...authorizedEntries.entries()].reduce>( (authorizedPairs, [alertTypeId, consumers]) => { for (const consumer of consumers) { authorizedPairs.push([alertTypeId, consumer]); diff --git a/x-pack/plugins/alerts/server/authorization/audit_logger.test.ts b/x-pack/plugins/alerts/server/authorization/audit_logger.test.ts index 367bf7a2d2c95..40973a3a67a51 100644 --- a/x-pack/plugins/alerts/server/authorization/audit_logger.test.ts +++ b/x-pack/plugins/alerts/server/authorization/audit_logger.test.ts @@ -166,7 +166,7 @@ describe(`#alertsBulkAuthorizationSuccess`, () => { const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); const username = 'foo-user'; const scopeType = ScopeType.Consumer; - const authorizedEntries = [ + const authorizedEntries: Array<[string, string]> = [ ['alert-type-id', 'myApp'], ['other-alert-type-id', 'myOtherApp'], ]; @@ -207,7 +207,7 @@ describe(`#alertsBulkAuthorizationSuccess`, () => { const alertsAuditLogger = new AlertsAuthorizationAuditLogger(auditLogger); const username = 'foo-user'; const scopeType = ScopeType.Producer; - const authorizedEntries = [ + const authorizedEntries: Array<[string, string]> = [ ['alert-type-id', 'myApp'], ['other-alert-type-id', 'myOtherApp'], ]; diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index 60cb8adee7084..27dc1dc53d651 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -12,6 +12,7 @@ import { taskManagerMock } from '../../task_manager/server/mocks'; import { eventLogServiceMock } from '../../event_log/server/event_log_service.mock'; import { KibanaRequest, CoreSetup } from 'kibana/server'; import { featuresPluginMock } from '../../features/server/mocks'; +import { Feature } from '../../features/server'; describe('Alerting Plugin', () => { describe('setup()', () => { @@ -34,7 +35,6 @@ describe('Alerting Plugin', () => { encryptedSavedObjects: encryptedSavedObjectsSetup, taskManager: taskManagerMock.createSetup(), eventLog: eventLogServiceMock.create(), - features: featuresPluginMock.createSetup(), } as unknown) as AlertingPluginsSetup ); @@ -73,7 +73,6 @@ describe('Alerting Plugin', () => { encryptedSavedObjects: encryptedSavedObjectsSetup, taskManager: taskManagerMock.createSetup(), eventLog: eventLogServiceMock.create(), - features: featuresPluginMock.createSetup(), } as unknown) as AlertingPluginsSetup ); @@ -85,6 +84,7 @@ describe('Alerting Plugin', () => { getActionsClientWithRequest: jest.fn(), }, encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), + features: mockFeatures(), } as unknown) as AlertingPluginsStart ); @@ -118,7 +118,6 @@ describe('Alerting Plugin', () => { encryptedSavedObjects: encryptedSavedObjectsSetup, taskManager: taskManagerMock.createSetup(), eventLog: eventLogServiceMock.create(), - features: featuresPluginMock.createSetup(), } as unknown) as AlertingPluginsSetup ); @@ -131,6 +130,7 @@ describe('Alerting Plugin', () => { }, spaces: () => null, encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), + features: mockFeatures(), } as unknown) as AlertingPluginsStart ); @@ -154,3 +154,31 @@ describe('Alerting Plugin', () => { }); }); }); + +function mockFeatures() { + const features = featuresPluginMock.createSetup(); + features.getFeatures.mockReturnValue([ + new Feature({ + id: 'appName', + name: 'appName', + app: [], + privileges: { + all: { + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + read: { + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, + }), + ]); + return features; +} From 8d8ea5462f3ec5fa09479df043d5dac938d6f5a8 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 8 Jul 2020 10:10:18 +0100 Subject: [PATCH 105/126] removed actio nSO privileges from builtin alert types --- x-pack/plugins/alerting_builtins/server/feature.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/alerting_builtins/server/feature.ts b/x-pack/plugins/alerting_builtins/server/feature.ts index b1b8e518d42c0..ccd711f51061c 100644 --- a/x-pack/plugins/alerting_builtins/server/feature.ts +++ b/x-pack/plugins/alerting_builtins/server/feature.ts @@ -24,7 +24,7 @@ export const BUILT_IN_ALERTS_FEATURE = { read: [], }, savedObject: { - all: ['action', 'action_task_params'], + all: [], read: [], }, api: ['actions-read', 'actions-all'], @@ -38,8 +38,8 @@ export const BUILT_IN_ALERTS_FEATURE = { read: [IndexThreshold], }, savedObject: { - all: ['action_task_params'], - read: ['action'], + all: [], + read: [], }, api: ['actions-read'], ui: ['alerting:show', 'actions:show'], From 98a00b619f6de976125d71d9f4e64f4f7413d773 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 9 Jul 2020 08:13:19 +0100 Subject: [PATCH 106/126] removed ui capabilities that are no longer in use --- examples/alerting_example/server/plugin.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts index b1842b190a5a6..1d7ad37a46551 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/examples/alerting_example/server/plugin.ts @@ -53,14 +53,7 @@ export class AlertingExamplePlugin implements Plugin Date: Thu, 9 Jul 2020 08:14:42 +0100 Subject: [PATCH 107/126] removed ui and api capabilities from built-in alerts that are no longer in use --- x-pack/plugins/alerting_builtins/server/feature.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/alerting_builtins/server/feature.ts b/x-pack/plugins/alerting_builtins/server/feature.ts index ccd711f51061c..4ebd3d929165a 100644 --- a/x-pack/plugins/alerting_builtins/server/feature.ts +++ b/x-pack/plugins/alerting_builtins/server/feature.ts @@ -27,8 +27,8 @@ export const BUILT_IN_ALERTS_FEATURE = { all: [], read: [], }, - api: ['actions-read', 'actions-all'], - ui: ['alerting:show', 'actions:show', 'actions:save', 'actions:delete'], + api: [], + ui: ['alerting:show'], }, read: { app: [], @@ -41,8 +41,8 @@ export const BUILT_IN_ALERTS_FEATURE = { all: [], read: [], }, - api: ['actions-read'], - ui: ['alerting:show', 'actions:show'], + api: [], + ui: ['alerting:show'], }, }, }; From 053ca76ead8d2bfd8b9ab04925e247f801d8bc58 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 9 Jul 2020 08:19:01 +0100 Subject: [PATCH 108/126] removed ui capabilities from solutions that are no longer in use --- x-pack/plugins/apm/server/feature.ts | 21 ++---------------- x-pack/plugins/infra/server/features.ts | 22 ++----------------- .../security_solution/server/plugin.ts | 21 ++---------------- x-pack/plugins/uptime/server/kibana.index.ts | 19 ++-------------- 4 files changed, 8 insertions(+), 75 deletions(-) diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index e6e7ef5f25e43..38d4e92c72a50 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -30,16 +30,7 @@ export const APM_FEATURE = { alerting: { all: Object.values(AlertType), }, - ui: [ - 'show', - 'save', - 'alerting:show', - 'actions:show', - 'alerting:save', - 'actions:save', - 'alerting:delete', - 'actions:delete', - ], + ui: ['show', 'save', 'alerting:show', 'alerting:save'], }, read: { app: ['apm', 'kibana'], @@ -52,15 +43,7 @@ export const APM_FEATURE = { alerting: { all: Object.values(AlertType), }, - ui: [ - 'show', - 'alerting:show', - 'actions:show', - 'alerting:save', - 'actions:save', - 'alerting:delete', - 'actions:delete', - ], + ui: ['show', 'alerting:show', 'alerting:save'], }, }, }; diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index 0e09d07149910..9cd216f6066d2 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -31,17 +31,7 @@ export const METRICS_FEATURE = { alerting: { all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], }, - ui: [ - 'show', - 'configureSource', - 'save', - 'alerting:show', - 'actions:show', - 'alerting:save', - 'actions:save', - 'alerting:delete', - 'actions:delete', - ], + ui: ['show', 'configureSource', 'save', 'alerting:show'], }, read: { app: ['infra', 'kibana'], @@ -54,15 +44,7 @@ export const METRICS_FEATURE = { alerting: { all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], }, - ui: [ - 'show', - 'alerting:show', - 'actions:show', - 'alerting:save', - 'actions:save', - 'alerting:delete', - 'actions:delete', - ], + ui: ['show', 'alerting:show'], }, }, }; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 7801e558c7ac7..1772829178318 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -176,16 +176,7 @@ export class Plugin implements IPlugin Date: Thu, 9 Jul 2020 08:44:34 +0100 Subject: [PATCH 109/126] improved "no permission" call out in UI --- .../components/actions_connectors_list.tsx | 26 ++++++++++---- .../alerts_list/components/alerts_list.tsx | 34 ++++++++++++++++--- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index 52f3026bca623..c1939bf6fa07a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -17,6 +17,7 @@ import { EuiBetaBadge, EuiToolTip, EuiButtonIcon, + EuiEmptyPrompt, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -352,12 +353,25 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { ); const noPermissionPrompt = ( -

- -

+ + + + } + body={ +

+ +

+ } + /> ); return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 4056cdaa02352..8cb7afbda0e70 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -18,6 +18,7 @@ import { EuiSpacer, EuiLink, EuiLoadingSpinner, + EuiEmptyPrompt, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; @@ -247,6 +248,9 @@ export const AlertsList: React.FunctionComponent = () => { ]; const authorizedAlertTypes = [...alertTypesState.data.values()]; + const authorizedToCreateAnyAlerts = authorizedAlertTypes.some( + (alertType) => alertType.authorizedConsumers[ALERTS_FEATURE_ID]?.all + ); const toolsRight = [ { />, ]; - if ( - authorizedAlertTypes.some((alertType) => alertType.authorizedConsumers[ALERTS_FEATURE_ID]?.all) - ) { + if (authorizedToCreateAnyAlerts) { toolsRight.push( { - ) : ( + ) : authorizedToCreateAnyAlerts ? ( setAlertFlyoutVisibility(true)} /> + ) : ( + noPermissionPrompt )} { ); }; +const noPermissionPrompt = ( + + + + } + body={ +

+ +

+ } + /> +); + function filterAlertsById(alerts: Alert[], ids: string[]): Alert[] { return alerts.filter((alert) => ids.includes(alert.id)); } From acc5f558563efa665eb84a457b763298f5ab68e8 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 9 Jul 2020 12:33:40 +0100 Subject: [PATCH 110/126] ensure user has authrization to actions when an alert has actions --- x-pack/plugins/actions/server/index.ts | 2 + x-pack/plugins/actions/server/mocks.ts | 5 + x-pack/plugins/actions/server/plugin.ts | 35 +-- .../alerts/server/alerts_client.test.ts | 216 +++++++++++++++++- x-pack/plugins/alerts/server/alerts_client.ts | 34 ++- .../server/alerts_client_factory.test.ts | 15 +- .../alerts/server/alerts_client_factory.ts | 1 + x-pack/plugins/alerts/server/plugin.test.ts | 2 + .../public/app/home/index.tsx | 2 +- .../security_and_spaces/scenarios.ts | 42 +++- .../tests/actions/create.ts | 5 + .../tests/actions/delete.ts | 4 + .../tests/actions/execute.ts | 7 + .../security_and_spaces/tests/actions/get.ts | 3 + .../tests/actions/get_all.ts | 3 + .../tests/actions/list_action_types.ts | 1 + .../tests/actions/update.ts | 7 + .../tests/alerting/alerts.ts | 81 +++++++ .../tests/alerting/create.ts | 17 ++ .../tests/alerting/delete.ts | 6 + .../tests/alerting/disable.ts | 39 +++- .../tests/alerting/enable.ts | 37 ++- .../tests/alerting/find.ts | 5 + .../security_and_spaces/tests/alerting/get.ts | 6 + .../tests/alerting/get_alert_state.ts | 4 + .../tests/alerting/list_alert_types.ts | 1 + .../tests/alerting/mute_all.ts | 35 ++- .../tests/alerting/mute_instance.ts | 36 ++- .../tests/alerting/unmute_all.ts | 35 ++- .../tests/alerting/unmute_instance.ts | 35 ++- .../tests/alerting/update.ts | 45 +++- .../tests/alerting/update_api_key.ts | 36 ++- 32 files changed, 772 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 88553c314112f..fef70c3a48455 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -8,8 +8,10 @@ import { PluginInitializerContext } from '../../../../src/core/server'; import { ActionsPlugin } from './plugin'; import { configSchema } from './config'; import { ActionsClient as ActionsClientClass } from './actions_client'; +import { ActionsAuthorization as ActionsAuthorizationClass } from './authorization/actions_authorization'; export type ActionsClient = PublicMethodsOf; +export type ActionsAuthorization = PublicMethodsOf; export { ActionsPlugin, diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index 87aa571ce6b8a..4baf453dcb564 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -11,7 +11,9 @@ import { elasticsearchServiceMock, savedObjectsClientMock, } from '../../../../src/core/server/mocks'; +import { actionsAuthorizationMock } from './authorization/actions_authorization.mock'; +export { actionsAuthorizationMock }; export { actionsClientMock }; const createSetupMock = () => { @@ -26,6 +28,9 @@ const createStartMock = () => { isActionTypeEnabled: jest.fn(), isActionExecutable: jest.fn(), getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()), + getActionsAuthorizationWithRequest: jest + .fn() + .mockReturnValue(actionsAuthorizationMock.create()), preconfiguredActions: [], }; return mock; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index e1606092cd14e..798be5f991add 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -77,6 +77,7 @@ export interface PluginStartContract { isActionTypeEnabled(id: string): boolean; isActionExecutable(actionId: string, actionTypeId: string): boolean; getActionsClientWithRequest(request: KibanaRequest): Promise>; + getActionsAuthorizationWithRequest(request: KibanaRequest): PublicMethodsOf; preconfiguredActions: PreConfiguredAction[]; } @@ -241,7 +242,7 @@ export class ActionsPlugin implements Plugin, Plugi kibanaIndex, isESOUsingEphemeralEncryptionKey, preconfiguredActions, - security, + instantiateAuthorization, } = this; const encryptedSavedObjectsClient = plugins.encryptedSavedObjects.getClient({ @@ -288,7 +289,9 @@ export class ActionsPlugin implements Plugin, Plugi isActionExecutable: (actionId: string, actionTypeId: string) => { return this.actionTypeRegistry!.isActionExecutable(actionId, actionTypeId); }, - // Ability to get an actions client from legacy code + getActionsAuthorizationWithRequest(request: KibanaRequest) { + return instantiateAuthorization(request); + }, async getActionsClientWithRequest(request: KibanaRequest) { if (isESOUsingEphemeralEncryptionKey === true) { throw new Error( @@ -302,13 +305,7 @@ export class ActionsPlugin implements Plugin, Plugi scopedClusterClient: core.elasticsearch.legacy.client.asScoped(request), preconfiguredActions, request, - authorization: new ActionsAuthorization({ - request, - authorization: security?.authz, - auditLogger: new ActionsAuthorizationAuditLogger( - security?.audit.getLogger(ACTIONS_FEATURE.id) - ), - }), + authorization: instantiateAuthorization(request), actionExecutor: actionExecutor!, executionEnqueuer: createExecutionEnqueuerFunction({ taskManager: plugins.taskManager, @@ -322,6 +319,16 @@ export class ActionsPlugin implements Plugin, Plugi }; } + private instantiateAuthorization = (request: KibanaRequest) => { + return new ActionsAuthorization({ + request, + authorization: this.security?.authz, + auditLogger: new ActionsAuthorizationAuditLogger( + this.security?.audit.getLogger(ACTIONS_FEATURE.id) + ), + }); + }; + private getServicesFactory( getScopedClient: (request: KibanaRequest) => SavedObjectsClientContract, elasticsearch: ElasticsearchServiceStart @@ -344,7 +351,7 @@ export class ActionsPlugin implements Plugin, Plugi isESOUsingEphemeralEncryptionKey, preconfiguredActions, actionExecutor, - security, + instantiateAuthorization, } = this; return async function actionsRouteHandlerContext(context, request) { @@ -363,13 +370,7 @@ export class ActionsPlugin implements Plugin, Plugi scopedClusterClient: context.core.elasticsearch.legacy.client, preconfiguredActions, request, - authorization: new ActionsAuthorization({ - request, - authorization: security?.authz, - auditLogger: new ActionsAuthorizationAuditLogger( - security?.audit.getLogger(ACTIONS_FEATURE.id) - ), - }), + authorization: instantiateAuthorization(request), actionExecutor: actionExecutor!, executionEnqueuer: createExecutionEnqueuerFunction({ taskManager, diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 95ee3680de143..6de9e74df47c3 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -14,20 +14,23 @@ import { TaskStatus } from '../../task_manager/server'; import { IntervalSchedule } from './types'; import { resolvable } from './test_utils'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; -import { actionsClientMock } from '../../actions/server/mocks'; +import { actionsClientMock, actionsAuthorizationMock } from '../../actions/server/mocks'; import { AlertsAuthorization } from './authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../actions/server'; const taskManager = taskManagerMock.start(); const alertTypeRegistry = alertTypeRegistryMock.create(); const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); const alertsClientParams: jest.Mocked = { taskManager, alertTypeRegistry, unsecuredSavedObjectsClient, authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, spaceId: 'default', namespace: 'default', getUserName: jest.fn(), @@ -141,6 +144,7 @@ describe('create()', () => { id: '1', type: 'action', attributes: { + actions: [], actionTypeId: 'test', }, references: [], @@ -193,6 +197,7 @@ describe('create()', () => { id: '1', type: 'alert', attributes: { + actions: [], scheduledTaskId: 'task-123', }, references: [ @@ -284,6 +289,7 @@ describe('create()', () => { id: '1', type: 'alert', attributes: { + actions: [], scheduledTaskId: 'task-123', }, references: [ @@ -499,6 +505,7 @@ describe('create()', () => { id: '1', type: 'alert', attributes: { + actions: [], scheduledTaskId: 'task-123', }, references: [], @@ -555,6 +562,7 @@ describe('create()', () => { id: '1', type: 'action', attributes: { + actions: [], actionTypeId: 'test', }, references: [], @@ -667,6 +675,7 @@ describe('create()', () => { id: '1', type: 'action', attributes: { + actions: [], actionTypeId: 'test', }, references: [], @@ -688,6 +697,7 @@ describe('create()', () => { id: '1', type: 'action', attributes: { + actions: [], actionTypeId: 'test', }, references: [], @@ -744,6 +754,7 @@ describe('create()', () => { id: '1', type: 'action', attributes: { + actions: [], actionTypeId: 'test', }, references: [], @@ -812,6 +823,7 @@ describe('create()', () => { id: '1', type: 'action', attributes: { + actions: [], actionTypeId: 'test', }, references: [], @@ -863,6 +875,7 @@ describe('create()', () => { id: '1', type: 'alert', attributes: { + actions: [], scheduledTaskId: 'task-123', }, references: [ @@ -923,6 +936,7 @@ describe('create()', () => { id: '1', type: 'action', attributes: { + actions: [], actionTypeId: 'test', }, references: [], @@ -974,6 +988,7 @@ describe('create()', () => { id: '1', type: 'alert', attributes: { + actions: [], scheduledTaskId: 'task-123', }, references: [ @@ -1037,6 +1052,17 @@ describe('enable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], }, version: '123', references: [], @@ -1090,6 +1116,7 @@ describe('enable()', () => { await alertsClient.enable({ id: '1' }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'enable'); + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); test('throws when user is not authorised to enable this type of alert', async () => { @@ -1124,6 +1151,17 @@ describe('enable()', () => { updatedBy: 'elastic', apiKey: null, apiKeyOwner: null, + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], }, { version: '123', @@ -1198,6 +1236,17 @@ describe('enable()', () => { apiKey: Buffer.from('123:abc').toString('base64'), apiKeyOwner: 'elastic', updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], }, { version: '123', @@ -1284,6 +1333,17 @@ describe('disable()', () => { alertTypeId: 'myType', enabled: true, scheduledTaskId: 'task-123', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], }, version: '123', references: [], @@ -1307,6 +1367,7 @@ describe('disable()', () => { await alertsClient.disable({ id: '1' }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable'); + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); test('throws when user is not authorised to disable this type of alert', async () => { @@ -1340,6 +1401,17 @@ describe('disable()', () => { enabled: false, scheduledTaskId: null, updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], }, { version: '123', @@ -1369,6 +1441,17 @@ describe('disable()', () => { enabled: false, scheduledTaskId: null, updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], }, { version: '123', @@ -1383,6 +1466,7 @@ describe('disable()', () => { ...existingDecryptedAlert, attributes: { ...existingDecryptedAlert.attributes, + actions: [], enabled: false, }, }); @@ -1445,6 +1529,17 @@ describe('muteAll()', () => { id: '1', type: 'alert', attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], muteAll: false, }, references: [], @@ -1464,6 +1559,17 @@ describe('muteAll()', () => { id: '1', type: 'alert', attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], consumer: 'myApp', schedule: { interval: '10s' }, alertTypeId: 'myType', @@ -1483,6 +1589,7 @@ describe('muteAll()', () => { await alertsClient.muteAll({ id: '1' }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); test('throws when user is not authorised to muteAll this type of alert', async () => { @@ -1507,6 +1614,17 @@ describe('unmuteAll()', () => { id: '1', type: 'alert', attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], muteAll: true, }, references: [], @@ -1526,6 +1644,17 @@ describe('unmuteAll()', () => { id: '1', type: 'alert', attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], consumer: 'myApp', schedule: { interval: '10s' }, alertTypeId: 'myType', @@ -1545,6 +1674,7 @@ describe('unmuteAll()', () => { await alertsClient.unmuteAll({ id: '1' }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll'); + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); test('throws when user is not authorised to unmuteAll this type of alert', async () => { @@ -1569,6 +1699,7 @@ describe('muteInstance()', () => { id: '1', type: 'alert', attributes: { + actions: [], schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, @@ -1597,6 +1728,7 @@ describe('muteInstance()', () => { id: '1', type: 'alert', attributes: { + actions: [], schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, @@ -1616,6 +1748,7 @@ describe('muteInstance()', () => { id: '1', type: 'alert', attributes: { + actions: [], schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, @@ -1636,6 +1769,17 @@ describe('muteInstance()', () => { id: '1', type: 'alert', attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], schedule: { interval: '10s' }, alertTypeId: 'myType', consumer: 'myApp', @@ -1652,6 +1796,7 @@ describe('muteInstance()', () => { const alertsClient = new AlertsClient(alertsClientParams); await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); expect(authorization.ensureAuthorized).toHaveBeenCalledWith( 'myType', 'myApp', @@ -1687,6 +1832,7 @@ describe('unmuteInstance()', () => { id: '1', type: 'alert', attributes: { + actions: [], schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, @@ -1715,6 +1861,7 @@ describe('unmuteInstance()', () => { id: '1', type: 'alert', attributes: { + actions: [], schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, @@ -1734,6 +1881,7 @@ describe('unmuteInstance()', () => { id: '1', type: 'alert', attributes: { + actions: [], schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, @@ -1754,6 +1902,17 @@ describe('unmuteInstance()', () => { id: '1', type: 'alert', attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], alertTypeId: 'myType', consumer: 'myApp', schedule: { interval: '10s' }, @@ -1770,6 +1929,7 @@ describe('unmuteInstance()', () => { const alertsClient = new AlertsClient(alertsClientParams); await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); expect(authorization.ensureAuthorized).toHaveBeenCalledWith( 'myType', 'myApp', @@ -2295,6 +2455,7 @@ describe('find()', () => { id: '1', type: 'alert', attributes: { + actions: [], alertTypeId: 'myType', consumer: 'myApp', tags: ['myTag'], @@ -2350,6 +2511,7 @@ describe('delete()', () => { actions: [ { group: 'default', + actionTypeId: '.no-op', actionRef: 'action_0', params: { foo: true, @@ -2502,6 +2664,17 @@ describe('update()', () => { alertTypeId: 'myType', consumer: 'myApp', scheduledTaskId: 'task-123', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], }, references: [], version: '123', @@ -2749,6 +2922,7 @@ describe('update()', () => { id: '1', type: 'action', attributes: { + actions: [], actionTypeId: 'test', }, references: [], @@ -2902,6 +3076,7 @@ describe('update()', () => { id: '1', type: 'action', attributes: { + actions: [], actionTypeId: 'test', }, references: [], @@ -3087,6 +3262,7 @@ describe('update()', () => { id: '1', type: 'action', attributes: { + actions: [], actionTypeId: 'test', }, references: [], @@ -3156,6 +3332,7 @@ describe('update()', () => { id: '1', type: 'action', attributes: { + actions: [], actionTypeId: 'test', }, references: [], @@ -3164,6 +3341,7 @@ describe('update()', () => { id: '2', type: 'action', attributes: { + actions: [], actionTypeId: 'test2', }, references: [], @@ -3290,6 +3468,7 @@ describe('update()', () => { id: '1', type: 'action', attributes: { + actions: [], actionTypeId: 'test', }, references: [], @@ -3300,6 +3479,7 @@ describe('update()', () => { id: alertId, type: 'alert', attributes: { + actions: [], enabled: true, alertTypeId: '123', schedule: currentSchedule, @@ -3570,6 +3750,17 @@ describe('updateApiKey()', () => { alertTypeId: 'myType', consumer: 'myApp', enabled: true, + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], }, version: '123', references: [], @@ -3609,6 +3800,17 @@ describe('updateApiKey()', () => { apiKey: Buffer.from('234:abc').toString('base64'), apiKeyOwner: 'elastic', updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], }, { version: '123' } ); @@ -3634,6 +3836,17 @@ describe('updateApiKey()', () => { apiKey: Buffer.from('234:abc').toString('base64'), apiKeyOwner: 'elastic', updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], }, { version: '123' } ); @@ -3674,6 +3887,7 @@ describe('updateApiKey()', () => { test('ensures user is authorised to updateApiKey this type of alert under the consumer', async () => { await alertsClient.updateApiKey({ id: '1' }); + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); expect(authorization.ensureAuthorized).toHaveBeenCalledWith( 'myType', 'myApp', diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index dbb3fb0eb72e0..9f1cd0b8ab6b6 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -13,7 +13,7 @@ import { SavedObjectReference, SavedObject, } from 'src/core/server'; -import { ActionsClient } from '../../actions/server'; +import { ActionsClient, ActionsAuthorization } from '../../actions/server'; import { Alert, PartialAlert, @@ -58,6 +58,7 @@ export interface ConstructorOptions { taskManager: TaskManagerStartContract; unsecuredSavedObjectsClient: SavedObjectsClientContract; authorization: AlertsAuthorization; + actionsAuthorization: ActionsAuthorization; alertTypeRegistry: AlertTypeRegistry; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; spaceId?: string; @@ -145,6 +146,7 @@ export class AlertsClient { params: InvalidateAPIKeyParams ) => Promise; private readonly getActionsClient: () => Promise; + private readonly actionsAuthorization: ActionsAuthorization; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; constructor({ @@ -160,6 +162,7 @@ export class AlertsClient { invalidateAPIKey, encryptedSavedObjectsClient, getActionsClient, + actionsAuthorization, }: ConstructorOptions) { this.logger = logger; this.getUserName = getUserName; @@ -173,6 +176,7 @@ export class AlertsClient { this.invalidateAPIKey = invalidateAPIKey; this.encryptedSavedObjectsClient = encryptedSavedObjectsClient; this.getActionsClient = getActionsClient; + this.actionsAuthorization = actionsAuthorization; } public async create({ data, options }: CreateOptions): Promise { @@ -474,6 +478,10 @@ export class AlertsClient { WriteOperations.UpdateApiKey ); + if (attributes.actions.length) { + await this.actionsAuthorization.ensureAuthorized('execute'); + } + const username = await this.getUserName(); await this.unsecuredSavedObjectsClient.update( 'alert', @@ -536,6 +544,10 @@ export class AlertsClient { WriteOperations.Enable ); + if (attributes.actions.length) { + await this.actionsAuthorization.ensureAuthorized('execute'); + } + if (attributes.enabled === false) { const username = await this.getUserName(); await this.unsecuredSavedObjectsClient.update( @@ -588,6 +600,10 @@ export class AlertsClient { WriteOperations.Disable ); + if (attributes.actions.length) { + await this.actionsAuthorization.ensureAuthorized('execute'); + } + if (attributes.enabled === true) { await this.unsecuredSavedObjectsClient.update( 'alert', @@ -620,6 +636,10 @@ export class AlertsClient { WriteOperations.MuteAll ); + if (attributes.actions.length) { + await this.actionsAuthorization.ensureAuthorized('execute'); + } + await this.unsecuredSavedObjectsClient.update('alert', id, { muteAll: true, mutedInstanceIds: [], @@ -635,6 +655,10 @@ export class AlertsClient { WriteOperations.UnmuteAll ); + if (attributes.actions.length) { + await this.actionsAuthorization.ensureAuthorized('execute'); + } + await this.unsecuredSavedObjectsClient.update('alert', id, { muteAll: false, mutedInstanceIds: [], @@ -654,6 +678,10 @@ export class AlertsClient { WriteOperations.MuteInstance ); + if (attributes.actions.length) { + await this.actionsAuthorization.ensureAuthorized('execute'); + } + const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && !mutedInstanceIds.includes(alertInstanceId)) { mutedInstanceIds.push(alertInstanceId); @@ -685,6 +713,10 @@ export class AlertsClient { attributes.consumer, WriteOperations.UnmuteInstance ); + if (attributes.actions.length) { + await this.actionsAuthorization.ensureAuthorized('execute'); + } + const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { await this.unsecuredSavedObjectsClient.update( diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index ae828ed0c1e35..8fb43882b5073 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -17,7 +17,8 @@ import { import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { AuthenticatedUser } from '../../../plugins/security/common/model'; import { securityMock } from '../../security/server/mocks'; -import { actionsMock } from '../../actions/server/mocks'; +import { PluginStartContract as ActionsStartContract } from '../../actions/server'; +import { actionsMock, actionsAuthorizationMock } from '../../actions/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; import { AuditLogger } from '../../security/server'; import { ALERTS_FEATURE_ID } from '../common'; @@ -57,8 +58,14 @@ const fakeRequest = ({ getSavedObjectsClient: () => savedObjectsClient, } as unknown) as Request; +const actionsAuthorization = actionsAuthorizationMock.create(); + beforeEach(() => { jest.resetAllMocks(); + alertsClientFactoryParams.actions = actionsMock.createStart(); + (alertsClientFactoryParams.actions as jest.Mocked< + ActionsStartContract + >).getActionsAuthorizationWithRequest.mockReturnValue(actionsAuthorization); alertsClientFactoryParams.getSpaceId.mockReturnValue('default'); alertsClientFactoryParams.spaceIdToNamespace.mockReturnValue('default'); }); @@ -95,9 +102,14 @@ test('creates an alerts client with proper constructor arguments when security i expect(AlertsAuthorizationAuditLogger).toHaveBeenCalledWith(logger); expect(securityPluginSetup.audit.getLogger).toHaveBeenCalledWith(ALERTS_FEATURE_ID); + expect(alertsClientFactoryParams.actions.getActionsAuthorizationWithRequest).toHaveBeenCalledWith( + request + ); + expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ unsecuredSavedObjectsClient: savedObjectsClient, authorization: expect.any(AlertsAuthorization), + actionsAuthorization, logger: alertsClientFactoryParams.logger, taskManager: alertsClientFactoryParams.taskManager, alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, @@ -138,6 +150,7 @@ test('creates an alerts client with proper constructor arguments', async () => { expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ unsecuredSavedObjectsClient: savedObjectsClient, authorization: expect.any(AlertsAuthorization), + actionsAuthorization, logger: alertsClientFactoryParams.logger, taskManager: alertsClientFactoryParams.taskManager, alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index 3e4133d83373d..eb0aea81fd88f 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -79,6 +79,7 @@ export class AlertsClientFactory { includedHiddenTypes: ['alert'], }), authorization, + actionsAuthorization: actions.getActionsAuthorizationWithRequest(request), namespace: this.spaceIdToNamespace(spaceId), encryptedSavedObjectsClient: this.encryptedSavedObjectsClient, async getUserName() { diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index 27dc1dc53d651..e65d195290259 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -82,6 +82,7 @@ describe('Alerting Plugin', () => { actions: { execute: jest.fn(), getActionsClientWithRequest: jest.fn(), + getActionsAuthorizationWithRequest: jest.fn(), }, encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), features: mockFeatures(), @@ -127,6 +128,7 @@ describe('Alerting Plugin', () => { actions: { execute: jest.fn(), getActionsClientWithRequest: jest.fn(), + getActionsAuthorizationWithRequest: jest.fn(), }, spaces: () => null, encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 909a1804456c2..8f03945df437c 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -17,7 +17,7 @@ import { UseUrlState } from '../../common/components/url_state'; import { useWithSource } from '../../common/containers/source'; import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline'; import { navTabs } from './home_navigations'; -import { useSignalIndex } from '../../alerts/containers/detection_engine/alerts/use_signal_index'; +import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index'; const WrappedByAutoSizer = styled.div` height: 100%; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts index fd1a79fa05778..2f57d05be4227 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts @@ -97,6 +97,34 @@ const Space1All: User = { }, }; +const Space1AllAlertingNoneActions: User = { + username: 'space_1_all_alerts_none_actions', + fullName: 'space_1_all_alerts_none_actions', + password: 'space_1_all_alerts_none_actions-password', + role: { + name: 'space_1_all_alerts_none_actions_role', + kibana: [ + { + feature: { + alertsFixture: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['space1'], + }, + ], + elasticsearch: { + // TODO: Remove once Elasticsearch doesn't require the permission for own keys + cluster: ['manage_api_key'], + indices: [ + { + names: [`${ES_TEST_INDEX_NAME}*`], + privileges: ['all'], + }, + ], + }, + }, +}; + const Space1AllWithRestrictedFixture: User = { username: 'space_1_all_with_restricted_fixture', fullName: 'space_1_all_with_restricted_fixture', @@ -132,6 +160,7 @@ export const Users: User[] = [ GlobalRead, Space1All, Space1AllWithRestrictedFixture, + Space1AllAlertingNoneActions, ]; const Space1: Space = { @@ -207,6 +236,15 @@ const Space1AllWithRestrictedFixtureAtSpace1: Space1AllWithRestrictedFixtureAtSp space: Space1, }; +interface Space1AllAlertingNoneActionsAtSpace1 extends Scenario { + id: 'space_1_all_alerts_none_actions at space1'; +} +const Space1AllAlertingNoneActionsAtSpace1: Space1AllAlertingNoneActionsAtSpace1 = { + id: 'space_1_all_alerts_none_actions at space1', + user: Space1AllAlertingNoneActions, + space: Space1, +}; + interface Space1AllAtSpace2 extends Scenario { id: 'space_1_all at space2'; } @@ -222,7 +260,8 @@ export const UserAtSpaceScenarios: [ GlobalReadAtSpace1, Space1AllAtSpace1, Space1AllAtSpace2, - Space1AllWithRestrictedFixtureAtSpace1 + Space1AllWithRestrictedFixtureAtSpace1, + Space1AllAlertingNoneActionsAtSpace1 ] = [ NoKibanaPrivilegesAtSpace1, SuperuserAtSpace1, @@ -230,4 +269,5 @@ export const UserAtSpaceScenarios: [ Space1AllAtSpace1, Space1AllAtSpace2, Space1AllWithRestrictedFixtureAtSpace1, + Space1AllAlertingNoneActionsAtSpace1, ]; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts index 703c9d78e5f89..75609d58f7792 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/create.ts @@ -41,6 +41,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'global_read at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ @@ -91,6 +92,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'global_read at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ @@ -124,6 +126,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'global_read at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': case 'superuser at space1': case 'space_1_all at space1': @@ -156,6 +159,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'global_read at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ @@ -193,6 +197,7 @@ export default function createActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'global_read at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts index 11c60c1af5686..97c933f2ef8c5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/delete.ts @@ -46,6 +46,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'global_read at space1': case 'space_1_all at space2': expect(response.statusCode).to.eql(403); @@ -91,6 +92,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'global_read at space1': case 'space_1_all at space2': case 'space_1_all at space1': @@ -123,6 +125,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'global_read at space1': case 'space_1_all at space2': expect(response.statusCode).to.eql(403); @@ -150,6 +153,7 @@ export default function deleteActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'global_read at space1': case 'space_1_all at space2': expect(response.statusCode).to.eql(403); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts index 0f177f91071dd..6c14dac0f12a2 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts @@ -73,6 +73,7 @@ export default function ({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ @@ -146,6 +147,7 @@ export default function ({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -216,6 +218,7 @@ export default function ({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ @@ -268,6 +271,7 @@ export default function ({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ @@ -301,6 +305,7 @@ export default function ({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': case 'global_read at space1': case 'superuser at space1': @@ -371,6 +376,7 @@ export default function ({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ @@ -420,6 +426,7 @@ export default function ({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts index c29f56262896e..fc08be3e30a6f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts @@ -45,6 +45,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ @@ -96,6 +97,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -127,6 +129,7 @@ export default function getActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index b53fb4000dee1..994072d5cb03c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -45,6 +45,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ @@ -151,6 +152,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ @@ -233,6 +235,7 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts index 57f2e2afd7707..83b7077cbaadd 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/list_action_types.ts @@ -31,6 +31,7 @@ export default function listActionTypesTests({ getService }: FtrProviderContext) expect(response.statusCode).to.eql(200); switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': case 'global_read at space1': case 'superuser at space1': diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts index 8281db67ee66e..82b12e6ce9a22 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/update.ts @@ -55,6 +55,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': case 'global_read at space1': expect(response.statusCode).to.eql(403); @@ -123,6 +124,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': @@ -159,6 +161,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': case 'global_read at space1': case 'superuser at space1': @@ -193,6 +196,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': case 'global_read at space1': expect(response.statusCode).to.eql(403); @@ -226,6 +230,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': case 'global_read at space1': case 'superuser at space1': @@ -277,6 +282,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': case 'global_read at space1': expect(response.statusCode).to.eql(403); @@ -319,6 +325,7 @@ export default function updateActionTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all at space2': case 'global_read at space1': expect(response.statusCode).to.eql(403); 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 6257acce800ec..26c7bb3b6c125 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 @@ -95,6 +95,14 @@ export default function alertTests({ getService }: FtrProviderContext) { statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to get actions`, + statusCode: 403, + }); + break; case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -194,6 +202,14 @@ instanceStateValue: true statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to get actions`, + statusCode: 403, + }); + break; case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -386,6 +402,14 @@ instanceStateValue: true statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to get actions`, + statusCode: 403, + }); + break; case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -476,6 +500,7 @@ instanceStateValue: true }); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); @@ -628,6 +653,14 @@ instanceStateValue: true }, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to get actions`, + statusCode: 403, + }); + break; case 'superuser at space1': expect(response.statusCode).to.eql(200); objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); @@ -683,6 +716,14 @@ instanceStateValue: true statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to get actions`, + statusCode: 403, + }); + break; case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': @@ -754,6 +795,14 @@ instanceStateValue: true statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to get actions`, + statusCode: 403, + }); + break; case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': @@ -809,6 +858,14 @@ instanceStateValue: true statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to get actions`, + statusCode: 403, + }); + break; case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': @@ -856,6 +913,14 @@ instanceStateValue: true statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to get actions`, + statusCode: 403, + }); + break; case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': @@ -906,6 +971,14 @@ instanceStateValue: true statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to get actions`, + statusCode: 403, + }); + break; case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': @@ -956,6 +1029,14 @@ instanceStateValue: true statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to get actions`, + statusCode: 403, + }); + break; case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': 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 d20d939011c16..eb78dbc085df1 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 @@ -80,6 +80,14 @@ export default function createAlertTests({ getService }: FtrProviderContext) { statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to get actions`, + statusCode: 403, + }); + break; case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -153,6 +161,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'space_1_all at space2': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -199,6 +208,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { }); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -248,6 +258,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { }); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); @@ -282,6 +293,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); objectRemover.add(space.id, response.body.id, 'alert', 'alerts'); @@ -308,6 +320,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'space_1_all at space2': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': expect(response.statusCode).to.eql(400); @@ -335,6 +348,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ @@ -376,6 +390,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ @@ -403,6 +418,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ @@ -429,6 +445,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts index 06c538c68d782..24ed952495fa3 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts @@ -68,6 +68,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); @@ -105,6 +106,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -168,6 +170,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { await getScheduledTask(createdAlert.scheduledTaskId); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -235,6 +238,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); @@ -269,6 +273,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': expect(response.body).to.eql({ @@ -326,6 +331,7 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); 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 2531d82771cff..55f02b24ac41e 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 @@ -41,10 +41,32 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte describe(scenario.id, () => { it('should handle disable alert request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + actionTypeId: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: true })) + .send( + getTestAlertData({ + enabled: true, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ) .expect(200); objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); @@ -67,6 +89,16 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte // Ensure task still exists await getScheduledTask(createdAlert.scheduledTaskId); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to execute actions`, + statusCode: 403, + }); + // Ensure task still exists + await getScheduledTask(createdAlert.scheduledTaskId); + break; case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -112,6 +144,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -171,6 +204,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte }); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -231,6 +265,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); @@ -287,6 +322,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); @@ -326,6 +362,7 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.body).to.eql({ statusCode: 404, 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 31b71e0decdb8..db37e772c8887 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 @@ -41,10 +41,32 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex describe(scenario.id, () => { it('should handle enable alert request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + actionTypeId: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send( + getTestAlertData({ + enabled: false, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ) .expect(200); objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); @@ -65,6 +87,14 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to execute actions`, + statusCode: 403, + }); + break; case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -117,6 +147,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -176,6 +207,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex }); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -230,6 +262,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); @@ -284,6 +317,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); @@ -330,6 +364,7 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.body).to.eql({ statusCode: 404, 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 ece2ee8e54788..268212d4294d0 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 @@ -53,6 +53,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body.page).to.equal(1); @@ -142,6 +143,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { }); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(200); expect(response.body.page).to.equal(1); expect(response.body.perPage).to.be.equal(perPage); @@ -239,6 +241,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body.page).to.equal(1); @@ -328,6 +331,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { }); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(200); expect(response.body.data).to.eql([]); break; @@ -373,6 +377,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ 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 9835b18b96e3a..17969bde0620e 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 @@ -53,6 +53,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body).to.eql({ @@ -104,6 +105,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -157,6 +159,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { }); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -199,6 +202,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -237,6 +241,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': case 'global_read at space1': case 'superuser at space1': @@ -262,6 +267,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(404); expect(response.body).to.eql({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts index e188a21fd0d36..2e89aa2961c73 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts @@ -53,6 +53,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body).to.key('alertInstances', 'previousStartedAt'); @@ -94,6 +95,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont }); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -133,6 +135,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont case 'no_kibana_privileges at space1': case 'space_1_all at space2': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': case 'global_read at space1': case 'superuser at space1': @@ -158,6 +161,7 @@ export default function createGetAlertStateTests({ getService }: FtrProviderCont case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(404); expect(response.body).to.eql({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index 8ff97fba65cc1..c3e5af0d1f771 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -60,6 +60,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { expect(response.body).to.eql([]); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType); expect(restrictedNoOpAlertType).to.eql(undefined); expect(noOpAlertType.authorizedConsumers).to.eql({ 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 index 21513513a8ccb..3b793feda632a 100644 --- 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 @@ -33,10 +33,32 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) describe(scenario.id, () => { it('should handle mute alert request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + actionTypeId: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send( + getTestAlertData({ + enabled: false, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ) .expect(200); objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); @@ -57,6 +79,14 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to execute actions`, + statusCode: 403, + }); + break; case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -102,6 +132,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -168,6 +199,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) }); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -223,6 +255,7 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', 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 index 0d8630445accd..2e4da7eb4158c 100644 --- 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 @@ -33,10 +33,32 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider describe(scenario.id, () => { it('should handle mute alert instance request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + actionTypeId: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send( + getTestAlertData({ + enabled: false, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ) .expect(200); objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); @@ -57,6 +79,14 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to execute actions`, + statusCode: 403, + }); + break; case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -102,6 +132,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -168,6 +199,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider }); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -223,6 +255,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -291,6 +324,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); 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 index 9d715c9146b5e..2413bd42460d4 100644 --- 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 @@ -33,10 +33,32 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex describe(scenario.id, () => { it('should handle unmute alert request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + actionTypeId: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send( + getTestAlertData({ + enabled: false, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ) .expect(200); objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); @@ -62,6 +84,14 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to execute actions`, + statusCode: 403, + }); + break; case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -112,6 +142,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -183,6 +214,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex }); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -243,6 +275,7 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', 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 index 2f1f883351aee..d67ecedaab14c 100644 --- 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 @@ -33,10 +33,32 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider describe(scenario.id, () => { it('should handle unmute alert instance request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + actionTypeId: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData({ enabled: false })) + .send( + getTestAlertData({ + enabled: false, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ) .expect(200); objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); @@ -64,6 +86,14 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to execute actions`, + statusCode: 403, + }); + break; case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -116,6 +146,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -189,6 +220,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider }); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -251,6 +283,7 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', 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 7007b4ce7e3ae..390b50acb3705 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 @@ -40,6 +40,17 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { const { user, space } = scenario; describe(scenario.id, () => { it('should handle update alert request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + actionTypeId: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') @@ -54,7 +65,13 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { foo: true, }, schedule: { interval: '12s' }, - actions: [], + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], throttle: '1m', }; const response = await supertestWithoutAuth @@ -78,6 +95,14 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to get actions`, + statusCode: 403, + }); + break; case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -93,6 +118,14 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { apiKeyOwner: user.username, muteAll: false, mutedInstanceIds: [], + actions: [ + { + id: createdAction.id, + actionTypeId: 'test.noop', + group: 'default', + params: {}, + }, + ], scheduledTaskId: createdAlert.scheduledTaskId, createdAt: response.body.createdAt, updatedAt: response.body.updatedAt, @@ -149,6 +182,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -241,6 +275,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { }); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -322,6 +357,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -422,6 +458,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); expect(response.body).to.eql({ @@ -486,6 +523,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': case 'superuser at space1': expect(response.body).to.eql({ @@ -529,6 +567,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ @@ -555,6 +594,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ @@ -613,6 +653,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ @@ -646,6 +687,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(400); expect(response.body).to.eql({ @@ -711,6 +753,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(200); await retry.try(async () => { 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 903bf6b40ee7e..6e8956d3326ea 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 @@ -33,10 +33,31 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte describe(scenario.id, () => { it('should handle update alert api key request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(space.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + actionTypeId: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + const { body: createdAlert } = await supertest .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) .set('kbn-xsrf', 'foo') - .send(getTestAlertData()) + .send( + getTestAlertData({ + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ) .expect(200); objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts'); @@ -56,6 +77,14 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte statusCode: 403, }); break; + case 'space_1_all_alerts_none_actions at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: `Unauthorized to execute actions`, + statusCode: 403, + }); + break; case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': @@ -99,6 +128,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -163,6 +193,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte }); break; case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -216,6 +247,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte case 'space_1_all at space2': case 'global_read at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', @@ -289,6 +321,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte break; case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.statusCode).to.eql(204); expect(response.body).to.eql(''); @@ -328,6 +361,7 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte case 'global_read at space1': case 'superuser at space1': case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': case 'space_1_all_with_restricted_fixture at space1': expect(response.body).to.eql({ statusCode: 404, From 662e4a2fd0cd2ad714e41e59fdd22bcd5dc204d1 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 9 Jul 2020 13:14:38 +0100 Subject: [PATCH 111/126] disabled switches on alert details page when there are no privileges --- .../components/actions_connectors_list.test.tsx | 2 +- .../alert_details/components/alert_details.tsx | 14 ++++++++++---- .../components/alert_instances.test.tsx | 7 ++++++- .../alert_details/components/alert_instances.tsx | 8 ++++++-- .../components/alert_instances_route.test.tsx | 6 +++--- .../components/alert_instances_route.tsx | 9 ++++++++- 6 files changed, 34 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 6199ec87bf5ac..9d95ef4cfc7e5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -288,7 +288,7 @@ describe('actions_connectors_list component empty with show only capability', () it('renders no permissions to create connector', async () => { await setup(); - expect(wrapper.find('[defaultMessage="No permissions to create connector"]')).toHaveLength(1); + expect(wrapper.find('[defaultMessage="No permissions to create connectors"]')).toHaveLength(1); expect(wrapper.find('[data-test-subj="createActionButton"]')).toHaveLength(0); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 5d619f728a191..b1dd78ff59f34 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -71,14 +71,16 @@ export const AlertDetails: React.FunctionComponent = ({ dataPlugin, } = useAppDependencies(); - const canSaveAlert = hasAllPrivilege(alert, alertType); const canExecuteActions = hasExecuteActionsCapability(capabilities); + const canSaveAlert = + hasAllPrivilege(alert, alertType) && + // if the alert has actions, can the user save the alert's action params + (canExecuteActions || (!canExecuteActions && alert.actions.length === 0)); + const actionTypesByTypeId = keyBy(actionTypes, 'id'); const hasEditButton = // can the user save the alert canSaveAlert && - // if the alert has actions, can the user save the alert's action params - (canExecuteActions || (!canExecuteActions && alert.actions.length === 0)) && // is this alert type editable from within Alerts Management (alertTypeRegistry.has(alert.alertTypeId) ? !alertTypeRegistry.get(alert.alertTypeId).requiresAppContext @@ -262,7 +264,11 @@ export const AlertDetails: React.FunctionComponent = ({ {alert.enabled ? ( - + ) : ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx index 2531fd2625b4b..dd2ee48b7a620 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.test.tsx @@ -52,7 +52,9 @@ describe('alert_instances', () => { ]; expect( - shallow() + shallow( + + ) .find(EuiBasicTable) .prop('items') ).toEqual(instances); @@ -68,6 +70,7 @@ describe('alert_instances', () => { durationEpoch={fake2MinutesAgo.getTime()} {...mockAPIs} alert={alert} + readOnly={false} alertState={alertState} /> ) @@ -95,6 +98,7 @@ describe('alert_instances', () => { { Promise; durationEpoch?: number; } & Pick; export const alertInstancesTableColumns = ( - onMuteAction: (instance: AlertInstanceListItem) => Promise + onMuteAction: (instance: AlertInstanceListItem) => Promise, + readOnly: boolean ) => [ { field: 'instance', @@ -90,6 +92,7 @@ export const alertInstancesTableColumns = ( showLabel={false} compressed={true} checked={alertInstance.isMuted} + disabled={readOnly} data-test-subj={`muteAlertInstanceButton_${alertInstance.instance}`} onChange={() => onMuteAction(alertInstance)} /> @@ -109,6 +112,7 @@ function durationAsString(duration: Duration): string { export function AlertInstances({ alert, + readOnly, alertState: { alertInstances = {} }, muteAlertInstance, unmuteAlertInstance, @@ -162,7 +166,7 @@ export function AlertInstances({ cellProps={() => ({ 'data-test-subj': 'cell', })} - columns={alertInstancesTableColumns(onMuteAction)} + columns={alertInstancesTableColumns(onMuteAction, readOnly)} data-test-subj="alertInstancesList" /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx index 9bff33e4aa69c..975856beba556 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.test.tsx @@ -22,9 +22,9 @@ describe('alert_state_route', () => { const alert = mockAlert(); expect( - shallow().containsMatchingElement( - - ) + shallow( + + ).containsMatchingElement() ).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx index a02b44523e26c..d8a7d18eb87a9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx @@ -18,11 +18,13 @@ import { AlertInstancesWithApi as AlertInstances } from './alert_instances'; type WithAlertStateProps = { alert: Alert; + readOnly: boolean; requestRefresh: () => Promise; } & Pick; export const AlertInstancesRoute: React.FunctionComponent = ({ alert, + readOnly, requestRefresh, loadAlertState, }) => { @@ -36,7 +38,12 @@ export const AlertInstancesRoute: React.FunctionComponent = }, [alert]); return alertState ? ( - + ) : (
Date: Tue, 14 Jul 2020 15:52:55 +0100 Subject: [PATCH 112/126] handle case where security is disabled in ES but enabled in kibana --- .../actions_authorization.test.ts | 30 +++++++-- .../authorization/actions_authorization.ts | 10 ++- x-pack/plugins/actions/server/plugin.ts | 1 + .../server/alerts_client_factory.test.ts | 1 + .../alerts/server/alerts_client_factory.ts | 1 + .../alerts_authorization.test.ts | 67 ++++++++++++++----- .../authorization/alerts_authorization.ts | 32 ++++++--- 7 files changed, 105 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts index d7d646ca4dd54..4fded7b9e1bce 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts @@ -16,13 +16,15 @@ const auditLogger = actionsAuthorizationAuditLoggerMock.create(); const realAuditLogger = new ActionsAuthorizationAuditLogger(); const mockAuthorizationAction = (type: string, operation: string) => `${type}/${operation}`; -function mockAuthorization() { - const authorization = securityMock.createSetup().authz; +function mockSecurity() { + const security = securityMock.createSetup(); + const authorization = security.authz; + const securityLicense = security.license; // typescript is having trouble inferring jest's automocking (authorization.actions.savedObject.get as jest.MockedFunction< typeof authorization.actions.savedObject.get >).mockImplementation(mockAuthorizationAction); - return authorization; + return { authorization, securityLicense }; } beforeEach(() => { @@ -45,13 +47,27 @@ describe('ensureAuthorized', () => { await actionsAuthorization.ensureAuthorized('create', 'myType'); }); + test('is a no-op when the security license is disabled', async () => { + const { authorization, securityLicense } = mockSecurity(); + securityLicense.isEnabled.mockReturnValue(false); + const actionsAuthorization = new ActionsAuthorization({ + request, + authorization, + securityLicense, + auditLogger, + }); + + await actionsAuthorization.ensureAuthorized('create', 'myType'); + }); + test('ensures the user has privileges to use the operation on the Actions Saved Object type', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const actionsAuthorization = new ActionsAuthorization({ + securityLicense, request, authorization, auditLogger, @@ -85,12 +101,13 @@ describe('ensureAuthorized', () => { }); test('ensures the user has privileges to execute an Actions Saved Object type', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const actionsAuthorization = new ActionsAuthorization({ + securityLicense, request, authorization, auditLogger, @@ -134,12 +151,13 @@ describe('ensureAuthorized', () => { }); test('throws if user lacks the required privieleges', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const actionsAuthorization = new ActionsAuthorization({ + securityLicense, request, authorization, auditLogger, diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.ts index bd8ff3d1fe397..41396a1243ea9 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.ts @@ -9,11 +9,13 @@ import { KibanaRequest } from 'src/core/server'; import { SecurityPluginSetup } from '../../../security/server'; import { ActionsAuthorizationAuditLogger } from './audit_logger'; import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects'; +import { SecurityLicense } from '../../../security/common/licensing'; export interface ConstructorOptions { request: KibanaRequest; auditLogger: ActionsAuthorizationAuditLogger; authorization?: SecurityPluginSetup['authz']; + securityLicense?: SecurityLicense; } const operationAlias: Record< @@ -30,17 +32,19 @@ const operationAlias: Record< export class ActionsAuthorization { private readonly request: KibanaRequest; private readonly authorization?: SecurityPluginSetup['authz']; + private readonly securityLicense?: SecurityLicense; private readonly auditLogger: ActionsAuthorizationAuditLogger; - constructor({ request, authorization, auditLogger }: ConstructorOptions) { + constructor({ request, authorization, securityLicense, auditLogger }: ConstructorOptions) { this.request = request; this.authorization = authorization; + this.securityLicense = securityLicense; this.auditLogger = auditLogger; } public async ensureAuthorized(operation: string, actionTypeId?: string) { - const { authorization } = this; - if (authorization) { + const { authorization, securityLicense } = this; + if (authorization && securityLicense?.isEnabled()) { const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); const { hasAllRequested, username } = await checkPrivileges( operationAlias[operation] diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 798be5f991add..c5a6db3cf4347 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -323,6 +323,7 @@ export class ActionsPlugin implements Plugin, Plugi return new ActionsAuthorization({ request, authorization: this.security?.authz, + securityLicense: this.security?.license, auditLogger: new ActionsAuthorizationAuditLogger( this.security?.audit.getLogger(ACTIONS_FEATURE.id) ), diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 8fb43882b5073..bdb5b14709dfd 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -94,6 +94,7 @@ test('creates an alerts client with proper constructor arguments when security i expect(AlertsAuthorization).toHaveBeenCalledWith({ request, authorization: securityPluginSetup.authz, + securityLicense: securityPluginSetup.license, alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, features: alertsClientFactoryParams.features, auditLogger: expect.any(AlertsAuthorizationAuditLogger), diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index eb0aea81fd88f..79c527c1b993d 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -61,6 +61,7 @@ export class AlertsClientFactory { const spaceId = this.getSpaceId(request); const authorization = new AlertsAuthorization({ authorization: securityPluginSetup?.authz, + securityLicense: securityPluginSetup?.license, request, alertTypeRegistry: this.alertTypeRegistry, features: features!, diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 442ee215a304b..06a9a4b892647 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -26,13 +26,15 @@ const realAuditLogger = new AlertsAuthorizationAuditLogger(); const mockAuthorizationAction = (type: string, app: string, operation: string) => `${type}/${app}/${operation}`; -function mockAuthorization() { - const authorization = securityMock.createSetup().authz; +function mockSecurity() { + const security = securityMock.createSetup(); + const authorization = security.authz; + const securityLicense = security.license; // typescript is having trouble inferring jest's automocking (authorization.actions.alerting.get as jest.MockedFunction< typeof authorization.actions.alerting.get >).mockImplementation(mockAuthorizationAction); - return authorization; + return { authorization, securityLicense }; } function mockFeature(appName: string, typeName?: string) { @@ -181,8 +183,25 @@ describe('ensureAuthorized', () => { expect(alertTypeRegistry.get).toHaveBeenCalledTimes(0); }); + test('is a no-op when the security license is disabled', async () => { + const { authorization, securityLicense } = mockSecurity(); + securityLicense.isEnabled.mockReturnValue(false); + const alertAuthorization = new AlertsAuthorization({ + request, + alertTypeRegistry, + authorization, + securityLicense, + features, + auditLogger, + }); + + await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); + + expect(alertTypeRegistry.get).toHaveBeenCalledTimes(0); + }); + test('ensures the user has privileges to execute the specified type, operation and consumer', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -190,6 +209,7 @@ describe('ensureAuthorized', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, + securityLicense, alertTypeRegistry, features, auditLogger, @@ -224,7 +244,7 @@ describe('ensureAuthorized', () => { }); test('ensures the user has privileges to execute the specified type and operation without consumer when consumer is alerts', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -232,6 +252,7 @@ describe('ensureAuthorized', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, + securityLicense, alertTypeRegistry, features, auditLogger, @@ -266,7 +287,7 @@ describe('ensureAuthorized', () => { }); test('ensures the user has privileges to execute the specified type, operation and producer when producer is different from consumer', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -280,6 +301,7 @@ describe('ensureAuthorized', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, + securityLicense, alertTypeRegistry, features, auditLogger, @@ -314,7 +336,7 @@ describe('ensureAuthorized', () => { }); test('throws if user lacks the required privieleges for the consumer', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -322,6 +344,7 @@ describe('ensureAuthorized', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, + securityLicense, alertTypeRegistry, features, auditLogger, @@ -362,7 +385,7 @@ describe('ensureAuthorized', () => { }); test('throws if user lacks the required privieleges for the producer', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -370,6 +393,7 @@ describe('ensureAuthorized', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, + securityLicense, alertTypeRegistry, features, auditLogger, @@ -410,7 +434,7 @@ describe('ensureAuthorized', () => { }); test('throws if user lacks the required privieleges for both consumer and producer', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -418,6 +442,7 @@ describe('ensureAuthorized', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, + securityLicense, alertTypeRegistry, features, auditLogger, @@ -520,7 +545,7 @@ describe('getFindAuthorizationFilter', () => { }); test('creates a filter based on the privileged types', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -534,6 +559,7 @@ describe('getFindAuthorizationFilter', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, + securityLicense, alertTypeRegistry, features, auditLogger, @@ -548,7 +574,7 @@ describe('getFindAuthorizationFilter', () => { }); test('creates an `ensureAlertTypeIsAuthorized` function which throws if type is unauthorized', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -579,6 +605,7 @@ describe('getFindAuthorizationFilter', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, + securityLicense, alertTypeRegistry, features, auditLogger, @@ -606,7 +633,7 @@ describe('getFindAuthorizationFilter', () => { }); test('creates an `ensureAlertTypeIsAuthorized` function which is no-op if type is authorized', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -637,6 +664,7 @@ describe('getFindAuthorizationFilter', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, + securityLicense, alertTypeRegistry, features, auditLogger, @@ -653,7 +681,7 @@ describe('getFindAuthorizationFilter', () => { }); test('creates an `logSuccessfulAuthorization` function which logs every authorized type', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -692,6 +720,7 @@ describe('getFindAuthorizationFilter', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, + securityLicense, alertTypeRegistry, features, auditLogger, @@ -826,7 +855,7 @@ describe('filterByAlertTypeAuthorization', () => { }); test('augments a list of types with consumers under which the operation is authorized', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -857,6 +886,7 @@ describe('filterByAlertTypeAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, + securityLicense, alertTypeRegistry, features, auditLogger, @@ -911,7 +941,7 @@ describe('filterByAlertTypeAuthorization', () => { }); test('authorizes user under the Alerts consumer when they are authorized by the producer', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -934,6 +964,7 @@ describe('filterByAlertTypeAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, + securityLicense, alertTypeRegistry, features, auditLogger, @@ -969,7 +1000,7 @@ describe('filterByAlertTypeAuthorization', () => { }); test('augments a list of types with consumers under which multiple operations are authorized', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -1016,6 +1047,7 @@ describe('filterByAlertTypeAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, + securityLicense, alertTypeRegistry, features, auditLogger, @@ -1078,7 +1110,7 @@ describe('filterByAlertTypeAuthorization', () => { }); test('omits types which have no consumers under which the operation is authorized', async () => { - const authorization = mockAuthorization(); + const { authorization, securityLicense } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -1109,6 +1141,7 @@ describe('filterByAlertTypeAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, + securityLicense, alertTypeRegistry, features, auditLogger, diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 98cbed061513c..cf4db17f84c8c 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -15,6 +15,7 @@ import { RegistryAlertType } from '../alert_type_registry'; import { FeatureKibanaPrivileges, SubFeaturePrivilegeConfig } from '../../../features/common'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; +import { SecurityLicense } from '../../../security/common/licensing'; export enum ReadOperations { Get = 'get', @@ -52,12 +53,14 @@ export interface ConstructorOptions { features: FeaturesPluginStart; auditLogger: AlertsAuthorizationAuditLogger; authorization?: SecurityPluginSetup['authz']; + securityLicense?: SecurityLicense; } export class AlertsAuthorization { private readonly alertTypeRegistry: AlertTypeRegistry; private readonly request: KibanaRequest; private readonly authorization?: SecurityPluginSetup['authz']; + private readonly securityLicense?: SecurityLicense; private readonly auditLogger: AlertsAuthorizationAuditLogger; private readonly featuresIds: string[]; private readonly allPossibleConsumers: AuthorizedConsumers; @@ -66,11 +69,13 @@ export class AlertsAuthorization { alertTypeRegistry, request, authorization, + securityLicense, features, auditLogger, }: ConstructorOptions) { this.request = request; this.authorization = authorization; + this.securityLicense = securityLicense; this.alertTypeRegistry = alertTypeRegistry; this.auditLogger = auditLogger; @@ -98,13 +103,18 @@ export class AlertsAuthorization { }); } + private shouldCheckAuthorization(): boolean { + const { authorization, securityLicense } = this; + return (authorization && securityLicense && securityLicense?.isEnabled()) ?? false; + } + public async ensureAuthorized( alertTypeId: string, consumer: string, operation: ReadOperations | WriteOperations ) { const { authorization } = this; - if (authorization) { + if (authorization && this.shouldCheckAuthorization()) { const alertType = this.alertTypeRegistry.get(alertTypeId); const requiredPrivilegesByScope = { consumer: authorization.actions.alerting.get(alertTypeId, consumer, operation), @@ -172,7 +182,7 @@ export class AlertsAuthorization { ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => void; logSuccessfulAuthorization: () => void; }> { - if (this.authorization) { + if (this.authorization && this.shouldCheckAuthorization()) { const { username, authorizedAlertTypes, @@ -262,15 +272,7 @@ export class AlertsAuthorization { hasAllRequested: boolean; authorizedAlertTypes: Set; }> { - if (!this.authorization) { - return { - hasAllRequested: true, - authorizedAlertTypes: this.augmentWithAuthorizedConsumers( - alertTypes, - this.allPossibleConsumers - ), - }; - } else { + if (this.authorization && this.shouldCheckAuthorization()) { const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( this.request ); @@ -338,6 +340,14 @@ export class AlertsAuthorization { return authorizedAlertTypes; }, new Set()), }; + } else { + return { + hasAllRequested: true, + authorizedAlertTypes: this.augmentWithAuthorizedConsumers( + alertTypes, + this.allPossibleConsumers + ), + }; } } From 67e913aa73df8bd948fdea6d030666893c83207d Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 14 Jul 2020 17:57:11 +0100 Subject: [PATCH 113/126] prevent unknown consumers from being authorized --- .../authorization/alerts_authorization.ts | 33 ++++++++++++++++- .../tests/alerting/create.ts | 36 +++++++++++++++++++ .../spaces_only/tests/alerting/create.ts | 26 +++++++++++++- 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index cf4db17f84c8c..c0e35c3282656 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { map, mapValues, remove, fromPairs } from 'lodash'; +import { map, mapValues, remove, fromPairs, has } from 'lodash'; import { KibanaRequest } from 'src/core/server'; import { RecursiveReadonly } from '@kbn/utility-types'; import { ALERTS_FEATURE_ID } from '../../common'; @@ -114,6 +114,8 @@ export class AlertsAuthorization { operation: ReadOperations | WriteOperations ) { const { authorization } = this; + + const isKnownConsumer = has(this.allPossibleConsumers, consumer); if (authorization && this.shouldCheckAuthorization()) { const alertType = this.alertTypeRegistry.get(alertTypeId); const requiredPrivilegesByScope = { @@ -141,6 +143,25 @@ export class AlertsAuthorization { ] ); + if (!isKnownConsumer) { + /** + * Under most circumstances this would have been caught by `checkPrivileges` as + * a user can't have Privileges to an unknown consumer, but super users + * don't actually get "privilege checked" so the made up consumer *will* return + * as Privileged. + * This check will ensure we don't accidentally let these through + */ + throw Boom.forbidden( + this.auditLogger.alertsAuthorizationFailure( + username, + alertTypeId, + ScopeType.Consumer, + consumer, + operation + ) + ); + } + if (hasAllRequested) { this.auditLogger.alertsAuthorizationSuccess( username, @@ -174,6 +195,16 @@ export class AlertsAuthorization { ) ); } + } else if (!isKnownConsumer) { + throw Boom.forbidden( + this.auditLogger.alertsAuthorizationFailure( + '', + alertTypeId, + ScopeType.Consumer, + consumer, + operation + ) + ); } } 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 eb78dbc085df1..7b53887709217 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 @@ -269,6 +269,42 @@ export default function createAlertTests({ getService }: FtrProviderContext) { } }); + it('should handle create alert request appropriately when consumer is unknown', async () => { + const response = await supertestWithoutAuth + .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password) + .send( + getTestAlertData({ + alertTypeId: 'test.noop', + consumer: 'some consumer patrick invented', + }) + ); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'global_read at space1': + case 'space_1_all at space2': + case 'space_1_all at space1': + case 'space_1_all_alerts_none_actions at space1': + case 'space_1_all_with_restricted_fixture at space1': + case 'superuser at space1': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'create', + 'test.noop', + 'some consumer patrick invented' + ), + statusCode: 403, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + it('should handle create alert request appropriately when an alert is disabled ', async () => { const response = await supertestWithoutAuth .post(`${getUrlPrefix(space.id)}/api/alerts/alert`) 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 8f42f12347728..86775f77a7671 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 @@ -6,7 +6,13 @@ import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; -import { checkAAD, getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib'; +import { + checkAAD, + getUrlPrefix, + getTestAlertData, + ObjectRemover, + getConsumerUnauthorizedErrorMessage, +} from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -102,6 +108,24 @@ export default function createAlertTests({ getService }: FtrProviderContext) { }); }); + it('should handle create alert request appropriately when consumer is unknown', async () => { + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData({ consumer: 'some consumer patrick invented' })); + + expect(response.status).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'create', + 'test.noop', + 'some consumer patrick invented' + ), + statusCode: 403, + }); + }); + it('should handle create alert request appropriately when an alert is disabled ', async () => { const response = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`) From d1cc1cd99ce05a694ab8f182371dbf891d890359 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 15 Jul 2020 09:40:06 +0100 Subject: [PATCH 114/126] moved migration to v7.10.0 as this feature hasnt made it into 7.9.0 --- .../server/saved_objects/migrations.test.ts | 34 +++++++++++++++++++ .../alerts/server/saved_objects/migrations.ts | 27 ++++++++------- .../spaces_only/tests/alerting/migrations.ts | 2 +- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts index 38cda5a9a0f7c..5115dd7da4e38 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts @@ -47,6 +47,40 @@ describe('7.9.0', () => { }); }); +describe('7.10.0', () => { + beforeEach(() => { + jest.resetAllMocks(); + encryptedSavedObjectsSetup.createMigration.mockImplementation( + (shouldMigrateWhenPredicate, migration) => migration + ); + }); + + test('changes nothing on alerts by other plugins', () => { + const migration790 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const alert = getMockData({}); + expect(migration790(alert, { log })).toMatchObject(alert); + + expect(encryptedSavedObjectsSetup.createMigration).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function) + ); + }); + + test('migrates the consumer for metrics', () => { + const migration790 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const alert = getMockData({ + consumer: 'metrics', + }); + expect(migration790(alert, { log })).toMatchObject({ + ...alert, + attributes: { + ...alert.attributes, + consumer: 'infrastructure', + }, + }); + }); +}); + function getMockData( overwrites: Record = {} ): SavedObjectUnsanitizedDoc { diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts index 79413aff907c4..57a4005887093 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -15,24 +15,27 @@ export function getMigrations( encryptedSavedObjects: EncryptedSavedObjectsPluginSetup ): SavedObjectMigrationMap { return { - '7.9.0': changeAlertingConsumer(encryptedSavedObjects), + /** + * In v7.9.0 we changed the Alerting plugin so it uses the `consumer` value of `alerts` + * prior to that we were using `alerting` and we need to keep these in sync + */ + '7.9.0': changeAlertingConsumer(encryptedSavedObjects, 'alerting', 'alerts'), + /** + * In v7.10.0 we changed the Matrics plugin so it uses the `consumer` value of `infrastructure` + * prior to that we were using `metrics` and we need to keep these in sync + */ + '7.10.0': changeAlertingConsumer(encryptedSavedObjects, 'metrics', 'infrastructure'), }; } -/** - * In v7.9.0 we changed the Alerting plugin so it uses the `consumer` value of `alerts` - * prior to that we were using `alerting` and we need to keep these in sync - */ function changeAlertingConsumer( - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, + from: string, + to: string ): SavedObjectMigrationFn { - const consumerMigration = new Map(); - consumerMigration.set('alerting', 'alerts'); - consumerMigration.set('metrics', 'infrastructure'); - return encryptedSavedObjects.createMigration( function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { - return consumerMigration.has(doc.attributes.consumer); + return doc.attributes.consumer === from; }, (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { const { @@ -42,7 +45,7 @@ function changeAlertingConsumer( ...doc, attributes: { ...doc.attributes, - consumer: consumerMigration.get(consumer) ?? consumer, + consumer: consumer === from ? to : consumer, }, }; } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts index e2c9879790fec..d0e1be12e762f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -31,7 +31,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { expect(response.body.consumer).to.equal('alerts'); }); - it('7.9.0 migrates the `metrics` consumer to be the `infrastructure`', async () => { + it('7.10.0 migrates the `metrics` consumer to be the `infrastructure`', async () => { const response = await supertest.get( `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-fdf248d5f2a4` ); From e9ac83c3ab35227d1c4d52f1c76e587d0ad9f687 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 15 Jul 2020 10:15:44 +0100 Subject: [PATCH 115/126] corrected var name --- .../alerts/server/saved_objects/migrations.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts index 5115dd7da4e38..19f4e918b7862 100644 --- a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts @@ -56,9 +56,9 @@ describe('7.10.0', () => { }); test('changes nothing on alerts by other plugins', () => { - const migration790 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; const alert = getMockData({}); - expect(migration790(alert, { log })).toMatchObject(alert); + expect(migration710(alert, { log })).toMatchObject(alert); expect(encryptedSavedObjectsSetup.createMigration).toHaveBeenCalledWith( expect.any(Function), @@ -67,11 +67,11 @@ describe('7.10.0', () => { }); test('migrates the consumer for metrics', () => { - const migration790 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; const alert = getMockData({ consumer: 'metrics', }); - expect(migration790(alert, { log })).toMatchObject({ + expect(migration710(alert, { log })).toMatchObject({ ...alert, attributes: { ...alert.attributes, From 07e1a1c86b7976cfec1f250d5cbe8299276e81bb Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 17 Jul 2020 13:40:31 +0100 Subject: [PATCH 116/126] take into account which features available in the active space --- examples/alerting_example/server/plugin.ts | 1 + .../alerting_builtins/server/feature.ts | 1 + .../server/alerts_client_factory.test.ts | 3 + .../alerts/server/alerts_client_factory.ts | 5 + .../alerts_authorization.test.ts | 1984 +++++++++-------- .../authorization/alerts_authorization.ts | 83 +- x-pack/plugins/alerts/server/plugin.ts | 3 + x-pack/plugins/apm/server/feature.ts | 1 + x-pack/plugins/features/common/feature.ts | 11 + .../plugins/features/server/feature_schema.ts | 6 +- x-pack/plugins/infra/server/features.ts | 2 + x-pack/plugins/monitoring/server/plugin.ts | 5 + .../security_solution/server/plugin.ts | 1 + x-pack/plugins/uptime/server/kibana.index.ts | 1 + .../fixtures/plugins/alerts/server/plugin.ts | 12 + .../alerts_restricted/server/plugin.ts | 1 + .../tests/alerting/create.ts | 9 +- .../tests/alerting/delete.ts | 7 + .../tests/alerting/disable.ts | 7 + .../tests/alerting/enable.ts | 14 + .../security_and_spaces/tests/alerting/get.ts | 11 + .../tests/alerting/index.ts | 2 +- .../tests/alerting/mute_all.ts | 11 + .../tests/alerting/mute_instance.ts | 11 + .../tests/alerting/unmute_all.ts | 11 + .../tests/alerting/unmute_instance.ts | 11 + .../tests/alerting/update.ts | 11 + .../tests/alerting/update_api_key.ts | 11 + .../fixtures/plugins/alerts/server/plugin.ts | 1 + 29 files changed, 1228 insertions(+), 1009 deletions(-) diff --git a/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts index 1d7ad37a46551..49352cc285693 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/examples/alerting_example/server/plugin.ts @@ -44,6 +44,7 @@ export class AlertingExamplePlugin implements Plugin = { taskManager: taskManagerMock.start(), alertTypeRegistry: alertTypeRegistryMock.create(), getSpaceId: jest.fn(), + getSpace: jest.fn(), spaceIdToNamespace: jest.fn(), encryptedSavedObjectsClient: encryptedSavedObjectsMock.createClient(), actions: actionsMock.createStart(), @@ -98,6 +99,7 @@ test('creates an alerts client with proper constructor arguments when security i alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, features: alertsClientFactoryParams.features, auditLogger: expect.any(AlertsAuthorizationAuditLogger), + getSpace: expect.any(Function), }); expect(AlertsAuthorizationAuditLogger).toHaveBeenCalledWith(logger); @@ -146,6 +148,7 @@ test('creates an alerts client with proper constructor arguments', async () => { alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, features: alertsClientFactoryParams.features, auditLogger: expect.any(AlertsAuthorizationAuditLogger), + getSpace: expect.any(Function), }); expect(jest.requireMock('./alerts_client').AlertsClient).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index 1b405e7fcd0b4..6d1cde2485407 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -15,6 +15,7 @@ import { TaskManagerStartContract } from '../../task_manager/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; import { AlertsAuthorization } from './authorization/alerts_authorization'; import { AlertsAuthorizationAuditLogger } from './authorization/audit_logger'; +import { Space } from '../../spaces/server'; export interface AlertsClientFactoryOpts { logger: Logger; @@ -22,6 +23,7 @@ export interface AlertsClientFactoryOpts { alertTypeRegistry: AlertTypeRegistry; securityPluginSetup?: SecurityPluginSetup; getSpaceId: (request: KibanaRequest) => string | undefined; + getSpace: (request: KibanaRequest) => Promise; spaceIdToNamespace: SpaceIdToNamespaceFunction; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; actions: ActionsPluginStartContract; @@ -35,6 +37,7 @@ export class AlertsClientFactory { private alertTypeRegistry!: AlertTypeRegistry; private securityPluginSetup?: SecurityPluginSetup; private getSpaceId!: (request: KibanaRequest) => string | undefined; + private getSpace!: (request: KibanaRequest) => Promise; private spaceIdToNamespace!: SpaceIdToNamespaceFunction; private encryptedSavedObjectsClient!: EncryptedSavedObjectsClient; private actions!: ActionsPluginStartContract; @@ -47,6 +50,7 @@ export class AlertsClientFactory { this.isInitialized = true; this.logger = options.logger; this.getSpaceId = options.getSpaceId; + this.getSpace = options.getSpace; this.taskManager = options.taskManager; this.alertTypeRegistry = options.alertTypeRegistry; this.securityPluginSetup = options.securityPluginSetup; @@ -63,6 +67,7 @@ export class AlertsClientFactory { authorization: securityPluginSetup?.authz, securityLicense: securityPluginSetup?.license, request, + getSpace: this.getSpace, alertTypeRegistry: this.alertTypeRegistry, features: features!, auditLogger: new AlertsAuthorizationAuditLogger( diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 06a9a4b892647..2a7150c8a4f65 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -16,6 +16,7 @@ import { } from './alerts_authorization'; import { alertsAuthorizationAuditLoggerMock } from './audit_logger.mock'; import { AlertsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; +import uuid from 'uuid'; const alertTypeRegistry = alertTypeRegistryMock.create(); const features: jest.Mocked = featuresPluginMock.createStart(); @@ -24,6 +25,8 @@ const request = {} as KibanaRequest; const auditLogger = alertsAuthorizationAuditLoggerMock.create(); const realAuditLogger = new AlertsAuthorizationAuditLogger(); +const getSpace = jest.fn(); + const mockAuthorizationAction = (type: string, app: string, operation: string) => `${type}/${app}/${operation}`; function mockSecurity() { @@ -42,6 +45,11 @@ function mockFeature(appName: string, typeName?: string) { id: appName, name: appName, app: [], + ...(typeName + ? { + alerting: [typeName], + } + : {}), privileges: { all: { ...(typeName @@ -80,6 +88,11 @@ function mockFeatureWithSubFeature(appName: string, typeName: string) { id: appName, name: appName, app: [], + ...(typeName + ? { + alerting: [typeName], + } + : {}), privileges: { all: { savedObject: { @@ -167,1049 +180,1092 @@ beforeEach(() => { myAppWithSubFeature, myFeatureWithoutAlerting, ]); + getSpace.mockResolvedValue(undefined); }); -describe('ensureAuthorized', () => { - test('is a no-op when there is no authorization api', async () => { - const alertAuthorization = new AlertsAuthorization({ - request, - alertTypeRegistry, - features, - auditLogger, - }); - - await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); - - expect(alertTypeRegistry.get).toHaveBeenCalledTimes(0); - }); - - test('is a no-op when the security license is disabled', async () => { - const { authorization, securityLicense } = mockSecurity(); - securityLicense.isEnabled.mockReturnValue(false); - const alertAuthorization = new AlertsAuthorization({ - request, - alertTypeRegistry, - authorization, - securityLicense, - features, - auditLogger, +describe('AlertsAuthorization', () => { + describe('constructor', () => { + test(`fetches the user's current space`, async () => { + const space = { + id: uuid.v4(), + name: uuid.v4(), + disabledFeatures: [], + }; + getSpace.mockResolvedValue(space); + + new AlertsAuthorization({ + request, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + + expect(getSpace).toHaveBeenCalledWith(request); }); - - await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); - - expect(alertTypeRegistry.get).toHaveBeenCalledTimes(0); }); - test('ensures the user has privileges to execute the specified type, operation and consumer', async () => { - const { authorization, securityLicense } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - const alertAuthorization = new AlertsAuthorization({ - request, - authorization, - securityLicense, - alertTypeRegistry, - features, - auditLogger, - }); - - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: true, - privileges: [], - }); - - await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); - - expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); - expect(checkPrivileges).toHaveBeenCalledWith([ - mockAuthorizationAction('myType', 'myApp', 'create'), - ]); - - expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); - expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", - "myType", - 0, - "myApp", - "create", - ] - `); - }); - - test('ensures the user has privileges to execute the specified type and operation without consumer when consumer is alerts', async () => { - const { authorization, securityLicense } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - const alertAuthorization = new AlertsAuthorization({ - request, - authorization, - securityLicense, - alertTypeRegistry, - features, - auditLogger, - }); - - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: true, - privileges: [], - }); + describe('ensureAuthorized', () => { + test('is a no-op when there is no authorization api', async () => { + const alertAuthorization = new AlertsAuthorization({ + request, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); - await alertAuthorization.ensureAuthorized('myType', 'alerts', WriteOperations.Create); - - expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); - expect(checkPrivileges).toHaveBeenCalledWith([ - mockAuthorizationAction('myType', 'myApp', 'create'), - ]); - - expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); - expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", - "myType", - 0, - "alerts", - "create", - ] - `); - }); + await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); - test('ensures the user has privileges to execute the specified type, operation and producer when producer is different from consumer', async () => { - const { authorization, securityLicense } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: true, - privileges: [], + expect(alertTypeRegistry.get).toHaveBeenCalledTimes(0); }); - const alertAuthorization = new AlertsAuthorization({ - request, - authorization, - securityLicense, - alertTypeRegistry, - features, - auditLogger, + test('is a no-op when the security license is disabled', async () => { + const { authorization, securityLicense } = mockSecurity(); + securityLicense.isEnabled.mockReturnValue(false); + const alertAuthorization = new AlertsAuthorization({ + request, + alertTypeRegistry, + authorization, + securityLicense, + features, + auditLogger, + getSpace, + }); + + await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); + + expect(alertTypeRegistry.get).toHaveBeenCalledTimes(0); }); - await alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create); - - expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); - - expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myOtherApp', - 'create' - ); - expect(checkPrivileges).toHaveBeenCalledWith([ - mockAuthorizationAction('myType', 'myOtherApp', 'create'), - mockAuthorizationAction('myType', 'myApp', 'create'), - ]); - - expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); - expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", - "myType", - 0, - "myOtherApp", - "create", - ] - `); - }); - - test('throws if user lacks the required privieleges for the consumer', async () => { - const { authorization, securityLicense } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - const alertAuthorization = new AlertsAuthorization({ - request, - authorization, - securityLicense, - alertTypeRegistry, - features, - auditLogger, + test('ensures the user has privileges to execute the specified type, operation and consumer', async () => { + const { authorization, securityLicense } = mockSecurity(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + securityLicense, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: [], + }); + + await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); + + expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); + expect(checkPrivileges).toHaveBeenCalledWith([ + mockAuthorizationAction('myType', 'myApp', 'create'), + ]); + + expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); + expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myType", + 0, + "myApp", + "create", + ] + `); }); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myType', 'myApp', 'create'), - authorized: true, - }, - ], + test('ensures the user has privileges to execute the specified type and operation without consumer when consumer is alerts', async () => { + const { authorization, securityLicense } = mockSecurity(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + securityLicense, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: [], + }); + + await alertAuthorization.ensureAuthorized('myType', 'alerts', WriteOperations.Create); + + expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); + expect(checkPrivileges).toHaveBeenCalledWith([ + mockAuthorizationAction('myType', 'myApp', 'create'), + ]); + + expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); + expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myType", + 0, + "alerts", + "create", + ] + `); }); - await expect( - alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` - ); - - expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", - "myType", - 0, - "myOtherApp", - "create", - ] - `); - }); - - test('throws if user lacks the required privieleges for the producer', async () => { - const { authorization, securityLicense } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - const alertAuthorization = new AlertsAuthorization({ - request, - authorization, - securityLicense, - alertTypeRegistry, - features, - auditLogger, + test('ensures the user has privileges to execute the specified type, operation and producer when producer is different from consumer', async () => { + const { authorization, securityLicense } = mockSecurity(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: [], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + securityLicense, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + + await alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create); + + expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); + + expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myOtherApp', + 'create' + ); + expect(checkPrivileges).toHaveBeenCalledWith([ + mockAuthorizationAction('myType', 'myOtherApp', 'create'), + mockAuthorizationAction('myType', 'myApp', 'create'), + ]); + + expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); + expect(auditLogger.alertsAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myType", + 0, + "myOtherApp", + "create", + ] + `); }); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myType', 'myApp', 'create'), - authorized: false, - }, - ], - }); + test('throws if user lacks the required privieleges for the consumer', async () => { + const { authorization, securityLicense } = mockSecurity(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + securityLicense, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myType', 'myApp', 'create'), + authorized: true, + }, + ], + }); - await expect( - alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unauthorized to create a \\"myType\\" alert by \\"myApp\\""` - ); - - expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", - "myType", - 1, - "myApp", - "create", - ] - `); - }); + await expect( + alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` + ); - test('throws if user lacks the required privieleges for both consumer and producer', async () => { - const { authorization, securityLicense } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - const alertAuthorization = new AlertsAuthorization({ - request, - authorization, - securityLicense, - alertTypeRegistry, - features, - auditLogger, + expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myType", + 0, + "myOtherApp", + "create", + ] + `); }); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myType', 'myApp', 'create'), - authorized: false, - }, - ], - }); + test('throws if user lacks the required privieleges for the producer', async () => { + const { authorization, securityLicense } = mockSecurity(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + securityLicense, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myType', 'myApp', 'create'), + authorized: false, + }, + ], + }); - await expect( - alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` - ); - - expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", - "myType", - 0, - "myOtherApp", - "create", - ] - `); - }); -}); + await expect( + alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized to create a \\"myType\\" alert by \\"myApp\\""` + ); -describe('getFindAuthorizationFilter', () => { - const myOtherAppAlertType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - id: 'myOtherAppAlertType', - name: 'myOtherAppAlertType', - producer: 'alerts', - }; - const myAppAlertType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - id: 'myAppAlertType', - name: 'myAppAlertType', - producer: 'myApp', - }; - const mySecondAppAlertType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - id: 'mySecondAppAlertType', - name: 'mySecondAppAlertType', - producer: 'myApp', - }; - const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]); - - test('omits filter when there is no authorization api', async () => { - const alertAuthorization = new AlertsAuthorization({ - request, - alertTypeRegistry, - features, - auditLogger, + expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myType", + 1, + "myApp", + "create", + ] + `); }); - const { - filter, - ensureAlertTypeIsAuthorized, - } = await alertAuthorization.getFindAuthorizationFilter(); + test('throws if user lacks the required privieleges for both consumer and producer', async () => { + const { authorization, securityLicense } = mockSecurity(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + securityLicense, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myType', 'myApp', 'create'), + authorized: false, + }, + ], + }); - expect(() => ensureAlertTypeIsAuthorized('someMadeUpType', 'myApp')).not.toThrow(); + await expect( + alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` + ); - expect(filter).toEqual(undefined); - }); - - test('ensureAlertTypeIsAuthorized is no-op when there is no authorization api', async () => { - const alertAuthorization = new AlertsAuthorization({ - request, - alertTypeRegistry, - features, - auditLogger, + expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myType", + 0, + "myOtherApp", + "create", + ] + `); }); - - const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); - - ensureAlertTypeIsAuthorized('someMadeUpType', 'myApp'); - - expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); }); - test('creates a filter based on the privileged types', async () => { - const { authorization, securityLicense } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: true, - privileges: [], + describe('getFindAuthorizationFilter', () => { + const myOtherAppAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myOtherAppAlertType', + name: 'myOtherAppAlertType', + producer: 'alerts', + }; + const myAppAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + }; + const mySecondAppAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'mySecondAppAlertType', + name: 'mySecondAppAlertType', + producer: 'myApp', + }; + const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]); + + test('omits filter when there is no authorization api', async () => { + const alertAuthorization = new AlertsAuthorization({ + request, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + + const { + filter, + ensureAlertTypeIsAuthorized, + } = await alertAuthorization.getFindAuthorizationFilter(); + + expect(() => ensureAlertTypeIsAuthorized('someMadeUpType', 'myApp')).not.toThrow(); + + expect(filter).toEqual(undefined); }); - const alertAuthorization = new AlertsAuthorization({ - request, - authorization, - securityLicense, - alertTypeRegistry, - features, - auditLogger, - }); - alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + test('ensureAlertTypeIsAuthorized is no-op when there is no authorization api', async () => { + const alertAuthorization = new AlertsAuthorization({ + request, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); - expect((await alertAuthorization.getFindAuthorizationFilter()).filter).toMatchInlineSnapshot( - `"((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:myOtherAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:mySecondAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)))"` - ); + const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); - expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); - }); + ensureAlertTypeIsAuthorized('someMadeUpType', 'myApp'); - test('creates an `ensureAlertTypeIsAuthorized` function which throws if type is unauthorized', async () => { - const { authorization, securityLicense } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), - authorized: false, - }, - ], + expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); }); - const alertAuthorization = new AlertsAuthorization({ - request, - authorization, - securityLicense, - alertTypeRegistry, - features, - auditLogger, + test('creates a filter based on the privileged types', async () => { + const { authorization, securityLicense } = mockSecurity(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: [], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + securityLicense, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + expect((await alertAuthorization.getFindAuthorizationFilter()).filter).toMatchInlineSnapshot( + `"((alert.attributes.alertTypeId:myAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:myOtherAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (alert.attributes.alertTypeId:mySecondAppAlertType and alert.attributes.consumer:(alerts or myApp or myOtherApp or myAppWithSubFeature)))"` + ); + + expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); }); - alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - - const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); - expect(() => { - ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); - }).toThrowErrorMatchingInlineSnapshot( - `"Unauthorized to find a \\"myAppAlertType\\" alert for \\"myOtherApp\\""` - ); - - expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); - expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", - "myAppAlertType", - 0, - "myOtherApp", - "find", - ] - `); - }); - test('creates an `ensureAlertTypeIsAuthorized` function which is no-op if type is authorized', async () => { - const { authorization, securityLicense } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), - authorized: true, - }, - ], - }); - - const alertAuthorization = new AlertsAuthorization({ - request, - authorization, - securityLicense, - alertTypeRegistry, - features, - auditLogger, + test('creates an `ensureAlertTypeIsAuthorized` function which throws if type is unauthorized', async () => { + const { authorization, securityLicense } = mockSecurity(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), + authorized: false, + }, + ], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + securityLicense, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); + expect(() => { + ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); + }).toThrowErrorMatchingInlineSnapshot( + `"Unauthorized to find a \\"myAppAlertType\\" alert for \\"myOtherApp\\""` + ); + + expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.alertsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(auditLogger.alertsAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myAppAlertType", + 0, + "myOtherApp", + "find", + ] + `); }); - alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - - const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); - expect(() => { - ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); - }).not.toThrow(); - expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); - }); - - test('creates an `logSuccessfulAuthorization` function which logs every authorized type', async () => { - const { authorization, securityLicense } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('mySecondAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('mySecondAppAlertType', 'myOtherApp', 'find'), - authorized: true, - }, - ], + test('creates an `ensureAlertTypeIsAuthorized` function which is no-op if type is authorized', async () => { + const { authorization, securityLicense } = mockSecurity(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), + authorized: true, + }, + ], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + securityLicense, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + const { ensureAlertTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter(); + expect(() => { + ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); + }).not.toThrow(); + + expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); }); - const alertAuthorization = new AlertsAuthorization({ - request, - authorization, - securityLicense, - alertTypeRegistry, - features, - auditLogger, - }); - alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - - const { - ensureAlertTypeIsAuthorized, - logSuccessfulAuthorization, - } = await alertAuthorization.getFindAuthorizationFilter(); - expect(() => { - ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); - ensureAlertTypeIsAuthorized('mySecondAppAlertType', 'myOtherApp'); - ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); - }).not.toThrow(); - - expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); - expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); - - logSuccessfulAuthorization(); - - expect(auditLogger.alertsBulkAuthorizationSuccess).toHaveBeenCalledTimes(1); - expect(auditLogger.alertsBulkAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", + test('creates an `logSuccessfulAuthorization` function which logs every authorized type', async () => { + const { authorization, securityLicense } = mockSecurity(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('mySecondAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('mySecondAppAlertType', 'myOtherApp', 'find'), + authorized: true, + }, + ], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + securityLicense, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + const { + ensureAlertTypeIsAuthorized, + logSuccessfulAuthorization, + } = await alertAuthorization.getFindAuthorizationFilter(); + expect(() => { + ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); + ensureAlertTypeIsAuthorized('mySecondAppAlertType', 'myOtherApp'); + ensureAlertTypeIsAuthorized('myAppAlertType', 'myOtherApp'); + }).not.toThrow(); + + expect(auditLogger.alertsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); + + logSuccessfulAuthorization(); + + expect(auditLogger.alertsBulkAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.alertsBulkAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ + "some-user", Array [ - "myAppAlertType", - "myOtherApp", - ], - Array [ - "mySecondAppAlertType", - "myOtherApp", + Array [ + "myAppAlertType", + "myOtherApp", + ], + Array [ + "mySecondAppAlertType", + "myOtherApp", + ], ], - ], - 0, - "find", - ] - `); + 0, + "find", + ] + `); + }); }); -}); -describe('filterByAlertTypeAuthorization', () => { - const myOtherAppAlertType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - id: 'myOtherAppAlertType', - name: 'myOtherAppAlertType', - producer: 'myOtherApp', - }; - const myAppAlertType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - id: 'myAppAlertType', - name: 'myAppAlertType', - producer: 'myApp', - }; - const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType]); - - test('augments a list of types with all features when there is no authorization api', async () => { - const alertAuthorization = new AlertsAuthorization({ - request, - alertTypeRegistry, - features, - auditLogger, - }); - alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - - await expect( - alertAuthorization.filterByAlertTypeAuthorization( - new Set([myAppAlertType, myOtherAppAlertType]), - [WriteOperations.Create] - ) - ).resolves.toMatchInlineSnapshot(` - Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "alerts": Object { - "all": true, - "read": true, - }, - "myApp": Object { - "all": true, - "read": true, - }, - "myAppWithSubFeature": Object { - "all": true, - "read": true, - }, - "myOtherApp": Object { - "all": true, - "read": true, + describe('filterByAlertTypeAuthorization', () => { + const myOtherAppAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myOtherAppAlertType', + name: 'myOtherAppAlertType', + producer: 'myOtherApp', + }; + const myAppAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + }; + const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType]); + + test('augments a list of types with all features when there is no authorization api', async () => { + const alertAuthorization = new AlertsAuthorization({ + request, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.filterByAlertTypeAuthorization( + new Set([myAppAlertType, myOtherAppAlertType]), + [WriteOperations.Create] + ) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + "myAppWithSubFeature": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, }, + "defaultActionGroupId": "default", + "id": "myAppAlertType", + "name": "myAppAlertType", + "producer": "myApp", }, - "defaultActionGroupId": "default", - "id": "myAppAlertType", - "name": "myAppAlertType", - "producer": "myApp", - }, - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "alerts": Object { - "all": true, - "read": true, - }, - "myApp": Object { - "all": true, - "read": true, - }, - "myAppWithSubFeature": Object { - "all": true, - "read": true, - }, - "myOtherApp": Object { - "all": true, - "read": true, + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + "myAppWithSubFeature": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, }, + "defaultActionGroupId": "default", + "id": "myOtherAppAlertType", + "name": "myOtherAppAlertType", + "producer": "myOtherApp", }, - "defaultActionGroupId": "default", - "id": "myOtherAppAlertType", - "name": "myOtherAppAlertType", - "producer": "myOtherApp", - }, - } - `); - }); - - test('augments a list of types with consumers under which the operation is authorized', async () => { - const { authorization, securityLicense } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), - authorized: true, - }, - ], + } + `); }); - const alertAuthorization = new AlertsAuthorization({ - request, - authorization, - securityLicense, - alertTypeRegistry, - features, - auditLogger, - }); - alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - - await expect( - alertAuthorization.filterByAlertTypeAuthorization( - new Set([myAppAlertType, myOtherAppAlertType]), - [WriteOperations.Create] - ) - ).resolves.toMatchInlineSnapshot(` - Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "myApp": Object { - "all": true, - "read": true, + test('augments a list of types with consumers under which the operation is authorized', async () => { + const { authorization, securityLicense } = mockSecurity(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + authorized: true, + }, + ], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + securityLicense, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.filterByAlertTypeAuthorization( + new Set([myAppAlertType, myOtherAppAlertType]), + [WriteOperations.Create] + ) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": true, + "read": true, + }, }, + "defaultActionGroupId": "default", + "id": "myOtherAppAlertType", + "name": "myOtherAppAlertType", + "producer": "myOtherApp", }, - "defaultActionGroupId": "default", - "id": "myOtherAppAlertType", - "name": "myOtherAppAlertType", - "producer": "myOtherApp", - }, - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "alerts": Object { - "all": true, - "read": true, - }, - "myApp": Object { - "all": true, - "read": true, - }, - "myOtherApp": Object { - "all": true, - "read": true, + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, }, + "defaultActionGroupId": "default", + "id": "myAppAlertType", + "name": "myAppAlertType", + "producer": "myApp", }, - "defaultActionGroupId": "default", - "id": "myAppAlertType", - "name": "myAppAlertType", - "producer": "myApp", - }, - } - `); - }); - - test('authorizes user under the Alerts consumer when they are authorized by the producer', async () => { - const { authorization, securityLicense } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), - authorized: false, - }, - ], + } + `); }); - const alertAuthorization = new AlertsAuthorization({ - request, - authorization, - securityLicense, - alertTypeRegistry, - features, - auditLogger, - }); - alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - - await expect( - alertAuthorization.filterByAlertTypeAuthorization(new Set([myAppAlertType]), [ - WriteOperations.Create, - ]) - ).resolves.toMatchInlineSnapshot(` - Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "alerts": Object { - "all": true, - "read": true, - }, - "myApp": Object { - "all": true, - "read": true, + test('authorizes user under the Alerts consumer when they are authorized by the producer', async () => { + const { authorization, securityLicense } = mockSecurity(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + ], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + securityLicense, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.filterByAlertTypeAuthorization(new Set([myAppAlertType]), [ + WriteOperations.Create, + ]) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, }, + "defaultActionGroupId": "default", + "id": "myAppAlertType", + "name": "myAppAlertType", + "producer": "myApp", }, - "defaultActionGroupId": "default", - "id": "myAppAlertType", - "name": "myAppAlertType", - "producer": "myApp", - }, - } - `); - }); - - test('augments a list of types with consumers under which multiple operations are authorized', async () => { - const { authorization, securityLicense } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'get'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'get'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'get'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'get'), - authorized: true, - }, - ], + } + `); }); - const alertAuthorization = new AlertsAuthorization({ - request, - authorization, - securityLicense, - alertTypeRegistry, - features, - auditLogger, - }); - alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - - await expect( - alertAuthorization.filterByAlertTypeAuthorization( - new Set([myAppAlertType, myOtherAppAlertType]), - [WriteOperations.Create, ReadOperations.Get] - ) - ).resolves.toMatchInlineSnapshot(` - Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "alerts": Object { - "all": false, - "read": true, - }, - "myApp": Object { - "all": true, - "read": true, - }, - "myOtherApp": Object { - "all": false, - "read": true, + test('augments a list of types with consumers under which multiple operations are authorized', async () => { + const { authorization, securityLicense } = mockSecurity(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'get'), + authorized: true, + }, + ], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + securityLicense, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.filterByAlertTypeAuthorization( + new Set([myAppAlertType, myOtherAppAlertType]), + [WriteOperations.Create, ReadOperations.Get] + ) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "alerts": Object { + "all": false, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": false, + "read": true, + }, }, + "defaultActionGroupId": "default", + "id": "myOtherAppAlertType", + "name": "myOtherAppAlertType", + "producer": "myOtherApp", }, - "defaultActionGroupId": "default", - "id": "myOtherAppAlertType", - "name": "myOtherAppAlertType", - "producer": "myOtherApp", - }, - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "alerts": Object { - "all": false, - "read": true, - }, - "myApp": Object { - "all": false, - "read": true, - }, - "myOtherApp": Object { - "all": false, - "read": true, + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "alerts": Object { + "all": false, + "read": true, + }, + "myApp": Object { + "all": false, + "read": true, + }, + "myOtherApp": Object { + "all": false, + "read": true, + }, }, + "defaultActionGroupId": "default", + "id": "myAppAlertType", + "name": "myAppAlertType", + "producer": "myApp", }, - "defaultActionGroupId": "default", - "id": "myAppAlertType", - "name": "myAppAlertType", - "producer": "myApp", - }, - } - `); - }); - - test('omits types which have no consumers under which the operation is authorized', async () => { - const { authorization, securityLicense } = mockSecurity(); - const checkPrivileges: jest.MockedFunction> = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), - authorized: false, - }, - ], + } + `); }); - const alertAuthorization = new AlertsAuthorization({ - request, - authorization, - securityLicense, - alertTypeRegistry, - features, - auditLogger, - }); - alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - - await expect( - alertAuthorization.filterByAlertTypeAuthorization( - new Set([myAppAlertType, myOtherAppAlertType]), - [WriteOperations.Create] - ) - ).resolves.toMatchInlineSnapshot(` - Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "alerts": Object { - "all": true, - "read": true, - }, - "myApp": Object { - "all": true, - "read": true, - }, - "myOtherApp": Object { - "all": true, - "read": true, + test('omits types which have no consumers under which the operation is authorized', async () => { + const { authorization, securityLicense } = mockSecurity(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + ], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + securityLicense, + alertTypeRegistry, + features, + auditLogger, + getSpace, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.filterByAlertTypeAuthorization( + new Set([myAppAlertType, myOtherAppAlertType]), + [WriteOperations.Create] + ) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, }, + "defaultActionGroupId": "default", + "id": "myOtherAppAlertType", + "name": "myOtherAppAlertType", + "producer": "myOtherApp", }, - "defaultActionGroupId": "default", - "id": "myOtherAppAlertType", - "name": "myOtherAppAlertType", - "producer": "myOtherApp", - }, - } - `); + } + `); + }); }); -}); -describe('ensureFieldIsSafeForQuery', () => { - test('throws if field contains character that isnt safe in a KQL query', () => { - expect(() => ensureFieldIsSafeForQuery('id', 'alert-*')).toThrowError( - `expected id not to include invalid character: *` - ); + describe('ensureFieldIsSafeForQuery', () => { + test('throws if field contains character that isnt safe in a KQL query', () => { + expect(() => ensureFieldIsSafeForQuery('id', 'alert-*')).toThrowError( + `expected id not to include invalid character: *` + ); - expect(() => ensureFieldIsSafeForQuery('id', '<=""')).toThrowError( - `expected id not to include invalid character: <=` - ); + expect(() => ensureFieldIsSafeForQuery('id', '<=""')).toThrowError( + `expected id not to include invalid character: <=` + ); - expect(() => ensureFieldIsSafeForQuery('id', '>=""')).toThrowError( - `expected id not to include invalid character: >=` - ); + expect(() => ensureFieldIsSafeForQuery('id', '>=""')).toThrowError( + `expected id not to include invalid character: >=` + ); - expect(() => ensureFieldIsSafeForQuery('id', '1 or alertid:123')).toThrowError( - `expected id not to include whitespace and invalid character: :` - ); + expect(() => ensureFieldIsSafeForQuery('id', '1 or alertid:123')).toThrowError( + `expected id not to include whitespace and invalid character: :` + ); - expect(() => ensureFieldIsSafeForQuery('id', ') or alertid:123')).toThrowError( - `expected id not to include whitespace and invalid characters: ), :` - ); + expect(() => ensureFieldIsSafeForQuery('id', ') or alertid:123')).toThrowError( + `expected id not to include whitespace and invalid characters: ), :` + ); - expect(() => ensureFieldIsSafeForQuery('id', 'some space')).toThrowError( - `expected id not to include whitespace` - ); - }); + expect(() => ensureFieldIsSafeForQuery('id', 'some space')).toThrowError( + `expected id not to include whitespace` + ); + }); - test('doesnt throws if field is safe as part of a KQL query', () => { - expect(() => ensureFieldIsSafeForQuery('id', '123-0456-678')).not.toThrow(); + test('doesnt throws if field is safe as part of a KQL query', () => { + expect(() => ensureFieldIsSafeForQuery('id', '123-0456-678')).not.toThrow(); + }); }); }); diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index c0e35c3282656..af67c5e40593c 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -7,15 +7,14 @@ import Boom from 'boom'; import { map, mapValues, remove, fromPairs, has } from 'lodash'; import { KibanaRequest } from 'src/core/server'; -import { RecursiveReadonly } from '@kbn/utility-types'; import { ALERTS_FEATURE_ID } from '../../common'; import { AlertTypeRegistry } from '../types'; import { SecurityPluginSetup } from '../../../security/server'; import { RegistryAlertType } from '../alert_type_registry'; -import { FeatureKibanaPrivileges, SubFeaturePrivilegeConfig } from '../../../features/common'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; import { SecurityLicense } from '../../../security/common/licensing'; +import { Space } from '../../../spaces/server'; export enum ReadOperations { Get = 'get', @@ -51,6 +50,7 @@ export interface ConstructorOptions { alertTypeRegistry: AlertTypeRegistry; request: KibanaRequest; features: FeaturesPluginStart; + getSpace: (request: KibanaRequest) => Promise; auditLogger: AlertsAuthorizationAuditLogger; authorization?: SecurityPluginSetup['authz']; securityLicense?: SecurityLicense; @@ -62,8 +62,8 @@ export class AlertsAuthorization { private readonly authorization?: SecurityPluginSetup['authz']; private readonly securityLicense?: SecurityLicense; private readonly auditLogger: AlertsAuthorizationAuditLogger; - private readonly featuresIds: string[]; - private readonly allPossibleConsumers: AuthorizedConsumers; + private readonly featuresIds: Promise>; + private readonly allPossibleConsumers: Promise; constructor({ alertTypeRegistry, @@ -72,6 +72,7 @@ export class AlertsAuthorization { securityLicense, features, auditLogger, + getSpace, }: ConstructorOptions) { this.request = request; this.authorization = authorization; @@ -79,28 +80,37 @@ export class AlertsAuthorization { this.alertTypeRegistry = alertTypeRegistry; this.auditLogger = auditLogger; - this.featuresIds = features - .getFeatures() - // ignore features which don't grant privileges to alerting - .filter(({ privileges, subFeatures }) => { - return ( - hasAnyAlertingPrivileges(privileges?.all) || - hasAnyAlertingPrivileges(privileges?.read) || - subFeatures.some((subFeature) => - subFeature.privilegeGroups.some((privilegeGroup) => - privilegeGroup.privileges.some((subPrivileges) => - hasAnyAlertingPrivileges(subPrivileges) + this.featuresIds = getSpace(request) + .then((maybeSpace) => new Set(maybeSpace?.disabledFeatures ?? [])) + .then( + (disabledFeatures) => + new Set( + features + .getFeatures() + .filter( + ({ id, alerting }) => + // ignore features which are disabled in the user's space + !disabledFeatures.has(id) && + // ignore features which don't grant privileges to alerting + (alerting?.length ?? 0 > 0) ) - ) + .map((feature) => feature.id) ) - ); - }) - .map((feature) => feature.id); - - this.allPossibleConsumers = asAuthorizedConsumers([ALERTS_FEATURE_ID, ...this.featuresIds], { - read: true, - all: true, - }); + ) + .catch(() => { + // failing to fetch the space means the user is likely not privileged in the + // active space at all, which means that their list of features should be empty + return new Set(); + }); + + this.allPossibleConsumers = this.featuresIds.then((featuresIds) => + featuresIds.size + ? asAuthorizedConsumers([ALERTS_FEATURE_ID, ...featuresIds], { + read: true, + all: true, + }) + : {} + ); } private shouldCheckAuthorization(): boolean { @@ -115,7 +125,7 @@ export class AlertsAuthorization { ) { const { authorization } = this; - const isKnownConsumer = has(this.allPossibleConsumers, consumer); + const isAvailableConsumer = has(await this.allPossibleConsumers, consumer); if (authorization && this.shouldCheckAuthorization()) { const alertType = this.alertTypeRegistry.get(alertTypeId); const requiredPrivilegesByScope = { @@ -143,7 +153,7 @@ export class AlertsAuthorization { ] ); - if (!isKnownConsumer) { + if (!isAvailableConsumer) { /** * Under most circumstances this would have been caught by `checkPrivileges` as * a user can't have Privileges to an unknown consumer, but super users @@ -195,7 +205,7 @@ export class AlertsAuthorization { ) ); } - } else if (!isKnownConsumer) { + } else if (!isAvailableConsumer) { throw Boom.forbidden( this.auditLogger.alertsAuthorizationFailure( '', @@ -303,6 +313,7 @@ export class AlertsAuthorization { hasAllRequested: boolean; authorizedAlertTypes: Set; }> { + const featuresIds = await this.featuresIds; if (this.authorization && this.shouldCheckAuthorization()) { const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( this.request @@ -320,7 +331,7 @@ export class AlertsAuthorization { // as we can't ask ES for the user's individual privileges we need to ask for each feature // and alertType in the system whether this user has this privilege for (const alertType of alertTypesWithAuthorization) { - for (const feature of this.featuresIds) { + for (const feature of featuresIds) { for (const operation of operations) { privilegeToAlertType.set( this.authorization!.actions.alerting.get(alertType.id, feature, operation), @@ -344,7 +355,7 @@ export class AlertsAuthorization { hasAllRequested, authorizedAlertTypes: hasAllRequested ? // has access to all features - this.augmentWithAuthorizedConsumers(alertTypes, this.allPossibleConsumers) + this.augmentWithAuthorizedConsumers(alertTypes, await this.allPossibleConsumers) : // only has some of the required privileges privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { if (authorized && privilegeToAlertType.has(privilege)) { @@ -375,8 +386,8 @@ export class AlertsAuthorization { return { hasAllRequested: true, authorizedAlertTypes: this.augmentWithAuthorizedConsumers( - alertTypes, - this.allPossibleConsumers + new Set([...alertTypes].filter((alertType) => featuresIds.has(alertType.producer))), + await this.allPossibleConsumers ), }; } @@ -428,16 +439,6 @@ export function ensureFieldIsSafeForQuery(field: string, value: string): boolean return true; } -function hasAnyAlertingPrivileges( - privileges?: - | RecursiveReadonly - | RecursiveReadonly -): boolean { - return ( - ((privileges?.alerting?.all?.length ?? 0) || (privileges?.alerting?.read?.length ?? 0)) > 0 - ); -} - function mergeHasPrivileges(left: HasPrivileges, right?: HasPrivileges): HasPrivileges { return { read: (left.read || right?.read) ?? false, diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 7a0916b9d6554..cf6e1c9aebba6 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -218,6 +218,9 @@ export class AlertingPlugin { getSpaceId(request: KibanaRequest) { return spaces?.getSpaceId(request); }, + async getSpace(request: KibanaRequest) { + return spaces?.getActiveSpace(request); + }, actions: plugins.actions, features: plugins.features, }); diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index 38d4e92c72a50..38e75f75ad04b 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -17,6 +17,7 @@ export const APM_FEATURE = { navLinkId: 'apm', app: ['apm', 'kibana'], catalogue: ['apm'], + alerting: Object.values(AlertType), // see x-pack/plugins/features/common/feature_kibana_privileges.ts privileges: { all: { diff --git a/x-pack/plugins/features/common/feature.ts b/x-pack/plugins/features/common/feature.ts index 4a293e0c962cc..1b700fb1a6ad0 100644 --- a/x-pack/plugins/features/common/feature.ts +++ b/x-pack/plugins/features/common/feature.ts @@ -94,6 +94,13 @@ export interface FeatureConfig { */ catalogue?: readonly string[]; + /** + * If your feature grants access to specific Alert Types, you can specify them here to control visibility based on the current space. + * Include both Alert Types registered by the feature and external Alert Types such as built-in + * Alert Types and Alert Types provided by other features to which you wish to grant access. + */ + alerting?: readonly string[]; + /** * Feature privilege definition. * @@ -179,6 +186,10 @@ export class Feature { return this.config.privileges; } + public get alerting() { + return this.config.alerting; + } + public get excludeFromBasePrivileges() { return this.config.excludeFromBasePrivileges ?? false; } diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 71c65a5fe0b0d..15ddbd9334c8d 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -26,6 +26,7 @@ const managementSchema = Joi.object().pattern( Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)) ); const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)); +const alertingSchema = Joi.array().items(Joi.string()); const privilegeSchema = Joi.object({ excludeFromBasePrivileges: Joi.boolean(), @@ -34,8 +35,8 @@ const privilegeSchema = Joi.object({ api: Joi.array().items(Joi.string()), app: Joi.array().items(Joi.string()), alerting: Joi.object({ - all: Joi.array().items(Joi.string()), - read: Joi.array().items(Joi.string()), + all: alertingSchema, + read: alertingSchema, }), savedObject: Joi.object({ all: Joi.array().items(Joi.string()).required(), @@ -86,6 +87,7 @@ const schema = Joi.object({ app: Joi.array().items(Joi.string()).required(), management: managementSchema, catalogue: catalogueSchema, + alerting: alertingSchema, privileges: Joi.object({ all: privilegeSchema, read: privilegeSchema, diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index 9cd216f6066d2..0de431186b151 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -19,6 +19,7 @@ export const METRICS_FEATURE = { navLinkId: 'metrics', app: ['infra', 'kibana'], catalogue: ['infraops'], + alerting: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], privileges: { all: { app: ['infra', 'kibana'], @@ -59,6 +60,7 @@ export const LOGS_FEATURE = { navLinkId: 'logs', app: ['infra', 'kibana'], catalogue: ['infralogging'], + alerting: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], privileges: { all: { app: ['infra', 'kibana'], diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 5f358badde401..a08734ff765bb 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -25,6 +25,7 @@ import { LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG, KIBANA_STATS_TYPE_MONITORING, + ALERTS, } from '../common/constants'; import { MonitoringConfig, createConfig, configSchema } from './config'; // @ts-ignore @@ -241,6 +242,7 @@ export class Plugin { app: ['monitoring', 'kibana'], catalogue: ['monitoring'], privileges: null, + alerting: ALERTS, reserved: { description: i18n.translate('xpack.monitoring.feature.reserved.description', { defaultMessage: 'To grant users access, you should also assign the monitoring_user role.', @@ -255,6 +257,9 @@ export class Plugin { all: [], read: [], }, + alerting: { + all: ALERTS, + }, ui: [], }, }, diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index a52081b296af0..91f072e5f4651 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -170,6 +170,7 @@ export class Plugin implements IPlugin { + loadTestFile(require.resolve('./find')); loadTestFile(require.resolve('./create')); loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./disable')); loadTestFile(require.resolve('./enable')); - loadTestFile(require.resolve('./find')); loadTestFile(require.resolve('./get')); loadTestFile(require.resolve('./get_alert_state')); loadTestFile(require.resolve('./list_alert_types')); 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 index 3b793feda632a..a497affa266e4 100644 --- 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 @@ -253,6 +253,17 @@ export default function createMuteAlertTests({ getService }: FtrProviderContext) switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'muteAll', + 'test.restricted-noop', + 'alerts' + ), + statusCode: 403, + }); + break; case 'global_read at space1': case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': 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 index 2e4da7eb4158c..b4277479d8fd9 100644 --- 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 @@ -253,6 +253,17 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'muteInstance', + 'test.restricted-noop', + 'alerts' + ), + statusCode: 403, + }); + break; case 'global_read at space1': case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': 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 index 2413bd42460d4..46653900cb1c7 100644 --- 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 @@ -273,6 +273,17 @@ export default function createUnmuteAlertTests({ getService }: FtrProviderContex switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'unmuteAll', + 'test.restricted-noop', + 'alerts' + ), + statusCode: 403, + }); + break; case 'global_read at space1': case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': 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 index d67ecedaab14c..2bc501c9a7c72 100644 --- 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 @@ -281,6 +281,17 @@ export default function createMuteAlertInstanceTests({ getService }: FtrProvider switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'unmuteInstance', + 'test.restricted-noop', + 'alerts' + ), + statusCode: 403, + }); + break; case 'global_read at space1': case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': 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 390b50acb3705..ab3a92d0b3f70 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 @@ -355,6 +355,17 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'update', + 'test.restricted-noop', + 'alerts' + ), + statusCode: 403, + }); + break; case 'global_read at space1': case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': 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 6e8956d3326ea..7dea591b895ee 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 @@ -245,6 +245,17 @@ export default function createUpdateApiKeyTests({ getService }: FtrProviderConte switch (scenario.id) { case 'no_kibana_privileges at space1': case 'space_1_all at space2': + expect(response.statusCode).to.eql(403); + expect(response.body).to.eql({ + error: 'Forbidden', + message: getConsumerUnauthorizedErrorMessage( + 'updateApiKey', + 'test.restricted-noop', + 'alerts' + ), + statusCode: 403, + }); + break; case 'global_read at space1': case 'space_1_all at space1': case 'space_1_all_alerts_none_actions at space1': diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index c750eb61fbee7..dd81c860e9fa8 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -25,6 +25,7 @@ export class AlertingFixturePlugin implements Plugin Date: Fri, 17 Jul 2020 16:44:48 +0100 Subject: [PATCH 117/126] corrected consumer on enable operation --- .../security_and_spaces/tests/alerting/enable.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 412149fd27837..d7f6546bf34a9 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 @@ -78,7 +78,11 @@ export default function createEnableAlertTests({ getService }: FtrProviderContex expect(response.statusCode).to.eql(403); expect(response.body).to.eql({ error: 'Forbidden', - message: getConsumerUnauthorizedErrorMessage('enable', 'test.noop', 'alerts'), + message: getConsumerUnauthorizedErrorMessage( + 'enable', + 'test.noop', + 'alertsFixture' + ), statusCode: 403, }); break; From e2fff84cace96dcb43e518a866d0c80170619a46 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 20 Jul 2020 09:59:16 +0100 Subject: [PATCH 118/126] added valifation of the alerting privileges at feature level --- .../features/server/feature_registry.test.ts | 162 ++++++++++++++++++ .../plugins/features/server/feature_schema.ts | 37 +++- 2 files changed, 198 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts index 75022922917b3..f123068e41758 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -743,6 +743,168 @@ describe('FeatureRegistry', () => { ); }); + it(`prevents privileges from specifying alerting entries that don't exist at the root level`, () => { + const feature: FeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + alerting: ['bar'], + privileges: { + all: { + alerting: { + all: ['foo', 'bar'], + read: ['baz'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + read: { + alerting: { read: ['foo', 'bar', 'baz'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown alerting entries: foo, baz"` + ); + }); + + it(`prevents features from specifying alerting entries that don't exist at the privilege level`, () => { + const feature: FeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + alerting: ['foo', 'bar', 'baz'], + privileges: { + all: { + alerting: { all: ['foo'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + read: { + alerting: { all: ['foo'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + subFeatures: [ + { + name: 'my sub feature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cool-sub-feature-privilege', + name: 'cool privilege', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], + alerting: { all: ['bar'] }, + }, + ], + }, + ], + }, + ], + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"` + ); + }); + + it(`prevents reserved privileges from specifying alerting entries that don't exist at the root level`, () => { + const feature: FeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + alerting: ['bar'], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + alerting: { all: ['foo', 'bar', 'baz'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown alerting entries: foo, baz"` + ); + }); + + it(`prevents features from specifying alerting entries that don't exist at the reserved privilege level`, () => { + const feature: FeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + alerting: ['foo', 'bar', 'baz'], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + alerting: { all: ['foo', 'bar'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"` + ); + }); + it(`prevents privileges from specifying management sections that don't exist at the root level`, () => { const feature: FeatureConfig = { id: 'test-feature', diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 15ddbd9334c8d..95298603d706a 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -51,6 +51,10 @@ const subFeaturePrivilegeSchema = Joi.object({ includeIn: Joi.string().allow('all', 'read', 'none').required(), management: managementSchema, catalogue: catalogueSchema, + alerting: Joi.object({ + all: alertingSchema, + read: alertingSchema, + }), api: Joi.array().items(Joi.string()), app: Joi.array().items(Joi.string()), savedObject: Joi.object({ @@ -119,7 +123,7 @@ export function validateFeature(feature: FeatureConfig) { throw validateResult.error; } // the following validation can't be enforced by the Joi schema, since it'd require us looking "up" the object graph for the list of valid value, which they explicitly forbid. - const { app = [], management = {}, catalogue = [] } = feature; + const { app = [], management = {}, catalogue = [], alerting = [] } = feature; const unseenApps = new Set(app); @@ -132,6 +136,8 @@ export function validateFeature(feature: FeatureConfig) { const unseenCatalogue = new Set(catalogue); + const unseenAlertTypes = new Set(alerting); + function validateAppEntry(privilegeId: string, entry: readonly string[] = []) { entry.forEach((privilegeApp) => unseenApps.delete(privilegeApp)); @@ -158,6 +164,23 @@ export function validateFeature(feature: FeatureConfig) { } } + function validateAlertingEntry(privilegeId: string, entry: FeatureKibanaPrivileges['alerting']) { + const all = entry?.all ?? []; + const read = entry?.read ?? []; + + all.forEach((privilegeAlertTypes) => unseenAlertTypes.delete(privilegeAlertTypes)); + read.forEach((privilegeAlertTypes) => unseenAlertTypes.delete(privilegeAlertTypes)); + + const unknownAlertingEntries = difference([...all, ...read], alerting); + if (unknownAlertingEntries.length > 0) { + throw new Error( + `Feature privilege ${ + feature.id + }.${privilegeId} has unknown alerting entries: ${unknownAlertingEntries.join(', ')}` + ); + } + } + function validateManagementEntry( privilegeId: string, managementEntry: Record = {} @@ -218,6 +241,7 @@ export function validateFeature(feature: FeatureConfig) { validateCatalogueEntry(privilegeId, privilegeDefinition.catalogue); validateManagementEntry(privilegeId, privilegeDefinition.management); + validateAlertingEntry(privilegeId, privilegeDefinition.alerting); }); const subFeatureEntries = feature.subFeatures ?? []; @@ -227,6 +251,7 @@ export function validateFeature(feature: FeatureConfig) { validateAppEntry(subFeaturePrivilege.id, subFeaturePrivilege.app); validateCatalogueEntry(subFeaturePrivilege.id, subFeaturePrivilege.catalogue); validateManagementEntry(subFeaturePrivilege.id, subFeaturePrivilege.management); + validateAlertingEntry(subFeaturePrivilege.id, subFeaturePrivilege.alerting); }); }); }); @@ -267,4 +292,14 @@ export function validateFeature(feature: FeatureConfig) { )}` ); } + + if (unseenAlertTypes.size > 0) { + throw new Error( + `Feature ${ + feature.id + } specifies alerting entries which are not granted to any privileges: ${Array.from( + unseenAlertTypes.values() + ).join(',')}` + ); + } } From d7ecd8675ef496b731a00f0953066d0a0426be63 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 20 Jul 2020 10:26:11 +0100 Subject: [PATCH 119/126] corrected security check for rbac --- .../actions_authorization.test.ts | 18 +++---- .../authorization/actions_authorization.ts | 12 ++--- x-pack/plugins/actions/server/plugin.ts | 1 - .../alerts/server/alerts_client_factory.ts | 1 - .../alerts_authorization.test.ts | 51 +++++++------------ .../authorization/alerts_authorization.ts | 8 +-- 6 files changed, 30 insertions(+), 61 deletions(-) diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts index 4fded7b9e1bce..a48124cdbcb6a 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts @@ -19,12 +19,12 @@ const mockAuthorizationAction = (type: string, operation: string) => `${type}/${ function mockSecurity() { const security = securityMock.createSetup(); const authorization = security.authz; - const securityLicense = security.license; // typescript is having trouble inferring jest's automocking (authorization.actions.savedObject.get as jest.MockedFunction< typeof authorization.actions.savedObject.get >).mockImplementation(mockAuthorizationAction); - return { authorization, securityLicense }; + authorization.mode.useRbacForRequest.mockReturnValue(true); + return { authorization }; } beforeEach(() => { @@ -48,12 +48,11 @@ describe('ensureAuthorized', () => { }); test('is a no-op when the security license is disabled', async () => { - const { authorization, securityLicense } = mockSecurity(); - securityLicense.isEnabled.mockReturnValue(false); + const { authorization } = mockSecurity(); + authorization.mode.useRbacForRequest.mockReturnValue(false); const actionsAuthorization = new ActionsAuthorization({ request, authorization, - securityLicense, auditLogger, }); @@ -61,13 +60,12 @@ describe('ensureAuthorized', () => { }); test('ensures the user has privileges to use the operation on the Actions Saved Object type', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const actionsAuthorization = new ActionsAuthorization({ - securityLicense, request, authorization, auditLogger, @@ -101,13 +99,12 @@ describe('ensureAuthorized', () => { }); test('ensures the user has privileges to execute an Actions Saved Object type', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const actionsAuthorization = new ActionsAuthorization({ - securityLicense, request, authorization, auditLogger, @@ -151,13 +148,12 @@ describe('ensureAuthorized', () => { }); test('throws if user lacks the required privieleges', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const actionsAuthorization = new ActionsAuthorization({ - securityLicense, request, authorization, auditLogger, diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.ts index 41396a1243ea9..da5a5a1cdc3eb 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.ts @@ -9,13 +9,11 @@ import { KibanaRequest } from 'src/core/server'; import { SecurityPluginSetup } from '../../../security/server'; import { ActionsAuthorizationAuditLogger } from './audit_logger'; import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects'; -import { SecurityLicense } from '../../../security/common/licensing'; export interface ConstructorOptions { request: KibanaRequest; auditLogger: ActionsAuthorizationAuditLogger; authorization?: SecurityPluginSetup['authz']; - securityLicense?: SecurityLicense; } const operationAlias: Record< @@ -26,25 +24,23 @@ const operationAlias: Record< authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, 'get'), authorization.actions.savedObject.get(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'), ], - list: (authorization) => authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, 'get'), + list: (authorization) => authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, 'find'), }; export class ActionsAuthorization { private readonly request: KibanaRequest; private readonly authorization?: SecurityPluginSetup['authz']; - private readonly securityLicense?: SecurityLicense; private readonly auditLogger: ActionsAuthorizationAuditLogger; - constructor({ request, authorization, securityLicense, auditLogger }: ConstructorOptions) { + constructor({ request, authorization, auditLogger }: ConstructorOptions) { this.request = request; this.authorization = authorization; - this.securityLicense = securityLicense; this.auditLogger = auditLogger; } public async ensureAuthorized(operation: string, actionTypeId?: string) { - const { authorization, securityLicense } = this; - if (authorization && securityLicense?.isEnabled()) { + const { authorization } = this; + if (authorization?.mode?.useRbacForRequest(this.request)) { const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); const { hasAllRequested, username } = await checkPrivileges( operationAlias[operation] diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 9a03bee41eeea..62bd1058774de 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -323,7 +323,6 @@ export class ActionsPlugin implements Plugin, Plugi return new ActionsAuthorization({ request, authorization: this.security?.authz, - securityLicense: this.security?.license, auditLogger: new ActionsAuthorizationAuditLogger( this.security?.audit.getLogger(ACTIONS_FEATURE.id) ), diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.ts b/x-pack/plugins/alerts/server/alerts_client_factory.ts index 6d1cde2485407..79b0ccaf1f0bc 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.ts @@ -65,7 +65,6 @@ export class AlertsClientFactory { const spaceId = this.getSpaceId(request); const authorization = new AlertsAuthorization({ authorization: securityPluginSetup?.authz, - securityLicense: securityPluginSetup?.license, request, getSpace: this.getSpace, alertTypeRegistry: this.alertTypeRegistry, diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 2a7150c8a4f65..b164d27ded648 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -32,12 +32,12 @@ const mockAuthorizationAction = (type: string, app: string, operation: string) = function mockSecurity() { const security = securityMock.createSetup(); const authorization = security.authz; - const securityLicense = security.license; // typescript is having trouble inferring jest's automocking (authorization.actions.alerting.get as jest.MockedFunction< typeof authorization.actions.alerting.get >).mockImplementation(mockAuthorizationAction); - return { authorization, securityLicense }; + authorization.mode.useRbacForRequest.mockReturnValue(true); + return { authorization }; } function mockFeature(appName: string, typeName?: string) { @@ -221,13 +221,12 @@ describe('AlertsAuthorization', () => { }); test('is a no-op when the security license is disabled', async () => { - const { authorization, securityLicense } = mockSecurity(); - securityLicense.isEnabled.mockReturnValue(false); + const { authorization } = mockSecurity(); + authorization.mode.useRbacForRequest.mockReturnValue(false); const alertAuthorization = new AlertsAuthorization({ request, alertTypeRegistry, authorization, - securityLicense, features, auditLogger, getSpace, @@ -239,7 +238,7 @@ describe('AlertsAuthorization', () => { }); test('ensures the user has privileges to execute the specified type, operation and consumer', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -247,7 +246,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -283,7 +281,7 @@ describe('AlertsAuthorization', () => { }); test('ensures the user has privileges to execute the specified type and operation without consumer when consumer is alerts', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -291,7 +289,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -327,7 +324,7 @@ describe('AlertsAuthorization', () => { }); test('ensures the user has privileges to execute the specified type, operation and producer when producer is different from consumer', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -341,7 +338,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -377,7 +373,7 @@ describe('AlertsAuthorization', () => { }); test('throws if user lacks the required privieleges for the consumer', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -385,7 +381,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -427,7 +422,7 @@ describe('AlertsAuthorization', () => { }); test('throws if user lacks the required privieleges for the producer', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -435,7 +430,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -477,7 +471,7 @@ describe('AlertsAuthorization', () => { }); test('throws if user lacks the required privieleges for both consumer and producer', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -485,7 +479,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -591,7 +584,7 @@ describe('AlertsAuthorization', () => { }); test('creates a filter based on the privileged types', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -605,7 +598,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -621,7 +613,7 @@ describe('AlertsAuthorization', () => { }); test('creates an `ensureAlertTypeIsAuthorized` function which throws if type is unauthorized', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -652,7 +644,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -681,7 +672,7 @@ describe('AlertsAuthorization', () => { }); test('creates an `ensureAlertTypeIsAuthorized` function which is no-op if type is authorized', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -712,7 +703,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -730,7 +720,7 @@ describe('AlertsAuthorization', () => { }); test('creates an `logSuccessfulAuthorization` function which logs every authorized type', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -769,7 +759,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -906,7 +895,7 @@ describe('AlertsAuthorization', () => { }); test('augments a list of types with consumers under which the operation is authorized', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -937,7 +926,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -993,7 +981,7 @@ describe('AlertsAuthorization', () => { }); test('authorizes user under the Alerts consumer when they are authorized by the producer', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -1016,7 +1004,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -1053,7 +1040,7 @@ describe('AlertsAuthorization', () => { }); test('augments a list of types with consumers under which multiple operations are authorized', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -1100,7 +1087,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, @@ -1164,7 +1150,7 @@ describe('AlertsAuthorization', () => { }); test('omits types which have no consumers under which the operation is authorized', async () => { - const { authorization, securityLicense } = mockSecurity(); + const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction> = jest.fn(); @@ -1195,7 +1181,6 @@ describe('AlertsAuthorization', () => { const alertAuthorization = new AlertsAuthorization({ request, authorization, - securityLicense, alertTypeRegistry, features, auditLogger, diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index af67c5e40593c..33a9a0bf0396e 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -13,7 +13,6 @@ import { SecurityPluginSetup } from '../../../security/server'; import { RegistryAlertType } from '../alert_type_registry'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; -import { SecurityLicense } from '../../../security/common/licensing'; import { Space } from '../../../spaces/server'; export enum ReadOperations { @@ -53,14 +52,12 @@ export interface ConstructorOptions { getSpace: (request: KibanaRequest) => Promise; auditLogger: AlertsAuthorizationAuditLogger; authorization?: SecurityPluginSetup['authz']; - securityLicense?: SecurityLicense; } export class AlertsAuthorization { private readonly alertTypeRegistry: AlertTypeRegistry; private readonly request: KibanaRequest; private readonly authorization?: SecurityPluginSetup['authz']; - private readonly securityLicense?: SecurityLicense; private readonly auditLogger: AlertsAuthorizationAuditLogger; private readonly featuresIds: Promise>; private readonly allPossibleConsumers: Promise; @@ -69,14 +66,12 @@ export class AlertsAuthorization { alertTypeRegistry, request, authorization, - securityLicense, features, auditLogger, getSpace, }: ConstructorOptions) { this.request = request; this.authorization = authorization; - this.securityLicense = securityLicense; this.alertTypeRegistry = alertTypeRegistry; this.auditLogger = auditLogger; @@ -114,8 +109,7 @@ export class AlertsAuthorization { } private shouldCheckAuthorization(): boolean { - const { authorization, securityLicense } = this; - return (authorization && securityLicense && securityLicense?.isEnabled()) ?? false; + return this.authorization?.mode?.useRbacForRequest(this.request) ?? false; } public async ensureAuthorized( From 46f46c7315c38045c009a4d10d4c9db7192cb043 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 20 Jul 2020 12:16:18 +0100 Subject: [PATCH 120/126] fixed unit in alerts client factory --- x-pack/plugins/alerts/server/alerts_client_factory.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts index 4e6a93499f4c9..16b5af499bb90 100644 --- a/x-pack/plugins/alerts/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client_factory.test.ts @@ -95,7 +95,6 @@ test('creates an alerts client with proper constructor arguments when security i expect(AlertsAuthorization).toHaveBeenCalledWith({ request, authorization: securityPluginSetup.authz, - securityLicense: securityPluginSetup.license, alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry, features: alertsClientFactoryParams.features, auditLogger: expect.any(AlertsAuthorizationAuditLogger), From a894e5ad7d5420690ee569ed55d0fe8f8299870b Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 20 Jul 2020 15:06:54 +0100 Subject: [PATCH 121/126] allow user to disable alert even if they dont have privileges to the underlying action --- x-pack/plugins/alerts/server/alerts_client.ts | 4 ---- .../security_and_spaces/tests/alerting/disable.ts | 9 --------- 2 files changed, 13 deletions(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 1f286b42c1449..eec60f924bf38 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -613,10 +613,6 @@ export class AlertsClient { WriteOperations.Disable ); - if (attributes.actions.length) { - await this.actionsAuthorization.ensureAuthorized('execute'); - } - if (attributes.enabled === true) { await this.unsecuredSavedObjectsClient.update( 'alert', 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 3a732424853a0..4e4f9053bd24f 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 @@ -90,15 +90,6 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte await getScheduledTask(createdAlert.scheduledTaskId); break; case 'space_1_all_alerts_none_actions at space1': - expect(response.statusCode).to.eql(403); - expect(response.body).to.eql({ - error: 'Forbidden', - message: `Unauthorized to execute actions`, - statusCode: 403, - }); - // Ensure task still exists - await getScheduledTask(createdAlert.scheduledTaskId); - break; case 'superuser at space1': case 'space_1_all at space1': case 'space_1_all_with_restricted_fixture at space1': From 81978c3b59a5497f45f0ddd03c8ab2c66e353975 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 20 Jul 2020 18:12:22 +0100 Subject: [PATCH 122/126] fixed alerts test --- x-pack/plugins/alerts/server/alerts_client.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 6de9e74df47c3..c25e040ad09ce 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -1367,7 +1367,6 @@ describe('disable()', () => { await alertsClient.disable({ id: '1' }); expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable'); - expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); }); test('throws when user is not authorised to disable this type of alert', async () => { From 5582f06443b2143911f2135645c0a86d2687a13c Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 21 Jul 2020 13:58:07 +0100 Subject: [PATCH 123/126] expclude security wrapper in SO client passed to ActionsClient --- .../server/lib/action_executor.test.ts | 64 +++++++++++++++---- .../actions/server/lib/action_executor.ts | 17 +++-- .../server/lib/task_runner_factory.test.ts | 3 +- x-pack/plugins/actions/server/plugin.ts | 61 +++++++++--------- 4 files changed, 94 insertions(+), 51 deletions(-) diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index c8e6669275e11..65fd0646c639e 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -9,15 +9,17 @@ import { schema } from '@kbn/config-schema'; import { ActionExecutor } from './action_executor'; import { actionTypeRegistryMock } from '../action_type_registry.mock'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; -import { loggingSystemMock, savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { loggingSystemMock } from '../../../../../src/core/server/mocks'; import { eventLoggerMock } from '../../../event_log/server/mocks'; import { spacesServiceMock } from '../../../spaces/server/spaces_service/spaces_service.mock'; import { ActionType } from '../types'; -import { actionsMock } from '../mocks'; +import { actionsMock, actionsClientMock } from '../mocks'; +import { pick } from 'lodash'; const actionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }); const services = actionsMock.createServices(); -const savedObjectsClientWithHidden = savedObjectsClientMock.create(); + +const actionsClient = actionsClientMock.create(); const encryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient(); const actionTypeRegistry = actionTypeRegistryMock.create(); @@ -30,11 +32,12 @@ const executeParams = { }; const spacesMock = spacesServiceMock.createSetupContract(); +const getActionsClientWithRequest = jest.fn(); actionExecutor.initialize({ logger: loggingSystemMock.create().get(), spaces: spacesMock, getServices: () => services, - getScopedSavedObjectsClient: () => savedObjectsClientWithHidden, + getActionsClientWithRequest, actionTypeRegistry, encryptedSavedObjectsClient, eventLogger: eventLoggerMock.create(), @@ -44,6 +47,7 @@ actionExecutor.initialize({ beforeEach(() => { jest.resetAllMocks(); spacesMock.getSpaceId.mockReturnValue('some-namespace'); + getActionsClientWithRequest.mockResolvedValue(actionsClient); }); test('successfully executes', async () => { @@ -67,7 +71,13 @@ test('successfully executes', async () => { }, references: [], }; - savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject); + const actionResult = { + id: actionSavedObject.id, + name: actionSavedObject.id, + ...pick(actionSavedObject.attributes, 'actionTypeId', 'config'), + isPreconfigured: false, + }; + actionsClient.get.mockResolvedValueOnce(actionResult); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); actionTypeRegistry.get.mockReturnValueOnce(actionType); await actionExecutor.execute(executeParams); @@ -108,7 +118,13 @@ test('provides empty config when config and / or secrets is empty', async () => }, references: [], }; - savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject); + const actionResult = { + id: actionSavedObject.id, + name: actionSavedObject.id, + actionTypeId: actionSavedObject.attributes.actionTypeId, + isPreconfigured: false, + }; + actionsClient.get.mockResolvedValueOnce(actionResult); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); actionTypeRegistry.get.mockReturnValueOnce(actionType); await actionExecutor.execute(executeParams); @@ -138,7 +154,13 @@ test('throws an error when config is invalid', async () => { }, references: [], }; - savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject); + const actionResult = { + id: actionSavedObject.id, + name: actionSavedObject.id, + actionTypeId: actionSavedObject.attributes.actionTypeId, + isPreconfigured: false, + }; + actionsClient.get.mockResolvedValueOnce(actionResult); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); actionTypeRegistry.get.mockReturnValueOnce(actionType); @@ -171,7 +193,13 @@ test('throws an error when params is invalid', async () => { }, references: [], }; - savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject); + const actionResult = { + id: actionSavedObject.id, + name: actionSavedObject.id, + actionTypeId: actionSavedObject.attributes.actionTypeId, + isPreconfigured: false, + }; + actionsClient.get.mockResolvedValueOnce(actionResult); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); actionTypeRegistry.get.mockReturnValueOnce(actionType); @@ -185,7 +213,7 @@ test('throws an error when params is invalid', async () => { }); test('throws an error when failing to load action through savedObjectsClient', async () => { - savedObjectsClientWithHidden.get.mockRejectedValueOnce(new Error('No access')); + actionsClient.get.mockRejectedValueOnce(new Error('No access')); await expect(actionExecutor.execute(executeParams)).rejects.toThrowErrorMatchingInlineSnapshot( `"No access"` ); @@ -206,7 +234,13 @@ test('throws an error if actionType is not enabled', async () => { }, references: [], }; - savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject); + const actionResult = { + id: actionSavedObject.id, + name: actionSavedObject.id, + actionTypeId: actionSavedObject.attributes.actionTypeId, + isPreconfigured: false, + }; + actionsClient.get.mockResolvedValueOnce(actionResult); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); actionTypeRegistry.get.mockReturnValueOnce(actionType); actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => { @@ -240,7 +274,13 @@ test('should not throws an error if actionType is preconfigured', async () => { }, references: [], }; - savedObjectsClientWithHidden.get.mockResolvedValueOnce(actionSavedObject); + const actionResult = { + id: actionSavedObject.id, + name: actionSavedObject.id, + ...pick(actionSavedObject.attributes, 'actionTypeId', 'config', 'secrets'), + isPreconfigured: false, + }; + actionsClient.get.mockResolvedValueOnce(actionResult); encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); actionTypeRegistry.get.mockReturnValueOnce(actionType); actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => { @@ -268,7 +308,7 @@ test('throws an error when passing isESOUsingEphemeralEncryptionKey with value o customActionExecutor.initialize({ logger: loggingSystemMock.create().get(), spaces: spacesMock, - getScopedSavedObjectsClient: () => savedObjectsClientWithHidden, + getActionsClientWithRequest, getServices: () => services, actionTypeRegistry, encryptedSavedObjectsClient, diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 250bfc2752f1b..0e63cc8f5956e 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger, KibanaRequest, SavedObjectsClientContract } from '../../../../../src/core/server'; +import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { validateParams, validateConfig, validateSecrets } from './validate_with_schema'; import { ActionTypeExecutorResult, @@ -15,14 +15,15 @@ import { } from '../types'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; import { SpacesServiceSetup } from '../../../spaces/server'; -import { EVENT_LOG_ACTIONS } from '../plugin'; +import { EVENT_LOG_ACTIONS, PluginStartContract } from '../plugin'; import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; +import { ActionsClient } from '../actions_client'; export interface ActionExecutorContext { logger: Logger; spaces?: SpacesServiceSetup; getServices: GetServicesFunction; - getScopedSavedObjectsClient: (req: KibanaRequest) => SavedObjectsClientContract; + getActionsClientWithRequest: PluginStartContract['getActionsClientWithRequest']; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; actionTypeRegistry: ActionTypeRegistryContract; eventLogger: IEventLogger; @@ -76,7 +77,7 @@ export class ActionExecutor { actionTypeRegistry, eventLogger, preconfiguredActions, - getScopedSavedObjectsClient, + getActionsClientWithRequest, } = this.actionExecutorContext!; const services = getServices(request); @@ -84,7 +85,7 @@ export class ActionExecutor { const namespace = spaceId && spaceId !== 'default' ? { namespace: spaceId } : {}; const { actionTypeId, name, config, secrets } = await getActionInfo( - getScopedSavedObjectsClient(request), + await getActionsClientWithRequest(request), encryptedSavedObjectsClient, preconfiguredActions, actionId, @@ -196,7 +197,7 @@ interface ActionInfo { } async function getActionInfo( - savedObjectsClient: SavedObjectsClientContract, + actionsClient: PublicMethodsOf, encryptedSavedObjectsClient: EncryptedSavedObjectsClient, preconfiguredActions: PreConfiguredAction[], actionId: string, @@ -217,9 +218,7 @@ async function getActionInfo( // if not pre-configured action, should be a saved object // ensure user can read the action before processing - const { - attributes: { actionTypeId, config, name }, - } = await savedObjectsClient.get('action', actionId); + const { actionTypeId, config, name } = await actionsClient.get({ id: actionId }); const { attributes: { secrets }, diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index 06cb84ad79a89..78522682054e1 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -15,6 +15,7 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks'; import { eventLoggerMock } from '../../../event_log/server/mocks'; import { ActionTypeDisabledError } from './errors'; +import { actionsClientMock } from '../mocks'; const spaceIdToNamespace = jest.fn(); const actionTypeRegistry = actionTypeRegistryMock.create(); @@ -59,7 +60,7 @@ const actionExecutorInitializerParams = { logger: loggingSystemMock.create().get(), getServices: jest.fn().mockReturnValue(services), actionTypeRegistry, - getScopedSavedObjectsClient: () => savedObjectsClientMock.create(), + getActionsClientWithRequest: jest.fn(async () => actionsClientMock.create()), encryptedSavedObjectsClient: mockedEncryptedSavedObjectsClient, eventLogger: eventLoggerMock.create(), preconfiguredActions: [], diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 62bd1058774de..3eedd69410d11 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -249,10 +249,32 @@ export class ActionsPlugin implements Plugin, Plugi includedHiddenTypes, }); - const getScopedSavedObjectsClient = (request: KibanaRequest) => - core.savedObjects.getScopedClient(request, { - includedHiddenTypes, + const getActionsClientWithRequest = async (request: KibanaRequest) => { + if (isESOUsingEphemeralEncryptionKey === true) { + throw new Error( + `Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` + ); + } + return new ActionsClient({ + savedObjectsClient: core.savedObjects.getScopedClient(request, { + excludedWrappers: ['security'], + includedHiddenTypes, + }), + actionTypeRegistry: actionTypeRegistry!, + defaultKibanaIndex: await kibanaIndex, + scopedClusterClient: core.elasticsearch.legacy.client.asScoped(request), + preconfiguredActions, + request, + authorization: instantiateAuthorization(request), + actionExecutor: actionExecutor!, + executionEnqueuer: createExecutionEnqueuerFunction({ + taskManager: plugins.taskManager, + actionTypeRegistry: actionTypeRegistry!, + isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!, + preconfiguredActions, + }), }); + }; const getScopedSavedObjectsClientWithoutAccessToActions = (request: KibanaRequest) => core.savedObjects.getScopedClient(request); @@ -261,7 +283,7 @@ export class ActionsPlugin implements Plugin, Plugi logger, eventLogger: this.eventLogger!, spaces: this.spaces, - getScopedSavedObjectsClient, + getActionsClientWithRequest, getServices: this.getServicesFactory( getScopedSavedObjectsClientWithoutAccessToActions, core.elasticsearch @@ -277,7 +299,7 @@ export class ActionsPlugin implements Plugin, Plugi encryptedSavedObjectsClient, getBasePath: this.getBasePath, spaceIdToNamespace: this.spaceIdToNamespace, - getScopedSavedObjectsClient, + getScopedSavedObjectsClient: core.savedObjects.getScopedClient, }); scheduleActionsTelemetry(this.telemetryLogger, plugins.taskManager); @@ -292,29 +314,7 @@ export class ActionsPlugin implements Plugin, Plugi getActionsAuthorizationWithRequest(request: KibanaRequest) { return instantiateAuthorization(request); }, - async getActionsClientWithRequest(request: KibanaRequest) { - if (isESOUsingEphemeralEncryptionKey === true) { - throw new Error( - `Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` - ); - } - return new ActionsClient({ - savedObjectsClient: getScopedSavedObjectsClient(request), - actionTypeRegistry: actionTypeRegistry!, - defaultKibanaIndex: await kibanaIndex, - scopedClusterClient: core.elasticsearch.legacy.client.asScoped(request), - preconfiguredActions, - request, - authorization: instantiateAuthorization(request), - actionExecutor: actionExecutor!, - executionEnqueuer: createExecutionEnqueuerFunction({ - taskManager: plugins.taskManager, - actionTypeRegistry: actionTypeRegistry!, - isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!, - preconfiguredActions, - }), - }); - }, + getActionsClientWithRequest, preconfiguredActions, }; } @@ -364,7 +364,10 @@ export class ActionsPlugin implements Plugin, Plugi ); } return new ActionsClient({ - savedObjectsClient: savedObjects.getScopedClient(request, { includedHiddenTypes }), + savedObjectsClient: savedObjects.getScopedClient(request, { + excludedWrappers: ['security'], + includedHiddenTypes, + }), actionTypeRegistry: actionTypeRegistry!, defaultKibanaIndex, scopedClusterClient: context.core.elasticsearch.legacy.client, From 12f6536a40db4a4564975226de600f6bdcc2613b Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 21 Jul 2020 14:00:18 +0100 Subject: [PATCH 124/126] removed uneeded tests --- .../server/authorization/audit_logger.test.ts | 50 +------------------ 1 file changed, 2 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/actions/server/authorization/audit_logger.test.ts b/x-pack/plugins/actions/server/authorization/audit_logger.test.ts index 6d3e69b822c96..d700abdaa70ff 100644 --- a/x-pack/plugins/actions/server/authorization/audit_logger.test.ts +++ b/x-pack/plugins/actions/server/authorization/audit_logger.test.ts @@ -26,7 +26,7 @@ describe(`#constructor`, () => { }); describe(`#actionsAuthorizationFailure`, () => { - test('logs auth failure with consumer scope', () => { + test('logs auth failure', () => { const auditLogger = createMockAuditLogger(); const actionsAuditLogger = new ActionsAuthorizationAuditLogger(auditLogger); const username = 'foo-user'; @@ -47,56 +47,10 @@ describe(`#actionsAuthorizationFailure`, () => { ] `); }); - - test('logs auth failure with producer scope', () => { - const auditLogger = createMockAuditLogger(); - const actionsAuditLogger = new ActionsAuthorizationAuditLogger(auditLogger); - const username = 'foo-user'; - const actionTypeId = 'action-type-id'; - - const operation = 'create'; - - actionsAuditLogger.actionsAuthorizationFailure(username, operation, actionTypeId); - - expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "actions_authorization_failure", - "foo-user Unauthorized to create a \\"action-type-id\\" action", - Object { - "actionTypeId": "action-type-id", - "operation": "create", - "username": "foo-user", - }, - ] - `); - }); }); describe(`#savedObjectsAuthorizationSuccess`, () => { - test('logs auth success with consumer scope', () => { - const auditLogger = createMockAuditLogger(); - const actionsAuditLogger = new ActionsAuthorizationAuditLogger(auditLogger); - const username = 'foo-user'; - const actionTypeId = 'action-type-id'; - - const operation = 'create'; - - actionsAuditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId); - - expect(auditLogger.log.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "actions_authorization_success", - "foo-user Authorized to create a \\"action-type-id\\" action", - Object { - "actionTypeId": "action-type-id", - "operation": "create", - "username": "foo-user", - }, - ] - `); - }); - - test('logs auth success with producer scope', () => { + test('logs auth success', () => { const auditLogger = createMockAuditLogger(); const actionsAuditLogger = new ActionsAuthorizationAuditLogger(auditLogger); const username = 'foo-user'; From 7bafd5d8a30a19f75b0476164de10cfe702ae655 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 21 Jul 2020 15:43:44 +0100 Subject: [PATCH 125/126] includes hidden params type in SO client --- x-pack/plugins/actions/server/plugin.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 3eedd69410d11..7016ec0fc4110 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -299,7 +299,10 @@ export class ActionsPlugin implements Plugin, Plugi encryptedSavedObjectsClient, getBasePath: this.getBasePath, spaceIdToNamespace: this.spaceIdToNamespace, - getScopedSavedObjectsClient: core.savedObjects.getScopedClient, + getScopedSavedObjectsClient: (request: KibanaRequest) => + core.savedObjects.getScopedClient(request, { + includedHiddenTypes, + }), }); scheduleActionsTelemetry(this.telemetryLogger, plugins.taskManager); From e7945185116757e491ddfc22c7d3522873164cc8 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 21 Jul 2020 17:29:09 +0100 Subject: [PATCH 126/126] renamed variable to make it clear the SO client is unsecured --- .../actions/server/actions_client.test.ts | 106 +++++++++--------- .../plugins/actions/server/actions_client.ts | 24 ++-- x-pack/plugins/actions/server/plugin.ts | 4 +- 3 files changed, 67 insertions(+), 67 deletions(-) diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 09dd42dc91dd5..90b989ac3b52e 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -26,7 +26,7 @@ import { ActionsAuthorization } from './authorization/actions_authorization'; import { actionsAuthorizationMock } from './authorization/actions_authorization.mock'; const defaultKibanaIndex = '.kibana'; -const savedObjectsClient = savedObjectsClientMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const scopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); const actionExecutor = actionExecutorMock.create(); const authorization = actionsAuthorizationMock.create(); @@ -58,7 +58,7 @@ beforeEach(() => { actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); actionsClient = new ActionsClient({ actionTypeRegistry, - savedObjectsClient, + unsecuredSavedObjectsClient, scopedClusterClient, defaultKibanaIndex, preconfiguredActions: [], @@ -88,7 +88,7 @@ describe('create()', () => { minimumLicenseRequired: 'basic', executor, }); - savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); await actionsClient.create({ action: { @@ -119,7 +119,7 @@ describe('create()', () => { minimumLicenseRequired: 'basic', executor, }); - savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); authorization.ensureAuthorized.mockRejectedValue( new Error(`Unauthorized to create a "my-action-type" action`) @@ -157,7 +157,7 @@ describe('create()', () => { minimumLicenseRequired: 'basic', executor, }); - savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); const result = await actionsClient.create({ action: { name: 'my name', @@ -173,8 +173,8 @@ describe('create()', () => { actionTypeId: 'my-action-type', config: {}, }); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", Object { @@ -235,7 +235,7 @@ describe('create()', () => { minimumLicenseRequired: 'basic', executor, }); - savedObjectsClient.create.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'type', attributes: { @@ -273,8 +273,8 @@ describe('create()', () => { c: true, }, }); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", Object { @@ -311,7 +311,7 @@ describe('create()', () => { actionTypeRegistry = new ActionTypeRegistry(localActionTypeRegistryParams); actionsClient = new ActionsClient({ actionTypeRegistry, - savedObjectsClient, + unsecuredSavedObjectsClient, scopedClusterClient, defaultKibanaIndex, preconfiguredActions: [], @@ -337,7 +337,7 @@ describe('create()', () => { minimumLicenseRequired: 'basic', executor, }); - savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); await expect( actionsClient.create({ @@ -373,7 +373,7 @@ describe('create()', () => { mockedLicenseState.ensureLicenseForActionType.mockImplementation(() => { throw new Error('Fail'); }); - savedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce(savedObjectCreateResult); await expect( actionsClient.create({ action: { @@ -390,7 +390,7 @@ describe('create()', () => { describe('get()', () => { describe('authorization', () => { test('ensures user is authorised to get the type of action', async () => { - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'type', attributes: { @@ -409,7 +409,7 @@ describe('get()', () => { test('ensures user is authorised to get preconfigured type of action', async () => { actionsClient = new ActionsClient({ actionTypeRegistry, - savedObjectsClient, + unsecuredSavedObjectsClient, scopedClusterClient, defaultKibanaIndex, actionExecutor, @@ -438,7 +438,7 @@ describe('get()', () => { }); test('throws when user is not authorised to create the type of action', async () => { - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'type', attributes: { @@ -463,7 +463,7 @@ describe('get()', () => { test('throws when user is not authorised to create preconfigured of action', async () => { actionsClient = new ActionsClient({ actionTypeRegistry, - savedObjectsClient, + unsecuredSavedObjectsClient, scopedClusterClient, defaultKibanaIndex, actionExecutor, @@ -498,8 +498,8 @@ describe('get()', () => { }); }); - test('calls savedObjectsClient with id', async () => { - savedObjectsClient.get.mockResolvedValueOnce({ + test('calls unsecuredSavedObjectsClient with id', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'type', attributes: {}, @@ -510,8 +510,8 @@ describe('get()', () => { id: '1', isPreconfigured: false, }); - expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", "1", @@ -522,7 +522,7 @@ describe('get()', () => { test('return predefined action with id', async () => { actionsClient = new ActionsClient({ actionTypeRegistry, - savedObjectsClient, + unsecuredSavedObjectsClient, scopedClusterClient, defaultKibanaIndex, actionExecutor, @@ -552,7 +552,7 @@ describe('get()', () => { isPreconfigured: true, name: 'test', }); - expect(savedObjectsClient.get).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); }); }); @@ -578,7 +578,7 @@ describe('getAll()', () => { }, ], }; - savedObjectsClient.find.mockResolvedValueOnce(expectedResult); + unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult); scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({ aggregations: { '1': { doc_count: 6 }, @@ -588,7 +588,7 @@ describe('getAll()', () => { actionsClient = new ActionsClient({ actionTypeRegistry, - savedObjectsClient, + unsecuredSavedObjectsClient, scopedClusterClient, defaultKibanaIndex, actionExecutor, @@ -629,7 +629,7 @@ describe('getAll()', () => { }); }); - test('calls savedObjectsClient with parameters', async () => { + test('calls unsecuredSavedObjectsClient with parameters', async () => { const expectedResult = { total: 1, per_page: 10, @@ -649,7 +649,7 @@ describe('getAll()', () => { }, ], }; - savedObjectsClient.find.mockResolvedValueOnce(expectedResult); + unsecuredSavedObjectsClient.find.mockResolvedValueOnce(expectedResult); scopedClusterClient.callAsInternalUser.mockResolvedValueOnce({ aggregations: { '1': { doc_count: 6 }, @@ -659,7 +659,7 @@ describe('getAll()', () => { actionsClient = new ActionsClient({ actionTypeRegistry, - savedObjectsClient, + unsecuredSavedObjectsClient, scopedClusterClient, defaultKibanaIndex, actionExecutor, @@ -704,7 +704,7 @@ describe('getAll()', () => { describe('getBulk()', () => { describe('authorization', () => { function getBulkOperation(): ReturnType { - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -729,7 +729,7 @@ describe('getBulk()', () => { actionsClient = new ActionsClient({ actionTypeRegistry, - savedObjectsClient, + unsecuredSavedObjectsClient, scopedClusterClient, defaultKibanaIndex, actionExecutor, @@ -770,8 +770,8 @@ describe('getBulk()', () => { }); }); - test('calls getBulk savedObjectsClient with parameters', async () => { - savedObjectsClient.bulkGet.mockResolvedValueOnce({ + test('calls getBulk unsecuredSavedObjectsClient with parameters', async () => { + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ { id: '1', @@ -796,7 +796,7 @@ describe('getBulk()', () => { actionsClient = new ActionsClient({ actionTypeRegistry, - savedObjectsClient, + unsecuredSavedObjectsClient, scopedClusterClient, defaultKibanaIndex, actionExecutor, @@ -861,13 +861,13 @@ describe('delete()', () => { }); }); - test('calls savedObjectsClient with id', async () => { + test('calls unsecuredSavedObjectsClient with id', async () => { const expectedResult = Symbol(); - savedObjectsClient.delete.mockResolvedValueOnce(expectedResult); + unsecuredSavedObjectsClient.delete.mockResolvedValueOnce(expectedResult); const result = await actionsClient.delete({ id: '1' }); expect(result).toEqual(expectedResult); - expect(savedObjectsClient.delete).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", "1", @@ -885,7 +885,7 @@ describe('update()', () => { minimumLicenseRequired: 'basic', executor, }); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'action', attributes: { @@ -893,7 +893,7 @@ describe('update()', () => { }, references: [], }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { @@ -938,7 +938,7 @@ describe('update()', () => { minimumLicenseRequired: 'basic', executor, }); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'action', attributes: { @@ -946,7 +946,7 @@ describe('update()', () => { }, references: [], }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { @@ -972,8 +972,8 @@ describe('update()', () => { name: 'my name', config: {}, }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", "my-action", @@ -985,8 +985,8 @@ describe('update()', () => { }, ] `); - expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", "my-action", @@ -1006,7 +1006,7 @@ describe('update()', () => { }, executor, }); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { @@ -1035,7 +1035,7 @@ describe('update()', () => { minimumLicenseRequired: 'basic', executor, }); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { @@ -1043,7 +1043,7 @@ describe('update()', () => { }, references: [], }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { @@ -1081,8 +1081,8 @@ describe('update()', () => { c: true, }, }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", "my-action", @@ -1110,7 +1110,7 @@ describe('update()', () => { mockedLicenseState.ensureLicenseForActionType.mockImplementation(() => { throw new Error('Fail'); }); - savedObjectsClient.get.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ id: '1', type: 'action', attributes: { @@ -1118,7 +1118,7 @@ describe('update()', () => { }, references: [], }); - savedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { @@ -1233,6 +1233,6 @@ describe('enqueueExecution()', () => { }; await expect(actionsClient.enqueueExecution(opts)).resolves.toMatchInlineSnapshot(`undefined`); - expect(executionEnqueuer).toHaveBeenCalledWith(savedObjectsClient, opts); + expect(executionEnqueuer).toHaveBeenCalledWith(unsecuredSavedObjectsClient, opts); }); }); diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index bd6e022353fad..6744a8d111623 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -54,7 +54,7 @@ interface ConstructorOptions { defaultKibanaIndex: string; scopedClusterClient: ILegacyScopedClusterClient; actionTypeRegistry: ActionTypeRegistry; - savedObjectsClient: SavedObjectsClientContract; + unsecuredSavedObjectsClient: SavedObjectsClientContract; preconfiguredActions: PreConfiguredAction[]; actionExecutor: ActionExecutorContract; executionEnqueuer: ExecutionEnqueuer; @@ -70,7 +70,7 @@ interface UpdateOptions { export class ActionsClient { private readonly defaultKibanaIndex: string; private readonly scopedClusterClient: ILegacyScopedClusterClient; - private readonly savedObjectsClient: SavedObjectsClientContract; + private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; private readonly actionTypeRegistry: ActionTypeRegistry; private readonly preconfiguredActions: PreConfiguredAction[]; private readonly actionExecutor: ActionExecutorContract; @@ -82,7 +82,7 @@ export class ActionsClient { actionTypeRegistry, defaultKibanaIndex, scopedClusterClient, - savedObjectsClient, + unsecuredSavedObjectsClient, preconfiguredActions, actionExecutor, executionEnqueuer, @@ -90,7 +90,7 @@ export class ActionsClient { authorization, }: ConstructorOptions) { this.actionTypeRegistry = actionTypeRegistry; - this.savedObjectsClient = savedObjectsClient; + this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient; this.scopedClusterClient = scopedClusterClient; this.defaultKibanaIndex = defaultKibanaIndex; this.preconfiguredActions = preconfiguredActions; @@ -114,7 +114,7 @@ export class ActionsClient { this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); - const result = await this.savedObjectsClient.create('action', { + const result = await this.unsecuredSavedObjectsClient.create('action', { actionTypeId, name, config: validatedActionTypeConfig as SavedObjectAttributes, @@ -150,7 +150,7 @@ export class ActionsClient { 'update' ); } - const existingObject = await this.savedObjectsClient.get('action', id); + const existingObject = await this.unsecuredSavedObjectsClient.get('action', id); const { actionTypeId } = existingObject.attributes; const { name, config, secrets } = action; const actionType = this.actionTypeRegistry.get(actionTypeId); @@ -159,7 +159,7 @@ export class ActionsClient { this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); - const result = await this.savedObjectsClient.update('action', id, { + const result = await this.unsecuredSavedObjectsClient.update('action', id, { actionTypeId, name, config: validatedActionTypeConfig as SavedObjectAttributes, @@ -192,7 +192,7 @@ export class ActionsClient { isPreconfigured: true, }; } - const result = await this.savedObjectsClient.get('action', id); + const result = await this.unsecuredSavedObjectsClient.get('action', id); return { id, @@ -210,7 +210,7 @@ export class ActionsClient { await this.authorization.ensureAuthorized('get'); const savedObjectsActions = ( - await this.savedObjectsClient.find({ + await this.unsecuredSavedObjectsClient.find({ perPage: MAX_ACTIONS_RETURNED, type: 'action', }) @@ -259,7 +259,7 @@ export class ActionsClient { ]; const bulkGetOpts = actionSavedObjectsIds.map((id) => ({ id, type: 'action' })); - const bulkGetResult = await this.savedObjectsClient.bulkGet(bulkGetOpts); + const bulkGetResult = await this.unsecuredSavedObjectsClient.bulkGet(bulkGetOpts); for (const action of bulkGetResult.saved_objects) { if (action.error) { @@ -292,7 +292,7 @@ export class ActionsClient { 'delete' ); } - return await this.savedObjectsClient.delete('action', id); + return await this.unsecuredSavedObjectsClient.delete('action', id); } public async execute({ @@ -305,7 +305,7 @@ export class ActionsClient { public async enqueueExecution(options: EnqueueExecutionOptions): Promise { await this.authorization.ensureAuthorized('execute'); - return this.executionEnqueuer(this.savedObjectsClient, options); + return this.executionEnqueuer(this.unsecuredSavedObjectsClient, options); } public async listTypes(): Promise { diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 7016ec0fc4110..5b8b25d02658b 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -256,7 +256,7 @@ export class ActionsPlugin implements Plugin, Plugi ); } return new ActionsClient({ - savedObjectsClient: core.savedObjects.getScopedClient(request, { + unsecuredSavedObjectsClient: core.savedObjects.getScopedClient(request, { excludedWrappers: ['security'], includedHiddenTypes, }), @@ -367,7 +367,7 @@ export class ActionsPlugin implements Plugin, Plugi ); } return new ActionsClient({ - savedObjectsClient: savedObjects.getScopedClient(request, { + unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, { excludedWrappers: ['security'], includedHiddenTypes, }),