import Boom from '@hapi/boom' ; import { map , mapValues , fromPairs , has } from 'lodash' ; import { KibanaRequest } from 'src/core/server' ; import { JsonObject } from '@kbn/utility-types' ; import { KueryNode } from '@kbn/es-query' ; import { ALERTS_FEATURE_ID , RuleTypeRegistry } from '../types' ; import { SecurityPluginSetup } from '../../../security/server' ; import { RegistryRuleType } from '../rule_type_registry' ; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server' ; import { Space } from '../../../spaces/server' ; import { asFiltersByRuleTypeAndConsumer , asFiltersBySpaceId , AlertingAuthorizationFilterOpts , } from './alerting_authorization_kuery' ; export enum AlertingAuthorizationEntity { Rule = 'rule' , Alert = 'alert' } export enum ReadOperations { Get = 'get' , GetRuleState = 'getRuleState' , GetAlertSummary = 'getAlertSummary' , Find = 'find' } export enum WriteOperations { Create = 'create' , Delete = 'delete' , Update = 'update' , UpdateApiKey = 'updateApiKey' , Enable = 'enable' , Disable = 'disable' , MuteAll = 'muteAll' , UnmuteAll = 'unmuteAll' , MuteAlert = 'muteAlert' , UnmuteAlert = 'unmuteAlert' } export interface EnsureAuthorizedOpts { ruleTypeId : string ; consumer : string ; operation : ReadOperations | WriteOperations ; entity : AlertingAuthorizationEntity ; } interface HasPrivileges { read : boolean ; all : boolean ; } type AuthorizedConsumers = Record < string , HasPrivileges > ; export interface RegistryAlertTypeWithAuth extends RegistryRuleType { authorizedConsumers : AuthorizedConsumers ; } type IsAuthorizedAtProducerLevel = boolean ; export interface ConstructorOptions { ruleTypeRegistry : RuleTypeRegistry ; request : KibanaRequest ; features : FeaturesPluginStart ; getSpace : ( request : KibanaRequest ) => Promise < Space | undefined > ; getSpaceId : ( request : KibanaRequest ) => string | undefined ; authorization ? : SecurityPluginSetup [ 'authz' ] ; } export class AlertingAuthorization { private readonly ruleTypeRegistry : RuleTypeRegistry ; private readonly request : KibanaRequest ; private readonly authorization ? : SecurityPluginSetup [ 'authz' ] ; private readonly featuresIds : Promise < Set < string > > ; private readonly allPossibleConsumers : Promise < AuthorizedConsumers > ; private readonly spaceId : string | undefined ; constructor ( { ruleTypeRegistry , request , authorization , features , getSpace , getSpaceId , } ) { this . request = request ; this . authorization = authorization ; this . ruleTypeRegistry = ruleTypeRegistry ; this . spaceId = getSpaceId ( request ) ; this . featuresIds = getSpace ( request ) . then ( ( maybeSpace ) => new Set ( maybeSpace ?. disabledFeatures ?? [ ] ) ) . then ( ( disabledFeatures ) => new Set ( features . getKibanaFeatures ( ) . filter ( ( { id , alerting } ) => ! disabledFeatures . has ( id ) && ( alerting ?. length ?? 0 > 0 ) ) . map ( ( feature ) => feature . id ) ) ) . catch ( ( ) => { return new Set ( ) ; } ) ; this . allPossibleConsumers = this . featuresIds . then ( ( featuresIds ) => { return featuresIds . size ? asAuthorizedConsumers ( [ ALERTS_FEATURE_ID , ... featuresIds ] , { read : true , all : true , } ) : { } ; } ) ; } private shouldCheckAuthorization ( ) { return this . authorization ?. mode ?. useRbacForRequest ( this . request ) ?? false ; } public getSpaceId ( ) { return this . spaceId ; } public async getAugmentedRuleTypesWithAuthorization ( featureIds , operations , authorizationEntity ) { return this . augmentRuleTypesWithAuthorization ( this . ruleTypeRegistry . list ( ) , operations , authorizationEntity , new Set ( featureIds ) ) ; } public async ensureAuthorized ( { ruleTypeId , consumer , operation , entity } ) { const { authorization } = this ; const isAvailableConsumer = has ( await this . allPossibleConsumers , consumer ) ; if ( authorization && this . shouldCheckAuthorization ( ) ) { const ruleType = this . ruleTypeRegistry . get ( ruleTypeId ) ; const requiredPrivilegesByScope = { consumer : authorization . actions . alerting . get ( ruleTypeId , consumer , entity , operation ) , producer : authorization . actions . alerting . get ( ruleTypeId , ruleType . producer , entity , operation ) , } ; const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID ; const checkPrivileges = authorization . checkPrivilegesDynamicallyWithRequest ( this . request ) ; const { hasAllRequested , privileges } = await checkPrivileges ( { kibana : shouldAuthorizeConsumer && consumer !== ruleType . producer ? [ requiredPrivilegesByScope . consumer , requiredPrivilegesByScope . producer , ] : [ requiredPrivilegesByScope . producer , ] , } ) ; if ( ! isAvailableConsumer ) { /**
                 * Under most circumstances this would have been caught by `checkPrivileges` as
                 * a user can't have Privileges to an unknown consumer, but super users
                 * don't actually get "privilege checked" so the made up consumer *will* return
                 * as Privileged.
                 * This check will ensure we don't accidentally let these through
                 */ throw Boom . forbidden ( getUnauthorizedMessage ( ruleTypeId , ScopeType . Consumer , consumer , operation , entity ) ) ; } if ( ! hasAllRequested ) { const authorizedPrivileges = map ( privileges . kibana . filter ( ( privilege ) => privilege . authorized ) , 'privilege' ) ; const unauthorizedScopes = mapValues ( requiredPrivilegesByScope , ( privilege ) => ! authorizedPrivileges . includes ( privilege ) ) ; const [ unauthorizedScopeType , unauthorizedScope ] = shouldAuthorizeConsumer && unauthorizedScopes . consumer ? [ ScopeType . Consumer , consumer ] : [ ScopeType . Producer , ruleType . producer ] ; throw Boom . forbidden ( getUnauthorizedMessage ( ruleTypeId , unauthorizedScopeType , unauthorizedScope , operation , entity ) ) ; } } else if ( ! isAvailableConsumer ) { throw Boom . forbidden ( getUnauthorizedMessage ( ruleTypeId , ScopeType . Consumer , consumer , operation , entity ) ) ; } } public async getFindAuthorizationFilter ( authorizationEntity , filterOpts ) { return this . getAuthorizationFilter ( authorizationEntity , filterOpts , ReadOperations . Find ) ; } public async getAuthorizationFilter ( authorizationEntity , filterOpts , operation ) { if ( this . authorization && this . shouldCheckAuthorization ( ) ) { const { authorizedRuleTypes } = await this . augmentRuleTypesWithAuthorization ( this . ruleTypeRegistry . list ( ) , [ operation ] , authorizationEntity ) ; if ( ! authorizedRuleTypes . size ) { throw Boom . forbidden ( `Unauthorized to find ${ authorizationEntity }s for any rule types` ) ; } const authorizedRuleTypeIdsToConsumers = new Set < string > ( [ ... authorizedRuleTypes ] . reduce < string [ ] > ( ( ruleTypeIdConsumerPairs , ruleType ) => { for ( const consumer of Object . keys ( ruleType . authorizedConsumers ) ) { ruleTypeIdConsumerPairs . push ( `${ ruleType . id }/${ consumer }/${ authorizationEntity }` ) ; } return ruleTypeIdConsumerPairs ; } , [ ] ) ) ; const authorizedEntries = new Map ( ) ; return { filter : asFiltersByRuleTypeAndConsumer ( authorizedRuleTypes , filterOpts , this . spaceId ) as JsonObject , ensureRuleTypeIsAuthorized : ( ruleTypeId , consumer , authType ) => { if ( ! authorizedRuleTypeIdsToConsumers . has ( `${ ruleTypeId }/${ consumer }/${ authType }` ) ) { throw Boom . forbidden ( getUnauthorizedMessage ( ruleTypeId , ScopeType . Consumer , consumer , 'find' , authorizationEntity ) ) ; } else { if ( authorizedEntries . has ( ruleTypeId ) ) { authorizedEntries . get ( ruleTypeId ) ! . add ( consumer ) ; } else { authorizedEntries . set ( ruleTypeId , new Set ( [ consumer ] ) ) ; } } } , } ; } return { filter : asFiltersBySpaceId ( filterOpts , this . spaceId ) as JsonObject , ensureRuleTypeIsAuthorized : ( ruleTypeId , consumer , authType ) => { } , } ; } public async filterByRuleTypeAuthorization ( ruleTypes , operations , authorizationEntity ) { const { authorizedRuleTypes } = await this . augmentRuleTypesWithAuthorization ( ruleTypes , operations , authorizationEntity ) ; return authorizedRuleTypes ; } private async augmentRuleTypesWithAuthorization ( ruleTypes , operations , authorizationEntity , featuresIds ? ) { const fIds = featuresIds ?? ( await this . featuresIds ) ; if ( this . authorization && this . shouldCheckAuthorization ( ) ) { const checkPrivileges = this . authorization . checkPrivilegesDynamicallyWithRequest ( this . request ) ; const ruleTypesWithAuthorization = this . augmentWithAuthorizedConsumers ( ruleTypes , { } ) ; const privilegeToRuleType = new Map < string , [ RegistryAlertTypeWithAuth , string , HasPrivileges , IsAuthorizedAtProducerLevel ] > ( ) ; for ( const ruleType of ruleTypesWithAuthorization ) { for ( const feature of fIds ) { for ( const operation of operations ) { privilegeToRuleType . set ( this . authorization ! . actions . alerting . get ( ruleType . id , feature , authorizationEntity , operation ) , [ ruleType , feature , hasPrivilegeByOperation ( operation ) , ruleType . producer === feature ] ) ; } } } const { username , hasAllRequested , privileges } = await checkPrivileges ( { kibana : [ ... privilegeToRuleType . keys ( ) ] , } ) ; return { username , hasAllRequested , authorizedRuleTypes : hasAllRequested ? this . augmentWithAuthorizedConsumers ( ruleTypes , await this . allPossibleConsumers ) : privileges . kibana . reduce ( ( authorizedRuleTypes , { authorized , privilege } ) => { if ( authorized && privilegeToRuleType . has ( privilege ) ) { const [ ruleType , feature , hasPrivileges , isAuthorizedAtProducerLevel ] = privilegeToRuleType . get ( privilege ) ! ; ruleType . authorizedConsumers [ feature ] = mergeHasPrivileges ( hasPrivileges , ruleType . authorizedConsumers [ feature ] ) ; if ( isAuthorizedAtProducerLevel ) { ruleType . authorizedConsumers [ ALERTS_FEATURE_ID ] = mergeHasPrivileges ( hasPrivileges , ruleType . authorizedConsumers [ ALERTS_FEATURE_ID ] ) ; } authorizedRuleTypes . add ( ruleType ) ; } return authorizedRuleTypes ; } , new Set < RegistryAlertTypeWithAuth > ( ) ) , } ; } else { return { hasAllRequested : true , authorizedRuleTypes : this . augmentWithAuthorizedConsumers ( new Set ( [ ... ruleTypes ] . filter ( ( ruleType ) => fIds . has ( ruleType . producer ) ) ) , await this . allPossibleConsumers ) , } ; } } private augmentWithAuthorizedConsumers ( ruleTypes , authorizedConsumers ) { return new Set ( Array . from ( ruleTypes ) . map ( ( ruleType ) => ( { ... ruleType , authorizedConsumers : { ... authorizedConsumers } , } ) ) ) ; } } function mergeHasPrivileges ( left , right ? ) { return { read : ( left . read || right ?. read ) ?? false , all : ( left . all || right ?. all ) ?? false , } ; } function hasPrivilegeByOperation ( operation ) { const read = Object . values ( ReadOperations ) . includes ( operation as unknown as ReadOperations ) ; const all = Object . values ( WriteOperations ) . includes ( operation as unknown as WriteOperations ) ; return { read : read || all , all , } ; } function asAuthorizedConsumers ( consumers , hasPrivileges ) { return fromPairs ( consumers . map ( ( feature ) => [ feature , hasPrivileges ] ) ) ; } enum ScopeType { Consumer , Producer } function getUnauthorizedMessage ( alertTypeId , scopeType , scope , operation , entity ) { return `Unauthorized to ${ operation } a "${ alertTypeId }" ${ entity } ${ scopeType === ScopeType . Consumer ? `for "${ scope }"` : `by "${ scope }"` }` ; }