diff --git a/docs/site/Loopback-component-authorisation.md b/docs/site/Loopback-component-authorisation.md new file mode 100644 index 000000000000..4f604b36e96e --- /dev/null +++ b/docs/site/Loopback-component-authorisation.md @@ -0,0 +1,330 @@ +--- +lang: en +title: 'Authorisation' +keywords: LoopBack 4.0, LoopBack 4 +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Loopback-component-authorisation.html +--- + +## Overview + +In every web application, we need to have a way to identify +access rights of a user for any resource, which is known as **Authorisation**. +This is a minimalistic guide for creating such an implementation +using Loopback component. +This can be part of your main REST Application project or +can be created as a Loopback extension for reuse in multiple projects. +Latter is the better option for obvious reasons - reusability. + +## The requirement + +1. Every protected API end point needs to be restricted by specific permissions. +2. API allows access only if logged in user has permission as per end point restrictions. +3. API throws **403 Forbidden** error if logged in user do not have sufficient permissions. +4. Publicly accessible APIs must be accessible regardless of user permissions. +5. Every user has a set of permissions. These permissions may be associated via role attached to the user or directly to the user. + +## The implementation + +First, let's define the types needed for this. + +{% include code-caption.html content="/src/authorisation/types.ts" %} + +```ts +/** + * Authorise action method interface + */ +export interface AuthoriseFn { + (userPermissions?: string[]): Promise; +} + +/** + * Authorisation metadata interface for the method decorator + */ +export interface AuthorisationMetadata { + permissions: string[]; +} +``` + +We define two interfaces. + +- ***AuthoriseFn*** - This is going to be the interface for authorisation action business logic. +- ***AuthorisationMetadata*** - This interface represents the information to be passed via decorator for each individual controller method. + +Next, we create the binding keys for each type and accessor key for method decorator. + +{% include code-caption.html content="/src/authorisation/keys.ts" %} + +```ts +import {BindingKey} from '@loopback/context'; +import {MetadataAccessor} from '@loopback/metadata'; +import {AuthoriseFn, AuthorisationMetadata} from './types'; + +/** + * Binding keys used by this component. + */ +export namespace AuthorisatonBindings { + export const AUTHORISE_ACTION = BindingKey.create( + 'userAuthorisation.actions.authorise', + ); + + export const METADATA = BindingKey.create( + 'userAuthorisation.operationMetadata', + ); +} + +/** + * Metadata accessor key for authorise method decorator + */ +export const AUTHORISATION_METADATA_ACCESSOR = MetadataAccessor.create< + AuthorisationMetadata, + MethodDecorator +>('userAuthorisation.accessor.operationMetadata'); +``` + +Now, we need to create two providers + +- ***AuthorisationMetadataProvider*** - This will read the decorator metadata from the controller methods wherever the decorator is used. +- ***AuthoriseActionProvider*** - This holds the business logic for access validation of the user based upon access permissions allowed at method level via decorator metadata above. + +{% include code-caption.html content="/src/authorisation/providers/authorisation-metadata.provider.ts" %} + +```ts +import { + Constructor, + inject, + MetadataInspector, + Provider, +} from '@loopback/context'; +import {CoreBindings} from '@loopback/core'; + +import {AUTHORISATION_METADATA_ACCESSOR} from '../keys'; +import {AuthorisationMetadata} from '../types'; + +export class AuthorisationMetadataProvider + implements Provider { + constructor( + @inject(CoreBindings.CONTROLLER_CLASS) + private readonly controllerClass: Constructor<{}>, + @inject(CoreBindings.CONTROLLER_METHOD_NAME) + private readonly methodName: string, + ) {} + + value(): AuthorisationMetadata | undefined { + return getAuthoriseMetadata(this.controllerClass, this.methodName); + } +} + +export function getAuthoriseMetadata( + controllerClass: Constructor<{}>, + methodName: string, +): AuthorisationMetadata | undefined { + return MetadataInspector.getMethodMetadata( + AUTHORISATION_METADATA_ACCESSOR, + controllerClass.prototype, + methodName, + ); +} +``` + +{% include code-caption.html content="/src/authorisation/providers/authorisation-action.provider.ts" %} + +```ts +import {Getter, inject, Provider} from '@loopback/context'; + +import {AuthorisatonBindings} from '../keys'; +import {AuthorisationMetadata, AuthoriseFn} from '../types'; + +import {intersection} from 'lodash'; + +export class AuthoriseActionProvider implements Provider { + constructor( + @inject.getter(AuthorisatonBindings.METADATA) + private readonly getMetadata: Getter, + ) {} + + value(): AuthoriseFn { + return response => this.action(response); + } + + async action(userPermissions: string[]): Promise { + const metadata: AuthorisationMetadata = await this.getMetadata(); + if (!metadata) { + return false; + } else if (metadata.permissions.indexOf('*') === 0) { + // Return immediately with true, if allowed to all + // This is for publicly open routes only + return true; + } + + // Add your own business logic to fetch or + // manipulate with user permissions here + + const permissionsToCheck = metadata.permissions; + return intersection(userPermissions, permissionsToCheck).length > 0; + } +} +``` + +Next, we need to expose these providers via Component to be bound to the context. + +{% include code-caption.html content="/src/authorisation/component.ts" %} + +```ts +import {Component, ProviderMap} from '@loopback/core'; +import {AuthorisatonBindings} from './keys'; +import {AuthoriseActionProvider} from './providers/authorisation-action.provider'; +import {AuthorisationMetadataProvider} from './providers/authorisation-metadata.provider'; + +export class AuthorisationComponent implements Component { + providers?: ProviderMap; + + constructor() { + this.providers = { + [AuthorisatonBindings.AUTHORISE_ACTION.key]: AuthoriseActionProvider, + [AuthorisatonBindings.METADATA.key]: AuthorisationMetadataProvider, + }; + } +} +``` + +You can see that we have used the same binding keys which we created earlier. + +Now, its time to create our method decorator function. Here it is. We will be using the same metadata accessor key which we created earlier and the metadata interface for accessing the data in decorator. + +{% include code-caption.html content="/src/authorisation/decorators/authorise.decorator.ts" %} + +```ts +import {MethodDecoratorFactory} from '@loopback/core'; +import {AuthorisationMetadata} from '../types'; +import {AUTHORISATION_METADATA_ACCESSOR} from '../keys'; + +export function authorise(permissions: string[]) { + return MethodDecoratorFactory.createDecorator( + AUTHORISATION_METADATA_ACCESSOR, + { + permissions: permissions || [], + }, + ); +} +``` + +For error handling keys, lets create an enum. + +{% include code-caption.html content="/src/authorisation/error-keys.ts" %} + +```ts +export const enum AuthoriseErrorKeys { + NotAllowedAccess = 'Not Allowed Access', +} + +``` + +Finally, we put everything together in one index file. + +{% include code-caption.html content="/src/authorisation/index.ts" %} + +```ts +export * from './component'; +export * from './types'; +export * from './keys'; +export * from './error-keys'; +export * from './decorators/authorise.decorator'; +export * from './providers/authorisation-metadata.provider'; +export * from './providers/authorisation-action.provider'; +``` + +That is all for the authorisation component. You can create all of the above into a loopback extension as well. Everything remains the same. Refer to the [extension generator](./Extension-generator.md) guide for creating an extension. + +## Usage + +In order to use the above component into our REST API application, we have a few more steps to go. + +- Add component to application. + +{% include code-caption.html content="/src/application.ts" %} + +```ts +this.component(AuthenticationComponent); +``` + +- Add a step in custom sequence to check for authorisation whenever any end point is hit. + +{% include code-caption.html content="/src/sequence.ts" %} + +```ts +import {inject} from '@loopback/context'; +import { + FindRoute, + InvokeMethod, + ParseParams, + Reject, + RequestContext, + RestBindings, + Send, + SequenceHandler, + HttpErrors, +} from '@loopback/rest'; +import { + AuthorisatonBindings, + AuthoriseFn, + AuthoriseErrorKeys, +} from './authorisation'; + +const SequenceActions = RestBindings.SequenceActions; + +export class MySequence implements SequenceHandler { + constructor( + @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, + @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, + @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, + @inject(SequenceActions.SEND) public send: Send, + @inject(SequenceActions.REJECT) public reject: Reject, + @inject(AuthorisatonBindings.AUTHORISE_ACTION) + protected checkAuthorisation: AuthoriseFn, + ) {} + + async handle(context: RequestContext) { + try { + const {request, response} = context; + const route = this.findRoute(request); + const args = await this.parseParams(request, route); + // Do authentication of the user and fetch user permissions below + const permissions: string[] = []; + // This is main line added to sequence + // where we are invoking the authorise action function to check for access + const isAccessAllowed: boolean = await this.checkAuthorisation( + permissions, + ); + if (!isAccessAllowed) { + throw new HttpErrors.Forbidden(AuthoriseErrorKeys.NotAllowedAccess); + } + const result = await this.invoke(route, args); + this.send(response, result); + } catch (err) { + this.reject(context, err); + } + } +} +``` + +Now we can add access permission keys to the controller methods using authorise decorator as below. + +```ts +@authorise(['CanCreateRole']) +@post(rolesPath, { + responses: { + [STATUS_CODE.OK]: { + description: 'Role model instance', + content: { + [CONTENT_TYPE.JSON]: {schema: {'x-ts-type': Role}}, + }, + }, + }, +}) +async create(@requestBody() role: Role): Promise { + return await this.roleRepository.create(role); +} +``` + +This endpoint will only be accessible if logged in user has permission 'CanCreateRole'. diff --git a/docs/site/Loopback-component.md b/docs/site/Loopback-component.md new file mode 100644 index 000000000000..eed6c46601f9 --- /dev/null +++ b/docs/site/Loopback-component.md @@ -0,0 +1,11 @@ +--- +lang: en +title: 'Loopback Component' +keywords: LoopBack 4.0, LoopBack 4 +sidebar: lb4_sidebar +permalink: /doc/en/lb4/Loopback-component.html +--- + +This section details a few useful and essential Loopback components. + +- [**Authorisation**](./Loopback-component-authorisation.md) diff --git a/docs/site/sidebars/lb4_sidebar.yml b/docs/site/sidebars/lb4_sidebar.yml index 1cef0ace6e8f..da26889103c2 100644 --- a/docs/site/sidebars/lb4_sidebar.yml +++ b/docs/site/sidebars/lb4_sidebar.yml @@ -357,6 +357,15 @@ children: url: Testing-your-extension.html output: 'web, pdf' +- title: 'Loopback Component' + url: Loopback-component.html + output: 'web, pdf' + children: + + - title: 'Authorisation' + url: Loopback-component-authorisation.html + output: 'web, pdf' + - title: 'Crafting LoopBack 4' url: Crafting-LoopBack-4.html output: 'web, pdf'