loopback-authorization-extension v11.1.0
loopback-authorization-extension
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-authorization-extension is a powerful and generic implementation of HRBAC (Hierarchical Role Based Access Control) access model
Installation
npm i --save loopback-authorization-extensionUsage
Follow 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
Step 1 (Define Models)
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 {
User as UserModel,
UserRelations as UserModelRelations
} from "loopback-authorization-extension";
@model({ settings: {} })
export class User extends UserModel {
@property({
type: "string"
})
name?: string;
@property({
type: "number"
})
age?: number;
constructor(data?: Partial<User>) {
super(data);
}
}
export interface UserRelations extends UserModelRelations {
// describe navigational properties here
}
export type UserWithRelations = User & UserRelations;Step 2 (Define Repositories)
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-authorization-extension";
export class UserRepository extends UserRepositoryMixin<
User,
UserRelations
>()() {
constructor(@inject("datasources.MySQL") dataSource: MySqlDataSource) {
super(User, dataSource);
}
}Step 3 (Define Permissions)
Create a class contaning your permissions
See this example:
import { PermissionsList } from "loopback-authorization-extension";
export class MyPermissions extends PermissionsList {
/** 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";
}Step 4 (Define DataSource)
Bind your dataSource you want to use for authorization tables using bindRelationalDataSource
See this example:
import { bindRelationalDataSource } from "loopback-authorization-extension";
@bindRelationalDataSource()
export class MySqlDataSource extends juggler.DataSource {
static dataSourceName = "MySQL";
constructor(
@inject("datasources.config.MySQL", { optional: true })
dsConfig: object = config
) {
super(dsConfig);
}
}Step 5 (Application Mixin)
Edit your application.ts file, add your permissions class to authorize mixin:
import {
AuthorizationApplication,
AuthorizationApplicationConfig
} from "loopback-authorization-extension";
import { MyPermissions } from "~/permissions.ts";
import { User, Role, Permission, UserRole, RolePermission } from "~/models";
export class TestApplication extends AuthorizationMixin(
BootMixin(ServiceMixin(RepositoryMixin(RestApplication)))
) {
constructor(options: ApplicationConfig = {}) {
super(options);
// ...
// Config authorization mixin
this.authorizationConfigs = {
permissions: MyPermissions,
userModel: User,
roleModel: Role,
permissionModel: Permission,
userRoleModel: UserRole,
rolePermissionModel: RolePermission
};
}
}Step 6
Then edit your sequence.ts file:
import {
AuthorizationBindings,
AuthorizeFn
} from "loopback-authorization-extension";
import { MyPermissions } from "~/permissions.ts";
const SequenceActions = RestBindings.SequenceActions;
export class MySequence implements SequenceHandler {
constructor(
@inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
@inject(SequenceActions.PARSE_PARAMS)
protected parseParams: ParseParams,
// add the AuthorizeActionProvider
@inject(AuthorizationBindings.AUTHORIZE_ACTION)
protected authorize: AuthorizeFn<MyPermissions>,
@inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
@inject(SequenceActions.SEND) public send: Send,
@inject(SequenceActions.REJECT) public reject: Reject
) {}
async handle(context: RequestContext) {
try {
const { request, response } = context;
const route = this.findRoute(request);
const args = await this.parseParams(request, route);
// use `@loopback/authentication` module
const userSession = await this.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) {
await this.authorize(userSession.permissions, args);
}
const result = await this.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
Step 7
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-authorization-extension";
import { inject } from "@loopback/context";
import { MyPermissions } from "~/permissions.ts";
export class SignInController {
constructor(
@inject(AuthorizationBindings.GET_USER_PERMISSIONS_ACTION)
protected getUserPermissions: GetUserPermissionsFn<MyPermissions>
) {}
// ...
@post("/signin", {
responses: {
"200": {
//...
}
}
})
async signIn(...args): Promise<Session> {
// authentication
// ...
// authorization
const permissions = await this.getUserPermissions(id);
return this.sessionRepository.create(
new Session({
//...
permissions: permissions
})
);
}
// ...
}Controllers
Now authorization extension is fully added and you can protect your endpoints using @authorize decorator
You can feel the power of loopback-authorization-extension 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"]
})
async editUser(...args): Promise<any> {...}
// ...More about @authorize
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" }] }
];
}AsyncAuthorizer
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: async invocationContext => {
let result = await controller.userRepository.find({...});
if (result.length > 0) {
return true;
}
return false;
}
}
]
}How to define Users, Roles, Permissions
You can add or remove users, roles and permissions using your repositories
Many-To-Many relations
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-authorization-extension";
import { inject } from "@loopback/context";
export class UserControllerController {
constructor(
@inject(AuthorizationBindings.USER_REPOSITORY)
public userRepository: DefaultUserRepository
) {}
@post("/users/...", {
responses: {
"200": {
...
}
}
})
async add(...args): Promise<any> {
// add user to role
await this.userRepository.userRoles("userId").create(new UserRoleModel({
userId: "userId",
roleId: "roleId"
}));
}
}Complete Example
See this example
Contributions
License
This project is licensed under the MIT license.
Copyright (c) KoLiBer (koliberr136a1@gmail.com)
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago