Access controlling is one of the most important parts of every application, there are many kinds of models for access controlling like MAC (Mandatory Access Control), DAC (Discretionary Access Control), RBAC (Role Based Access Control) and etc (for more information about access models see this link)
loopback-component-authorization is a powerful and generic implementation of HRBAC (Hierarchical Role Based Access Control) access model
npm i --save loopback-component-authorizationFollow these steps to add authorization extension to your loopback4 application
- Optional: Define
User,Role,Permissionmodels - Optional: Define
User,Role,Permissionrepositories - Optional: Define
Permissionsclass - Define your
dataSource - Add
AuthorizationMixinto your application - Add
AuthorizationActionProviderto your custom http sequence handler - Use
GetUserPermissionsProviderto find user permissions whensigninandsavethe permissions in user's session
Now, let's try these
Use the command lb4 model for simplifing your Entity model creation, then just replace Entity class with User, Role or Permission as the parent class
See this example:
import{model,property}from"@loopback/repository";import{UserasUserModel,UserRelationsasUserModelRelations,}from"loopback-component-authorization";
@model({settings: {}})exportclassUserextendsUserModel{
@property({type: "string",})name?: string;
@property({type: "number",})age?: number;constructor(data?: Partial<User>){super(data);}}exportinterfaceUserRelationsextendsUserModelRelations{// describe navigational properties here}exporttypeUserWithRelations=User&UserRelations;Use the command lb4 repository for simplifing your Repository creation, then replace DefaultCrudRepository class with UserRepositoryMixin()(), RoleRepositoryMixin()() or PermissionRepositoryMixin()() as the parent class, then bind them
See this example:
import{User,UserRelations}from"~/models";import{MySqlDataSource}from"~/datasources";import{inject}from"@loopback/core";import{UserRepositoryMixin}from"loopback-component-authorization";exportclassUserRepositoryextendsUserRepositoryMixin<User,UserRelations>()(){constructor(@inject("datasources.MySQL")dataSource: MySqlDataSource){super(User,dataSource);}}Create a class contaning your permissions
See this example:
import{PermissionsList}from"loopback-component-authorization";exportclassMyPermissionsextendsPermissionsList{/** Files */FILES_READ="Read files";FILES_WRITE="Write files";/** Roles */ROLES_READ="Read roles";ROLES_WRITE="Write roles";/** Users */USERS_READ="Read users";USERS_WRITE="Write users";}Bind your dataSource you want to use for authorization tables using bindRelationalDataSource
See this example:
import{bindRelationalDataSource}from"loopback-component-authorization";
@bindRelationalDataSource()exportclassMySqlDataSourceextendsjuggler.DataSource{staticdataSourceName="MySQL";constructor(
@inject("datasources.config.MySQL",{optional: true})dsConfig: object=config){super(dsConfig);}}Edit your application.ts file, add your permissions class to authorize mixin:
import{AuthorizationApplication,AuthorizationApplicationConfig,}from"loopback-component-authorization";import{MyPermissions}from"~/permissions.ts";import{User,Role,Permission,UserRole,RolePermission}from"~/models";exportclassTestApplicationextendsAuthorizationMixin(BootMixin(ServiceMixin(RepositoryMixin(RestApplication)))){constructor(options: ApplicationConfig={}){super(options);// ...// Config authorization mixinthis.authorizationConfigs={permissions: MyPermissions,userModel: User,roleModel: Role,permissionModel: Permission,userRoleModel: UserRole,rolePermissionModel: RolePermission,};}}Then edit your sequence.ts file:
import{AuthorizationBindings,AuthorizeFn}from"loopback-component-authorization";import{MyPermissions}from"~/permissions.ts";constSequenceActions=RestBindings.SequenceActions;exportclassMySequenceimplementsSequenceHandler{constructor(
@inject(SequenceActions.FIND_ROUTE)protectedfindRoute: FindRoute,
@inject(SequenceActions.PARSE_PARAMS)protectedparseParams: ParseParams,// add the AuthorizeActionProvider
@inject(AuthorizationBindings.AUTHORIZE_ACTION)protectedauthorize: AuthorizeFn<MyPermissions>,
@inject(SequenceActions.INVOKE_METHOD)protectedinvoke: InvokeMethod,
@inject(SequenceActions.SEND)publicsend: Send,
@inject(SequenceActions.REJECT)publicreject: Reject){}asynchandle(context: RequestContext){try{const{ request, response }=context;constroute=this.findRoute(request);constargs=awaitthis.parseParams(request,route);// use `@loopback/authentication` moduleconstuserSession=awaitthis.authenticate(...);// check user permissions/* * User permissions, will passed to this method, * they are loaded, before using `getUserPermissions(id)` * action on `sign-in` step of your application, * then you must save them in the client's session * and at the end, you must pass them to this method */if(userSession){awaitthis.authorize(userSession.permissions,args);}constresult=awaitthis.invoke(route,args);this.send(response,result);}catch(err){this.reject(context,err);}}}If the client doesn't have correct permissions, will see
Http Forbidden (403)error code
At the final step you must get user permissions using getUserPermissions(id) provider and save it in the user's session or token
import{AuthorizationBindings,GetUserPermissionsFn,authorize,}from"loopback-component-authorization";import{inject}from"@loopback/context";import{MyPermissions}from"~/permissions.ts";exportclassSignInController{constructor(
@inject(AuthorizationBindings.GET_USER_PERMISSIONS_ACTION)protectedgetUserPermissions: GetUserPermissionsFn<MyPermissions>){}// ...
@post("/signin",{responses: {"200": {//...},},})asyncsignIn(...args): Promise<Session>{// authentication// ...// authorizationconstpermissions=awaitthis.getUserPermissions(id);returnthis.sessionRepository.create(newSession({//...permissions: permissions,}));}// ...}Now authorization extension is fully added and you can protect your endpoints using @authorize decorator
You can feel the power of loopback-component-authorization is in this step, by using And types, Or types, Async Authorizers
// ...import{MyPermissions}from"~/permissions.ts";
@authenticate(...)
@authorize<MyPermissions>({and: ["CREATE_USER","DELETE_USER"]})asynceditUser(...args): Promise<any>{...}// ...This decorator accepts an object of type And or Or or StringPermissionKey or AsyncPermissionKey
your can define any logical combinations of your Permissions to control access much better
Example:
{
and: [{key: "A"},{key: "B"},{key: "C",not: true},{or: [{key: "D"},{key: "E"}]},];}In some special cases we need to check some other permissions or conditions such as querying in database or etc, for these cases we can use AsyncAuthorizer for running an async function of type (invocationContext) => Promise<boolean>
Example:
{or: [{and: [{key: "A"},{key: "B"}]},{key: asyncinvocationContext=>{letresult=awaitcontroller.userRepository.find({...});if(result.length>0){returntrue;}returnfalse;}}]}You can add or remove users, roles and permissions using your repositories
Users, Roles, Permissions has many-to-many relations, using, DefaultUserRoleRepository, DefaultRolePermissionRepository you can add some users to roles or assign permissions to roles
Example:
import{AuthorizationBindings,GetUserPermissionsFn,authorize}from"loopback-component-authorization";import{inject}from"@loopback/context";exportclassUserControllerController{constructor(
@inject(AuthorizationBindings.USER_REPOSITORY)publicuserRepository: DefaultUserRepository){}
@post("/users/...",{responses: {"200": {
...
}}})asyncadd(...args): Promise<any>{// add user to roleawaitthis.userRepository.userRoles("userId").create(newUserRoleModel({userId: "userId",roleId: "roleId"}));}}See this example
This project is licensed under the MIT license.
Copyright (c) KoLiBer (koliberr136a1@gmail.com)