import { asGlobalInterceptor , BindingAddress , config , Context , injectable , Interceptor , InvocationContext , Next , NonVoid , Provider , } from '@loopback/core' ; import { SecurityBindings , UserProfile } from '@loopback/security' ; import debugFactory from 'debug' ; import { getAuthorizationMetadata } from './decorators/authorize' ; import { AuthorizationBindings , AuthorizationTags } from './keys' ; import { AuthorizationContext , AuthorizationDecision , AuthorizationError , AuthorizationOptions , Authorizer , } from './types' ; import { createPrincipalFromUserProfile } from './util' ; const debug = debugFactory ( 'loopback:authorization:interceptor' ) ; @ injectable ( asGlobalInterceptor ( 'authorization' ) ) export class AuthorizationInterceptor implements Provider < Interceptor > { private options : AuthorizationOptions ; constructor ( @ config ( { fromBinding : AuthorizationBindings . COMPONENT } ) options = { } ) { this . options = { defaultDecision : AuthorizationDecision . DENY , precedence : AuthorizationDecision . DENY , defaultStatusCodeForDeny : 403 , ... options , } ; debug ( 'Authorization options' , this . options ) ; } value ( ) { return this . intercept . bind ( this ) ; } async intercept ( invocationCtx , next ) { const description = debug . enabled ? invocationCtx . description : '' ; let metadata = getAuthorizationMetadata ( invocationCtx . target , invocationCtx . methodName ) ; if ( ! metadata ) { debug ( 'No authorization metadata is found for %s' , description ) ; } metadata = metadata ?? this . options . defaultMetadata ; if ( ! metadata || metadata ?. skip ) { debug ( 'Authorization is skipped for %s' , description ) ; const result = await next ( ) ; return result ; } debug ( 'Authorization metadata for %s' , description , metadata ) ; const user = await invocationCtx . get < UserProfile > ( SecurityBindings . USER , { optional : true , } ) ; debug ( 'Current user' , user ) ; const authorizationCtx = { principals : user ? [ createPrincipalFromUserProfile ( user ) ] : [ ] , roles : [ ] , scopes : [ ] , resource : invocationCtx . targetName , invocationContext : invocationCtx , } ; debug ( 'Security context for %s' , description , authorizationCtx ) ; const authorizers = await loadAuthorizers ( invocationCtx , metadata . voters ?? [ ] ) ; let finalDecision = this . options . defaultDecision ; for ( const fn of authorizers ) { const decision = await fn ( authorizationCtx , metadata ) ; debug ( 'Decision' , decision ) ; if ( decision && decision !== AuthorizationDecision . ABSTAIN ) { finalDecision = decision ; } if ( decision === AuthorizationDecision . DENY && this . options . precedence === AuthorizationDecision . DENY ) { debug ( 'Access denied' ) ; const error = new AuthorizationError ( 'Access denied' ) ; error . statusCode = this . options . defaultStatusCodeForDeny ; throw error ; } if ( decision === AuthorizationDecision . ALLOW && this . options . precedence === AuthorizationDecision . ALLOW ) { debug ( 'Access allowed' ) ; break ; } } debug ( 'Final decision' , finalDecision ) ; if ( finalDecision === AuthorizationDecision . DENY ) { const error = new AuthorizationError ( 'Access denied' ) ; error . statusCode = this . options . defaultStatusCodeForDeny ; throw error ; } return next ( ) ; } } async function loadAuthorizers ( ctx , authorizers ) { const authorizerFunctions = [ ] ; const bindings = ctx . findByTag < Authorizer > ( AuthorizationTags . AUTHORIZER ) ; authorizers = authorizers . concat ( bindings . map ( b => b . key ) ) ; for ( const keyOrFn of authorizers ) { if ( typeof keyOrFn === 'function' ) { authorizerFunctions . push ( keyOrFn ) ; } else { const fn = await ctx . get ( keyOrFn ) ; authorizerFunctions . push ( fn ) ; } } return authorizerFunctions ; }