UNPKG

@loopback/authorization

Version:

A LoopBack component for authorization support.

224 lines (206 loc) 5.93 kB
// 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 { BindingAddress, ClassDecoratorFactory, DecoratorFactory, MetadataAccessor, MetadataInspector, MetadataMap, MethodDecoratorFactory, } from '@loopback/core'; import { AUTHENTICATED, AuthorizationMetadata, Authorizer, EVERYONE, UNAUTHENTICATED, } from '../types'; export const AUTHORIZATION_METHOD_KEY = MetadataAccessor.create< AuthorizationMetadata, MethodDecorator >('authorization:method'); export const AUTHORIZATION_CLASS_KEY = MetadataAccessor.create< AuthorizationMetadata, ClassDecorator >('authorization:class'); class AuthorizeClassDecoratorFactory extends ClassDecoratorFactory<AuthorizationMetadata> {} export class AuthorizeMethodDecoratorFactory extends MethodDecoratorFactory<AuthorizationMetadata> { protected mergeWithOwn( ownMetadata: MetadataMap<AuthorizationMetadata>, target: Object, methodName?: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any methodDescriptor?: TypedPropertyDescriptor<any> | number, ) { ownMetadata = ownMetadata || {}; let methodMeta = ownMetadata[methodName!]; if (!methodMeta) { methodMeta = {...this.spec}; ownMetadata[methodName!] = methodMeta; } if (this.spec.allowedRoles) { methodMeta.allowedRoles = this.merge( methodMeta.allowedRoles, this.spec.allowedRoles, ); } if (this.spec.deniedRoles) { methodMeta.deniedRoles = this.merge( methodMeta.deniedRoles, this.spec.deniedRoles, ); } if (this.spec.scopes) { methodMeta.scopes = this.merge(methodMeta.scopes, this.spec.scopes); } if (this.spec.voters) { methodMeta.voters = this.merge(methodMeta.voters, this.spec.voters); } return ownMetadata; } private merge<T>(src?: T[], target?: T[]): T[] { const list: T[] = []; if (src === target) return src ?? list; const set = new Set<T>(src ?? []); if (target) { for (const i of target) { set.add(i); } } for (const i of set.values()) list.push(i); return list; } } /** * Decorator `@authorize` to mark methods that require authorization * * @param spec Authorization metadata */ export function authorize(spec: AuthorizationMetadata) { return function authorizeDecoratorForClassOrMethod( // Class or a prototype // eslint-disable-next-line @typescript-eslint/no-explicit-any target: any, method?: string, // Use `any` to for `TypedPropertyDescriptor` // See https://github.com/loopbackio/loopback-next/pull/2704 // eslint-disable-next-line @typescript-eslint/no-explicit-any methodDescriptor?: TypedPropertyDescriptor<any>, ) { if (method && methodDescriptor) { // Method return AuthorizeMethodDecoratorFactory.createDecorator( AUTHORIZATION_METHOD_KEY, spec, {decoratorName: '@authorize'}, )(target, method, methodDescriptor!); } if (typeof target === 'function' && !method && !methodDescriptor) { // Class return AuthorizeClassDecoratorFactory.createDecorator( AUTHORIZATION_CLASS_KEY, spec, {decoratorName: '@authorize'}, )(target); } // Not on a class or method throw new Error( '@intercept cannot be used on a property: ' + DecoratorFactory.getTargetName(target, method, methodDescriptor), ); }; } export namespace authorize { /** * Shortcut to configure allowed roles * @param roles */ export const allow = (...roles: string[]) => authorize({allowedRoles: roles}); /** * Shortcut to configure denied roles * @param roles */ export const deny = (...roles: string[]) => authorize({deniedRoles: roles}); /** * Shortcut to specify access scopes * @param scopes */ export const scope = (...scopes: string[]) => authorize({scopes}); /** * Shortcut to configure voters * @param voters */ export const vote = ( ...voters: (Authorizer | BindingAddress<Authorizer>)[] ) => authorize({voters}); /** * Allows all */ export const allowAll = () => allow(EVERYONE); /** * Allow all but the given roles * @param roles */ export const allowAllExcept = (...roles: string[]) => authorize({ deniedRoles: roles, allowedRoles: [EVERYONE], }); /** * Deny all */ export const denyAll = () => deny(EVERYONE); /** * Deny all but the given roles * @param roles */ export const denyAllExcept = (...roles: string[]) => authorize({ allowedRoles: roles, deniedRoles: [EVERYONE], }); /** * Allow authenticated users */ export const allowAuthenticated = () => allow(AUTHENTICATED); /** * Deny unauthenticated users */ export const denyUnauthenticated = () => deny(UNAUTHENTICATED); /** * Skip authorization */ export const skip = () => authorize({skip: true}); } /** * Fetch authorization metadata stored by `@authorize` decorator. * * @param target Target object/class * @param methodName Target method */ export function getAuthorizationMetadata( target: object, methodName: string, ): AuthorizationMetadata | undefined { let targetClass: Function; if (typeof target === 'function') { targetClass = target; target = target.prototype; } else { targetClass = target.constructor; } const metadata = MetadataInspector.getMethodMetadata<AuthorizationMetadata>( AUTHORIZATION_METHOD_KEY, target, methodName, ); if (metadata) return metadata; // Check if the class level has `@authorize` return MetadataInspector.getClassMetadata<AuthorizationMetadata>( AUTHORIZATION_CLASS_KEY, targetClass, ); }