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 01687f33f631d4..0e273f05ffd17f 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 913b4e2e81fe14..e2d5c406c26e38 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 08353656359905..97619bff98b9f0 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 77a2d716e6d879..219a82d323efe0 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 93b98f6a0fe037..fcaeb275c47d74 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 0e273f05ffd17f..3149f11bedabff 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 cc792d11c890dd..ea3e0188725eb2 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 a6407366bbd3b2..adf532389d88f9 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 d58c999ddccdf8..74132c3c67961c 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 e78e5ab7932c2b..2e4f71abdce843 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 0163cb71166e8a..6f3b2d0a32a222 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 1848b3432ae5a5..be70e441b6fc5c 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 fcaeb275c47d74..f9fac8795bd618 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 3149f11bedabff..bf4305998f5c52 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 a4910495c8a40a..5e0dce64910558 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 0c038b6490483d..82f4c586248c99 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 416628d015b5a1..a2fa221f870980 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 7f6600b1ec48e1..12c8d52e200a5e 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 fde095e9145b66..622923594949db 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 c7e7b1001f82d1..c294dde42a94dd 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 e4e89e3f06380b..5ea017092ef9e7 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 3ed4fb0739d3d5..65a42c17c7980c 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 cc601bd42b8cae..bf00623f83a0b5 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 c723419a965c5c..3b6b7ade7c10e4 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 7335f13c85a4d1..50df4ae5d90af8 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 6d652d1304f655..a000c9a03226d1 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 20a420ca00986f..a44dd81800c0d2 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 552bfea22a42b7..9c14570aa9cf98 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 e940b2d1020451..81d5cff70450c9 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 7ab64cf9320516..032d882afe25c2 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 5ef9e3694f8f45..cda2519582538f 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 d1b4322bd1ccb1..2c1ff65aaf15d9 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 2e6adedb76df9b..1643312e5a098e 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 fbdda62836d747..bf96724b5ca6fe 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 1756dbd3fb41d0..e775fe51ddfe43 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 e09f2fe6b8b937..c2521523851658 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 9b9542c606741c..be4f7b85a3f343 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 64ba22dc3ea0b1..3df4bdd583a570 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 cd96f289b87147..bf93ffb155acf3 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 7f077493115982..a621d265af97f6 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 0347feb24a2359..ea713d24ed114a 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 9d0c34fc1a0155..2c9a5b58f62835 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 ae0f5510cd80e8..92a4466b69b66c 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 768c8c6ae10880..c642f3e5b6fd44 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 403d9586bf1600..16361ddef605f7 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 fa228e03194a93..00dc2c9ac1b627 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 00000000000000..e9cd8bf48a4004 --- /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 00000000000000..f41faaa3dd52ce --- /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 00293e88abe764..34258bdcf972d3 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 00000000000000..fcd1e4aea06287 --- /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 00000000000000..a3e56701a60d3e --- /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 930ede4157723a..22eed47c17bfea 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 cf970a561b93f9..06b9bad0af9725 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 00000000000000..1eaebac2c78f68 --- /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 00000000000000..7935959c331ce0 --- /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 c293319070419f..42792cb1797cdb 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 9a8935f80a174f..1fa1b21083921e 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 0011737d857345..013330ec652b92 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 3c336991f3d9d8..b96c6a8cc4160c 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 ce11fb8052b458..ff850544463d5b 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 f91b54514ae05a..d830003fc6e622 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 f2598ff7c5493b..c3e449264b2092 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 ca58b58e5e822a..22cb7c09e91311 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 f7ccc6c97bcf0a..df5bc445640167 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 cd821a739a9eba..fc2aebfe2b6040 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 319834452a2120..8f38277a86f530 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 5f50c0d64f3539..ff2d338bd26a55 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 66cd8a72440810..777864fbdd402a 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 6f1aec901760e4..24d72714f4ac48 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 845a6f79557394..963e6f07ffc133 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 5a35d4bf838659..3ef899b631946c 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 6b091a5a4503b8..42b4044394408f 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 af546f965d7df7..b7d1bf6e8cf311 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 324bc9fbfb72bf..e46be4efeaf9a0 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 bdda0be9b15a76..b947292eb0aef0 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 9685f58b8fb31c..0e197089475840 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 42b4044394408f..5fcaac17a7d323 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 50dafba00a7e48..10d7e7f9cafdfd 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 72a946d6c51557..c2adcc74f14733 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 3e30ff9447f3e0..42c9b5c5be4e56 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 6d7cf621ab0cae..ae3633cdde62bb 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 8f36afe062aa54..300cfc5b5f5492 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 1848b3432ae5a5..be70e441b6fc5c 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 0e197089475840..4abf9bcd12b78c 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 5fcaac17a7d323..f1ea77829a5be5 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 9e941903eeaedf..274acaf01c4751 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 6238fca024e553..91a81f6d84b714 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 9ba4e20312e170..d9c5aa2d59c87a 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 2034bd21fbed65..b073c591491718 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 a82d09854a6043..74f7b2eb8a5702 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 dfc5dfbdd5aa28..234f8ed959a5d1 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 4ee3a12a59dc75..c9575ef87f7670 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 b6f86b97d6a3a2..c162b4a9844b33 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 f20ee0a54dcd94..46702f96a2e106 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 80c9c20eec7da2..632772eadddedd 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 b11224ff4794e2..8c4b06adf70f76 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 ae9ebe1299371b..0f3fc4b2f3e413 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 8c9051093f85b9..6fa12a17eb9f3c 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 b27ae3758e1b9d..089fc80fca3557 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 3192154f6664c4..5cf36cb2c0786a 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 51a4558108e293..bf516120fbe93c 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 bcdb8cbd022ac0..efa3cdebad8ffe 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 5b05d7231c3857..6735121d4edb08 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 c382c12de21cd8..6e700e4e3fd468 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 00550f4af34185..5e2ffc7d519edc 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 e13af38fe4cb1b..81fdc5bb4dd764 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 1efc9ed40054e2..a9873805416964 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 b2e2f24e91de9f..04e97dbe5e538f 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 967f9f890c9fb8..15b882e5858040 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 c7d23f2670b45c..dedb08a9972c20 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 99b81dfc5b56e9..9b2fe9a43810b2 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 babae59553b5b3..5aa91d215be900 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 4736351a25cbd1..d44649b05b9298 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 60a7be9391eea3..ee6e5a445f996d 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 768c8c6ae10880..c642f3e5b6fd44 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 7497548cf89049..86c7bc48527429 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 fa228e03194a93..00dc2c9ac1b627 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 00000000000000..e9cd8bf48a4004 --- /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 00000000000000..f41faaa3dd52ce --- /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 00293e88abe764..34258bdcf972d3 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 00000000000000..fcd1e4aea06287 --- /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 00000000000000..a3e56701a60d3e --- /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 930ede4157723a..22eed47c17bfea 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 cf970a561b93f9..06b9bad0af9725 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 00000000000000..1eaebac2c78f68 --- /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 00000000000000..7935959c331ce0 --- /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 3d6dfbdac02512..76b664cbbe2a78 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 f3b2881e79ece5..a84eea3933eeaa 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 a0a06b537213d8..e25613fc5936f4 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 a8858c91d677c5..ea379451dc1985 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 47563f8a5f078c..504d67352be1ac 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 2416bc2ea1d12d..ce210f1128fa61 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 c59b9f4503a03d..885443bfbd1b23 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 fd22752ccc11af..9a649a3b7af733 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 72b524282354a1..6f9e7ce5a6e083 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 2bcc035beb7a93..bf3ccf6ec479ef 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 bf72b970dc0f1a..d441be8a82fd34 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 fa256712a012b0..8f42f12347728d 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 06f27d666c3dac..b28ce89b304724 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 ff671e16654b55..165eaa09126a81 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 d3f08d7c509a09..e3f87a9be00bac 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 aef87eefba2ade..4baae603f29609 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 b01a1b140f2d62..9c8e6f6b8d94c8 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 f1ea77829a5be5..53c6d01c1d4180 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 10d7e7f9cafdfd..7278c7ab2c837d 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 b7d1bf6e8cf311..7ebe505913b958 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 6fa12a17eb9f3c..d5bf9737d39ab7 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 5cf36cb2c0786a..14143021290af6 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 00dc2c9ac1b627..598e619a211430 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 1eaebac2c78f68..2add0dbb203029 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 f21cb9f486fb53..b54767cb5ebc55 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 cc613d5247ef4d..dd70e53604f16f 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 12d1b080c7c639..d49321016daa66 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 95d34371a6d1e6..38a9b6c52ecb70 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 74f740f52a8b2d..4ad7aa3126e889 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 fb431351a382dc..5f4afd84522cfd 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 e46be4efeaf9a0..0a3dd8f825e49b 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 7935959c331ce0..a47d1ffd5185cb 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 b947292eb0aef0..fc2d6a74720438 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 dd70e53604f16f..cc613d5247ef4d 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 00613213d5aed5..029547344fa664 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 d49321016daa66..12d1b080c7c639 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 38a9b6c52ecb70..95d34371a6d1e6 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 3509f79dbbe4d3..e10d9dc4a4db26 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 00000000000000..8f92c6cc3b5675 --- /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 0a3dd8f825e49b..932e3f0dfb46ff 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 8f92c6cc3b5675..119a9f06a48441 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 008a9bb804c5bf..b676c099e490f6 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 bcadd4fa063605..6599c476b8bae4 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 082484d5fa6b49..93cf6dccc8a6dd 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 42c9b5c5be4e56..8b60f375bfa520 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 fcd1e4aea06287..75d5a70e9302ca 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 a3e56701a60d3e..e8c7e8005b5d22 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 2add0dbb203029..4036154aef9a65 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 a47d1ffd5185cb..4ef93f5f07276f 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 f21cb9f486fb53..b54767cb5ebc55 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 ed93b627f003c3..bcb32c67438211 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 1270c03b8a9774..880f6c75480250 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 4ef93f5f07276f..e5123303354181 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 2b6389649cef98..aa5b0dd45895e1 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 cdb005feca35c6..9128281fb72e54 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 88a8da5a3e575f..af067e08d73f69 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 07d5ba5829ec8e..f0f35717a5d221 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 53c6d01c1d4180..172d2ceea48641 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 7278c7ab2c837d..1952aeb27d2193 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 7ebe505913b958..c84b1c1b1bd153 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 119a9f06a48441..108e3e43002514 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 b676c099e490f6..1cea6778e7c421 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 932e3f0dfb46ff..fb917cfc8c476f 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 14143021290af6..276915973a391d 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 c642f3e5b6fd44..dd77073a628342 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 598e619a211430..2c16493a61445e 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 75d5a70e9302ca..744543f38a914a 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 e8c7e8005b5d22..99d04efe6892d7 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 4036154aef9a65..99d69602db1376 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 e5123303354181..d697884e251049 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 874091b2bb7a84..69f1b0a1837660 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 2929ce6defeaf9..4c6e8d3984b015 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 00000000000000..304410744c6046 --- /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 504d67352be1ac..43c99f8c329fbf 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 00000000000000..54d6de50cff4d6 --- /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 00000000000000..044ad8444dd056 --- /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 7506f1d42bf0f5..89ea7c768f28bc 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 69dcb7c813815c..27d35f0bf53921 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 d96ffc5bb3be39..86930a58a4f6f8 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 70a3663c1c798c..20e751747087e5 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 c610ac670f690c..78eafcf684ce6c 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 45491aa2d28fcf..c7eeda14733cc8 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 22c89a1a8148f6..1e1659636aa313 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 cb0e0efda0b1a8..f86656531b267f 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 54e8a66b403372..b3dd4664b4b6f9 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 2148b3710d893e..b7e22da90be381 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 5092519e8d155b..0e997add9e6e3f 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 acd3340927b79f..76ebe3c8bd9027 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 ff31375ed1367f..ca3947478af2f7 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 b54767cb5ebc55..0ebb532ecb2012 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 3ad19450ffc0cb..9cf635139bab1f 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 7b8e8e838a4755..3ed32614f5f2a5 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 a02e95c80c95d8..f8feff24fc1d09 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 ce210f1128fa61..9c850e08a7138a 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 885443bfbd1b23..e6907bc3aa9d9d 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 9a649a3b7af733..ac72d2d7725164 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 6f9e7ce5a6e083..448289af8e011d 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 bf3ccf6ec479ef..89d2b45685a902 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 d441be8a82fd34..502ee0299f7f75 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 54f9c5e11a8d18..62de3a3242f208 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 304410744c6046..00000000000000 --- 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 43c99f8c329fbf..36147fa1f4961e 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 9128281fb72e54..9a93a6f8f4d6ed 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 86c7bc48527429..ccc455cb2de5b0 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 e9cd8bf48a4004..afa907fe098373 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 e25613fc5936f4..c7bd0258388649 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 044ad8444dd056..c9155029899add 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 76ebe3c8bd9027..e4d8f656cd1a72 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 ca3947478af2f7..61f886f218b1ac 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 0ebb532ecb2012..646e8bed577bc2 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 9cf635139bab1f..475ae6712f2ac5 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 3ed32614f5f2a5..6152ecb16d796f 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 f8feff24fc1d09..ee56f741d3e2a1 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 9c850e08a7138a..a87a96fd35a203 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 e6907bc3aa9d9d..6d70744d1c51bc 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 ac72d2d7725164..af3ead1eda5a53 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 448289af8e011d..7bf3e730e97a2c 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 89d2b45685a902..4d885024cc7f67 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 502ee0299f7f75..2b7f448c3e4bd4 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 5f4afd84522cfd..256394136ee69a 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 a93dde83ebe706..47597de1f79e2c 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 36147fa1f4961e..8e2b1c67b00453 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 475ae6712f2ac5..9835b18b96e3a5 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 6152ecb16d796f..e188a21fd0d364 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 ee56f741d3e2a1..c1a856ff841403 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 a87a96fd35a203..21513513a8ccb7 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 6d70744d1c51bc..0d8630445accd8 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 af3ead1eda5a53..9d715c9146b5ec 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 7bf3e730e97a2c..2f1f883351aee8 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 4d885024cc7f67..7007b4ce7e3ae8 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 2b7f448c3e4bd4..903bf6b40ee7e2 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 13bf47676cc09c..07c3115a9b67c7 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 2bc299ede930bb..503c328017a9a3 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 00000000000000..58b041f8b0370d --- /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 00000000000000..e0469d8f49604c --- /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 f0f35717a5d221..7a610d82dd21eb 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 47597de1f79e2c..1fa8a99da1d631 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 1952aeb27d2193..5253e38c20dac1 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 c84b1c1b1bd153..b3024ca03d5663 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 58b041f8b0370d..f151a843aedeb4 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 e0469d8f49604c..a992bbc1f2ad7b 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 7a610d82dd21eb..c9a28177dc80a8 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 1fa8a99da1d631..45c4f5bd851c44 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 50e8347b3bebe5..163f831ea6c78a 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 8412c09eefcda3..92db0458c0639b 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 4baae603f29609..dde75b57b6b20d 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 00000000000000..3117cbd8429a18 --- /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 a992bbc1f2ad7b..89af132ec8a221 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 45c4f5bd851c44..657729bf4a78fa 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 c9a28177dc80a8..33c5d4a3099eee 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 657729bf4a78fa..0d35bc7a85f5eb 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 5a2a950f21bcf2..953cad62402a56 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 567274be6a9f8b..172d7c50d9fa39 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 811478426a8d34..f77a11f9e489ee 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 727e38d9ba56b0..515de771e7d6ba 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 33c5d4a3099eee..63a781b82c8c87 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 0d35bc7a85f5eb..8eef472590b9b1 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 5253e38c20dac1..9dd02f41be68e0 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 b3024ca03d5663..86379f00b04ac4 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 3117cbd8429a18..7bfc61242f80c3 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 89af132ec8a221..a48a7803d10672 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 00000000000000..6b29eedac030ba --- /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 00000000000000..de302cb936779b --- /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 00000000000000..5d563bbd6db8db --- /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 108e3e43002514..d38b0c25759f11 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 632772eadddedd..ef3b16dc9e5175 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 c7bd0258388649..a0a06b537213d8 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 a48a7803d10672..318be1033ff0db 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 485783253d29de..bb1f0c33fdee9d 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 e239a6e280aec6..afa0ffb87b6fab 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 ae3633cdde62bb..a1bc6c386033a6 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 300cfc5b5f5492..4344ef6fc31182 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 4344ef6fc31182..c466d0e96382cf 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 163f831ea6c78a..d04f35458f30ac 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 0999063945cb59..9c61bc0d3d9434 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 9a93a6f8f4d6ed..86772a79e669b1 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 86772a79e669b1..2e4600bedab9ee 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 63a781b82c8c87..e6b461935a0f2b 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 8eef472590b9b1..50f9763248ced8 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 19c91ae27da3d1..bee74d4e4bec43 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 86379f00b04ac4..3e4133d83373da 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 f151a843aedeb4..d7705f834ad41c 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 7bfc61242f80c3..307490c96a0d30 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 318be1033ff0db..e9cea33ababafb 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 d38b0c25759f11..e841852ecb8a26 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 1cea6778e7c421..56bf3a09dc06bf 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 69f1b0a1837660..32b1809609e433 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 4c6e8d3984b015..9ce64d47960931 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 2e4600bedab9ee..f6c0948a6c30c4 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 e841852ecb8a26..47c42bb9c7562b 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 e6b461935a0f2b..e7ab67076f25dc 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 50f9763248ced8..8d1f6183f0aa0e 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 d04f35458f30ac..7160347e813ae8 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 1a5da8a422b9e1..285abbef64f0da 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 b839c07a9db89a..480cdd70275555 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 307490c96a0d30..105e44811d1432 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 d20d939011c161..a27ca6b710f063 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 06c538c68d782c..9905c5020779a7 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 2531d82771cff4..a3fa9586cc47ba 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 31b71e0decdb88..f3e082d5202b7a 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 9835b18b96e3a5..ade0c706c767e1 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 21513513a8ccb7..682f679e36ebcf 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 0d8630445accd8..748eb837fdffb2 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 9d715c9146b5ec..42c49e6103e3a9 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 2f1f883351aee8..ac36aab6d1885b 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 7007b4ce7e3ae8..582512a5f10a39 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 903bf6b40ee7e2..391493b752ef81 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 2c16493a61445e..c6cb029f858f7b 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 8d1f6183f0aa0e..d614cab6c00129 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 c9ce2848c56704..1c29bbc71154e1 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 1c29bbc71154e1..ed36bc6c8d5803 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 578c93fc4cba87..638b42d3aa7ce7 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 94d9166b409099..fa225de4fc9a64 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 d8f0d0b6b20a0b..1a06f69580c12e 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 f6e8dc49ec2753..a8be282bc3b6df 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 ed36bc6c8d5803..c89bb36be3cba6 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 e23dfed448c578..39f396b372f2a4 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 02d9ca3e5f0c93..caa05375ec9c2f 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 618338dc4d02f0..1db45a16304a0c 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 1a139e331b8899..8e64f0e5c432f3 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 a472d8a4df4a49..8f6826cec5365f 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 83e9b0de16f064..4459d8078b174f 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 855b0a3a0d7e3b..411329a0455d0f 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 fc42e3199095d6..083386480c540d 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 89510d10fdc098..0e3d235293ac9b 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 bd12f7bd62c0d3..841b3c319d1bf0 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 b24e603c9d2686..a0849e0882485c 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 47c42bb9c7562b..00000000000000 --- 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 fb917cfc8c476f..2f14919a0190f9 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 105e44811d1432..c23f4ce2503c5e 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 e9cea33ababafb..6e62f12c9de5a6 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 c23f4ce2503c5e..32e9e0e1184b9e 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 480cdd70275555..b839c07a9db89a 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 45b9f5ba8fe2e0..3ee67b79b7bda7 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 ff8a3a1c311c17..72c955923a0cc7 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 5a4b6294bfb132..096d064685a920 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 32e9e0e1184b9e..d66207441adf06 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 d31b15030fd3a9..1e6c26c02e65b8 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 56bf3a09dc06bf..60cb8adee70846 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 276915973a391d..6440326cc77471 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 dd5a9f531bd58b..88dab4c050a7bb 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 690971bc870062..28c7517872cfbd 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 7d9710d8a3e082..5a9ac225c737af 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 a27ca6b710f063..d20d939011c161 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 9905c5020779a7..06c538c68d782c 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 a3fa9586cc47ba..2531d82771cff4 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 f3e082d5202b7a..31b71e0decdb88 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 ade0c706c767e1..9835b18b96e3a5 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 682f679e36ebcf..21513513a8ccb7 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 748eb837fdffb2..0d8630445accd8 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 42c49e6103e3a9..9d715c9146b5ec 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 ac36aab6d1885b..2f1f883351aee8 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 582512a5f10a39..7007b4ce7e3ae8 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 391493b752ef81..903bf6b40ee7e2 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 256394136ee69a..c750eb61fbee79 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 d66207441adf06..507fd8f0304a27 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 6e62f12c9de5a6..a0362b1d7a312d 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 07c3115a9b67c7..17f4b2c4309de8 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 1db45a16304a0c..de0abe2350eb5b 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 8e64f0e5c432f3..00bfcdc119e475 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 12baa7ffd5cede..ce0ecbc29c8e6f 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 c1a856ff841403..b83a81badc14a8 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 93137cdb97b5da..11fb9b2de71991 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 7160347e813ae8..ece2ee8e54788a 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 507fd8f0304a27..c0c4c9caa08445 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 a0362b1d7a312d..5a43e3050ae93a 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 c0c4c9caa08445..60b4735e6f8014 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 5a43e3050ae93a..d174fefeaf486b 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 28e263a283ed33..3cdbece390d3be 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 4c06dde0201854..ae828ed0c1e355 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 60b4735e6f8014..32e5e36f4db103 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 d174fefeaf486b..3b70905d24c5ae 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 32e5e36f4db103..280798e0028222 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 3b70905d24c5ae..ddb7bbc404169c 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 ddb7bbc404169c..1260deb64e6f19 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 638b42d3aa7ce7..ef7a044bc4799b 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 fa225de4fc9a64..f444e0c3ba732a 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 1a06f69580c12e..5340835461ba40 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 a8be282bc3b6df..90a57eafd66d16 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 c89bb36be3cba6..66883d468312bc 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 32b1809609e433..c22029e2f70cd5 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 9ce64d47960931..a237e9b3fba7ff 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 52179dd35767ca..6eaae4c1b91a33 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 ece2ee8e54788a..221b685395ef7c 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 b83a81badc14a8..0b2377c537f938 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 142102dd711c71..79413aff907c4e 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 fc61f59d129d79..e2c9879790fec4 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 3703473606ea28..cc246b0fe44da4 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 7e85a2bdf7e9bb..804ff9602c81cc 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 b0c8cdb9d41959..b19a399b0e50d1 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 14ddb8257ff377..ef604a9cf64173 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 efd044c7e2493a..48122a5ce4e0f0 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 69fab828e63de4..1b40e4458b77f4 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 a2feac83cba9f5..ca7b98f3cfc026 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 00000000000000..6b55c18241c559 --- /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 00000000000000..a876483f025a28 --- /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 00000000000000..e2aa80e3cbdd0f --- /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 00000000000000..95d4f4ebcd3aad --- /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 00000000000000..6d3e69b822c966 --- /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 00000000000000..7e0adc92066568 --- /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 00000000000000..5fb87b5bd73f24 --- /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 1602b26559bed6..ac4b332e7fd7a3 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 ad3fa97ee0c366..1d0d49cd4c3fd1 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 940b8ecc61f4ec..76f2a79c9f3ee2 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 81355671575837..462d3f42b506ca 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 8d759f1a7565e5..3bd2d93f255df5 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 9d4fa4019744ca..a7303247e95b03 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 6e8ebbf6f91cd7..38fca656bef5ac 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 28e6a54f5e92d9..0d49d9a3a256ee 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 ee2586851366c4..434bd6a9bc2242 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 224de241c7374e..33577fad87c049 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 6550921278aa5c..35db22d2da4861 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 03a4a97855b6b0..1b57f31d14a0da 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 f231efe1a07f35..982b64c339a5fd 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 bfb5fabe127f3e..c960a6bac6de07 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 323a52f2fc6e2d..6d5b78650ba2a2 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 1e107a4d6edb42..328ce74ef0b084 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 d68c96a5e92706..f5052d2e7f6ab7 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 80f722bae08686..bc05235257eed7 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 fa228e03194a93..0efa512ca9f1ae 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 879c132ddec54d..616895bca84ebd 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 00000000000000..3a6038b3f55c31 --- /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 930ede4157723a..62b254d132d9ed 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 c2d99433b03466..4ce0ec6e3c10e2 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 a14617c8489ccb..8a15106a868728 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 a876483f025a28..d7d646ca4dd549 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 2bad33d56f228e..85052eef93e051 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 5fb87b5bd73f24..93f94337dff60a 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 a962497f906a9a..9204c41b9288c2 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 1d0d49cd4c3fd1..cf022bc90b43ac 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 f5052d2e7f6ab7..54f186acc1ba57 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 bc05235257eed7..c02b894ff1f3aa 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 0efa512ca9f1ae..65524dd5e4df7e 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 616895bca84ebd..9cf431611d7c4c 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 096d064685a920..c7403907137153 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 1260deb64e6f19..5ad34b69272f7d 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 19433193010e16..c8faf75b348fde 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 d697884e251049..42dd7794ba184f 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 f6c0948a6c30c4..2bd742fc58bccc 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 d614cab6c00129..0be60673881fb3 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 280798e0028222..42c244108b6a47 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 5ad34b69272f7d..22532ca030419c 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 6440326cc77471..af20dd6e202ba7 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 ef7a044bc4799b..ddd03df8bee6b8 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 f444e0c3ba732a..23caf2cfb31a8a 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 82d03be41e1aa8..135721e1856f61 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 5340835461ba40..c0c7991a65a005 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 3a25417f7db4c9..e87b621e442105 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 90a57eafd66d16..e2413670706104 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 66883d468312bc..76b447cde68372 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 c22029e2f70cd5..83deabef473f3a 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 dc2c1f972a5db8..7aa45d2d557010 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 a237e9b3fba7ff..b5f386b1e633fb 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 2b746e5dea4574..9279f8a1745fc9 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 6eaae4c1b91a33..32eb3ff9c53640 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 0b2377c537f938..023506776ab332 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 00000000000000..4f2c1666693552 --- /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 cc613d5247ef4d..dd70e53604f16f 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 285abbef64f0da..153334cb64047d 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 00000000000000..fcaec214d49d9a --- /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 12d1b080c7c639..41871c01bfb505 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 1fb5314ca4fd9e..f3abc26be8dab4 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 22532ca030419c..46d6407ae51f89 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 79413aff907c4e..806d3deb44c05b 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 135721e1856f61..b31eab7f18ad43 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 221b685395ef7c..ece2ee8e54788a 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 023506776ab332..a5430e6ea2a1ee 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 806d3deb44c05b..79413aff907c4e 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 19f235a3f3e4e2..b2d3cec269b729 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 fcaec214d49d9a..34b004c19e583f 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 52c281761f2c18..20cbd42e34b67e 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 076f4b69fb496e..f991cea9c009c2 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 83deabef473f3a..6c73ca1a2e45b1 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 dde75b57b6b20d..dd09a14b4cb81d 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 71a904dcbab3da..15ad066523502f 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 e2413670706104..10efabd70adedb 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 76b447cde68372..6091519f5851e2 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 6c73ca1a2e45b1..ceef73ea7924c9 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 42c244108b6a47..4dfca13f87be36 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 a5430e6ea2a1ee..8ff97fba65cc1f 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 de0abe2350eb5b..d084c3a47e1164 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 00bfcdc119e475..7bf2793dbca253 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 34b004c19e583f..3b0a98d3e06376 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 a0849e0882485c..ee6141bec65ca5 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 11fb9b2de71991..fbbad8a765f5eb 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 dbf9258d3ba799..5194d3b6b1fb85 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 0be60673881fb3..9fb302193d6029 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 182bde560700d2..6ca65ac152ee33 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 2373ae264c492d..4abe58de5a904a 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 3512ab16a37125..90a5bc413d273f 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 ba151c2356191f..9af7d9ddc44eb3 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 ca762cf2b2105f..6f83e34cdbe031 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 46d6407ae51f89..539e0b0d44a8c1 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 ca7b98f3cfc026..fcd201231e4f3d 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 93f94337dff60a..c06acb67614545 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 6b091a5a4503b8..1b1524f00dc730 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 c02b894ff1f3aa..4c0159e7da93dc 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 65524dd5e4df7e..4914ca3c2b07f9 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 9cf431611d7c4c..21fc16a080e655 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 1b35b5636872da..3d621367fc40a4 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 09d94e2418cb83..44ea9624692ce2 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 d8f0d0b6b20a0b..1a07ffd81b1769 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 3a25417f7db4c9..4424f0a3d10fb3 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 874091b2bb7a84..79cc965d71d7a6 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 52179dd35767ca..314219de4048d5 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 4f17f0ae9d2b34..90e01857abdbee 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 60bcdc39358cc3..8c9f134878e86a 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 78915f6580299f..357da5203e336e 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 d2bfdbe4dc967b..da960e565ae837 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 a1bedea9f7debe..9f21117d3296e6 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 734ffc49649de5..015dcb57832158 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 b5aa42cfd539ab..35fa1c42eae5a9 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 48da3f1778b488..6399e1f80984c9 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 11934d3af3ceba..e9e8724272719b 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 5cea7d087f33a7..1b0211dc57f129 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 2dd1f83749372d..861400a3d968d7 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 1d19f436950c7a..2a149df95ad67d 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 cbbbbfaea7ea30..bc2812d7a06992 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 5d52896cc628f8..f92d0d4642b3e4 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 314219de4048d5..52010df1bc35b5 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 d6a6af146730a1..5d619f728a1910 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 8ee953c00795ee..6856e553ab400b 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 4cb397927b53ed..f5f14cb041335c 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 86730c0ab4ac7b..53e68e64536909 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 bd905c1c956506..5bc778830b6e63 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 3b7865e59b9e6b..4b0465743fbd4e 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 17a1d929a0def4..b7c9865cbd9d04 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 64af6fc857273d..ddedccf0578a26 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 e65002780d0b40..e6e7ef5f25e433 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 4c940f1d8d2e1c..ccaa180de0edc2 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 9a87c6e5055bc5..4056cdaa02352e 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 0be3e416966107..2af5506436e20f 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 11fb9b2de71991..117152e2d1872b 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 3b0a98d3e06376..dd2fe2552ee557 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 2af5506436e20f..d2003d982e0b6f 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 452d9c288926e1..3727d80eb2d1fc 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 a5c4849cb63d91..0b377d55f96814 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 651228e4deed1f..a1c02e196fbe4e 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 2bd742fc58bccc..b1842b190a5a65 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 539e0b0d44a8c1..6736c27e687e21 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 6b29eedac030ba..ca6a35b24bcacf 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 de302cb936779b..367bf7a2d2c956 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 5d563bbd6db8db..f930da2ce428c7 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 5194d3b6b1fb85..95ee3680de143e 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 cfa3fe6be2bcb8..06af01e30aff19 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 44d9351657a426..442ee215a304bb 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 6736c27e687e21..98cbed061513c2 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 367bf7a2d2c956..40973a3a67a51d 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 60cb8adee70846..27dc1dc53d6514 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 b1b8e518d42c00..ccd711f51061cc 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 b1842b190a5a65..1d7ad37a46551c 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 ccd711f51061cc..4ebd3d929165a9 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 e6e7ef5f25e433..38d4e92c72a500 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 0e09d071499100..9cd216f6066d21 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 7801e558c7ac77..17728291783186 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 52f3026bca623a..c1939bf6fa07ac 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 4056cdaa02352e..8cb7afbda0e70a 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 88553c314112f3..fef70c3a484552 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 87aa571ce6b8a0..4baf453dcb5644 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 e1606092cd14e2..798be5f991add8 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 95ee3680de143e..6de9e74df47c3b 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 dbb3fb0eb72e0b..9f1cd0b8ab6b6c 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 ae828ed0c1e355..8fb43882b50730 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 3e4133d83373da..eb0aea81fd88f3 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 27dc1dc53d6514..e65d195290259e 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 909a1804456c29..8f03945df437c8 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 fd1a79fa05778a..2f57d05be42278 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 703c9d78e5f89c..75609d58f7792f 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 11c60c1af56869..97c933f2ef8c5e 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 0f177f91071dd9..6c14dac0f12a27 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 c29f56262896e4..fc08be3e30a6fe 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 b53fb4000dee1f..994072d5cb03ce 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 57f2e2afd77072..83b7077cbaaddf 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 8281db67ee66e2..82b12e6ce9a224 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 6257acce800ec8..26c7bb3b6c125c 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 d20d939011c161..eb78dbc085df1b 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 06c538c68d782c..24ed952495fa3b 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 2531d82771cff4..55f02b24ac41e4 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 31b71e0decdb88..db37e772c88870 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 ece2ee8e54788a..268212d4294d0c 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 9835b18b96e3a5..17969bde0620eb 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 e188a21fd0d364..2e89aa2961c73a 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 8ff97fba65cc1f..c3e5af0d1f7711 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 21513513a8ccb7..3b793feda632ac 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 0d8630445accd8..2e4da7eb4158c2 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 9d715c9146b5ec..2413bd42460d42 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 2f1f883351aee8..d67ecedaab14ca 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 7007b4ce7e3ae8..390b50acb37057 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 903bf6b40ee7e2..6e8956d3326ea0 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 6199ec87bf5acf..9d95ef4cfc7e57 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 5d619f728a1910..b1dd78ff59f343 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 2531fd2625b4b7..dd2ee48b7a6206 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 9bff33e4aa69ce..975856beba556f 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 a02b44523e26c1..d8a7d18eb87a91 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 d7d646ca4dd549..4fded7b9e1bce9 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 bd8ff3d1fe3973..41396a1243ea95 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 798be5f991add8..c5a6db3cf4347b 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 8fb43882b50730..bdb5b14709dfdd 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 eb0aea81fd88f3..79c527c1b993d1 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 442ee215a304bb..06a9a4b892647c 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 98cbed061513c2..cf4db17f84c8c9 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 cf4db17f84c8c9..c0e35c3282656c 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 eb78dbc085df1b..7b53887709217b 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 8f42f12347728d..86775f77a7671c 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 38cda5a9a0f7c9..5115dd7da4e38c 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 79413aff907c4e..57a40058870931 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 e2c9879790fec4..d0e1be12e762f1 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 5115dd7da4e38c..19f4e918b78624 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 1d7ad37a46551c..49352cc285693b 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 1b405e7fcd0b4c..6d1cde24854071 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 06a9a4b892647c..2a7150c8a4f65b 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 c0e35c3282656c..af67c5e40593c5 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 7a0916b9d6554c..cf6e1c9aebba65 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 38d4e92c72a500..38e75f75ad04b9 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 4a293e0c962ccc..1b700fb1a6ad09 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 71c65a5fe0b0d9..15ddbd9334c8d1 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 9cd216f6066d21..0de431186b1512 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 5f358badde4012..a08734ff765bbc 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 a52081b296af05..91f072e5f4651e 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 3b793feda632ac..a497affa266e47 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 2e4da7eb4158c2..b4277479d8fd9c 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 2413bd42460d42..46653900cb1c78 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 d67ecedaab14ca..2bc501c9a7c722 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 390b50acb37057..ab3a92d0b3f706 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 6e8956d3326ea0..7dea591b895eeb 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 c750eb61fbee79..dd81c860e9fa83 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 412149fd27837c..d7f6546bf34a92 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 75022922917b32..f123068e417584 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 15ddbd9334c8d1..95298603d706ad 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 4fded7b9e1bce9..a48124cdbcb6ad 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 41396a1243ea95..da5a5a1cdc3eb7 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 9a03bee41eeeaf..62bd1058774de7 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 6d1cde24854071..79b0ccaf1f0bc0 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 2a7150c8a4f65b..b164d27ded6486 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 af67c5e40593c5..33a9a0bf0396ea 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 4e6a93499f4c9b..16b5af499bb90f 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 1f286b42c14491..eec60f924bf384 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 3a732424853a08..4e4f9053bd24f4 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 6de9e74df47c3b..c25e040ad09ce8 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 c8e6669275e117..65fd0646c639ec 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 250bfc2752f1bc..0e63cc8f5956e9 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 06cb84ad79a891..78522682054e16 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 62bd1058774de7..3eedd69410d11f 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 6d3e69b822c966..d700abdaa70ffc 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 3eedd69410d11f..7016ec0fc4110b 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 09dd42dc91dd5c..90b989ac3b52eb 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 bd6e022353fadc..6744a8d1116234 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 7016ec0fc4110b..5b8b25d02658ba 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, }),