diff --git a/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts index f6c0948a6c30c..2bd742fc58bcc 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/examples/alerting_example/server/plugin.ts @@ -25,6 +25,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../../x-pack/plug import { alertType as alwaysFiringAlert } from './alert_types/always_firing'; import { alertType as peopleInSpaceAlert } from './alert_types/astros'; import { INDEX_THRESHOLD_ID } from '../../../x-pack/plugins/alerting_builtins/server'; +import { ALERTING_EXAMPLE_APP_ID } from '../common/constants'; // this plugin's dependendencies export interface AlertingExampleDeps { @@ -38,7 +39,7 @@ export class AlertingExamplePlugin implements Plugin { actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', producer: 'alerts', - authorizedConsumers: ['myApp'], + authorizedConsumers: { + myApp: { read: true, all: true }, + }, }, ]) ); @@ -3712,6 +3714,12 @@ describe('listAlertTypes', () => { }; const setOfAlertTypes = new Set([myAppAlertType, alertingAlertType]); + const authorizedConsumers = { + alerts: { read: true, all: true }, + myApp: { read: true, all: true }, + myOtherApp: { read: true, all: true }, + }; + beforeEach(() => { alertsClient = new AlertsClient(alertsClientParams); }); @@ -3720,14 +3728,14 @@ describe('listAlertTypes', () => { alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); authorization.filterByAlertTypeAuthorization.mockResolvedValue( new Set([ - { ...myAppAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, - { ...alertingAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, + { ...myAppAlertType, authorizedConsumers }, + { ...alertingAlertType, authorizedConsumers }, ]) ); expect(await alertsClient.listAlertTypes()).toEqual( new Set([ - { ...myAppAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, - { ...alertingAlertType, authorizedConsumers: ['alerts', 'myApp', 'myOtherApp'] }, + { ...myAppAlertType, authorizedConsumers }, + { ...alertingAlertType, authorizedConsumers }, ]) ); }); @@ -3762,7 +3770,9 @@ describe('listAlertTypes', () => { actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', producer: 'alerts', - authorizedConsumers: ['myApp'], + authorizedConsumers: { + myApp: { read: true, all: true }, + }, }, ]); authorization.filterByAlertTypeAuthorization.mockResolvedValue(authorizedTypes); diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index d614cab6c0012..0be60673881fb 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -36,7 +36,11 @@ import { TaskManagerStartContract } from '../../task_manager/server'; import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists'; import { RegistryAlertType } from './alert_type_registry'; -import { AlertsAuthorization } from './authorization/alerts_authorization'; +import { + AlertsAuthorization, + WriteOperations, + ReadOperations, +} from './authorization/alerts_authorization'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; @@ -172,7 +176,11 @@ export class AlertsClient { } public async create({ data, options }: CreateOptions): Promise { - await this.authorization.ensureAuthorized(data.alertTypeId, data.consumer, 'create'); + await this.authorization.ensureAuthorized( + data.alertTypeId, + data.consumer, + WriteOperations.Create + ); // Throws an error if alert type isn't registered const alertType = this.alertTypeRegistry.get(data.alertTypeId); @@ -233,14 +241,18 @@ export class AlertsClient { await this.authorization.ensureAuthorized( result.attributes.alertTypeId, result.attributes.consumer, - 'get' + ReadOperations.Get ); return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references); } public async getAlertState({ id }: { id: string }): Promise { const alert = await this.get({ id }); - await this.authorization.ensureAuthorized(alert.alertTypeId, alert.consumer, 'getAlertState'); + await this.authorization.ensureAuthorized( + alert.alertTypeId, + alert.consumer, + ReadOperations.GetAlertState + ); if (alert.scheduledTaskId) { const { state } = taskInstanceToAlertTaskInstance( await this.taskManager.get(alert.scheduledTaskId), @@ -317,7 +329,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, - 'delete' + WriteOperations.Delete ); const removeResult = await this.unsecuredSavedObjectsClient.delete('alert', id); @@ -348,7 +360,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( alertSavedObject.attributes.alertTypeId, alertSavedObject.attributes.consumer, - 'update' + WriteOperations.Update ); const updateResult = await this.updateAlert({ id, data }, alertSavedObject); @@ -454,7 +466,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, - 'updateApiKey' + WriteOperations.UpdateApiKey ); const username = await this.getUserName(); @@ -516,7 +528,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, - 'enable' + WriteOperations.Enable ); if (attributes.enabled === false) { @@ -568,7 +580,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, - 'disable' + WriteOperations.Disable ); if (attributes.enabled === true) { @@ -600,7 +612,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, - 'muteAll' + WriteOperations.MuteAll ); await this.unsecuredSavedObjectsClient.update('alert', id, { @@ -615,7 +627,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, - 'unmuteAll' + WriteOperations.UnmuteAll ); await this.unsecuredSavedObjectsClient.update('alert', id, { @@ -634,7 +646,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, - 'muteInstance' + WriteOperations.MuteInstance ); const mutedInstanceIds = attributes.mutedInstanceIds || []; @@ -666,7 +678,7 @@ export class AlertsClient { await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, - 'unmuteInstance' + WriteOperations.UnmuteInstance ); const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { @@ -684,10 +696,10 @@ export class AlertsClient { } public async listAlertTypes() { - return await this.authorization.filterByAlertTypeAuthorization( - this.alertTypeRegistry.list(), - 'get' - ); + return await this.authorization.filterByAlertTypeAuthorization(this.alertTypeRegistry.list(), [ + ReadOperations.Get, + WriteOperations.Create, + ]); } private async scheduleAlert(id: string, alertTypeId: string) { diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index 280798e002822..42c244108b6a4 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -8,7 +8,12 @@ import { alertTypeRegistryMock } from '../alert_type_registry.mock'; import { securityMock } from '../../../../plugins/security/server/mocks'; import { PluginStartContract as FeaturesStartContract, Feature } from '../../../features/server'; import { featuresPluginMock } from '../../../features/server/mocks'; -import { AlertsAuthorization, ensureFieldIsSafeForQuery } from './alerts_authorization'; +import { + AlertsAuthorization, + ensureFieldIsSafeForQuery, + WriteOperations, + ReadOperations, +} from './alerts_authorization'; import { alertsAuthorizationAuditLoggerMock } from './audit_logger.mock'; import { AlertsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; @@ -171,7 +176,7 @@ describe('ensureAuthorized', () => { auditLogger, }); - await alertAuthorization.ensureAuthorized('myType', 'myApp', 'create'); + await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); expect(alertTypeRegistry.get).toHaveBeenCalledTimes(0); }); @@ -196,7 +201,7 @@ describe('ensureAuthorized', () => { privileges: [], }); - await alertAuthorization.ensureAuthorized('myType', 'myApp', 'create'); + await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); @@ -238,7 +243,7 @@ describe('ensureAuthorized', () => { privileges: [], }); - await alertAuthorization.ensureAuthorized('myType', 'alerts', 'create'); + await alertAuthorization.ensureAuthorized('myType', 'alerts', WriteOperations.Create); expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); @@ -280,7 +285,7 @@ describe('ensureAuthorized', () => { auditLogger, }); - await alertAuthorization.ensureAuthorized('myType', 'myOtherApp', 'create'); + await alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create); expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); @@ -338,7 +343,7 @@ describe('ensureAuthorized', () => { }); await expect( - alertAuthorization.ensureAuthorized('myType', 'myOtherApp', 'create') + alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` ); @@ -386,7 +391,7 @@ describe('ensureAuthorized', () => { }); await expect( - alertAuthorization.ensureAuthorized('myType', 'myOtherApp', 'create') + alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unauthorized to create a \\"myType\\" alert by \\"myApp\\""` ); @@ -434,7 +439,7 @@ describe('ensureAuthorized', () => { }); await expect( - alertAuthorization.ensureAuthorized('myType', 'myOtherApp', 'create') + alertAuthorization.ensureAuthorized('myType', 'myOtherApp', WriteOperations.Create) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unauthorized to create a \\"myType\\" alert for \\"myOtherApp\\""` ); @@ -544,10 +549,6 @@ describe('getFindAuthorizationFilter', () => { username: 'some-user', hasAllRequested: false, privileges: [ - { - privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'find'), - authorized: true, - }, { privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'find'), authorized: true, @@ -556,10 +557,6 @@ describe('getFindAuthorizationFilter', () => { privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'find'), authorized: false, }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'find'), - authorized: true, - }, { privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), authorized: true, @@ -610,10 +607,6 @@ describe('getFindAuthorizationFilter', () => { username: 'some-user', hasAllRequested: false, privileges: [ - { - privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'find'), - authorized: true, - }, { privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'find'), authorized: true, @@ -622,10 +615,6 @@ describe('getFindAuthorizationFilter', () => { privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'find'), authorized: false, }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'find'), - authorized: true, - }, { privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), authorized: true, @@ -696,19 +685,31 @@ describe('filterByAlertTypeAuthorization', () => { await expect( alertAuthorization.filterByAlertTypeAuthorization( new Set([myAppAlertType, alertingAlertType]), - 'create' + [WriteOperations.Create] ) ).resolves.toMatchInlineSnapshot(` Set { Object { "actionGroups": Array [], "actionVariables": undefined, - "authorizedConsumers": Array [ - "alerts", - "myApp", - "myOtherApp", - "myAppWithSubFeature", - ], + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + "myAppWithSubFeature": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, + }, "defaultActionGroupId": "default", "id": "myAppAlertType", "name": "myAppAlertType", @@ -717,12 +718,24 @@ describe('filterByAlertTypeAuthorization', () => { Object { "actionGroups": Array [], "actionVariables": undefined, - "authorizedConsumers": Array [ - "alerts", - "myApp", - "myOtherApp", - "myAppWithSubFeature", - ], + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + "myAppWithSubFeature": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, + }, "defaultActionGroupId": "default", "id": "alertingAlertType", "name": "alertingAlertType", @@ -742,10 +755,6 @@ describe('filterByAlertTypeAuthorization', () => { username: 'some-user', hasAllRequested: false, privileges: [ - { - privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'create'), - authorized: true, - }, { privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'create'), authorized: true, @@ -754,10 +763,6 @@ describe('filterByAlertTypeAuthorization', () => { privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'create'), authorized: false, }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'create'), - authorized: true, - }, { privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), authorized: true, @@ -781,17 +786,23 @@ describe('filterByAlertTypeAuthorization', () => { await expect( alertAuthorization.filterByAlertTypeAuthorization( new Set([myAppAlertType, alertingAlertType]), - 'create' + [WriteOperations.Create] ) ).resolves.toMatchInlineSnapshot(` Set { Object { "actionGroups": Array [], "actionVariables": undefined, - "authorizedConsumers": Array [ - "alerts", - "myApp", - ], + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + }, "defaultActionGroupId": "default", "id": "alertingAlertType", "name": "alertingAlertType", @@ -800,11 +811,20 @@ describe('filterByAlertTypeAuthorization', () => { Object { "actionGroups": Array [], "actionVariables": undefined, - "authorizedConsumers": Array [ - "alerts", - "myApp", - "myOtherApp", - ], + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, + }, "defaultActionGroupId": "default", "id": "myAppAlertType", "name": "myAppAlertType", @@ -814,7 +834,7 @@ describe('filterByAlertTypeAuthorization', () => { `); }); - test('omits types which have no consumers under which the operation is authorized', async () => { + test('authorizes user under the Alerts consumer when they are authorized by the producer', async () => { const authorization = mockAuthorization(); const checkPrivileges: jest.MockedFunction { hasAllRequested: false, privileges: [ { - privilege: mockAuthorizationAction('alertingAlertType', 'alerts', 'create'), + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), authorized: true, }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + ], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + auditLogger, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.filterByAlertTypeAuthorization(new Set([myAppAlertType]), [ + WriteOperations.Create, + ]) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + }, + "defaultActionGroupId": "default", + "id": "myAppAlertType", + "name": "myAppAlertType", + "producer": "myApp", + }, + } + `); + }); + + test('augments a list of types with consumers under which multiple operations are authorized', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: [ { privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'create'), authorized: true, }, { privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'create'), - authorized: true, + authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'create'), + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), authorized: false, }, + { + privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'get'), + authorized: true, + }, + ], + }); + + const alertAuthorization = new AlertsAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + auditLogger, + }); + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.filterByAlertTypeAuthorization( + new Set([myAppAlertType, alertingAlertType]), + [WriteOperations.Create, ReadOperations.Get] + ) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": false, + "read": true, + }, + }, + "defaultActionGroupId": "default", + "id": "alertingAlertType", + "name": "alertingAlertType", + "producer": "alerts", + }, + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "alerts": Object { + "all": false, + "read": true, + }, + "myApp": Object { + "all": false, + "read": true, + }, + "myOtherApp": Object { + "all": false, + "read": true, + }, + }, + "defaultActionGroupId": "default", + "id": "myAppAlertType", + "name": "myAppAlertType", + "producer": "myApp", + }, + } + `); + }); + + test('omits types which have no consumers under which the operation is authorized', async () => { + const authorization = mockAuthorization(); + const checkPrivileges: jest.MockedFunction> = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: [ + { + privilege: mockAuthorizationAction('alertingAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('alertingAlertType', 'myOtherApp', 'create'), + authorized: true, + }, { privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), authorized: false, @@ -863,18 +1042,27 @@ describe('filterByAlertTypeAuthorization', () => { await expect( alertAuthorization.filterByAlertTypeAuthorization( new Set([myAppAlertType, alertingAlertType]), - 'create' + [WriteOperations.Create] ) ).resolves.toMatchInlineSnapshot(` Set { Object { "actionGroups": Array [], "actionVariables": undefined, - "authorizedConsumers": Array [ - "alerts", - "myApp", - "myOtherApp", - ], + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "myApp": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, + }, "defaultActionGroupId": "default", "id": "alertingAlertType", "name": "alertingAlertType", diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 5ad34b69272f7..22532ca030419 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { pluck, mapValues, remove, omit, isUndefined } from 'lodash'; +import { pluck, mapValues, remove, omit, isUndefined, zipObject } from 'lodash'; import { KibanaRequest } from 'src/core/server'; import { RecursiveReadonly } from '@kbn/utility-types'; import { ALERTS_FEATURE_ID } from '../../common'; @@ -16,10 +16,36 @@ import { FeatureKibanaPrivileges, SubFeaturePrivilegeConfig } from '../../../fea import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; +export enum ReadOperations { + Get = 'get', + GetAlertState = 'getAlertState', + Find = 'find', +} + +export enum WriteOperations { + Create = 'create', + Delete = 'delete', + Update = 'update', + UpdateApiKey = 'updateApiKey', + Enable = 'enable', + Disable = 'disable', + MuteAll = 'muteAll', + UnmuteAll = 'unmuteAll', + MuteInstance = 'muteInstance', + UnmuteInstance = 'unmuteInstance', +} + +interface HasPrivileges { + read: boolean; + all: boolean; +} +type AuthorizedConsumers = Record; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { - authorizedConsumers: string[]; + authorizedConsumers: AuthorizedConsumers; } +type IsAuthorizedAtProducerLevel = boolean; + export interface ConstructorOptions { alertTypeRegistry: AlertTypeRegistry; request: KibanaRequest; @@ -49,7 +75,11 @@ export class AlertsAuthorization { this.auditLogger = auditLogger; } - public async ensureAuthorized(alertTypeId: string, consumer: string, operation: string) { + public async ensureAuthorized( + alertTypeId: string, + consumer: string, + operation: ReadOperations | WriteOperations + ) { const { authorization } = this; if (authorization) { const alertType = this.alertTypeRegistry.get(alertTypeId); @@ -123,10 +153,12 @@ export class AlertsAuthorization { ensureAlertTypeIsAuthorized: (alertTypeId: string, consumer: string) => void; }> { if (this.authorization) { - const { username, authorizedAlertTypes } = await this.augmentAlertTypesWithAuthorization( - this.alertTypeRegistry.list(), - 'find' - ); + const { + username, + authorizedAlertTypes, + } = await this.augmentAlertTypesWithAuthorization(this.alertTypeRegistry.list(), [ + ReadOperations.Find, + ]); if (!authorizedAlertTypes.size) { throw Boom.forbidden( @@ -136,7 +168,7 @@ export class AlertsAuthorization { const authorizedAlertTypeIdsToConsumers = new Set( [...authorizedAlertTypes].reduce((alertTypeIdConsumerPairs, alertType) => { - for (const consumer of alertType.authorizedConsumers) { + for (const consumer of Object.keys(alertType.authorizedConsumers)) { alertTypeIdConsumerPairs.push(`${alertType.id}/${consumer}`); } return alertTypeIdConsumerPairs; @@ -175,18 +207,18 @@ export class AlertsAuthorization { public async filterByAlertTypeAuthorization( alertTypes: Set, - operation: string + operations: Array ): Promise> { const { authorizedAlertTypes } = await this.augmentAlertTypesWithAuthorization( alertTypes, - operation + operations ); return authorizedAlertTypes; } private async augmentAlertTypesWithAuthorization( alertTypes: Set, - operation: string + operations: Array ): Promise<{ username?: string; hasAllRequested: boolean; @@ -210,7 +242,10 @@ export class AlertsAuthorization { }) .map((feature) => feature.id); - const allPossibleConsumers = [ALERTS_FEATURE_ID, ...featuresIds]; + const allPossibleConsumers: AuthorizedConsumers = asAuthorizedConsumers( + [ALERTS_FEATURE_ID, ...featuresIds], + { read: true, all: true } + ); if (!this.authorization) { return { @@ -223,29 +258,35 @@ export class AlertsAuthorization { ); // add an empty `authorizedConsumers` array on each alertType - const alertTypesWithAutherization = this.augmentWithAuthorizedConsumers(alertTypes, []); + const alertTypesWithAutherization = this.augmentWithAuthorizedConsumers(alertTypes, {}); const preAuthorizedAlertTypes = new Set(); // map from privilege to alertType which we can refer back to when analyzing the result // of checkPrivileges - const privilegeToAlertType = new Map(); + const privilegeToAlertType = new Map< + string, + [RegistryAlertTypeWithAuth, string, HasPrivileges, IsAuthorizedAtProducerLevel] + >(); // as we can't ask ES for the user's individual privileges we need to ask for each feature // and alertType in the system whether this user has this privilege for (const alertType of alertTypesWithAutherization) { if (alertType.producer === ALERTS_FEATURE_ID) { - alertType.authorizedConsumers.push(ALERTS_FEATURE_ID); + alertType.authorizedConsumers[ALERTS_FEATURE_ID] = { read: true, all: true }; preAuthorizedAlertTypes.add(alertType); } for (const feature of featuresIds) { - privilegeToAlertType.set( - this.authorization!.actions.alerting.get(alertType.id, feature, operation), - [ - alertType, - // granting privileges under the producer automatically authorized the Alerts Management UI as well - alertType.producer === feature ? [ALERTS_FEATURE_ID, feature] : [feature], - ] - ); + for (const operation of operations) { + privilegeToAlertType.set( + this.authorization!.actions.alerting.get(alertType.id, feature, operation), + [ + alertType, + feature, + hasPrivilegeByOperation(operation), + alertType.producer === feature, + ] + ); + } } } @@ -262,8 +303,24 @@ export class AlertsAuthorization { : // only has some of the required privileges privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { if (authorized && privilegeToAlertType.has(privilege)) { - const [alertType, consumers] = privilegeToAlertType.get(privilege)!; - alertType.authorizedConsumers.push(...consumers); + const [ + alertType, + feature, + hasPrivileges, + isAuthorizedAtProducerLevel, + ] = privilegeToAlertType.get(privilege)!; + alertType.authorizedConsumers[feature] = mergeHasPrivileges( + hasPrivileges, + alertType.authorizedConsumers[feature] + ); + + if (isAuthorizedAtProducerLevel) { + // granting privileges under the producer automatically authorized the Alerts Management UI as well + alertType.authorizedConsumers[ALERTS_FEATURE_ID] = mergeHasPrivileges( + hasPrivileges, + alertType.authorizedConsumers[ALERTS_FEATURE_ID] + ); + } authorizedAlertTypes.add(alertType); } return authorizedAlertTypes; @@ -274,12 +331,12 @@ export class AlertsAuthorization { private augmentWithAuthorizedConsumers( alertTypes: Set, - authorizedConsumers: string[] + authorizedConsumers: AuthorizedConsumers ): Set { return new Set( Array.from(alertTypes).map((alertType) => ({ ...alertType, - authorizedConsumers: [...authorizedConsumers], + authorizedConsumers: { ...authorizedConsumers }, })) ); } @@ -288,7 +345,9 @@ export class AlertsAuthorization { return Array.from(alertTypes).reduce((filters, { id, authorizedConsumers }) => { ensureFieldIsSafeForQuery('alertTypeId', id); filters.push( - `(alert.attributes.alertTypeId:${id} and alert.attributes.consumer:(${authorizedConsumers + `(alert.attributes.alertTypeId:${id} and alert.attributes.consumer:(${Object.keys( + authorizedConsumers + ) .map((consumer) => { ensureFieldIsSafeForQuery('alertTypeId', id); return consumer; @@ -325,3 +384,26 @@ function hasAnyAlertingPrivileges( ((privileges?.alerting?.all?.length ?? 0) || (privileges?.alerting?.read?.length ?? 0)) > 0 ); } + +function mergeHasPrivileges(left: HasPrivileges, right?: HasPrivileges): HasPrivileges { + return { + read: (left.read || right?.read) ?? false, + all: (left.all || right?.all) ?? false, + }; +} + +function hasPrivilegeByOperation(operation: ReadOperations | WriteOperations): HasPrivileges { + const read = Object.values(ReadOperations).includes((operation as unknown) as ReadOperations); + const all = Object.values(WriteOperations).includes((operation as unknown) as WriteOperations); + return { + read: read || all, + all, + }; +} + +function asAuthorizedConsumers( + consumers: string[], + hasPrivileges: HasPrivileges +): AuthorizedConsumers { + return zipObject(consumers.map((feature) => [feature, hasPrivileges])); +} diff --git a/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts index 6440326cc7747..af20dd6e202ba 100644 --- a/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts +++ b/x-pack/plugins/alerts/server/routes/list_alert_types.test.ts @@ -43,7 +43,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', - authorizedConsumers: [], + authorizedConsumers: {}, actionVariables: { context: [], state: [], @@ -69,7 +69,7 @@ describe('listAlertTypesRoute', () => { "context": Array [], "state": Array [], }, - "authorizedConsumers": Array [], + "authorizedConsumers": Object {}, "defaultActionGroupId": "default", "id": "1", "name": "name", @@ -107,7 +107,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', - authorizedConsumers: [], + authorizedConsumers: {}, actionVariables: { context: [], state: [], @@ -156,7 +156,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', - authorizedConsumers: [], + authorizedConsumers: {}, actionVariables: { context: [], state: [], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts index ef7a044bc4799..ddd03df8bee6b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts @@ -184,7 +184,7 @@ function getAlertType(actionVariables: ActionVariables): AlertType { actionVariables, actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', - authorizedConsumers: [], + authorizedConsumers: {}, producer: ALERTS_FEATURE_ID, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts index f444e0c3ba732..23caf2cfb31a8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts @@ -46,7 +46,7 @@ describe('loadAlertTypes', () => { producer: ALERTS_FEATURE_ID, actionGroups: [{ id: 'default', name: 'Default' }], defaultActionGroupId: 'default', - authorizedConsumers: [], + authorizedConsumers: {}, }, ]; http.get.mockResolvedValueOnce(resolvedValue); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts index 82d03be41e1aa..135721e1856f6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/capabilities.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Alert, AlertType } from '../../types'; + /** * NOTE: Applications that want to show the alerting UIs will need to add * check against their features here until we have a better solution. This @@ -23,8 +25,14 @@ function createCapabilityCheck(capability: string) { } export const hasShowAlertsCapability = createCapabilityCheck('alerting:show'); + export const hasShowActionsCapability = createCapabilityCheck('actions:show'); -export const hasSaveAlertsCapability = createCapabilityCheck('alerting:save'); export const hasSaveActionsCapability = createCapabilityCheck('actions:save'); -export const hasDeleteAlertsCapability = createCapabilityCheck('alerting:delete'); export const hasDeleteActionsCapability = createCapabilityCheck('actions:delete'); + +export function hasAllPrivilege(alert: Alert, alertType?: AlertType): boolean { + return alertType?.authorizedConsumers[alert.consumer]?.all ?? false; +} +export function hasReadPrivilege(alert: Alert, alertType?: AlertType): boolean { + return alertType?.authorizedConsumers[alert.consumer]?.read ?? false; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 5340835461ba4..c0c7991a65a00 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -31,8 +31,6 @@ jest.mock('../../../app_context', () => ({ get: jest.fn(() => ({})), securitySolution: { 'alerting:show': true, - 'alerting:save': true, - 'alerting:delete': true, }, }, actionTypeRegistry: jest.fn(), @@ -68,7 +66,7 @@ jest.mock('react-router-dom', () => ({ })); jest.mock('../../../lib/capabilities', () => ({ - hasSaveAlertsCapability: jest.fn(() => true), + hasAllPrivilege: jest.fn(() => true), })); const mockAlertApis = { @@ -79,6 +77,10 @@ const mockAlertApis = { requestRefresh: jest.fn(), }; +const authorizedConsumers = { + [ALERTS_FEATURE_ID]: { read: true, all: true }, +}; + // const AlertDetails = withBulkAlertOperations(RawAlertDetails); describe('alert_details', () => { // mock Api handlers @@ -92,7 +94,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; expect( @@ -131,7 +133,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; expect( @@ -161,7 +163,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const actionTypes: ActionType[] = [ @@ -215,7 +217,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const actionTypes: ActionType[] = [ { @@ -274,7 +276,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; expect( @@ -294,7 +296,7 @@ describe('alert_details', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; expect( @@ -323,7 +325,7 @@ describe('disable button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const enableButton = shallow( @@ -351,7 +353,7 @@ describe('disable button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const enableButton = shallow( @@ -379,7 +381,7 @@ describe('disable button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const disableAlert = jest.fn(); @@ -416,7 +418,7 @@ describe('disable button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const enableAlert = jest.fn(); @@ -456,7 +458,7 @@ describe('mute button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const enableButton = shallow( @@ -485,7 +487,7 @@ describe('mute button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const enableButton = shallow( @@ -514,7 +516,7 @@ describe('mute button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const muteAlert = jest.fn(); @@ -552,7 +554,7 @@ describe('mute button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const unmuteAlert = jest.fn(); @@ -590,7 +592,7 @@ describe('mute button', () => { actionVariables: { context: [], state: [] }, defaultActionGroupId: 'default', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID], + authorizedConsumers, }; const enableButton = shallow( @@ -614,7 +616,7 @@ function mockAlert(overloads: Partial = {}): Alert { name: `alert-${uuid.v4()}`, tags: [], alertTypeId: '.noop', - consumer: 'consumer', + consumer: ALERTS_FEATURE_ID, schedule: { interval: '1m' }, actions: [], params: {}, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 3a25417f7db4c..e87b621e44210 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -28,7 +28,6 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useAppDependencies } from '../../../app_context'; -import { hasSaveAlertsCapability } from '../../../lib/capabilities'; import { Alert, AlertType, ActionType } from '../../../../types'; import { ComponentOpts as BulkOperationsComponentOpts, @@ -40,6 +39,7 @@ import { PLUGIN } from '../../../constants/plugin'; import { AlertEdit } from '../../alert_form'; import { AlertsContextProvider } from '../../../context/alerts_context'; import { routeToAlertDetails } from '../../../constants'; +import { hasAllPrivilege } from '../../../lib/capabilities'; type AlertDetailsProps = { alert: Alert; @@ -71,7 +71,7 @@ export const AlertDetails: React.FunctionComponent = ({ dataPlugin, } = useAppDependencies(); - const canSave = hasSaveAlertsCapability(capabilities); + const canSave = hasAllPrivilege(alert, alertType); const actionTypesByTypeId = indexBy(actionTypes, 'id'); const hasEditButton = canSave && alertTypeRegistry.has(alert.alertTypeId) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 90a57eafd66d1..e241367070610 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -61,7 +61,10 @@ describe('alert_add', () => { ], defaultActionGroupId: 'testActionGroup', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID, 'test'], + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + test: { read: true, all: true }, + }, actionVariables: { context: [], state: [], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index 66883d468312b..76b447cde6837 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -82,7 +82,10 @@ describe('alert_form', () => { ], defaultActionGroupId: 'testActionGroup', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID, 'test'], + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + test: { read: true, all: true }, + }, }, ]; loadAlertTypes.mockResolvedValue(alertTypes); @@ -191,7 +194,10 @@ describe('alert_form', () => { ], defaultActionGroupId: 'testActionGroup', producer: ALERTS_FEATURE_ID, - authorizedConsumers: [ALERTS_FEATURE_ID, 'test'], + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + test: { read: true, all: true }, + }, }, { id: 'same-consumer-producer-alert-type', @@ -204,7 +210,10 @@ describe('alert_form', () => { ], defaultActionGroupId: 'testActionGroup', producer: 'test', - authorizedConsumers: [ALERTS_FEATURE_ID, 'test'], + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + test: { read: true, all: true }, + }, }, ]); const mocks = coreMock.createSetup(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index c22029e2f70cd..83deabef473f3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -174,9 +174,9 @@ export const AlertForm = ({ .filter( (alertTypeRegistryItem: AlertTypeModel) => alertTypesIndex.has(alertTypeRegistryItem.id) && - alertTypesIndex - .get(alertTypeRegistryItem.id)! - .authorizedConsumers.includes(alert.consumer) + (alertTypesIndex.get(alertTypeRegistryItem.id)?.authorizedConsumers[alert.consumer] + ?.all ?? + false) ) .filter((alertTypeRegistryItem: AlertTypeModel) => alert.consumer === ALERTS_FEATURE_ID diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index dc2c1f972a5db..7aa45d2d55701 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -18,6 +18,7 @@ import { AppContextProvider } from '../../../app_context'; import { chartPluginMock } from '../../../../../../../../src/plugins/charts/public/mocks'; import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; import { alertingPluginMock } from '../../../../../../alerts/public/mocks'; +import { ALERTS_FEATURE_ID } from '../../../../../../alerts/common'; jest.mock('../../../lib/action_connector_api', () => ({ loadActionTypes: jest.fn(), @@ -48,6 +49,17 @@ const alertType = { alertParamsExpression: () => null, requiresAppContext: false, }; +const alertTypeFromApi = { + id: 'test_alert_type', + name: 'some alert type', + actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, + defaultActionGroupId: 'default', + producer: ALERTS_FEATURE_ID, + authorizedConsumers: { + [ALERTS_FEATURE_ID]: { read: true, all: true }, + }, +}; alertTypeRegistry.list.mockReturnValue([alertType]); actionTypeRegistry.list.mockReturnValue([]); @@ -74,7 +86,7 @@ describe('alerts_list component empty', () => { name: 'Test2', }, ]); - loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]); + loadAlertTypes.mockResolvedValue([alertTypeFromApi]); loadAllActions.mockResolvedValue([]); const mockes = coreMock.createSetup(); @@ -99,8 +111,6 @@ describe('alerts_list component empty', () => { ...capabilities, securitySolution: { 'alerting:show': true, - 'alerting:save': true, - 'alerting:delete': true, }, }, history: (scopedHistoryMock.create() as unknown) as ScopedHistory, @@ -194,7 +204,7 @@ describe('alerts_list component with items', () => { name: 'Test2', }, ]); - loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]); + loadAlertTypes.mockResolvedValue([alertTypeFromApi]); loadAllActions.mockResolvedValue([]); const mockes = coreMock.createSetup(); const [ @@ -218,8 +228,6 @@ describe('alerts_list component with items', () => { ...capabilities, securitySolution: { 'alerting:show': true, - 'alerting:save': true, - 'alerting:delete': true, }, }, history: (scopedHistoryMock.create() as unknown) as ScopedHistory, @@ -300,8 +308,6 @@ describe('alerts_list component empty with show only capability', () => { ...capabilities, securitySolution: { 'alerting:show': true, - 'alerting:save': false, - 'alerting:delete': false, }, }, history: (scopedHistoryMock.create() as unknown) as ScopedHistory, @@ -391,7 +397,8 @@ describe('alerts_list with show only capability', () => { name: 'Test2', }, ]); - loadAlertTypes.mockResolvedValue([{ id: 'test_alert_type', name: 'some alert type' }]); + + loadAlertTypes.mockResolvedValue([alertTypeFromApi]); loadAllActions.mockResolvedValue([]); const mockes = coreMock.createSetup(); const [ @@ -415,8 +422,6 @@ describe('alerts_list with show only capability', () => { ...capabilities, securitySolution: { 'alerting:show': true, - 'alerting:save': false, - 'alerting:delete': false, }, }, history: (scopedHistoryMock.create() as unknown) as ScopedHistory, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index a237e9b3fba7f..b5f386b1e633f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -33,11 +33,11 @@ import { TypeFilter } from './type_filter'; import { ActionTypeFilter } from './action_type_filter'; import { loadAlerts, loadAlertTypes, deleteAlerts } from '../../../lib/alert_api'; import { loadActionTypes } from '../../../lib/action_connector_api'; -import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities'; import { routeToAlertDetails, DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation'; import { EmptyPrompt } from '../../../components/prompts/empty_prompt'; import { ALERTS_FEATURE_ID } from '../../../../../../alerts/common'; +import { hasAllPrivilege } from '../../../lib/capabilities'; const ENTER_KEY = 13; @@ -65,9 +65,6 @@ export const AlertsList: React.FunctionComponent = () => { charts, dataPlugin, } = useAppDependencies(); - const canDelete = hasDeleteAlertsCapability(capabilities); - const canSave = hasSaveAlertsCapability(capabilities); - const [actionTypes, setActionTypes] = useState([]); const [selectedIds, setSelectedIds] = useState([]); const [isPerformingAction, setIsPerformingAction] = useState(false); @@ -246,11 +243,13 @@ export const AlertsList: React.FunctionComponent = () => { }, ]; + const authorizedAlertTypes = [...alertTypesState.data.values()]; + const toolsRight = [ setTypesFilter(types)} - options={Object.values(alertTypesState.data) + options={authorizedAlertTypes .map((alertType) => ({ value: alertType.id, name: alertType.name, @@ -264,7 +263,9 @@ export const AlertsList: React.FunctionComponent = () => { />, ]; - if (canSave) { + if ( + authorizedAlertTypes.some((alertType) => alertType.authorizedConsumers[ALERTS_FEATURE_ID]?.all) + ) { toolsRight.push( { ); } + const authorizedToModifySelectedAlerts = selectedIds.length + ? filterAlertsById(alertsState.data, selectedIds).every((selectedAlert) => + hasAllPrivilege(selectedAlert, alertTypesState.data.get(selectedAlert.alertTypeId)) + ) + : false; + const table = ( - {selectedIds.length > 0 && canDelete && ( + {selectedIds.length > 0 && authorizedToModifySelectedAlerts && ( { /* Don't display alert count until we have the alert types initialized */ totalItemCount: alertTypesState.isInitialized === false ? 0 : alertsState.totalItemCount, }} - selection={ - canDelete - ? { - onSelectionChange(updatedSelectedItemsList: AlertTableItem[]) { - setSelectedIds(updatedSelectedItemsList.map((item) => item.id)); - }, - } - : undefined - } + selection={{ + selectable: (alert: AlertTableItem) => alert.isEditable, + onSelectionChange(updatedSelectedItemsList: AlertTableItem[]) { + setSelectedIds(updatedSelectedItemsList.map((item) => item.id)); + }, + }} onChange={({ page: changedPage }: { page: Pagination }) => { setPage(changedPage); }} @@ -459,5 +463,6 @@ function convertAlertsToTableItems(alerts: Alert[], alertTypesIndex: AlertTypeIn actionsText: alert.actions.length, tagsText: alert.tags.join(', '), alertType: alertTypesIndex.get(alert.alertTypeId)?.name ?? alert.alertTypeId, + isEditable: hasAllPrivilege(alert, alertTypesIndex.get(alert.alertTypeId)), })); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx index 2b746e5dea457..9279f8a1745fc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx @@ -20,8 +20,6 @@ import { } from '@elastic/eui'; import { AlertTableItem } from '../../../../types'; -import { useAppDependencies } from '../../../app_context'; -import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities'; import { ComponentOpts as BulkOperationsComponentOpts, withBulkAlertOperations, @@ -43,16 +41,11 @@ export const CollapsedItemActions: React.FunctionComponent = ({ muteAlert, setAlertsToDelete, }: ComponentOpts) => { - const { capabilities } = useAppDependencies(); - - const canDelete = hasDeleteAlertsCapability(capabilities); - const canSave = hasSaveAlertsCapability(capabilities); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); const button = ( setIsPopoverOpen(!isPopoverOpen)} aria-label={i18n.translate( @@ -75,7 +68,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({
= ({ { @@ -134,7 +127,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({
setAlertsToDelete([item.id])} > diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 6eaae4c1b91a3..32eb3ff9c5364 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -99,7 +99,7 @@ export interface AlertType { actionGroups: ActionGroup[]; actionVariables: ActionVariables; defaultActionGroupId: ActionGroup['id']; - authorizedConsumers: string[]; + authorizedConsumers: Record; producer: string; } @@ -110,6 +110,7 @@ export type AlertWithoutId = Omit; export interface AlertTableItem extends Alert { alertType: AlertType['name']; tagsText: string; + isEditable: boolean; } export interface AlertTypeParamsExpressionProps< diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index 0b2377c537f93..023506776ab33 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -67,34 +67,77 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { case 'space_1_all at space1': expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType); expect(restrictedNoOpAlertType).to.eql(undefined); - expect(noOpAlertType.authorizedConsumers).to.eql(['alerts', 'alertsFixture']); + expect(noOpAlertType.authorizedConsumers).to.eql({ + alerts: { read: true, all: true }, + alertsFixture: { read: true, all: true }, + }); break; case 'global_read at space1': + expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType); + expect(noOpAlertType.authorizedConsumers.alertsFixture).to.eql({ + read: true, + all: false, + }); + expect(noOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({ + read: true, + all: false, + }); + + expect(omit(restrictedNoOpAlertType, 'authorizedConsumers')).to.eql( + expectedRestrictedNoOpType + ); + expect(Object.keys(restrictedNoOpAlertType.authorizedConsumers)).not.to.contain( + 'alertsFixture' + ); + expect(restrictedNoOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({ + read: true, + all: false, + }); + break; case 'space_1_all_with_restricted_fixture at space1': expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType); - expect(noOpAlertType.authorizedConsumers).to.contain('alertsFixture'); - expect(noOpAlertType.authorizedConsumers).to.contain('alertsRestrictedFixture'); + expect(noOpAlertType.authorizedConsumers.alertsFixture).to.eql({ + read: true, + all: true, + }); + expect(noOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({ + read: true, + all: true, + }); expect(omit(restrictedNoOpAlertType, 'authorizedConsumers')).to.eql( expectedRestrictedNoOpType ); - expect(restrictedNoOpAlertType.authorizedConsumers).not.to.contain('alertsFixture'); - expect(restrictedNoOpAlertType.authorizedConsumers).to.contain( - 'alertsRestrictedFixture' + expect(Object.keys(restrictedNoOpAlertType.authorizedConsumers)).not.to.contain( + 'alertsFixture' ); + expect(restrictedNoOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({ + read: true, + all: true, + }); break; case 'superuser at space1': expect(omit(noOpAlertType, 'authorizedConsumers')).to.eql(expectedNoOpType); - expect(noOpAlertType.authorizedConsumers).to.contain('alertsFixture'); - expect(noOpAlertType.authorizedConsumers).to.contain('alertsRestrictedFixture'); + expect(noOpAlertType.authorizedConsumers.alertsFixture).to.eql({ + read: true, + all: true, + }); + expect(noOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({ + read: true, + all: true, + }); expect(omit(restrictedNoOpAlertType, 'authorizedConsumers')).to.eql( expectedRestrictedNoOpType ); - expect(restrictedNoOpAlertType.authorizedConsumers).to.contain('alertsFixture'); - expect(restrictedNoOpAlertType.authorizedConsumers).to.contain( - 'alertsRestrictedFixture' - ); + expect(noOpAlertType.authorizedConsumers.alertsFixture).to.eql({ + read: true, + all: true, + }); + expect(noOpAlertType.authorizedConsumers.alertsRestrictedFixture).to.eql({ + read: true, + all: true, + }); break; default: throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);