Role permissions system for AdonisJS V6+
Checkout other AdonisJS packages
![license](https://poser.pugx.org/silber/bouncer/license.svg)
Table of Contents
Click to expand
Introduction
AdonisJs Acl is an elegant and powerful package for managing roles and permissions in any AdonisJs app. With an expressive and fluent syntax, it stays out of your way as much as possible: use it when you want, ignore it when you don't.
For a quick, glanceable list of Acl's features, check out the cheat sheet
Once installed, you can simply tell the Acl what you want to allow:
import {Acl} from '@holoyan/adonisjs-permissions'
await Acl.model(user).allow('edit');
const post = await Post.first()
await Acl.model(user).allow('delete', post);
await user.allow('delete', post)
To be able to use the full power of Acl, you should have a clear understanding of how it is structured and how it works. That's why the documentation will be divided into two parts: Basic usage and Advanced usage. For most applications, Basic Usage will be enough.
Installation
npm i @holoyan/adonisjs-permissions
Next publish config files
node ace configure @holoyan/adonisjs-permissions
this will create permissions.ts
file in configs
directory, migration file in the database/migrations
directory
Next run migration
node ace migration:run
Configuration
All models that will interact with Acl
MUST use the @MorphMap('ALIAS_FOR_CLASS')
decorator and implement the AclModelInterface
contract.
Example.
import { BaseModel, column } from '@adonisjs/lucid/orm'
import { MorphMap } from '@holoyan/adonisjs-permissions'
import { AclModelInterface } from '@holoyan/adonisjs-permissions/types'
@MorphMap('users')
export default class User extends BaseModel implements AclModelInterface {
getModelId(): number {
return this.id
}
}
@MorphMap('admins')
export default class Admin extends BaseModel implements AclModelInterface {
getModelId(): number {
return this.id
}
}
@MorphMap('posts')
export default class Post extends BaseModel implements AclModelInterface {
getModelId(): number {
return this.id
}
}
Release Notes
Version: >= v1.0.2
- Update: Mixin getModelId method return type
Mixins
If you want to be able to call Acl
methods on a User
model then consider using hasPermissions
mixin
import { BaseModel, column } from '@adonisjs/lucid/orm'
import { MorphMap } from '@holoyan/adonisjs-permissions'
import { AclModelInterface } from '@holoyan/adonisjs-permissions/types'
import { hasPermissions } from '@holoyan/adonisjs-permissions'
import { compose } from '@adonisjs/core/helpers'
@MorphMap('users')
export default class User extends compose(BaseModel, hasPermissions()) implements AclModelInterface {
getModelId(): number {
return this.id
}
}
const user = await User.first()
const roles = await user.roles()
await user.allow('edit')
Support
Database Support
Currently supported databases: postgres
, mysql
, mssql
UUID support
from v0.7.11
UUID support available, all you need to do change uuidSupport
value to true
in config/permissions.ts
file, then run the migration and don't forget to change return type for the getModelId()
method
check todo list for more details
Basic Usage
On this section, we will explore basic role permission methods.
Creating roles and permissions
Let's manually create create,update,read,delete
permissions, as well as admin,manager
roles
Look also Creating permissions on a fly section
import { Permission } from '@holoyan/adonisjs-permissions'
import { Role } from '@holoyan/adonisjs-permissions'
import {Acl} from "@holoyan/adonisjs-permissions";
const create = await Permission.create({
slug:'create',
title:'Create some resource',
})
const update = await Permission.create({
slug:'update',
})
const read = await Acl.permission().create({
slug: 'read',
})
const delete = await Acl.permission().create({
slug: 'delete',
})
const admin = await Role.create({
slug:'admin',
title:'Cool title for Admin',
})
const manager = await Acl.role().create({
slug: 'manager',
})
The next step is to assign permissions to the roles
Assigning permissions to the roles (Globally)
Now that we have created roles and permissions, let's assign them.
import {Acl} from "@holoyan/adonisjs-permissions";
await Acl.role(admin).assign('create')
await Acl.role(admin).allow('update')
await Acl.role(admin).giveAll(['read', 'delete'])
Creating permissions on a fly
In case you are assigning a permission that is not already available, Acl
will create new permission behind the scenes and assign them.
await Acl.role(admin).allow('uploadFile')
Assigning permissions and roles to the users (models)
Let's see in examples how to assign roles and permissions to the users
import {Acl} from "@holoyan/adonisjs-permissions";
import User from "#models/user";
const user1 = await User.query().where(condition1).first()
await Acl.model(user1).assignRole('manager')
const user2 = await User.query().where(condition2).first()
await Acl.model(user2).assign(admin)
Or we can give permissions directly to users without having any role
import {Acl} from "@holoyan/adonisjs-permissions";
Acl.model(user1).assignDirectPermission('upload-file-slug')
Acl.model(user1).allow('permissionSlug')
Multi-model support
We are not limited to using only the User model. If you have a multi-auth system like User and Admin, you are free to use both of them with Acl.
await Acl.model(user).assignRole('manager')
await Acl.model(admin).assignRole('admin')
Getting roles and permissions
In this section we will see how to get roles and permissions for a model and vice versa
Getting all roles for a user
const roles = await Acl.model(user).roles()
Getting all permissions for a role
const roles = await Acl.role(role).permissions()
Getting all permissions for a user (model)
const roles = await Acl.model(user).permissions()
Getting models from the permission
const models = await Acl.permission(permission).models()
this will return array of ModelPermission
which will contain modelType,modelId
attributes, where modelType
is alias which you had specified in morphMap decorator, modelId
is the value of column, you've specified inside getModelId method.
Most of the time, you will have only one model (User). It's better to use the modelsFor()
method to get concrete models.
const models = await Acl.permission(permission).modelsFor(User)
this will return array of User models
Getting models for a role
const models = await Acl.role(permission).models()
Or if you want to get for a specific model
const models = await Acl.role(permission).modelsFor(User)
Checking for a role
To check if user has role
await Acl.model(user).hasRole('admin')
you can pass list of roles
await Acl.model(user).hasAllRoles('admin', 'manager')
To check if a user has any of the roles
await Acl.model(user).hasAnyRole('admin', 'manager')
Checking for a permission
Check if user has permission
await Acl.model(user).hasPermission('update')
await Acl.model(user).can('update')
await user.hasPermission('update')
To check array of permissions
await Acl.model(user).hasAllPermissions(['update', 'delete'])
await Acl.model(user).canAll(['update', 'delete'])
to check if user has any of the permission
await Acl.model(user).hasAnyPermission(['update', 'delete'])
await Acl.model(user).canAny(['update', 'delete'])
Same applies for the roles
await Acl.role(role).hasPermission('update')
await Acl.role(role).hasAllPermissions(['update', 'read'])
await Acl.role(role).hasAnyPermission(['update', 'read'])
Middleware
You are free to do your check anywhere, for example we can create named middleware and do checking
don't forget to register your middleware inside kernel.ts
import { middleware } from '#start/kernel'
router.get('/posts/:id', [ProductsController, 'show']).use(middleware.acl({permission: 'edit'}))
export default class AclMiddleware {
async handle(ctx: HttpContext, next: NextFn, options: { permission: string }) {
const hasPermission = await ctx.auth.user.hasPermission(options.permission)
if(!hasPermission) {
ctx.response.abort({ message: 'Cannot edit post' }, 403)
}
const output = await next()
return output
}
}
Removing (revoking/detach) roles and permissions from the model
To revoke(detach) role from the user we can use revoke
method
await Acl.model(user).revokeRole('admin')
await Acl.model(user).revokeAllRoles(['admin', 'manager'])
await Acl.model(user).flushRoles()
Revoking permissions from the user
await Acl.model(user).revokePermission('update')
await Acl.model(user).revoke('delete')
await Acl.model(user).revokeAllPermissions(['update', 'delete'])
await Acl.model(user).revokeAll(['update', 'delete'])
await Acl.model(user).flushPermissions()
await Acl.model(user).flush()
Removing permissions from the role
await Acl.role(role).revokePermission('update')
await Acl.role(role).revoke('update')
await Acl.role(role).revokeAllPermissions(['update', 'delete'])
await Acl.role(role).flushPermissions()
Deleting roles and permissions (Important!)
Recommended! use Acl to delete roles and permissions instead of directly making queries on the Role,Permission model, under the hood Acl does some checking
await Acl.role().delete('admin')
await Acl.permission().delete('edit')
To see in dept usage of this methods check next section
Digging deeper
In the previous section, we looked at basic examples and usage. Most of the time, basic usage will probably be enough for your project. However, there is much more we can do with Acl
.
Restricting a permission on a resource
Sometimes you might want to restrict a permission on a specific model(resource). Simply pass the model as a second argument:
import Product from "#models/product";
await Acl.model(user).allow('edit', Product)
Important! - Don't forget to add MorphMap
decorator and AclModelInterface
on Product class
@MorphMap('products')
export default class Product extends BaseModel implements AclModelInterface {
getModelId(): number {
return this.id
}
}
Warning: All models which interact with Acl MUST use MorphMap decorator and implement AclModelInterface
Then we can make checking again
import Product from "#models/product";
import Post from "#models/post";
const productModel1 = Product.find(id1)
const productModel50 = Product.find(id50)
const postModel = Post.find(postId)
await Acl.model(user).hasPermission('edit', productModel1)
await Acl.model(user).hasPermission('edit', productModel50)
await Acl.model(user).hasPermission('edit', Product)
await Acl.model(user).hasPermission('edit', postModel)
await Acl.model(user).hasPermission('edit', Post)
await Acl.model(user).hasPermission('edit')
await Acl.model(user).containsPermission('edit')
Check ContainsPermission vs hasPermission section for more details
We can restrict even more, and give permission to the specific model
import Product from "#models/product";
const product1 = Product.find(1)
await Acl.model(user).assignDirectPermission('edit', product1)
const product2 = Product.find(2)
await Acl.model(user).hasPermission('edit', product1)
await Acl.model(user).hasPermission('edit', product2)
await Acl.model(user).hasPermission('edit', Product)
await Acl.model(user).hasPermission('edit')
await Acl.model(user).containsPermission('edit')
This will behave the same way if you assign the permission through the role instead of directly
const product1 = Product.find(1)
await Acl.role(admin).allow('edit', product1)
const user = await User.first()
await Acl.model(user).assignRole(role)
const product2 = Product.find(2)
await Acl.model(user).hasPermission('edit', product1)
await Acl.model(user).hasPermission('edit', product2)
await Acl.model(user).hasPermission('edit', Product)
await Acl.model(user).hasPermission('edit')
await Acl.model(user).containsPermission('edit')
Forbidding permissions
Let's imagine a situation where manager
role has create,update,read,delete
permissions.
All your users have manager
role but there are small amount of users you want to forbid delete
action.
Good news!, we can do that
await Acl.role(manager).giveAll(['create','update','read','delete'])
await Acl.model(user1).assign('manager')
await Acl.model(user3).assign('manager')
await Acl.model(user3).forbid('delete')
await Acl.model(user1).hasRole('manager')
await Acl.model(user1).can('delete')
await Acl.model(user3).hasRole('manager')
await Acl.model(user3).can('delete')
await Acl.model(user3).contains('delete')
Forbidding permissions on a resource
You can also forbid single action on a resource
const post = Post.find(id1)
await Acl.model(user3).forbid('delete', post)
Checking for forbidden permissions
In previous section we saw how to forbid certain permissions for the model, even if user has that permission through the role, now we will look how to check if permission is forbidden or not
await Acl.model(user3).assignRole('manager')
await Acl.model(user3).forbid('delete')
await Acl.model(user3).forbidden('delete')
const post1 = Post.find(id1)
await Acl.model(user).allow('edit', Post)
await Acl.model(user).forbid('edit', post1)
await Acl.model(user).forbidden('edit', post1)
const post7 = Post.find(id7)
await Acl.model(user).forbidden('edit', post7)
Unforbidding the permissions
await Acl.model(user3).assignRole('manager')
await Acl.model(user3).forbid('delete')
await Acl.model(user3).forbidden('delete')
await Acl.model(user3).can('delete')
await Acl.model(user3).can('delete')
await Acl.model(user3).unforbid('delete')
await Acl.model(user3).forbidden('delete')
await Acl.model(user3).can('delete')
Same behaviour applies with roles
await Acl.role(role).assignRole('manager')
await Acl.role(role).forbid('delete')
await Acl.role(role).forbidden('delete')
await Acl.role(role).hasPermission('delete')
await Acl.role(role).contains('delete')
Global vs resource permissions (Important!)
Important! Actions performed globally will affect on a resource models
It is very important to understand difference between global and resource permissions and their scope.
Look at this way, if there is no entity
model, then actions will be performed globally, otherwise on resource
|--------------Global--------------|
| |
| |------Class level------| |
| | | |
| | |--Model level--| | |
| | | | | |
| | | | | |
| | | | | |
| | |---------------| | |
| | | |
| |-----------------------| |
| |
|----------------------------------|
import {Acl} from "@holoyan/adonisjs-permissions";
import Post from "#models/post";
await Acl.model(admin).allow('create');
await Acl.model(admin).allow('edit');
await Acl.model(admin).allow('view');
await Acl.model(manager).allow('create', Post)
const myPost = await Post.find(id)
await Acl.model(client).allow('view', myPost)
await Acl.model(admin).hasPermission('create')
await Acl.model(admin).hasPermission('create', Post)
await Acl.model(admin).hasPermission('create', myPost)
await Acl.model(manager).hasPermission('create')
await Acl.model(manager).hasPermission('create', Post)
await Acl.model(manager).hasPermission('create', myPost)
await Acl.model(manager).hasPermission('create', myOtherPost)
await Acl.model(client).hasPermission('create')
await Acl.model(client).hasPermission('create', Post)
await Acl.model(client).hasPermission('create', myPost)
await Acl.model(client).hasPermission('create', myOtherPost)
Same is true when using forbidden
action
await Acl.model(manager).allow('edit', Post)
await Acl.model(manager).forbid('edit', myPost)
await Acl.model(client).hasPermission('edit', Post)
await Acl.model(client).hasPermission('edit', myPost)
await Acl.model(client).hasPermission('edit', myOtherPost)
containsPermission v hasPermission
As you've already seen there are difference between containsPermission
and hasPermission
methods. containsPermission()
method will return true
if user has that permission, it doesn't matter if it's global, on resource or forbidden.
contains()
method is alias for containsPermission()
Lets in example see this difference
await Acl.model(user).allow('edit');
await Acl.model(user).containsPermission('edit')
await Acl.model(user).allow('delete', Post);
await Acl.model(user).containsPermission('delete')
await Acl.model(user).forbid('read');
await Acl.model(user).containsPermission('read')
Scopes or Multi-tenancy
Acl fully supports multi-tenant apps, allowing you to seamlessly integrate roles and permissions for all tenants within the same app.
await Acl.model(user).on(user.project_id).allow('edit')
await Acl.model(user).on(user.project_id).allow('delete')
await Acl.model(user).on(user.project_id).hasPermission('edit')
await Acl.model(user).on(user.project_id).hasPermission('delete')
await Acl.model(user).hasPermission('edit')
The Scope middleware
Acl
has built-in middleware to make scope checking easier.
This middleware is where you tell Acl
which tenant to use for the current request. For example, assuming your users all have an account_id attribute, this is what your middleware would look like:
export default class AclScopeMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
const scope = new Scope()
scope.set(auth.user.account_id)
ctx.acl = new AclManager(true).scope(scope)
const output = await next()
return output
}
}
export default class PostController {
async show({acl}: HttpContext){
await acl.model().hasPermission('view')
}
}
Important! If you are using AclScopeMiddleware
and want to have scope functional per-request then use acl
from the ctx
instead of using global Acl
(NOTE: lower case) object, otherwise changes inside AclScopeMiddleware
will not make effect
Let's see in example
export default class AclScopeMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
const scope = new Scope()
scope.set('dashboard')
ctx.acl = new AclManager().scope(scope)
const output = await next()
return output
}
}
import {Acl} from "@holoyan/adonisjs-permissions";
export default class PostController {
async show({acl}: HttpContext){
const scope = acl.getScope()
console.log(scope.get())
console.log(Acl.getScope())
acl.scope('dashboard_1')
console.log(acl.getScope())
Acl.scope('dashboard_2')
await Acl.model(user).on(8).permissions()
}
}
Important! By default, you can't update scope on global object, it will throw an error, because by updating global Acl
scope it will rewrite for an entire application and this will lead to unexpected behavior BUT if you still want to do that then you can do it by passing forceUpdate
param
import {Acl} from "@holoyan/adonisjs-permissions";
Acl.scope('dashboard_2')
let forceUpdate = true;
Acl.scope('dashboard_2', forceUpdate)
Acl.scope('scope_1', forceUpdate)
Acl.scope('scope_2', forceUpdate)
Default scope (Tenant)
Default Scope value is equal to 'default'
Transactions
In case you want to use Acl
inside the transaction then you can pass options
directly to query method.
import {Acl} from '@holoyan/adonisjs-permissions'
const trx = await db.transaction()
await Acl.model(user).withQueryOptions({ client: trx }).allow('delete')
await trx.commit()
Cheat sheet
Coming soon
TODO
Test
npm run test
Version Map
AdonisJS Lucid version | Package version |
---|
v20.x | 0.8.x |
v21.x | 1.x |
License
MIT