@loopback/authorization
Version:
A LoopBack component for authorization support.
149 lines (136 loc) • 4.58 kB
text/typescript
// Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved.
// Node module: @loopback/authorization
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
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');
(asGlobalInterceptor('authorization'))
export class AuthorizationInterceptor implements Provider<Interceptor> {
private options: AuthorizationOptions;
constructor(
options: AuthorizationOptions = {},
) {
({fromBinding: AuthorizationBindings.COMPONENT})
this.options = {
defaultDecision: AuthorizationDecision.DENY,
precedence: AuthorizationDecision.DENY,
defaultStatusCodeForDeny: 403,
...options,
};
debug('Authorization options', this.options);
}
value(): Interceptor {
return this.intercept.bind(this);
}
async intercept(
invocationCtx: InvocationContext,
next: Next,
): Promise<NonVoid> {
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);
// retrieve it from authentication module
const user = await invocationCtx.get<UserProfile>(SecurityBindings.USER, {
optional: true,
});
debug('Current user', user);
const authorizationCtx: AuthorizationContext = {
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);
// Reset the final decision if an explicit Deny or Allow is voted
if (decision && decision !== AuthorizationDecision.ABSTAIN) {
finalDecision = decision;
}
// we can add another interceptor to process the error
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);
// Handle the final decision
if (finalDecision === AuthorizationDecision.DENY) {
const error = new AuthorizationError('Access denied');
error.statusCode = this.options.defaultStatusCodeForDeny;
throw error;
}
return next();
}
}
async function loadAuthorizers(
ctx: Context,
authorizers: (Authorizer | BindingAddress<Authorizer>)[],
) {
const authorizerFunctions: Authorizer[] = [];
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;
}