From a2b73f9750c888d8235bcc80a9319bd943d4ee80 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Sat, 13 Jul 2024 18:10:20 +0300 Subject: [PATCH] Add tests and support filtering --- .../rule/methods/find/find_rules.ts | 2 +- .../alerting_authorization.test.ts | 3828 +++++++++++------ .../authorization/alerting_authorization.ts | 313 +- .../alerting_authorization_kuery.ts | 57 +- .../alerting/server/authorization/types.ts | 46 + .../lib/get_authorization_filter.ts | 2 +- .../server/search_strategy/search_strategy.ts | 4 +- 7 files changed, 2663 insertions(+), 1589 deletions(-) create mode 100644 x-pack/plugins/alerting/server/authorization/types.ts diff --git a/x-pack/plugins/alerting/server/application/rule/methods/find/find_rules.ts b/x-pack/plugins/alerting/server/application/rule/methods/find/find_rules.ts index 10aef42955e74..efd4c6fe10e74 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/find/find_rules.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/find/find_rules.ts @@ -9,8 +9,8 @@ import Boom from '@hapi/boom'; import { isEmpty, pick } from 'lodash'; import { KueryNode, nodeBuilder } from '@kbn/es-query'; import { AlertConsumers } from '@kbn/rule-data-utils'; +import { AlertingAuthorizationEntity } from '../../../../authorization/types'; import { SanitizedRule, Rule as DeprecatedRule, RawRule } from '../../../../types'; -import { AlertingAuthorizationEntity } from '../../../../authorization'; import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events'; import { mapSortField, diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts index c5c543e413b55..f44b40f647a6b 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts @@ -5,27 +5,26 @@ * 2.0. */ -import { fromKueryExpression } from '@kbn/es-query'; +import { fromKueryExpression, KueryNode, toKqlExpression } from '@kbn/es-query'; import { KibanaRequest } from '@kbn/core/server'; import { ruleTypeRegistryMock } from '../rule_type_registry.mock'; import { securityMock } from '@kbn/security-plugin/server/mocks'; -import { - PluginStartContract as FeaturesStartContract, - KibanaFeature, -} from '@kbn/features-plugin/server'; +import { KibanaFeature } from '@kbn/features-plugin/server'; import { featuresPluginMock } from '@kbn/features-plugin/server/mocks'; -import { - AlertingAuthorization, - WriteOperations, - ReadOperations, - AlertingAuthorizationEntity, -} from './alerting_authorization'; +import { AlertingAuthorization } from './alerting_authorization'; import { RecoveredActionGroup } from '../../common'; import { NormalizedRuleType, RegistryRuleType } from '../rule_type_registry'; import { AlertingAuthorizationFilterType } from './alerting_authorization_kuery'; import { schema } from '@kbn/config-schema'; import { httpServerMock } from '@kbn/core-http-server-mocks'; -import { SecurityPluginStart } from '@kbn/security-plugin-types-server'; +import { + CheckPrivilegesDynamically, + CheckPrivilegesResponse, + SecurityPluginStart, +} from '@kbn/security-plugin-types-server'; +import type { PluginStartContract as FeaturesPluginStart } from '@kbn/features-plugin/server'; +import { WriteOperations, AlertingAuthorizationEntity, ReadOperations } from './types'; +import { SubFeature } from '@kbn/features-plugin/common'; const getSpace = jest.fn(); const getSpaceId = () => 'space1'; @@ -50,86 +49,10 @@ function mockSecurity() { return { authorization }; } -function mockFeatureWithSubFeature(appName: string, typeName: string[]) { - const alertingFeatures = typeName?.map((ruleTypeId) => ({ - ruleTypeId, - consumers: [appName], - })); - - return new KibanaFeature({ - id: appName, - name: appName, - app: [], - category: { id: 'foo', label: 'foo' }, - ...(alertingFeatures - ? { - alerting: alertingFeatures, - } - : {}), - 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: { - rule: { - all: alertingFeatures, - }, - }, - savedObject: { - all: [], - read: [], - }, - ui: ['doSomethingAlertRelated'], - }, - { - id: 'doSomethingAlertRelated', - name: 'sub feature alert', - includeIn: 'read', - alerting: { - rule: { - read: alertingFeatures, - }, - }, - savedObject: { - all: [], - read: [], - }, - ui: ['doSomethingAlertRelated'], - }, - ], - }, - ], - }, - ], - }); -} - function mockFeatureWithConsumers( appName: string, - alertingFeatures?: ReadonlyArray<{ ruleTypeId: string; consumers: readonly string[] }> + alertingFeatures?: ReadonlyArray<{ ruleTypeId: string; consumers: readonly string[] }>, + subFeature?: boolean ) { return new KibanaFeature({ id: appName, @@ -181,6 +104,52 @@ function mockFeatureWithConsumers( ui: [], }, }, + ...(subFeature + ? { + subFeatures: [ + { + name: appName, + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'doSomethingAlertRelated', + name: 'sub feature alert', + includeIn: 'all', + alerting: { + rule: { + all: alertingFeatures, + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: ['doSomethingAlertRelated'], + }, + { + id: 'doSomethingAlertRelated', + name: 'sub feature alert', + includeIn: 'read', + alerting: { + rule: { + read: alertingFeatures, + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: ['doSomethingAlertRelated'], + }, + ], + }, + ], + }, + ], + } + : {}), }); } @@ -192,15 +161,19 @@ const myOtherAppFeature = mockFeatureWithConsumers('myOtherApp', [ { ruleTypeId: 'myType', consumers: ['myOtherApp'] }, ]); -const myAppWithSubFeature = mockFeatureWithSubFeature('myAppWithSubFeature', ['myType']); +const myAppWithSubFeature = mockFeatureWithConsumers( + 'myAppWithSubFeature', + [{ ruleTypeId: 'myType', consumers: ['myAppWithSubFeature'] }], + true +); + const myFeatureWithoutAlerting = mockFeatureWithConsumers('myOtherApp'); describe('AlertingAuthorization', () => { - const ruleTypeRegistry = ruleTypeRegistryMock.create(); - const features: jest.Mocked = featuresPluginMock.createStart(); const allRegisteredConsumers = new Set(); const ruleTypesConsumersMap = new Map>(); let request: KibanaRequest; + let ruleTypeRegistry = ruleTypeRegistryMock.create(); beforeEach(() => { jest.clearAllMocks(); @@ -225,12 +198,6 @@ describe('AlertingAuthorization', () => { validLegacyConsumers: [], })); - features.getKibanaFeatures.mockReturnValue([ - myAppFeature, - myOtherAppFeature, - myAppWithSubFeature, - myFeatureWithoutAlerting, - ]); getSpace.mockResolvedValue(undefined); allRegisteredConsumers.clear(); @@ -239,11 +206,15 @@ describe('AlertingAuthorization', () => { describe('create', () => { let securityStart: jest.Mocked; + let features: jest.Mocked; beforeEach(() => { jest.clearAllMocks(); securityStart = securityMock.createStart(); + features = featuresPluginMock.createStart(); + getSpace.mockReturnValue({ id: 'default', name: 'Default', disabledFeatures: [] }); + features.getKibanaFeatures.mockReturnValue([mockFeatureWithConsumers('my-feature-1')]); }); it('creates an AlertingAuthorization object', async () => { @@ -424,9 +395,19 @@ describe('AlertingAuthorization', () => { }); describe('ensureAuthorized', () => { + const checkPrivileges = jest.fn(async () => ({ hasAllRequested: true })); + let securityStart: ReturnType; + beforeEach(() => { + jest.clearAllMocks(); allRegisteredConsumers.clear(); allRegisteredConsumers.add('myApp'); + securityStart = securityMock.createStart(); + securityStart.authz.mode.useRbacForRequest.mockReturnValue(true); + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + const alertingGet = securityStart.authz.actions.alerting.get as jest.Mock; + alertingGet.mockImplementation(mockAuthorizationAction); }); it('is a no-op when there is no authorization api', async () => { @@ -445,17 +426,16 @@ describe('AlertingAuthorization', () => { entity: AlertingAuthorizationEntity.Rule, }); - expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + expect(checkPrivileges).not.toHaveBeenCalled(); }); it('is a no-op when the security license is disabled', async () => { - const { authorization } = mockSecurity(); - authorization.mode.useRbacForRequest.mockReturnValue(false); + securityStart.authz.mode.useRbacForRequest.mockReturnValue(false); const alertAuthorization = new AlertingAuthorization({ request, ruleTypeRegistry, - authorization, + authorization: securityStart.authz, getSpaceId, allRegisteredConsumers, ruleTypesConsumersMap, @@ -468,30 +448,19 @@ describe('AlertingAuthorization', () => { entity: AlertingAuthorizationEntity.Rule, }); - expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); + expect(checkPrivileges).not.toHaveBeenCalled(); }); - it('ensures the user has privileges to execute rules for the specified rule type and operation without consumer when producer and consumer are the same', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + it('authorized correctly', async () => { const alertAuthorization = new AlertingAuthorization({ request, ruleTypeRegistry, - authorization, + authorization: securityStart.authz, getSpaceId, allRegisteredConsumers, ruleTypesConsumersMap, }); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: true, - privileges: { kibana: [] }, - }); - await alertAuthorization.ensureAuthorized({ ruleTypeId: 'myType', consumer: 'myApp', @@ -499,248 +468,179 @@ describe('AlertingAuthorization', () => { entity: AlertingAuthorizationEntity.Rule, }); - expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType'); + expect(checkPrivileges).toBeCalledTimes(1); + expect(checkPrivileges.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "kibana": Array [ + "myType/myApp/rule/create", + ], + }, + ] + `); + }); - expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'rule', - 'create' + it('throws if user lacks the required rule privileges for the consumer', async () => { + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue( + jest.fn(async () => ({ hasAllRequested: false })) ); - expect(checkPrivileges).toHaveBeenCalledWith({ - kibana: [mockAuthorizationAction('myType', 'myApp', 'rule', 'create')], - }); - }); - it('ensures the user has privileges to execute alerts for the specified rule type and operation without consumer when producer and consumer are the same', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); const alertAuthorization = new AlertingAuthorization({ request, ruleTypeRegistry, - authorization, + authorization: securityStart.authz, getSpaceId, allRegisteredConsumers, ruleTypesConsumersMap, }); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: true, - privileges: { kibana: [] }, - }); - - await alertAuthorization.ensureAuthorized({ - ruleTypeId: 'myType', - consumer: 'myApp', - operation: WriteOperations.Update, - entity: AlertingAuthorizationEntity.Alert, - }); - - expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType'); - - expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'alert', - 'update' + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'myApp', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"myApp\\" to create \\"myType\\" rule"` ); - expect(checkPrivileges).toHaveBeenCalledWith({ - kibana: [mockAuthorizationAction('myType', 'myApp', 'alert', 'update')], - }); }); - it('ensures the user has privileges to execute rules for the specified rule type and operation without consumer when consumer is alerts', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + it('throws if user lacks the required alert privileges for the consumer', async () => { + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue( + jest.fn(async () => ({ hasAllRequested: false })) + ); + const alertAuthorization = new AlertingAuthorization({ request, ruleTypeRegistry, - authorization, + authorization: securityStart.authz, getSpaceId, allRegisteredConsumers, ruleTypesConsumersMap, }); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: true, - privileges: { kibana: [] }, - }); - - await alertAuthorization.ensureAuthorized({ - ruleTypeId: 'myType', - consumer: 'alerts', - operation: WriteOperations.Create, - entity: AlertingAuthorizationEntity.Rule, - }); - - expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType'); - - expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'rule', - 'create' + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'myApp', + operation: WriteOperations.Update, + entity: AlertingAuthorizationEntity.Alert, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"myApp\\" to update \\"myType\\" alert"` ); - expect(checkPrivileges).toHaveBeenCalledWith({ - kibana: [mockAuthorizationAction('myType', 'myApp', 'rule', 'create')], - }); }); - it('ensures the user has privileges to execute alerts for the specified rule type and operation without consumer when consumer is alerts', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + it('throws if the user has access but the consumer is not registered', async () => { const alertAuthorization = new AlertingAuthorization({ request, ruleTypeRegistry, - authorization, + authorization: securityStart.authz, getSpaceId, allRegisteredConsumers, ruleTypesConsumersMap, }); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: true, - privileges: { kibana: [] }, - }); - - await alertAuthorization.ensureAuthorized({ - ruleTypeId: 'myType', - consumer: 'alerts', - operation: WriteOperations.Update, - entity: AlertingAuthorizationEntity.Alert, - }); - - expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType'); - - expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'alert', - 'update' + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'not-exist', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"not-exist\\" to create \\"myType\\" rule"` ); - expect(checkPrivileges).toHaveBeenCalledWith({ - kibana: [mockAuthorizationAction('myType', 'myApp', 'alert', 'update')], - }); }); - it('ensures the user has privileges to execute rules for the specified rule type, operation and producer when producer is different from consumer', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: true, - privileges: { kibana: [] }, - }); - allRegisteredConsumers.add('myOtherApp'); - + it('throws when there is no authorization api but the consumer is not registered', async () => { const alertAuthorization = new AlertingAuthorization({ request, ruleTypeRegistry, - authorization, getSpaceId, allRegisteredConsumers, ruleTypesConsumersMap, }); - await alertAuthorization.ensureAuthorized({ - ruleTypeId: 'myType', - consumer: 'myOtherApp', - operation: WriteOperations.Create, - entity: AlertingAuthorizationEntity.Rule, - }); - - expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType'); - - expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myOtherApp', - 'rule', - 'create' + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'not-exist', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"not-exist\\" to create \\"myType\\" rule"` ); - expect(checkPrivileges).toHaveBeenCalledWith({ - kibana: [mockAuthorizationAction('myType', 'myOtherApp', 'rule', 'create')], - }); }); - it('ensures the user has privileges to execute alerts for the specified rule type, operation and producer when producer is different from consumer', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: true, - privileges: { kibana: [] }, - }); - allRegisteredConsumers.add('myOtherApp'); + it('throws when the security license is disabled but the consumer is not registered', async () => { + securityStart.authz.mode.useRbacForRequest.mockReturnValue(false); const alertAuthorization = new AlertingAuthorization({ request, ruleTypeRegistry, - authorization, getSpaceId, allRegisteredConsumers, ruleTypesConsumersMap, }); - await alertAuthorization.ensureAuthorized({ - ruleTypeId: 'myType', - consumer: 'myOtherApp', - operation: WriteOperations.Update, - entity: AlertingAuthorizationEntity.Alert, - }); + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'not-exist', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"not-exist\\" to create \\"myType\\" rule"` + ); + }); - expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType'); + it('throws an error when the consumer does not exist because it was from a disabled plugin', async () => { + const features = featuresPluginMock.createStart(); + features.getKibanaFeatures.mockReturnValue([ + mockFeatureWithConsumers('my-feature-1', [ + { ruleTypeId: 'rule-type-1', consumers: ['disabled-feature-consumer'] }, + ]), + mockFeatureWithConsumers('my-feature-2', [ + { ruleTypeId: 'rule-type-2', consumers: ['enabled-feature-consumer'] }, + ]), + ]); - expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myOtherApp', - 'alert', - 'update' - ); - expect(checkPrivileges).toHaveBeenCalledWith({ - kibana: [mockAuthorizationAction('myType', 'myOtherApp', 'alert', 'update')], + getSpace.mockReturnValue({ + id: 'default', + name: 'Default', + disabledFeatures: ['my-feature-1'], }); - }); - it('ensures the producer is used for authorization if the consumer is `alerts`', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: true, - privileges: { kibana: [] }, + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, }); + await expect( + auth.ensureAuthorized({ + ruleTypeId: 'rule-type-1', + consumer: 'disabled-feature-consumer', + operation: WriteOperations.Create, + entity: AlertingAuthorizationEntity.Rule, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"disabled-feature-consumer\\" to create \\"rule-type-1\\" rule"` + ); + }); + + it('checks additional privileges correctly', async () => { const alertAuthorization = new AlertingAuthorization({ request, ruleTypeRegistry, - authorization, + authorization: securityStart.authz, getSpaceId, allRegisteredConsumers, ruleTypesConsumersMap, @@ -748,175 +648,270 @@ describe('AlertingAuthorization', () => { await alertAuthorization.ensureAuthorized({ ruleTypeId: 'myType', - consumer: 'alerts', + consumer: 'myApp', operation: WriteOperations.Create, entity: AlertingAuthorizationEntity.Rule, + additionalPrivileges: ['test/create'], }); - expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType'); - - expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'rule', - 'create' - ); - expect(checkPrivileges).toHaveBeenCalledWith({ - kibana: [mockAuthorizationAction('myType', 'myApp', 'rule', 'create')], - }); + expect(checkPrivileges).toBeCalledTimes(1); + expect(checkPrivileges.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "kibana": Array [ + "myType/myApp/rule/create", + "test/create", + ], + }, + ] + `); }); + }); - it('throws if user lacks the required rule privileges for the consumer', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - const alertAuthorization = new AlertingAuthorization({ - request, - ruleTypeRegistry, - authorization, + describe('getFindAuthorizationFilter', () => { + type CheckPrivilegesResponseWithoutES = Omit & { + privileges: Omit; + }; + + const checkPrivileges = jest.fn, []>(async () => ({ + username: 'elastic', + hasAllRequested: true, + privileges: { kibana: [] }, + })); + + const ruleTypeIds = ['rule-type-id-1', 'rule-type-id-2', 'rule-type-id-3', 'rule-type-id-4']; + + let securityStart: ReturnType; + let features: jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + allRegisteredConsumers.clear(); + securityStart = securityMock.createStart(); + securityStart.authz.mode.useRbacForRequest.mockReturnValue(true); + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + features = featuresPluginMock.createStart(); + + const alertingGet = securityStart.authz.actions.alerting.get as jest.Mock; + alertingGet.mockImplementation(mockAuthorizationAction); + + ruleTypeRegistry = ruleTypeRegistryMock.create(); + ruleTypeRegistry.getAllTypes.mockReturnValue(ruleTypeIds); + ruleTypeRegistry.has.mockImplementation((ruleTypeId: string) => + ruleTypeIds.includes(ruleTypeId) + ); + + // @ts-expect-error: only the id is needed for the tests + ruleTypeRegistry.get.mockImplementation((ruleTypeId: string) => ({ id: ruleTypeId })); + }); + + it('omits filter when there is no authorization api', async () => { + const alertAuthorization = new AlertingAuthorization({ + request, + ruleTypeRegistry, getSpaceId, allRegisteredConsumers, ruleTypesConsumersMap, }); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: { - kibana: [ - { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'rule', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myType', 'myApp', 'rule', 'create'), - authorized: true, - }, - ], - }, - }); + const { filter, ensureRuleTypeIsAuthorized } = + await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }, + }); - await expect( - alertAuthorization.ensureAuthorized({ - ruleTypeId: 'myType', - consumer: 'myOtherApp', - operation: WriteOperations.Create, - entity: AlertingAuthorizationEntity.Rule, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unauthorized by \\"myOtherApp\\" to create \\"myType\\" rule"` - ); + expect(() => ensureRuleTypeIsAuthorized('someMadeUpType', 'myApp', 'rule')).not.toThrow(); + expect(filter).toEqual(undefined); }); - it('throws if user lacks the required alert privileges for the consumer', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + it('ensureRuleTypeIsAuthorized is no-op when there is no authorization api', async () => { const alertAuthorization = new AlertingAuthorization({ request, ruleTypeRegistry, - authorization, getSpaceId, allRegisteredConsumers, ruleTypesConsumersMap, }); + const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }, + } + ); + + expect(() => ensureRuleTypeIsAuthorized('someMadeUpType', 'myApp', 'rule')).not.toThrow(); + }); + + it('creates a filter based on the privileged types', async () => { + features.getKibanaFeatures.mockReturnValue([ + mockFeatureWithConsumers('feature-id-1', [ + { + ruleTypeId: 'rule-type-id-1', + consumers: ['alerts', 'consumer-a'], + }, + { ruleTypeId: 'rule-type-id-2', consumers: ['alerts', 'consumer-b'] }, + ]), + mockFeatureWithConsumers('feature-id-2', [ + { + ruleTypeId: 'rule-type-id-1', + consumers: ['alerts', 'consumer-b'], + }, + { + ruleTypeId: 'rule-type-id-3', + consumers: ['alerts', 'consumer-c'], + }, + ]), + mockFeatureWithConsumers('feature-id-3', [ + { + ruleTypeId: 'rule-type-id-4', + consumers: ['consumer-d'], + }, + ]), + ]); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + checkPrivileges.mockResolvedValueOnce({ username: 'some-user', - hasAllRequested: false, + hasAllRequested: true, privileges: { kibana: [ { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'alert', 'update'), - authorized: false, + privilege: mockAuthorizationAction('rule-type-id-1', 'alerts', 'rule', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-b', 'rule', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('rule-type-id-2', 'alerts', 'rule', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('rule-type-id-2', 'consumer-b', 'rule', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('rule-type-id-3', 'consumer-c', 'rule', 'find'), + authorized: true, }, { - privilege: mockAuthorizationAction('myType', 'myApp', 'alert', 'update'), + privilege: mockAuthorizationAction('rule-type-id-3', 'alerts', 'rule', 'find'), authorized: true, }, { - privilege: mockAuthorizationAction('myType', 'myAppRulesOnly', 'alert', 'update'), + privilege: mockAuthorizationAction('rule-type-id-3', 'consumer-d', 'rule', 'find'), authorized: false, }, ], }, }); - await expect( - alertAuthorization.ensureAuthorized({ - ruleTypeId: 'myType', - consumer: 'myAppRulesOnly', - operation: WriteOperations.Update, - entity: AlertingAuthorizationEntity.Alert, + const filter = ( + await auth.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule_type_id', + consumer: 'consumer-field', + }, }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unauthorized by \\"myAppRulesOnly\\" to update \\"myType\\" alert"` + ).filter; + + expect(checkPrivileges).toBeCalledTimes(1); + expect(checkPrivileges.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "kibana": Array [ + "rule-type-id-1/alerts/rule/find", + "rule-type-id-1/consumer-a/rule/find", + "rule-type-id-1/consumer-b/rule/find", + "rule-type-id-2/alerts/rule/find", + "rule-type-id-2/consumer-b/rule/find", + "rule-type-id-3/alerts/rule/find", + "rule-type-id-3/consumer-c/rule/find", + "rule-type-id-4/consumer-d/rule/find", + ], + }, + ] + `); + + expect(toKqlExpression(filter as KueryNode)).toMatchInlineSnapshot( + `"((path.to.rule_type_id: rule-type-id-1 AND (consumer-field: alerts OR consumer-field: consumer-a OR consumer-field: consumer-b)) OR (path.to.rule_type_id: rule-type-id-2 AND (consumer-field: alerts OR consumer-field: consumer-b)) OR (path.to.rule_type_id: rule-type-id-3 AND (consumer-field: consumer-c OR consumer-field: alerts)))"` ); }); - it('throws if user lacks the required privileges for the producer', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - const alertAuthorization = new AlertingAuthorization({ - request, - ruleTypeRegistry, - authorization, - getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, - }); - + it('throws if user has no privileges to any rule type', async () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, privileges: { kibana: [ { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'alert', 'update'), - authorized: true, + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'rule', 'create'), + authorized: false, }, { - privilege: mockAuthorizationAction('myType', 'myApp', 'alert', 'update'), + privilege: mockAuthorizationAction('myType', 'myApp', 'rule', 'create'), authorized: false, }, ], }, }); + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + await expect( - alertAuthorization.ensureAuthorized({ - ruleTypeId: 'myType', - consumer: 'myOtherApp', - operation: WriteOperations.Update, - entity: AlertingAuthorizationEntity.Alert, + auth.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule_type_id', + consumer: 'consumer-field', + }, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unauthorized by \\"myOtherApp\\" to update \\"myType\\" alert"` + `"Unauthorized to find rules for any rule types"` ); }); - it('throws if user lacks the required privileges for both consumer and producer', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - const alertAuthorization = new AlertingAuthorization({ - request, - ruleTypeRegistry, - authorization, - getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, - }); + it('creates an `ensureRuleTypeIsAuthorized` function which throws if type is unauthorized', async () => { + features.getKibanaFeatures.mockReturnValue([ + mockFeatureWithConsumers('myApp', [ + { ruleTypeId: 'myOtherAppAlertType', consumers: ['myApp'] }, + { ruleTypeId: 'myAppAlertType', consumers: ['myApp'] }, + ]), + mockFeatureWithConsumers('myOtherApp', [ + { ruleTypeId: 'myOtherAppAlertType', consumers: ['myOtherApp'] }, + { ruleTypeId: 'myAppAlertType', consumers: ['myOtherApp'] }, + ]), + ]); checkPrivileges.mockResolvedValueOnce({ username: 'some-user', @@ -924,212 +919,73 @@ describe('AlertingAuthorization', () => { privileges: { kibana: [ { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'alert', 'create'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction( + 'myOtherAppAlertType', + 'myOtherApp', + 'alert', + 'find' + ), authorized: false, }, { - privilege: mockAuthorizationAction('myType', 'myApp', 'alert', 'create'), + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'alert', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'alert', 'find'), authorized: false, }, ], }, }); - await expect( - alertAuthorization.ensureAuthorized({ - ruleTypeId: 'myType', - consumer: 'myOtherApp', - operation: WriteOperations.Create, - entity: AlertingAuthorizationEntity.Alert, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unauthorized by \\"myOtherApp\\" to create \\"myType\\" alert"` - ); - }); - - it('checks additional privileges correctly', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - const alertAuthorization = new AlertingAuthorization({ + const auth = await AlertingAuthorization.create({ request, ruleTypeRegistry, - authorization, getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, - }); - - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: true, - privileges: { kibana: [] }, + features, + getSpace, + authorization: securityStart.authz, }); - await alertAuthorization.ensureAuthorized({ - ruleTypeId: 'myType', - consumer: 'myApp', - operation: WriteOperations.Create, - entity: AlertingAuthorizationEntity.Rule, - additionalPrivileges: ['test/create'], - }); + const { ensureRuleTypeIsAuthorized } = await auth.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Alert, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }, + } + ); - expect(ruleTypeRegistry.get).toHaveBeenCalledWith('myType'); - - expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(1); - expect(authorization.actions.alerting.get).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'rule', - 'create' - ); - expect(checkPrivileges).toHaveBeenCalledWith({ - kibana: [mockAuthorizationAction('myType', 'myApp', 'rule', 'create'), 'test/create'], - }); - }); - }); - - describe('getFindAuthorizationFilter', () => { - const myOtherAppAlertType: RegistryRuleType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - isExportable: true, - recoveryActionGroup: RecoveredActionGroup, - id: 'myOtherAppAlertType', - name: 'myOtherAppAlertType', - category: 'test', - producer: 'alerts', - enabledInLicense: true, - hasAlertsMappings: false, - hasFieldsForAAD: false, - validLegacyConsumers: [], - }; - const myAppAlertType: RegistryRuleType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - isExportable: true, - recoveryActionGroup: RecoveredActionGroup, - id: 'myAppAlertType', - name: 'myAppAlertType', - category: 'test', - producer: 'myApp', - enabledInLicense: true, - hasAlertsMappings: false, - hasFieldsForAAD: false, - validLegacyConsumers: [], - }; - const mySecondAppAlertType: RegistryRuleType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - isExportable: true, - recoveryActionGroup: RecoveredActionGroup, - id: 'mySecondAppAlertType', - name: 'mySecondAppAlertType', - category: 'test', - producer: 'myApp', - enabledInLicense: true, - hasAlertsMappings: false, - hasFieldsForAAD: false, - validLegacyConsumers: [], - }; - const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]); - it('omits filter when there is no authorization api', async () => { - const alertAuthorization = new AlertingAuthorization({ - request, - ruleTypeRegistry, - getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, - }); - const { filter, ensureRuleTypeIsAuthorized } = - await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { - type: AlertingAuthorizationFilterType.KQL, - fieldNames: { - ruleTypeId: 'ruleId', - consumer: 'consumer', - }, - }); - expect(() => ensureRuleTypeIsAuthorized('someMadeUpType', 'myApp', 'rule')).not.toThrow(); - expect(filter).toEqual(undefined); - }); - it('ensureRuleTypeIsAuthorized is no-op when there is no authorization api', async () => { - const alertAuthorization = new AlertingAuthorization({ - request, - ruleTypeRegistry, - getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, - }); - const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( - AlertingAuthorizationEntity.Rule, - { - type: AlertingAuthorizationFilterType.KQL, - fieldNames: { - ruleTypeId: 'ruleId', - consumer: 'consumer', - }, - } + expect(() => { + ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'alert'); + }).toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"myOtherApp\\" to find \\"myAppAlertType\\" alert"` ); - ensureRuleTypeIsAuthorized('someMadeUpType', 'myApp', 'rule'); }); - it('creates a filter based on the privileged types', async () => { + it('creates an `ensureRuleTypeIsAuthorized` function which is no-op if type is authorized', async () => { features.getKibanaFeatures.mockReturnValue([ mockFeatureWithConsumers('myApp', [ + { ruleTypeId: 'myOtherAppAlertType', consumers: ['myApp'] }, { ruleTypeId: 'myAppAlertType', consumers: ['myApp'] }, - { ruleTypeId: 'mySecondAppAlertType', consumers: ['myApp'] }, ]), - - mockFeatureWithConsumers('alerts', [ - { ruleTypeId: 'myOtherAppAlertType', consumers: ['alerts'] }, + mockFeatureWithConsumers('myOtherApp', [ + { ruleTypeId: 'myOtherApp', consumers: ['myOtherApp'] }, ]), - - myOtherAppFeature, - myAppWithSubFeature, ]); - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ username: 'some-user', - hasAllRequested: true, + hasAllRequested: false, privileges: { kibana: [ - { - privilege: mockAuthorizationAction('myAppAlertType', 'alerts', 'rule', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'rule', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'rule', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction( - 'myAppAlertType', - 'myAppWithSubFeature', - 'rule', - 'find' - ), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'alerts', 'rule', 'find'), - authorized: true, - }, { privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'rule', 'find'), authorized: true, @@ -1141,321 +997,111 @@ describe('AlertingAuthorization', () => { 'rule', 'find' ), - authorized: true, - }, - { - privilege: mockAuthorizationAction( - 'myOtherAppAlertType', - 'myAppWithSubFeature', - 'rule', - 'find' - ), - authorized: true, - }, - { - privilege: mockAuthorizationAction('mySecondAppAlertType', 'alerts', 'rule', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('mySecondAppAlertType', 'myApp', 'rule', 'find'), - authorized: true, + authorized: false, }, { - privilege: mockAuthorizationAction( - 'mySecondAppAlertType', - 'myOtherApp', - 'rule', - 'find' - ), + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'rule', 'find'), authorized: true, }, { - privilege: mockAuthorizationAction( - 'mySecondAppAlertType', - 'myAppWithSubFeature', - 'rule', - 'find' - ), + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'rule', 'find'), authorized: true, }, ], }, }); - const alertAuthorization = new AlertingAuthorization({ - request, - ruleTypeRegistry, - authorization, - getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, - }); - ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - expect( - ( - await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { - type: AlertingAuthorizationFilterType.KQL, - fieldNames: { - ruleTypeId: 'path.to.rule_type_id', - consumer: 'consumer-field', - }, - }) - ).filter - ).toEqual( - fromKueryExpression( - `((path.to.rule_type_id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:mySecondAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` - ) - ); - }); - it('throws if user has no privileges to any rule type', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: { - kibana: [ - { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'rule', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myType', 'myApp', 'rule', 'create'), - authorized: false, - }, - ], - }, - }); - const alertAuthorization = new AlertingAuthorization({ + + const auth = await AlertingAuthorization.create({ request, ruleTypeRegistry, - authorization, getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, + features, + getSpace, + authorization: securityStart.authz, }); - ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - await expect( - alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { + + const { ensureRuleTypeIsAuthorized } = await auth.getFindAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + { type: AlertingAuthorizationFilterType.KQL, fieldNames: { - ruleTypeId: 'path.to.rule_type_id', - consumer: 'consumer-field', + ruleTypeId: 'ruleId', + consumer: 'consumer', }, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unauthorized to find rules for any rule types"` + } ); + + expect(() => { + ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'rule'); + }).not.toThrow(); }); - it('creates an `ensureRuleTypeIsAuthorized` function which throws if type is unauthorized', async () => { + it('creates an `logSuccessfulAuthorization` function which logs every authorized type', async () => { features.getKibanaFeatures.mockReturnValue([ mockFeatureWithConsumers('myApp', [ { ruleTypeId: 'myOtherAppAlertType', consumers: ['myApp'] }, { ruleTypeId: 'myAppAlertType', consumers: ['myApp'] }, + { ruleTypeId: 'mySecondAppAlertType', consumers: ['myApp'] }, ]), mockFeatureWithConsumers('myOtherApp', [ - { ruleTypeId: 'myOtherAppAlertType', consumers: ['myOtherApp'] }, + { ruleTypeId: 'mySecondAppAlertType', consumers: ['myOtherApp'] }, { ruleTypeId: 'myAppAlertType', consumers: ['myOtherApp'] }, ]), ]); - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, privileges: { kibana: [ { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'find'), + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'rule', 'find'), authorized: true, }, { privilege: mockAuthorizationAction( 'myOtherAppAlertType', 'myOtherApp', - 'alert', + 'rule', 'find' ), authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'alert', 'find'), + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'rule', 'find'), authorized: true, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'alert', 'find'), - authorized: false, + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'rule', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('mySecondAppAlertType', 'myApp', 'rule', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction( + 'mySecondAppAlertType', + 'myOtherApp', + 'rule', + 'find' + ), + authorized: true, }, ], }, }); - const alertAuthorization = new AlertingAuthorization({ - request, - ruleTypeRegistry, - authorization, - getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, - }); - ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( - AlertingAuthorizationEntity.Alert, - { - type: AlertingAuthorizationFilterType.KQL, - fieldNames: { - ruleTypeId: 'ruleId', - consumer: 'consumer', - }, - } - ); - expect(() => { - ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'alert'); - }).toThrowErrorMatchingInlineSnapshot( - `"Unauthorized by \\"myOtherApp\\" to find \\"myAppAlertType\\" alert"` - ); - }); - it('creates an `ensureRuleTypeIsAuthorized` function which is no-op if type is authorized', async () => { - features.getKibanaFeatures.mockReturnValue([ - mockFeatureWithConsumers('myApp', [ - { ruleTypeId: 'myOtherAppAlertType', consumers: ['myApp'] }, - { ruleTypeId: 'myAppAlertType', consumers: ['myApp'] }, - ]), - mockFeatureWithConsumers('myOtherApp', [ - { ruleTypeId: 'myOtherApp', consumers: ['myOtherApp'] }, - ]), - ]); - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: { - kibana: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'rule', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction( - 'myOtherAppAlertType', - 'myOtherApp', - 'rule', - 'find' - ), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'rule', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'rule', 'find'), - authorized: true, - }, - ], - }, - }); - const alertAuthorization = new AlertingAuthorization({ - request, - ruleTypeRegistry, - authorization, - getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, - }); - ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( - AlertingAuthorizationEntity.Rule, - { - type: AlertingAuthorizationFilterType.KQL, - fieldNames: { - ruleTypeId: 'ruleId', - consumer: 'consumer', - }, - } - ); - expect(() => { - ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'rule'); - }).not.toThrow(); - }); - it('creates an `logSuccessfulAuthorization` function which logs every authorized type', async () => { - features.getKibanaFeatures.mockReturnValue([ - mockFeatureWithConsumers('myApp', [ - { ruleTypeId: 'myOtherAppAlertType', consumers: ['myApp'] }, - { ruleTypeId: 'myAppAlertType', consumers: ['myApp'] }, - { ruleTypeId: 'mySecondAppAlertType', consumers: ['myApp'] }, - ]), - mockFeatureWithConsumers('myOtherApp', [ - { ruleTypeId: 'mySecondAppAlertType', consumers: ['myOtherApp'] }, - { ruleTypeId: 'myAppAlertType', consumers: ['myOtherApp'] }, - ]), - ]); - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: { - kibana: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'rule', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction( - 'myOtherAppAlertType', - 'myOtherApp', - 'rule', - 'find' - ), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'rule', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'rule', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('mySecondAppAlertType', 'myApp', 'rule', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction( - 'mySecondAppAlertType', - 'myOtherApp', - 'rule', - 'find' - ), - authorized: true, - }, - ], - }, - }); - const alertAuthorization = new AlertingAuthorization({ + + const auth = await AlertingAuthorization.create({ request, ruleTypeRegistry, - authorization, getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, + features, + getSpace, + authorization: securityStart.authz, }); - ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( + + const { ensureRuleTypeIsAuthorized } = await auth.getFindAuthorizationFilter( AlertingAuthorizationEntity.Rule, { type: AlertingAuthorizationFilterType.KQL, @@ -1465,6 +1111,7 @@ describe('AlertingAuthorization', () => { }, } ); + expect(() => { ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'rule'); ensureRuleTypeIsAuthorized('mySecondAppAlertType', 'myOtherApp', 'rule'); @@ -1476,24 +1123,22 @@ describe('AlertingAuthorization', () => { // Space ids are stored in the alerts documents and even if security is disabled // still need to consider the users space privileges it('creates a spaceId only filter if security is disabled, but require space awareness', async () => { - const alertAuthorization = new AlertingAuthorization({ + const auth = await AlertingAuthorization.create({ request, ruleTypeRegistry, getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, + features, + getSpace, + }); + + const { filter } = await auth.getFindAuthorizationFilter(AlertingAuthorizationEntity.Alert, { + type: AlertingAuthorizationFilterType.ESDSL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + spaceIds: 'path.to.space.id', + }, }); - const { filter } = await alertAuthorization.getFindAuthorizationFilter( - AlertingAuthorizationEntity.Alert, - { - type: AlertingAuthorizationFilterType.ESDSL, - fieldNames: { - ruleTypeId: 'ruleId', - consumer: 'consumer', - spaceIds: 'path.to.space.id', - }, - } - ); expect(filter).toEqual({ bool: { minimum_should_match: 1, should: [{ match: { 'path.to.space.id': 'space1' } }] }, @@ -1501,71 +1146,479 @@ describe('AlertingAuthorization', () => { }); }); - describe('filterByRuleTypeAuthorization', () => { - const myOtherAppAlertType: RegistryRuleType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - isExportable: true, - recoveryActionGroup: RecoveredActionGroup, - id: 'myOtherAppAlertType', - name: 'myOtherAppAlertType', - category: 'test', - producer: 'myOtherApp', - enabledInLicense: true, - hasAlertsMappings: false, - hasFieldsForAAD: false, - validLegacyConsumers: [], - }; - const myAppAlertType: RegistryRuleType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - isExportable: true, - recoveryActionGroup: RecoveredActionGroup, - id: 'myAppAlertType', - name: 'myAppAlertType', - category: 'test', - producer: 'myApp', - enabledInLicense: true, - hasAlertsMappings: false, - hasFieldsForAAD: false, - validLegacyConsumers: [], + describe.only('getAuthorizationFilter', () => { + type CheckPrivilegesResponseWithoutES = Omit & { + privileges: Omit; }; - const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType]); + + const checkPrivileges = jest.fn, []>(async () => ({ + username: 'elastic', + hasAllRequested: true, + privileges: { kibana: [] }, + })); + + const ruleTypeIds = ['rule-type-id-1', 'rule-type-id-2', 'rule-type-id-3', 'rule-type-id-4']; + const consumers = ['consumer-a', 'consumer-b', 'consumer-c', 'consumer-d']; + + let securityStart: ReturnType; + let features: jest.Mocked; + beforeEach(() => { + jest.clearAllMocks(); + allRegisteredConsumers.clear(); + securityStart = securityMock.createStart(); + securityStart.authz.mode.useRbacForRequest.mockReturnValue(true); + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + features = featuresPluginMock.createStart(); features.getKibanaFeatures.mockReturnValue([ - mockFeatureWithConsumers('myApp', [ - { ruleTypeId: 'myOtherAppAlertType', consumers: ['myApp'] }, - { ruleTypeId: 'myAppAlertType', consumers: ['myApp'] }, + mockFeatureWithConsumers('feature-id-1', [ + { + ruleTypeId: 'rule-type-id-1', + consumers: ['alerts', 'consumer-a'], + }, + { ruleTypeId: 'rule-type-id-2', consumers: ['alerts', 'consumer-b'] }, ]), - mockFeatureWithConsumers('myOtherApp', [ - { ruleTypeId: 'myOtherAppAlertType', consumers: ['myApp'] }, - { ruleTypeId: 'myAppAlertType', consumers: ['myApp'] }, + mockFeatureWithConsumers('feature-id-2', [ + { + ruleTypeId: 'rule-type-id-1', + consumers: ['alerts', 'consumer-b'], + }, + { + ruleTypeId: 'rule-type-id-3', + consumers: ['alerts', 'consumer-c'], + }, + ]), + mockFeatureWithConsumers('feature-id-3', [ + { + ruleTypeId: 'rule-type-id-4', + consumers: ['consumer-d'], + }, ]), ]); + + const alertingGet = securityStart.authz.actions.alerting.get as jest.Mock; + alertingGet.mockImplementation(mockAuthorizationAction); + + ruleTypeRegistry = ruleTypeRegistryMock.create(); + ruleTypeRegistry.getAllTypes.mockReturnValue(ruleTypeIds); + ruleTypeRegistry.has.mockImplementation((ruleTypeId: string) => + ruleTypeIds.includes(ruleTypeId) + ); + + // @ts-expect-error: only the id is needed for the tests + ruleTypeRegistry.get.mockImplementation((ruleTypeId: string) => ({ id: ruleTypeId })); }); - it('augments a list of types with all features when there is no authorization api', async () => { - features.getKibanaFeatures.mockReturnValue([ - myAppFeature, - myOtherAppFeature, - myAppWithSubFeature, - myFeatureWithoutAlerting, - ]); - const alertAuthorization = new AlertingAuthorization({ - request, - ruleTypeRegistry, - getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, - }); - ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - await expect( - alertAuthorization.filterByRuleTypeAuthorization( - new Set([myAppAlertType, myOtherAppAlertType]), + describe('filter', () => { + it('gets the filter correctly with no security and no spaceIds in the fields names', async () => { + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + }); + + const { filter } = await auth.getAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }, + }, + ReadOperations.Get + ); + + expect(filter).toEqual(undefined); + }); + + it('gets the filter correctly with no security and spaceIds in the fields names', async () => { + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + }); + + const { filter } = await auth.getAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + spaceIds: 'space-id', + }, + }, + ReadOperations.Get + ); + + expect(toKqlExpression(filter as KueryNode)).toMatchInlineSnapshot(`"space-id: space1"`); + }); + + it('gets the filter correctly with security disabled', async () => { + securityStart.authz.mode.useRbacForRequest.mockReturnValue(false); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + const { filter } = await auth.getAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }, + }, + ReadOperations.Get + ); + + expect(filter).toEqual(undefined); + }); + + it('gets the filter correctly for all rule types and consumers', async () => { + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-b', 'rule', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('rule-type-id-2', 'consumer-b', 'rule', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('rule-type-id-3', 'consumer-c', 'rule', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('rule-type-id-4', 'consumer-d', 'rule', 'get'), + authorized: true, + }, + ], + }, + }); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + const { filter } = await auth.getAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }, + }, + ReadOperations.Get, + new Set(consumers) + ); + + expect(toKqlExpression(filter as KueryNode)).toMatchInlineSnapshot( + `"((ruleId: rule-type-id-1 AND (consumer: consumer-a OR consumer: consumer-b)) OR (ruleId: rule-type-id-2 AND consumer: consumer-b) OR (ruleId: rule-type-id-3 AND consumer: consumer-c) OR (ruleId: rule-type-id-4 AND consumer: consumer-d))"` + ); + }); + + it('throws when the user is not authorized for any rule type', async () => { + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: { + kibana: [], + }, + }); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + await expect( + auth.getAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }, + }, + ReadOperations.Get, + new Set(consumers) + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized to get rules for any rule types"` + ); + }); + }); + + describe('ensureRuleTypeIsAuthorized', () => { + it('does not throw if the rule type is authorized', async () => { + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'get'), + authorized: true, + }, + ], + }, + }); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + const { ensureRuleTypeIsAuthorized } = await auth.getAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }, + }, + ReadOperations.Get, + new Set(consumers) + ); + + expect(() => + ensureRuleTypeIsAuthorized('rule-type-id-1', 'consumer-a', 'rule') + ).not.toThrow(); + }); + + it('throws if the rule type is not authorized', async () => { + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'get'), + authorized: true, + }, + ], + }, + }); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + const { ensureRuleTypeIsAuthorized } = await auth.getAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }, + }, + ReadOperations.Get, + new Set(consumers) + ); + + expect(() => + ensureRuleTypeIsAuthorized('rule-type-id-2', 'consumer-a', 'rule') + ).toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"consumer-a\\" to get \\"rule-type-id-2\\" rule"` + ); + }); + + it('throws if the rule type is not authorized for the entity', async () => { + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'get'), + authorized: true, + }, + ], + }, + }); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + const { ensureRuleTypeIsAuthorized } = await auth.getAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }, + }, + ReadOperations.Get, + new Set(consumers) + ); + + expect(() => + ensureRuleTypeIsAuthorized('rule-type-id-1', 'consumer-a', 'alert') + ).toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"consumer-a\\" to get \\"rule-type-id-1\\" alert"` + ); + }); + + it('throws if the rule type is not authorized for the consumer', async () => { + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'get'), + authorized: true, + }, + ], + }, + }); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + const { ensureRuleTypeIsAuthorized } = await auth.getAuthorizationFilter( + AlertingAuthorizationEntity.Rule, + { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'ruleId', + consumer: 'consumer', + }, + }, + ReadOperations.Get, + new Set(consumers) + ); + + expect(() => + ensureRuleTypeIsAuthorized('rule-type-id-1', 'consumer-b', 'rule') + ).toThrowErrorMatchingInlineSnapshot( + `"Unauthorized by \\"consumer-b\\" to get \\"rule-type-id-1\\" rule"` + ); + }); + }); + }); + + describe('filterByRuleTypeAuthorization', () => { + const myOtherAppAlertType: RegistryRuleType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + recoveryActionGroup: RecoveredActionGroup, + id: 'myOtherAppAlertType', + name: 'myOtherAppAlertType', + category: 'test', + producer: 'myOtherApp', + enabledInLicense: true, + hasAlertsMappings: false, + hasFieldsForAAD: false, + validLegacyConsumers: [], + }; + const myAppAlertType: RegistryRuleType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + recoveryActionGroup: RecoveredActionGroup, + id: 'myAppAlertType', + name: 'myAppAlertType', + category: 'test', + producer: 'myApp', + enabledInLicense: true, + hasAlertsMappings: false, + hasFieldsForAAD: false, + validLegacyConsumers: [], + }; + const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType]); + beforeEach(() => { + features.getKibanaFeatures.mockReturnValue([ + mockFeatureWithConsumers('myApp', [ + { ruleTypeId: 'myOtherAppAlertType', consumers: ['myApp'] }, + { ruleTypeId: 'myAppAlertType', consumers: ['myApp'] }, + ]), + mockFeatureWithConsumers('myOtherApp', [ + { ruleTypeId: 'myOtherAppAlertType', consumers: ['myApp'] }, + { ruleTypeId: 'myAppAlertType', consumers: ['myApp'] }, + ]), + ]); + }); + it('augments a list of types with all features when there is no authorization api', async () => { + features.getKibanaFeatures.mockReturnValue([ + myAppFeature, + myOtherAppFeature, + myAppWithSubFeature, + myFeatureWithoutAlerting, + ]); + const alertAuthorization = new AlertingAuthorization({ + request, + ruleTypeRegistry, + getSpaceId, + allRegisteredConsumers, + ruleTypesConsumersMap, + }); + ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.filterByRuleTypeAuthorization( + new Set([myAppAlertType, myOtherAppAlertType]), [WriteOperations.Create], AlertingAuthorizationEntity.Rule ) @@ -1643,621 +1696,1666 @@ describe('AlertingAuthorization', () => { "id": "recovered", "name": "Recovered", }, - "validLegacyConsumers": Array [], + "validLegacyConsumers": Array [], + }, + } + `); + }); + + it('augments a list of types with consumers under which the operation is authorized', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'rule', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction( + 'myOtherAppAlertType', + 'myOtherApp', + 'rule', + 'create' + ), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'rule', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'rule', 'create'), + authorized: true, + }, + ], + }, + }); + + const alertAuthorization = new AlertingAuthorization({ + request, + ruleTypeRegistry, + authorization: securityStart.authz, + getSpaceId, + allRegisteredConsumers, + ruleTypesConsumersMap, + }); + ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.filterByRuleTypeAuthorization( + new Set([myAppAlertType, myOtherAppAlertType]), + [WriteOperations.Create], + AlertingAuthorizationEntity.Rule + ) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": true, + "read": true, + }, + }, + "category": "test", + "defaultActionGroupId": "default", + "enabledInLicense": true, + "hasAlertsMappings": false, + "hasFieldsForAAD": false, + "id": "myOtherAppAlertType", + "isExportable": true, + "minimumLicenseRequired": "basic", + "name": "myOtherAppAlertType", + "producer": "myOtherApp", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", + }, + "validLegacyConsumers": Array [], + }, + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, + }, + "category": "test", + "defaultActionGroupId": "default", + "enabledInLicense": true, + "hasAlertsMappings": false, + "hasFieldsForAAD": false, + "id": "myAppAlertType", + "isExportable": true, + "minimumLicenseRequired": "basic", + "name": "myAppAlertType", + "producer": "myApp", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", + }, + "validLegacyConsumers": Array [], + }, + } + `); + }); + + it('authorizes user under the `alerts` consumer when they are authorized by the producer', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'alert', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'alert', 'create'), + authorized: false, + }, + ], + }, + }); + + const alertAuthorization = new AlertingAuthorization({ + request, + ruleTypeRegistry, + authorization: securityStart.authz, + getSpaceId, + allRegisteredConsumers, + ruleTypesConsumersMap, + }); + ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.filterByRuleTypeAuthorization( + new Set([myAppAlertType]), + [WriteOperations.Create], + AlertingAuthorizationEntity.Alert + ) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": true, + "read": true, + }, + }, + "category": "test", + "defaultActionGroupId": "default", + "enabledInLicense": true, + "hasAlertsMappings": false, + "hasFieldsForAAD": false, + "id": "myAppAlertType", + "isExportable": true, + "minimumLicenseRequired": "basic", + "name": "myAppAlertType", + "producer": "myApp", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", + }, + "validLegacyConsumers": Array [], + }, + } + `); + }); + + it('augments a list of types with consumers under which multiple operations are authorized', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction( + 'myOtherAppAlertType', + 'myOtherApp', + 'alert', + 'create' + ), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'alert', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'alert', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction( + 'myOtherAppAlertType', + 'myOtherApp', + 'alert', + 'get' + ), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'alert', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'alert', 'get'), + authorized: true, + }, + ], + }, + }); + + const alertAuthorization = new AlertingAuthorization({ + request, + ruleTypeRegistry, + authorization: securityStart.authz, + getSpaceId, + allRegisteredConsumers, + ruleTypesConsumersMap, + }); + ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.filterByRuleTypeAuthorization( + new Set([myAppAlertType, myOtherAppAlertType]), + [WriteOperations.Create, ReadOperations.Get], + AlertingAuthorizationEntity.Alert + ) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": false, + "read": true, + }, + }, + "category": "test", + "defaultActionGroupId": "default", + "enabledInLicense": true, + "hasAlertsMappings": false, + "hasFieldsForAAD": false, + "id": "myOtherAppAlertType", + "isExportable": true, + "minimumLicenseRequired": "basic", + "name": "myOtherAppAlertType", + "producer": "myOtherApp", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", + }, + "validLegacyConsumers": Array [], + }, + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": false, + "read": true, + }, + "myOtherApp": Object { + "all": false, + "read": true, + }, + }, + "category": "test", + "defaultActionGroupId": "default", + "enabledInLicense": true, + "hasAlertsMappings": false, + "hasFieldsForAAD": false, + "id": "myAppAlertType", + "isExportable": true, + "minimumLicenseRequired": "basic", + "name": "myAppAlertType", + "producer": "myApp", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", + }, + "validLegacyConsumers": Array [], + }, + } + `); + }); + + it('omits types which have no consumers under which the operation is authorized', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction( + 'myOtherAppAlertType', + 'myOtherApp', + 'alert', + 'create' + ), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'alert', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'alert', 'create'), + authorized: false, + }, + ], + }, + }); + + const alertAuthorization = new AlertingAuthorization({ + request, + ruleTypeRegistry, + authorization: securityStart.authz, + getSpaceId, + allRegisteredConsumers, + ruleTypesConsumersMap, + }); + ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); + + await expect( + alertAuthorization.filterByRuleTypeAuthorization( + new Set([myAppAlertType, myOtherAppAlertType]), + [WriteOperations.Create], + AlertingAuthorizationEntity.Alert + ) + ).resolves.toMatchInlineSnapshot(` + Set { + Object { + "actionGroups": Array [], + "actionVariables": undefined, + "authorizedConsumers": Object { + "myApp": Object { + "all": true, + "read": true, + }, + "myOtherApp": Object { + "all": true, + "read": true, + }, + }, + "category": "test", + "defaultActionGroupId": "default", + "enabledInLicense": true, + "hasAlertsMappings": false, + "hasFieldsForAAD": false, + "id": "myOtherAppAlertType", + "isExportable": true, + "minimumLicenseRequired": "basic", + "name": "myOtherAppAlertType", + "producer": "myOtherApp", + "recoveryActionGroup": Object { + "id": "recovered", + "name": "Recovered", + }, + "validLegacyConsumers": Array [], + }, + } + `); + }); + }); + + describe('getAuthorizedRuleTypesWithAuthorizedConsumers', () => { + type CheckPrivilegesResponseWithoutES = Omit & { + privileges: Omit; + }; + + const checkPrivileges = jest.fn, []>(async () => ({ + username: 'elastic', + hasAllRequested: true, + privileges: { kibana: [] }, + })); + + const ruleTypeIds = ['rule-type-id-1', 'rule-type-id-2', 'rule-type-id-3', 'rule-type-id-4']; + const consumers = ['consumer-a', 'consumer-b', 'consumer-c', 'consumer-d']; + + let securityStart: ReturnType; + let features: jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + allRegisteredConsumers.clear(); + securityStart = securityMock.createStart(); + securityStart.authz.mode.useRbacForRequest.mockReturnValue(true); + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + features = featuresPluginMock.createStart(); + features.getKibanaFeatures.mockReturnValue([ + mockFeatureWithConsumers('feature-id-1', [ + { + ruleTypeId: 'rule-type-id-1', + consumers: ['alerts', 'consumer-a'], + }, + { ruleTypeId: 'rule-type-id-2', consumers: ['alerts', 'consumer-b'] }, + ]), + mockFeatureWithConsumers('feature-id-2', [ + { + ruleTypeId: 'rule-type-id-1', + consumers: ['alerts', 'consumer-b'], + }, + { + ruleTypeId: 'rule-type-id-3', + consumers: ['alerts', 'consumer-c'], + }, + ]), + mockFeatureWithConsumers('feature-id-3', [ + { + ruleTypeId: 'rule-type-id-4', + consumers: ['consumer-d'], + }, + ]), + ]); + + const alertingGet = securityStart.authz.actions.alerting.get as jest.Mock; + alertingGet.mockImplementation(mockAuthorizationAction); + + ruleTypeRegistry = ruleTypeRegistryMock.create(); + ruleTypeRegistry.getAllTypes.mockReturnValue(ruleTypeIds); + ruleTypeRegistry.has.mockImplementation((ruleTypeId: string) => + ruleTypeIds.includes(ruleTypeId) + ); + + // @ts-expect-error: only the id is needed for the tests + ruleTypeRegistry.get.mockImplementation((ruleTypeId: string) => ({ id: ruleTypeId })); + }); + + it('get authorized rule types with authorized consumers', async () => { + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('rule-type-id-2', 'consumer-b', 'rule', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('rule-type-id-3', 'consumer-c', 'rule', 'create'), + authorized: false, + }, + ], + }, + }); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + expect( + await auth.getAuthorizedRuleTypesWithAuthorizedConsumers( + consumers, + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule + ) + ).toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Map { + "rule-type-id-1" => Object { + "authorizedConsumers": Object { + "consumer-a": Object { + "all": true, + "read": true, + }, + }, + }, + "rule-type-id-2" => Object { + "authorizedConsumers": Object { + "consumer-b": Object { + "all": false, + "read": true, + }, + }, + }, + }, + "hasAllRequested": true, + "username": "some-user", + } + `); + }); + + it('calls checkPrivileges with the correct actions', async () => { + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + await auth.getAuthorizedRuleTypesWithAuthorizedConsumers( + consumers, + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule + ); + + expect(checkPrivileges).toBeCalledTimes(1); + expect(checkPrivileges.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "kibana": Array [ + "rule-type-id-1/consumer-a/rule/get", + "rule-type-id-1/consumer-a/rule/create", + "rule-type-id-1/consumer-b/rule/get", + "rule-type-id-1/consumer-b/rule/create", + "rule-type-id-2/consumer-b/rule/get", + "rule-type-id-2/consumer-b/rule/create", + "rule-type-id-3/consumer-c/rule/get", + "rule-type-id-3/consumer-c/rule/create", + "rule-type-id-4/consumer-d/rule/get", + "rule-type-id-4/consumer-d/rule/create", + ], + }, + ] + `); + }); + }); + + describe('_getAuthorizedRuleTypesWithAuthorizedConsumers', () => { + type CheckPrivilegesResponseWithoutES = Omit & { + privileges: Omit; + }; + + const checkPrivileges = jest.fn, []>(async () => ({ + username: 'elastic', + hasAllRequested: true, + privileges: { kibana: [] }, + })); + + const ruleTypeIds = ['rule-type-id-1', 'rule-type-id-2', 'rule-type-id-3', 'rule-type-id-4']; + const consumers = ['consumer-a', 'consumer-b', 'consumer-c', 'consumer-d']; + + let securityStart: ReturnType; + let features: jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + allRegisteredConsumers.clear(); + securityStart = securityMock.createStart(); + securityStart.authz.mode.useRbacForRequest.mockReturnValue(true); + securityStart.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + + features = featuresPluginMock.createStart(); + features.getKibanaFeatures.mockReturnValue([ + mockFeatureWithConsumers('feature-id-1', [ + { + ruleTypeId: 'rule-type-id-1', + consumers: ['alerts', 'consumer-a'], + }, + { ruleTypeId: 'rule-type-id-2', consumers: ['alerts', 'consumer-b'] }, + ]), + mockFeatureWithConsumers('feature-id-2', [ + { + ruleTypeId: 'rule-type-id-1', + consumers: ['alerts', 'consumer-b'], + }, + { + ruleTypeId: 'rule-type-id-3', + consumers: ['alerts', 'consumer-c'], + }, + ]), + mockFeatureWithConsumers('feature-id-3', [ + { + ruleTypeId: 'rule-type-id-4', + consumers: ['consumer-d'], + }, + ]), + ]); + + const alertingGet = securityStart.authz.actions.alerting.get as jest.Mock; + alertingGet.mockImplementation(mockAuthorizationAction); + + ruleTypeRegistry = ruleTypeRegistryMock.create(); + ruleTypeRegistry.getAllTypes.mockReturnValue(ruleTypeIds); + ruleTypeRegistry.has.mockImplementation((ruleTypeId: string) => + ruleTypeIds.includes(ruleTypeId) + ); + + // @ts-expect-error: only the id is needed for the tests + ruleTypeRegistry.get.mockImplementation((ruleTypeId: string) => ({ id: ruleTypeId })); + }); + + it('returns all rule types with all consumers as authorized with no authorization', async () => { + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + }); + + expect( + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set(ruleTypeIds), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set(consumers) + ) + ).toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Map { + "rule-type-id-1" => Object { + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "consumer-a": Object { + "all": true, + "read": true, + }, + "consumer-b": Object { + "all": true, + "read": true, + }, + "consumer-c": Object { + "all": true, + "read": true, + }, + "consumer-d": Object { + "all": true, + "read": true, + }, + }, + }, + "rule-type-id-2" => Object { + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "consumer-a": Object { + "all": true, + "read": true, + }, + "consumer-b": Object { + "all": true, + "read": true, + }, + "consumer-c": Object { + "all": true, + "read": true, + }, + "consumer-d": Object { + "all": true, + "read": true, + }, + }, + }, + "rule-type-id-3" => Object { + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "consumer-a": Object { + "all": true, + "read": true, + }, + "consumer-b": Object { + "all": true, + "read": true, + }, + "consumer-c": Object { + "all": true, + "read": true, + }, + "consumer-d": Object { + "all": true, + "read": true, + }, + }, + }, + "rule-type-id-4" => Object { + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "consumer-a": Object { + "all": true, + "read": true, + }, + "consumer-b": Object { + "all": true, + "read": true, + }, + "consumer-c": Object { + "all": true, + "read": true, + }, + "consumer-d": Object { + "all": true, + "read": true, + }, + }, + }, + }, + "hasAllRequested": true, + } + `); + }); + + it('filters out rule types with no authorization', async () => { + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + }); + + expect( + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set([ruleTypeIds[0], ruleTypeIds[1]]), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set(consumers) + ) + ).toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Map { + "rule-type-id-1" => Object { + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "consumer-a": Object { + "all": true, + "read": true, + }, + "consumer-b": Object { + "all": true, + "read": true, + }, + "consumer-c": Object { + "all": true, + "read": true, + }, + "consumer-d": Object { + "all": true, + "read": true, + }, + }, + }, + "rule-type-id-2" => Object { + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "consumer-a": Object { + "all": true, + "read": true, + }, + "consumer-b": Object { + "all": true, + "read": true, + }, + "consumer-c": Object { + "all": true, + "read": true, + }, + "consumer-d": Object { + "all": true, + "read": true, + }, + }, + }, + }, + "hasAllRequested": true, + } + `); + }); + + it('returns all rule types with all consumers as authorized with disabled authorization', async () => { + securityStart.authz.mode.useRbacForRequest.mockReturnValue(false); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + expect( + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set(ruleTypeIds), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set(consumers) + ) + ).toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Map { + "rule-type-id-1" => Object { + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "consumer-a": Object { + "all": true, + "read": true, + }, + "consumer-b": Object { + "all": true, + "read": true, + }, + "consumer-c": Object { + "all": true, + "read": true, + }, + "consumer-d": Object { + "all": true, + "read": true, + }, + }, + }, + "rule-type-id-2" => Object { + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "consumer-a": Object { + "all": true, + "read": true, + }, + "consumer-b": Object { + "all": true, + "read": true, + }, + "consumer-c": Object { + "all": true, + "read": true, + }, + "consumer-d": Object { + "all": true, + "read": true, + }, + }, + }, + "rule-type-id-3" => Object { + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "consumer-a": Object { + "all": true, + "read": true, + }, + "consumer-b": Object { + "all": true, + "read": true, + }, + "consumer-c": Object { + "all": true, + "read": true, + }, + "consumer-d": Object { + "all": true, + "read": true, + }, + }, + }, + "rule-type-id-4" => Object { + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "consumer-a": Object { + "all": true, + "read": true, + }, + "consumer-b": Object { + "all": true, + "read": true, + }, + "consumer-c": Object { + "all": true, + "read": true, + }, + "consumer-d": Object { + "all": true, + "read": true, + }, + }, + }, + }, + "hasAllRequested": true, + } + `); + }); + + it('filters out rule types with disabled authorization', async () => { + securityStart.authz.mode.useRbacForRequest.mockReturnValue(false); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + expect( + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set([ruleTypeIds[0], ruleTypeIds[1]]), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set(consumers) + ) + ).toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Map { + "rule-type-id-1" => Object { + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "consumer-a": Object { + "all": true, + "read": true, + }, + "consumer-b": Object { + "all": true, + "read": true, + }, + "consumer-c": Object { + "all": true, + "read": true, + }, + "consumer-d": Object { + "all": true, + "read": true, + }, + }, + }, + "rule-type-id-2" => Object { + "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, + "consumer-a": Object { + "all": true, + "read": true, + }, + "consumer-b": Object { + "all": true, + "read": true, + }, + "consumer-c": Object { + "all": true, + "read": true, + }, + "consumer-d": Object { + "all": true, + "read": true, + }, + }, + }, + }, + "hasAllRequested": true, + } + `); + }); + + it('get authorized rule types with authorized consumers with read access only', async () => { + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'get'), + authorized: true, + }, + ], + }, + }); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + expect( + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set(ruleTypeIds), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set(consumers) + ) + ).toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Map { + "rule-type-id-1" => Object { + "authorizedConsumers": Object { + "consumer-a": Object { + "all": false, + "read": true, + }, + }, + }, + }, + "hasAllRequested": true, + "username": "some-user", + } + `); + }); + + it('get authorized rule types with authorized consumers with full access', async () => { + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'create'), + authorized: true, + }, + ], + }, + }); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + expect( + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set(ruleTypeIds), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set(consumers) + ) + ).toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Map { + "rule-type-id-1" => Object { + "authorizedConsumers": Object { + "consumer-a": Object { + "all": true, + "read": true, + }, + }, + }, + }, + "hasAllRequested": true, + "username": "some-user", + } + `); + }); + + it('filters out not requested consumers', async () => { + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-b', 'rule', 'create'), + authorized: true, + }, + ], + }, + }); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + expect( + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set(ruleTypeIds), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set(['consumer-a']) + ) + ).toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Map { + "rule-type-id-1" => Object { + "authorizedConsumers": Object { + "consumer-a": Object { + "all": false, + "read": true, + }, + }, + }, + }, + "hasAllRequested": true, + "username": "some-user", + } + `); + }); + + it('filters out not requested rule types', async () => { + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-b', 'rule', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('rule-type-id-3', 'consumer-c', 'rule', 'create'), + authorized: true, + }, + ], + }, + }); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + expect( + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set([ruleTypeIds[0], ruleTypeIds[1]]), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set(['consumer-a', 'consumer-b']) + ) + ).toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Map { + "rule-type-id-1" => Object { + "authorizedConsumers": Object { + "consumer-a": Object { + "all": false, + "read": true, + }, + "consumer-b": Object { + "all": true, + "read": true, + }, + }, + }, }, + "hasAllRequested": true, + "username": "some-user", } `); }); - it('augments a list of types with consumers under which the operation is authorized', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + it('returns an empty map with no requested consumers', async () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', - hasAllRequested: false, + hasAllRequested: true, privileges: { kibana: [ { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'rule', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction( - 'myOtherAppAlertType', - 'myOtherApp', - 'rule', - 'create' - ), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'rule', 'create'), + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'get'), authorized: true, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'rule', 'create'), + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'create'), authorized: true, }, ], }, }); - const alertAuthorization = new AlertingAuthorization({ + const auth = await AlertingAuthorization.create({ request, ruleTypeRegistry, - authorization, getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, + features, + getSpace, + authorization: securityStart.authz, }); - ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - await expect( - alertAuthorization.filterByRuleTypeAuthorization( - new Set([myAppAlertType, myOtherAppAlertType]), - [WriteOperations.Create], - AlertingAuthorizationEntity.Rule + expect( + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set(ruleTypeIds), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set([]) ) - ).resolves.toMatchInlineSnapshot(` - Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "myApp": Object { - "all": true, - "read": true, - }, - }, - "category": "test", - "defaultActionGroupId": "default", - "enabledInLicense": true, - "hasAlertsMappings": false, - "hasFieldsForAAD": false, - "id": "myOtherAppAlertType", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "myOtherAppAlertType", - "producer": "myOtherApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - "validLegacyConsumers": Array [], - }, - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "myApp": Object { - "all": true, - "read": true, - }, - "myOtherApp": Object { - "all": true, - "read": true, - }, - }, - "category": "test", - "defaultActionGroupId": "default", - "enabledInLicense": true, - "hasAlertsMappings": false, - "hasFieldsForAAD": false, - "id": "myAppAlertType", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "myAppAlertType", - "producer": "myApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - "validLegacyConsumers": Array [], - }, + ).toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Map {}, + "hasAllRequested": true, + "username": "some-user", } `); }); - it('authorizes user under the `alerts` consumer when they are authorized by the producer', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + it('returns an empty map with no requested rule types', async () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', - hasAllRequested: false, + hasAllRequested: true, privileges: { kibana: [ { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'alert', 'create'), + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'get'), authorized: true, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'alert', 'create'), - authorized: false, + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'create'), + authorized: true, }, ], }, }); - const alertAuthorization = new AlertingAuthorization({ + const auth = await AlertingAuthorization.create({ request, ruleTypeRegistry, - authorization, getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, + features, + getSpace, + authorization: securityStart.authz, }); - ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - await expect( - alertAuthorization.filterByRuleTypeAuthorization( - new Set([myAppAlertType]), - [WriteOperations.Create], - AlertingAuthorizationEntity.Alert + expect( + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set([]), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set(consumers) ) - ).resolves.toMatchInlineSnapshot(` - Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "myApp": Object { - "all": true, - "read": true, - }, - }, - "category": "test", - "defaultActionGroupId": "default", - "enabledInLicense": true, - "hasAlertsMappings": false, - "hasFieldsForAAD": false, - "id": "myAppAlertType", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "myAppAlertType", - "producer": "myApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - "validLegacyConsumers": Array [], - }, + ).toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Map {}, + "hasAllRequested": true, + "username": "some-user", } `); }); - it('augments a list of types with consumers under which multiple operations are authorized', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + it('get authorized rule types with authorized consumers when some rule types are not authorized', async () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', - hasAllRequested: false, + hasAllRequested: true, privileges: { kibana: [ { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction( - 'myOtherAppAlertType', - 'myOtherApp', - 'alert', - 'create' - ), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'alert', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'alert', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'get'), + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'get'), authorized: true, }, { - privilege: mockAuthorizationAction( - 'myOtherAppAlertType', - 'myOtherApp', - 'alert', - 'get' - ), + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'create'), authorized: true, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'alert', 'get'), - authorized: true, + privilege: mockAuthorizationAction('rule-type-id-2', 'consumer-b', 'rule', 'create'), + authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'alert', 'get'), + privilege: mockAuthorizationAction('rule-type-id-3', 'consumer-c', 'rule', 'get'), authorized: true, }, ], }, }); - const alertAuthorization = new AlertingAuthorization({ + const auth = await AlertingAuthorization.create({ request, ruleTypeRegistry, - authorization, getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, + features, + getSpace, + authorization: securityStart.authz, }); - ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - await expect( - alertAuthorization.filterByRuleTypeAuthorization( - new Set([myAppAlertType, myOtherAppAlertType]), - [WriteOperations.Create, ReadOperations.Get], - AlertingAuthorizationEntity.Alert + expect( + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set(ruleTypeIds), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set(consumers) ) - ).resolves.toMatchInlineSnapshot(` - Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "myApp": Object { - "all": true, - "read": true, - }, - "myOtherApp": Object { - "all": false, - "read": true, + ).toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Map { + "rule-type-id-1" => Object { + "authorizedConsumers": Object { + "consumer-a": Object { + "all": true, + "read": true, + }, }, }, - "category": "test", - "defaultActionGroupId": "default", - "enabledInLicense": true, - "hasAlertsMappings": false, - "hasFieldsForAAD": false, - "id": "myOtherAppAlertType", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "myOtherAppAlertType", - "producer": "myOtherApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - "validLegacyConsumers": Array [], - }, - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "myApp": Object { - "all": false, - "read": true, - }, - "myOtherApp": Object { - "all": false, - "read": true, + "rule-type-id-3" => Object { + "authorizedConsumers": Object { + "consumer-c": Object { + "all": false, + "read": true, + }, }, }, - "category": "test", - "defaultActionGroupId": "default", - "enabledInLicense": true, - "hasAlertsMappings": false, - "hasFieldsForAAD": false, - "id": "myAppAlertType", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "myAppAlertType", - "producer": "myApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - "validLegacyConsumers": Array [], }, + "hasAllRequested": true, + "username": "some-user", } `); }); - it('omits types which have no consumers under which the operation is authorized', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + it('get authorized rule types with authorized consumers when consumers are not valid for a rule type', async () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', - hasAllRequested: false, + hasAllRequested: true, privileges: { kibana: [ { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'create'), + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'get'), authorized: true, }, { - privilege: mockAuthorizationAction( - 'myOtherAppAlertType', - 'myOtherApp', - 'alert', - 'create' - ), + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'create'), authorized: true, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'alert', 'create'), + privilege: mockAuthorizationAction('rule-type-id-2', 'consumer-b', 'rule', 'create'), authorized: false, }, { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'alert', 'create'), - authorized: false, + privilege: mockAuthorizationAction('rule-type-id-3', 'consumer-d', 'rule', 'get'), + authorized: true, }, ], }, }); - const alertAuthorization = new AlertingAuthorization({ + const auth = await AlertingAuthorization.create({ request, ruleTypeRegistry, - authorization, getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, + features, + getSpace, + authorization: securityStart.authz, }); - ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - await expect( - alertAuthorization.filterByRuleTypeAuthorization( - new Set([myAppAlertType, myOtherAppAlertType]), - [WriteOperations.Create], - AlertingAuthorizationEntity.Alert + expect( + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set(ruleTypeIds), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set(consumers) ) - ).resolves.toMatchInlineSnapshot(` - Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "myApp": Object { - "all": true, - "read": true, - }, - "myOtherApp": Object { - "all": true, - "read": true, + ).toMatchInlineSnapshot(` + Object { + "authorizedRuleTypes": Map { + "rule-type-id-1" => Object { + "authorizedConsumers": Object { + "consumer-a": Object { + "all": true, + "read": true, + }, }, }, - "category": "test", - "defaultActionGroupId": "default", - "enabledInLicense": true, - "hasAlertsMappings": false, - "hasFieldsForAAD": false, - "id": "myOtherAppAlertType", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "myOtherAppAlertType", - "producer": "myOtherApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - "validLegacyConsumers": Array [], }, + "hasAllRequested": true, + "username": "some-user", } `); }); - }); - describe('getAugmentedRuleTypesWithAuthorization', () => { - const myOtherAppAlertType: RegistryRuleType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - recoveryActionGroup: RecoveredActionGroup, - id: 'myOtherAppAlertType', - name: 'myOtherAppAlertType', - category: 'test', - producer: 'alerts', - enabledInLicense: true, - isExportable: true, - hasAlertsMappings: false, - hasFieldsForAAD: false, - validLegacyConsumers: [], - }; - const myAppAlertType: RegistryRuleType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - recoveryActionGroup: RecoveredActionGroup, - id: 'myAppAlertType', - name: 'myAppAlertType', - category: 'test', - producer: 'myApp', - enabledInLicense: true, - isExportable: true, - hasAlertsMappings: true, - hasFieldsForAAD: true, - validLegacyConsumers: [], - }; - const mySecondAppAlertType: RegistryRuleType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - recoveryActionGroup: RecoveredActionGroup, - id: 'mySecondAppAlertType', - name: 'mySecondAppAlertType', - category: 'test', - producer: 'myApp', - enabledInLicense: true, - isExportable: true, - hasAlertsMappings: false, - hasFieldsForAAD: false, - validLegacyConsumers: [], - }; - const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]); - beforeEach(() => { - features.getKibanaFeatures.mockReturnValue([ - mockFeatureWithConsumers('myOtherApp', [ - { ruleTypeId: 'myOtherAppAlertType', consumers: ['myApp'] }, - ]), - ]); - }); - it('it returns authorized rule types given a set of feature ids', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + it('filters out rule types that are not in the rule type registry but registered in the feature', async () => { + ruleTypeRegistry.has.mockImplementation((ruleTypeId: string) => false); + checkPrivileges.mockResolvedValueOnce({ username: 'some-user', - hasAllRequested: false, + hasAllRequested: true, privileges: { kibana: [ { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'find'), + privilege: mockAuthorizationAction('rule-type-id-1', 'consumer-a', 'rule', 'get'), authorized: true, }, ], }, }); - const alertAuthorization = new AlertingAuthorization({ + + const auth = await AlertingAuthorization.create({ request, ruleTypeRegistry, - authorization, getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, + features, + getSpace, + authorization: securityStart.authz, }); - ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - await expect( - alertAuthorization.getAugmentedRuleTypesWithAuthorization( - ['myApp'], - [ReadOperations.Find, ReadOperations.Get, WriteOperations.Update], - AlertingAuthorizationEntity.Alert + expect( + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set(['rule-type-id-1']), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set(['consumer-a']) ) - ).resolves.toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` Object { - "authorizedRuleTypes": Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "myApp": Object { - "all": false, - "read": true, - }, - }, - "category": "test", - "defaultActionGroupId": "default", - "enabledInLicense": true, - "hasAlertsMappings": false, - "hasFieldsForAAD": false, - "id": "myOtherAppAlertType", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "myOtherAppAlertType", - "producer": "alerts", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - "validLegacyConsumers": Array [], - }, - }, - "hasAllRequested": false, + "authorizedRuleTypes": Map {}, + "hasAllRequested": true, "username": "some-user", } `); }); - it('it returns all authorized if user has read, get and update alert privileges', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + it('filters out rule types that are registered in the rule type registry but not in the feature', async () => { + ruleTypeRegistry.has.mockImplementation((ruleTypeId: string) => true); + checkPrivileges.mockResolvedValueOnce({ username: 'some-user', - hasAllRequested: false, + hasAllRequested: true, privileges: { kibana: [ { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'get'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'alert', 'update'), + privilege: mockAuthorizationAction('not-exist', 'consumer-a', 'rule', 'get'), authorized: true, }, ], }, }); - const alertAuthorization = new AlertingAuthorization({ + + const auth = await AlertingAuthorization.create({ request, ruleTypeRegistry, - authorization, getSpaceId, - allRegisteredConsumers, - ruleTypesConsumersMap, + features, + getSpace, + authorization: securityStart.authz, }); - ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - await expect( - alertAuthorization.getAugmentedRuleTypesWithAuthorization( - ['myApp'], - [ReadOperations.Find, ReadOperations.Get, WriteOperations.Update], - AlertingAuthorizationEntity.Alert + expect( + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set(['not-exist']), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set(['consumer-a']) ) - ).resolves.toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` Object { - "authorizedRuleTypes": Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "myApp": Object { - "all": true, - "read": true, - }, - }, - "category": "test", - "defaultActionGroupId": "default", - "enabledInLicense": true, - "hasAlertsMappings": false, - "hasFieldsForAAD": false, - "id": "myOtherAppAlertType", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "myOtherAppAlertType", - "producer": "alerts", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - "validLegacyConsumers": Array [], - }, - }, - "hasAllRequested": false, + "authorizedRuleTypes": Map {}, + "hasAllRequested": true, "username": "some-user", } `); }); + + it('call checkPrivileges with the correct actions', async () => { + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set([...ruleTypeIds, 'rule-type-not-exist']), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set([...consumers, 'consumer-not-exist']) + ); + + expect(checkPrivileges).toBeCalledTimes(1); + expect(checkPrivileges.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "kibana": Array [ + "rule-type-id-1/consumer-a/rule/get", + "rule-type-id-1/consumer-a/rule/create", + "rule-type-id-1/consumer-b/rule/get", + "rule-type-id-1/consumer-b/rule/create", + "rule-type-id-2/consumer-b/rule/get", + "rule-type-id-2/consumer-b/rule/create", + "rule-type-id-3/consumer-c/rule/get", + "rule-type-id-3/consumer-c/rule/create", + "rule-type-id-4/consumer-d/rule/get", + "rule-type-id-4/consumer-d/rule/create", + ], + }, + ] + `); + }); + + it('call checkPrivileges with the correct actions when the rule type does not exist in the registry', async () => { + ruleTypeRegistry.has.mockImplementation((ruleTypeId: string) => false); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set(['rule-type-id-1']), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set(['consumer-a']) + ); + + expect(checkPrivileges).toBeCalledTimes(1); + expect(checkPrivileges.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "kibana": Array [], + }, + ] + `); + }); + + it('call checkPrivileges with the correct actions when the rule type does not exist in the feature', async () => { + ruleTypeRegistry.has.mockImplementation((ruleTypeId: string) => true); + + const auth = await AlertingAuthorization.create({ + request, + ruleTypeRegistry, + getSpaceId, + features, + getSpace, + authorization: securityStart.authz, + }); + + // @ts-expect-error: need to test the private method + await auth._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set(['not-exist']), + [ReadOperations.Get, WriteOperations.Create], + AlertingAuthorizationEntity.Rule, + new Set(['consumer-a']) + ); + + expect(checkPrivileges).toBeCalledTimes(1); + expect(checkPrivileges.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "kibana": Array [], + }, + ] + `); + }); }); describe('8.11+', () => { @@ -2460,7 +3558,7 @@ describe('AlertingAuthorization', () => { alertAuthorization = new AlertingAuthorization({ request, ruleTypeRegistry, - authorization, + authorization: securityStart.authz, getSpaceId, allRegisteredConsumers, ruleTypesConsumersMap, @@ -2615,7 +3713,7 @@ describe('AlertingAuthorization', () => { alertAuthorization = new AlertingAuthorization({ request, ruleTypeRegistry, - authorization, + authorization: securityStart.authz, getSpaceId, allRegisteredConsumers, ruleTypesConsumersMap, @@ -2768,7 +3866,7 @@ describe('AlertingAuthorization', () => { alertAuthorization = new AlertingAuthorization({ request, ruleTypeRegistry, - authorization, + authorization: securityStart.authz, getSpaceId, allRegisteredConsumers, ruleTypesConsumersMap, diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index f992381b88ad1..8ad667306a671 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -6,7 +6,6 @@ */ import Boom from '@hapi/boom'; -import { isEmpty } from 'lodash'; import { KibanaRequest } from '@kbn/core/server'; import { JsonObject } from '@kbn/utility-types'; import { KueryNode } from '@kbn/es-query'; @@ -14,52 +13,13 @@ import { SecurityPluginStart } from '@kbn/security-plugin/server'; import { PluginStartContract as FeaturesPluginStart } from '@kbn/features-plugin/server'; import { Space } from '@kbn/spaces-plugin/server'; import { RegistryRuleType } from '../rule_type_registry'; -import { ALERTING_FEATURE_ID, RuleTypeRegistry } from '../types'; +import { RuleTypeRegistry } from '../types'; import { asFiltersByRuleTypeAndConsumer, asFiltersBySpaceId, AlertingAuthorizationFilterOpts, } from './alerting_authorization_kuery'; - -export enum AlertingAuthorizationEntity { - Rule = 'rule', - Alert = 'alert', -} - -export enum ReadOperations { - Get = 'get', - GetRuleState = 'getRuleState', - GetAlertSummary = 'getAlertSummary', - GetExecutionLog = 'getExecutionLog', - GetActionErrorLog = 'getActionErrorLog', - Find = 'find', - GetAuthorizedAlertsIndices = 'getAuthorizedAlertsIndices', - GetRuleExecutionKPI = 'getRuleExecutionKPI', - GetBackfill = 'getBackfill', - FindBackfill = 'findBackfill', -} - -export enum WriteOperations { - Create = 'create', - Delete = 'delete', - Update = 'update', - UpdateApiKey = 'updateApiKey', - Enable = 'enable', - Disable = 'disable', - MuteAll = 'muteAll', - UnmuteAll = 'unmuteAll', - MuteAlert = 'muteAlert', - UnmuteAlert = 'unmuteAlert', - Snooze = 'snooze', - BulkEdit = 'bulkEdit', - BulkDelete = 'bulkDelete', - BulkEnable = 'bulkEnable', - BulkDisable = 'bulkDisable', - Unsnooze = 'unsnooze', - RunSoon = 'runSoon', - ScheduleBackfill = 'scheduleBackfill', - DeleteBackfill = 'deleteBackfill', -} +import { ReadOperations, WriteOperations, AlertingAuthorizationEntity } from './types'; export interface EnsureAuthorizedOpts { ruleTypeId: string; @@ -73,12 +33,13 @@ interface HasPrivileges { read: boolean; all: boolean; } + type AuthorizedConsumers = Record; export interface RegistryAlertTypeWithAuth extends RegistryRuleType { authorizedConsumers: AuthorizedConsumers; } -type IsAuthorizedAtProducerLevel = boolean; +export type AuthorizedRuleTypes = Map; export interface CreateOptions { ruleTypeRegistry: RuleTypeRegistry; @@ -146,6 +107,24 @@ export class AlertingAuthorization { const allRegisteredConsumers = new Set(); const ruleTypesConsumersMap = new Map>(); + /** + * Each feature configures a set of rule types. Each + * rule type configures its valid consumers. For example, + * + * { id: 'my-feature-id-1', alerting: [{ ruleTypeId: 'my-rule-type', consumers: ['consumer-a', 'consumer-b'] }] } + * { id: 'my-feature-id-2', alerting: [{ ruleTypeId: 'my-rule-type-2', consumers: ['consumer-a', 'consumer-d'] }] } + * + * In this loop we iterate over all features and we construct: + * a) a set that contains all registered consumers and + * b) a map that contains all valid consumers per rule type. + * We remove duplicates in the process. For example, + * + * allRegisteredConsumers: Set(1) { 'consumer-a', 'consumer-b', 'consumer-d' } + * ruleTypesConsumersMap: Map(1) { + * 'my-rule-type' => Set(1) { 'consumer-a', 'consumer-b' } + * 'my-rule-type-2' => Set(1) { 'consumer-a', 'consumer-d' } + * } + */ for (const feature of featuresWithAlertingConfigured) { if (feature.alerting) { for (const entry of feature.alerting) { @@ -182,20 +161,20 @@ export class AlertingAuthorization { } /* - * This method exposes the private 'augmentRuleTypesWithAuthorization' to be + * This method exposes the private '_getAuthorizedRuleTypesWithAuthorizedConsumers' to be * used by the RAC/Alerts client */ - public async getAugmentedRuleTypesWithAuthorization( + public async getAuthorizedRuleTypesWithAuthorizedConsumers( featureIds: readonly string[], operations: Array, authorizationEntity: AlertingAuthorizationEntity ): Promise<{ username?: string; hasAllRequested: boolean; - authorizedRuleTypes: Set; + authorizedRuleTypes: AuthorizedRuleTypes; }> { - return this.augmentRuleTypesWithAuthorization( - this.ruleTypeRegistry.list(), + return this._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set(this.ruleTypeRegistry.getAllTypes()), operations, authorizationEntity, new Set(featureIds) @@ -204,18 +183,12 @@ export class AlertingAuthorization { public async ensureAuthorized({ ruleTypeId, - consumer: legacyConsumer, + consumer, operation, entity, additionalPrivileges = [], }: EnsureAuthorizedOpts) { const { authorization } = this; - const ruleType = this.ruleTypeRegistry.get(ruleTypeId); - const consumer = getValidConsumer({ - validLegacyConsumers: ruleType.validLegacyConsumers, - legacyConsumer, - producer: ruleType.producer, - }); const isAvailableConsumer = this.allRegisteredConsumers.has(consumer); if (authorization && this.shouldCheckAuthorization()) { @@ -236,7 +209,7 @@ export class AlertingAuthorization { * as Privileged. * This check will ensure we don't accidentally let these through */ - throw Boom.forbidden(getUnauthorizedMessage(ruleTypeId, legacyConsumer, operation, entity)); + throw Boom.forbidden(getUnauthorizedMessage(ruleTypeId, consumer, operation, entity)); } if (!hasAllRequested) { @@ -266,14 +239,15 @@ export class AlertingAuthorization { public async getAuthorizedRuleTypes( authorizationEntity: AlertingAuthorizationEntity, featuresIds?: Set - ): Promise { - const { authorizedRuleTypes } = await this.augmentRuleTypesWithAuthorization( - this.ruleTypeRegistry.list(), + ): Promise { + const { authorizedRuleTypes } = await this._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set(this.ruleTypeRegistry.getAllTypes()), [ReadOperations.Find], authorizationEntity, featuresIds ); - return Array.from(authorizedRuleTypes); + + return authorizedRuleTypes; } public async getAuthorizationFilter( @@ -286,27 +260,19 @@ export class AlertingAuthorization { ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, auth: string) => void; }> { if (this.authorization && this.shouldCheckAuthorization()) { - const { authorizedRuleTypes } = await this.augmentRuleTypesWithAuthorization( - this.ruleTypeRegistry.list(), + const { authorizedRuleTypes } = await this._getAuthorizedRuleTypesWithAuthorizedConsumers( + new Set(this.ruleTypeRegistry.getAllTypes()), [operation], authorizationEntity, featuresIds ); if (!authorizedRuleTypes.size) { - throw Boom.forbidden(`Unauthorized to find ${authorizationEntity}s for any rule types`); + throw Boom.forbidden( + `Unauthorized to ${operation} ${authorizationEntity}s for any rule types` + ); } - const authorizedRuleTypeIdsToConsumers = new Set( - [...authorizedRuleTypes].reduce((ruleTypeIdConsumerPairs, ruleType) => { - for (const consumer of Object.keys(ruleType.authorizedConsumers)) { - ruleTypeIdConsumerPairs.push(`${ruleType.id}/${consumer}/${authorizationEntity}`); - } - return ruleTypeIdConsumerPairs; - }, []) - ); - - const authorizedEntries: Map> = new Map(); return { filter: asFiltersByRuleTypeAndConsumer( authorizedRuleTypes, @@ -314,16 +280,17 @@ export class AlertingAuthorization { this.spaceId ) as JsonObject, ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, authType: string) => { - if (!authorizedRuleTypeIdsToConsumers.has(`${ruleTypeId}/${consumer}/${authType}`)) { + if (!authorizedRuleTypes.has(ruleTypeId) || authType !== authorizationEntity) { + throw Boom.forbidden(getUnauthorizedMessage(ruleTypeId, consumer, operation, authType)); + } + + const authorizedRuleType = authorizedRuleTypes.get(ruleTypeId)!; + const authorizedConsumers = authorizedRuleType.authorizedConsumers; + + if (!authorizedConsumers[consumer]) { throw Boom.forbidden( - getUnauthorizedMessage(ruleTypeId, consumer, 'find', authorizationEntity) + getUnauthorizedMessage(ruleTypeId, consumer, operation, authorizationEntity) ); - } else { - if (authorizedEntries.has(ruleTypeId)) { - authorizedEntries.get(ruleTypeId)!.add(consumer); - } else { - authorizedEntries.set(ruleTypeId, new Set([consumer])); - } } }, }; @@ -336,99 +303,78 @@ export class AlertingAuthorization { } public async filterByRuleTypeAuthorization( - ruleTypes: Set, + ruleTypeIds: Set, operations: Array, authorizationEntity: AlertingAuthorizationEntity - ): Promise> { - const { authorizedRuleTypes } = await this.augmentRuleTypesWithAuthorization( - ruleTypes, + ): Promise { + const { authorizedRuleTypes } = await this._getAuthorizedRuleTypesWithAuthorizedConsumers( + ruleTypeIds, operations, authorizationEntity ); return authorizedRuleTypes; } - private async augmentRuleTypesWithAuthorization( - ruleTypes: Set, + private async _getAuthorizedRuleTypesWithAuthorizedConsumers( + ruleTypeIds: Set, operations: Array, authorizationEntity: AlertingAuthorizationEntity, consumers?: Set ): Promise<{ username?: string; hasAllRequested: boolean; - authorizedRuleTypes: Set; + authorizedRuleTypes: Map; }> { const requiredPrivileges = new Map< string, - [RegistryAlertTypeWithAuth, string, HasPrivileges, IsAuthorizedAtProducerLevel] + { ruleTypeId: string; consumer: string; operation: ReadOperations | WriteOperations } >(); - const authorizedRuleTypes = new Set(); - const addLegacyConsumerPrivileges = (legacyConsumer: string) => - legacyConsumer === ALERTING_FEATURE_ID || isEmpty(consumers); + const consumersToAuthorize: Set = consumers ?? this.allRegisteredConsumers; if (this.authorization && this.shouldCheckAuthorization()) { + const authorizedRuleTypes = new Map(); + const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( this.request ); - const ruleTypesWithAuthorization = Array.from( - this.augmentWithAuthorizedConsumers(ruleTypes, {}) - ); - - for (const ruleTypeWithAuth of ruleTypesWithAuthorization) { - if (!this.ruleTypesConsumersMap.has(ruleTypeWithAuth.id)) { + for (const ruleTypeId of ruleTypeIds) { + /** + * Skip if the ruleTypeId is not configured in any feature + * or it is not set int the rule type registry. + */ + if (!this.ruleTypesConsumersMap.has(ruleTypeId) || !this.ruleTypeRegistry.has(ruleTypeId)) { continue; } - for (const consumerToAuthorize of this.allRegisteredConsumers) { - const ruleTypeConsumers = this.ruleTypesConsumersMap.get(ruleTypeWithAuth.id); + const ruleType = this.ruleTypeRegistry.get(ruleTypeId)!; + + for (const consumerToAuthorize of consumersToAuthorize) { + const ruleTypeConsumers = this.ruleTypesConsumersMap.get(ruleTypeId); + /** + * Skip if the consumer is not configured for the + * specific rule type when configuring a feature + */ if (!ruleTypeConsumers?.has(consumerToAuthorize)) { continue; } for (const operation of operations) { requiredPrivileges.set( - this.authorization!.actions.alerting.get( - ruleTypeWithAuth.id, + this.authorization.actions.alerting.get( + ruleTypeId, consumerToAuthorize, authorizationEntity, operation ), - [ - ruleTypeWithAuth, - consumerToAuthorize, - hasPrivilegeByOperation(operation), - ruleTypeWithAuth.producer === consumerToAuthorize, - ] + { + ruleTypeId: ruleType.id, + consumer: consumerToAuthorize, + operation, + } ); - - // FUTURE ENGINEER - // We are just trying to add back the legacy consumers associated - // to the rule type to get back the privileges that was given at one point - if (!isEmpty(ruleTypeWithAuth.validLegacyConsumers)) { - ruleTypeWithAuth.validLegacyConsumers.forEach((legacyConsumer) => { - if (addLegacyConsumerPrivileges(legacyConsumer)) { - if (!allRegisteredConsumers[legacyConsumer]) { - allRegisteredConsumers[legacyConsumer] = { - read: true, - all: true, - }; - } - - requiredPrivileges.set( - this.authorization!.actions.alerting.get( - ruleTypeWithAuth.id, - legacyConsumer, - authorizationEntity, - operation - ), - [ruleTypeWithAuth, legacyConsumer, hasPrivilegeByOperation(operation), false] - ); - } - }); - } } } } @@ -439,62 +385,56 @@ export class AlertingAuthorization { for (const { authorized, privilege } of privileges.kibana) { if (authorized && requiredPrivileges.has(privilege)) { - const [ruleType, consumer, hasPrivileges, isAuthorizedAtProducerLevel] = - requiredPrivileges.get(privilege)!; - - if (consumersToAuthorize.has(consumer)) { - ruleType.authorizedConsumers[consumer] = mergeHasPrivileges( - hasPrivileges, - ruleType.authorizedConsumers[consumer] - ); - - if (isAuthorizedAtProducerLevel) { - // granting privileges under the producer automatically authorized the Rules Management UI as well - ruleType.validLegacyConsumers.forEach((legacyConsumer) => { - if (addLegacyConsumerPrivileges(legacyConsumer)) { - ruleType.authorizedConsumers[legacyConsumer] = mergeHasPrivileges( - hasPrivileges, - ruleType.authorizedConsumers[legacyConsumer] - ); - } - }); - } - - authorizedRuleTypes.add(ruleType); - } + const { ruleTypeId, consumer, operation } = requiredPrivileges.get(privilege)!; + + const authorizedRuleType = authorizedRuleTypes.get(ruleTypeId) ?? { + authorizedConsumers: {}, + }; + + const authorizedConsumers = authorizedRuleType.authorizedConsumers; + const mergedOperations = mergeHasPrivileges( + getPrivilegesFromOperation(operation), + authorizedConsumers[consumer] + ); + + authorizedRuleTypes.set(ruleTypeId, { + authorizedConsumers: { + ...authorizedConsumers, + [consumer]: mergedOperations, + }, + }); } } return { username, hasAllRequested, - authorizedRuleTypes: - hasAllRequested && consumers === undefined - ? // has access to all features - this.augmentWithAuthorizedConsumers(authorizedRuleTypes, allRegisteredConsumers) - : authorizedRuleTypes, + authorizedRuleTypes, }; } else { return { hasAllRequested: true, - authorizedRuleTypes: this.augmentWithAuthorizedConsumers( - new Set([...ruleTypes].filter((ruleType) => consumersToAuthorize.has(ruleType.producer))), - allRegisteredConsumers - ), + authorizedRuleTypes: this.getRegisteredRuleTypesWithAllRegisteredConsumers(ruleTypeIds), }; } } - private augmentWithAuthorizedConsumers( - ruleTypes: Set, - authorizedConsumers: AuthorizedConsumers - ): Set { - return new Set( - Array.from(ruleTypes).map((ruleType) => ({ - ...ruleType, - authorizedConsumers: { ...authorizedConsumers }, - })) - ); + private getRegisteredRuleTypesWithAllRegisteredConsumers(ruleTypeIds: Set) { + const authorizedRuleTypes = new Map(); + const authorizedConsumers = getConsumersWithPrivileges(this.allRegisteredConsumers, { + all: true, + read: true, + }); + + Array.from(this.ruleTypesConsumersMap.keys()) + .filter((ruleTypeId) => ruleTypeIds.has(ruleTypeId)) + .forEach((ruleTypeId) => { + authorizedRuleTypes.set(ruleTypeId, { + authorizedConsumers, + }); + }); + + return authorizedRuleTypes; } } @@ -505,7 +445,7 @@ function mergeHasPrivileges(left: HasPrivileges, right?: HasPrivileges): HasPriv }; } -function hasPrivilegeByOperation(operation: ReadOperations | WriteOperations): HasPrivileges { +function getPrivilegesFromOperation(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 { @@ -514,11 +454,11 @@ function hasPrivilegeByOperation(operation: ReadOperations | WriteOperations): H }; } -function asAuthorizedConsumers( - consumers: string[], +function getConsumersWithPrivileges( + consumers: Set, hasPrivileges: HasPrivileges ): AuthorizedConsumers { - return consumers.reduce((acc, feature) => { + return Array.from(consumers).reduce((acc, feature) => { acc[feature] = hasPrivileges; return acc; }, {}); @@ -532,16 +472,3 @@ function getUnauthorizedMessage( ): string { return `Unauthorized by "${scope}" to ${operation} "${alertTypeId}" ${entity}`; } - -export const getValidConsumer = ({ - validLegacyConsumers, - legacyConsumer, - producer, -}: { - validLegacyConsumers: string[]; - legacyConsumer: string; - producer: string; -}): string => - legacyConsumer === ALERTING_FEATURE_ID || validLegacyConsumers.includes(legacyConsumer) - ? producer - : legacyConsumer; diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts index 01e30dfb38327..666059c7a7b47 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts @@ -9,7 +9,7 @@ import { remove } from 'lodash'; import { EsQueryConfig, nodeBuilder, toElasticsearchQuery, KueryNode } from '@kbn/es-query'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { RegistryAlertTypeWithAuth } from './alerting_authorization'; +import { AuthorizedRuleTypes } from './alerting_authorization'; export enum AlertingAuthorizationFilterType { KQL = 'kql', @@ -35,36 +35,39 @@ const esQueryConfig: EsQueryConfig = { }; export function asFiltersByRuleTypeAndConsumer( - ruleTypes: Set, + ruleTypes: AuthorizedRuleTypes, opts: AlertingAuthorizationFilterOpts, spaceId: string | undefined ): KueryNode | estypes.QueryDslQueryContainer { const kueryNode = nodeBuilder.or( - Array.from(ruleTypes).reduce((filters, { id, authorizedConsumers }) => { - ensureFieldIsSafeForQuery('ruleTypeId', id); - - const andNodes: KueryNode[] = [nodeBuilder.is(opts.fieldNames.ruleTypeId, id)]; - - const authorizedConsumersKeys = Object.keys(authorizedConsumers); - if (authorizedConsumersKeys.length) { - andNodes.push( - nodeBuilder.or( - authorizedConsumersKeys.map((consumer) => { - ensureFieldIsSafeForQuery('consumer', consumer); - return nodeBuilder.is(opts.fieldNames.consumer, consumer); - }) - ) - ); - } - - if (opts.fieldNames.spaceIds != null && spaceId != null) { - andNodes.push(nodeBuilder.is(opts.fieldNames.spaceIds, spaceId)); - } - - filters.push(nodeBuilder.and(andNodes)); - - return filters; - }, []) + Array.from(ruleTypes.entries()).reduce( + (filters, [id, { authorizedConsumers }]) => { + ensureFieldIsSafeForQuery('ruleTypeId', id); + + const andNodes: KueryNode[] = [nodeBuilder.is(opts.fieldNames.ruleTypeId, id)]; + + const authorizedConsumersKeys = Object.keys(authorizedConsumers); + if (authorizedConsumersKeys.length) { + andNodes.push( + nodeBuilder.or( + authorizedConsumersKeys.map((consumer) => { + ensureFieldIsSafeForQuery('consumer', consumer); + return nodeBuilder.is(opts.fieldNames.consumer, consumer); + }) + ) + ); + } + + if (opts.fieldNames.spaceIds != null && spaceId != null) { + andNodes.push(nodeBuilder.is(opts.fieldNames.spaceIds, spaceId)); + } + + filters.push(nodeBuilder.and(andNodes)); + + return filters; + }, + [] + ) ); if (opts.type === AlertingAuthorizationFilterType.ESDSL) { diff --git a/x-pack/plugins/alerting/server/authorization/types.ts b/x-pack/plugins/alerting/server/authorization/types.ts new file mode 100644 index 0000000000000..77357456c74b6 --- /dev/null +++ b/x-pack/plugins/alerting/server/authorization/types.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum AlertingAuthorizationEntity { + Rule = 'rule', + Alert = 'alert', +} + +export enum ReadOperations { + Get = 'get', + GetRuleState = 'getRuleState', + GetAlertSummary = 'getAlertSummary', + GetExecutionLog = 'getExecutionLog', + GetActionErrorLog = 'getActionErrorLog', + Find = 'find', + GetAuthorizedAlertsIndices = 'getAuthorizedAlertsIndices', + GetRuleExecutionKPI = 'getRuleExecutionKPI', + GetBackfill = 'getBackfill', + FindBackfill = 'findBackfill', +} + +export enum WriteOperations { + Create = 'create', + Delete = 'delete', + Update = 'update', + UpdateApiKey = 'updateApiKey', + Enable = 'enable', + Disable = 'disable', + MuteAll = 'muteAll', + UnmuteAll = 'unmuteAll', + MuteAlert = 'muteAlert', + UnmuteAlert = 'unmuteAlert', + Snooze = 'snooze', + BulkEdit = 'bulkEdit', + BulkDelete = 'bulkDelete', + BulkEnable = 'bulkEnable', + BulkDisable = 'bulkDisable', + Unsnooze = 'unsnooze', + RunSoon = 'runSoon', + ScheduleBackfill = 'scheduleBackfill', + DeleteBackfill = 'deleteBackfill', +} diff --git a/x-pack/plugins/alerting/server/rules_client/lib/get_authorization_filter.ts b/x-pack/plugins/alerting/server/rules_client/lib/get_authorization_filter.ts index b9cc41a0fd7c4..591ea70972ed9 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/get_authorization_filter.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/get_authorization_filter.ts @@ -6,11 +6,11 @@ */ import { withSpan } from '@kbn/apm-utils'; -import { AlertingAuthorizationEntity } from '../../authorization'; import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events'; import { RulesClientContext } from '../types'; import { alertingAuthorizationFilterOpts } from '../common/constants'; import { BulkAction } from '../types'; +import { AlertingAuthorizationEntity } from '../../authorization/types'; export const getAuthorizationFilter = async ( context: RulesClientContext, diff --git a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts index 49e7d706b3117..477baa21b230d 100644 --- a/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts +++ b/x-pack/plugins/rule_registry/server/search_strategy/search_strategy.ts @@ -13,7 +13,7 @@ import { ENHANCED_ES_SEARCH_STRATEGY } from '@kbn/data-plugin/common'; import { ISearchStrategy, PluginStart } from '@kbn/data-plugin/server'; import { ReadOperations, - PluginStartContract as AlertingStart, + AlertingServerStart, AlertingAuthorizationEntity, } from '@kbn/alerting-plugin/server'; import { SecurityPluginSetup } from '@kbn/security-plugin/server'; @@ -38,7 +38,7 @@ const EXCLUDED_RULE_TYPE_IDS = ['siem.notifications']; export const ruleRegistrySearchStrategyProvider = ( data: PluginStart, - alerting: AlertingStart, + alerting: AlertingServerStart, logger: Logger, security?: SecurityPluginSetup, spaces?: SpacesPluginStart