@holoyan/adonisjs-permissions
v1.0.3
Published
Adonisjs roles and permissions system
Downloads
786
Readme
Role permissions system for AdonisJS V6+
Checkout other AdonisJS packages
Table of Contents
- Introduction
- Installation
- Configuration
- Mixins
- Support
- Basic Usage
- Creating roles and permissions
- Assigning permissions to the roles (Globally)
- Creating permission on a fly
- Assigning permissions and roles to the users (models)
- Multi-model support
- Getting all roles for a user
- Getting all permissions for a role
- Getting all permissions for a user (model)
- Getting users (models) for a permission
- Getting models for a role
- Checking for a permission
- Middleware
- Removing (revoking) roles and permissions from the model
- Digging deeper
- Cheat sheet
- Todo
- Test
- Version Map
- License
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'
// Give a user the permission to edit
await Acl.model(user).allow('edit');
// Behind the scenes Acl will create 'edit' permission and assign to the user if not available
// You can also grant a permission only to a specific model
const post = await Post.first()
await Acl.model(user).allow('delete', post);
// or
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
}
// other code goes here
}
@MorphMap('admins')
export default class Admin extends BaseModel implements AclModelInterface {
getModelId(): number {
return this.id
}
// other code goes here
}
@MorphMap('posts')
export default class Post extends BaseModel implements AclModelInterface {
getModelId(): number { // use `string` return type if your model has uuid/string primary keys
return this.id
}
// other code goes here
}
Release Notes
Version: >= v1.0.3
- Update: UUID version to ^10.0.0
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 mixin
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
}
// other code goes here
}
// then all methods are available on the user
const user = await User.first()
const roles = await user.roles() // get user roles
await user.allow('edit') // give edit permission
// and so on...
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";
// create permissions
const create = await Permission.create({
slug:'create',
title:'Create some resource', // optional
})
const update = await Permission.create({
slug:'update',
})
// or create using Acl (recomended way)
const read = await Acl.permission().create({
slug: 'read',
})
const delete = await Acl.permission().create({
slug: 'delete',
})
// create roles
const admin = await Role.create({
slug:'admin',
title:'Cool title for Admin', // optional
})
// or create using Acl (recomended way)
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')
// alternatively you can use allow(), give() method, as they are identical
await Acl.role(admin).allow('update')
await Acl.role(admin).giveAll(['read', 'delete'])
// alternatively you use giveAll(), assigneAll(), allowAll() for bulk assign
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.
// uploadFile permission not available
await Acl.role(admin).allow('uploadFile')
// 'uploadFile' permission created and assigned
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()
// give manager role to the user1
await Acl.model(user1).assignRole('manager')
// or just use assign() method, they are alias
// await Acl.model(user1).assign('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";
// create and assign a new permission
Acl.model(user1).assignDirectPermission('upload-file-slug')
// or use allow() method
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') // :boolean
you can pass list of roles
// returns true only if user has all 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')
// it will return true if the user has at least one role.
Checking for a permission
Check if user has permission
await Acl.model(user).hasPermission('update')
// or
await Acl.model(user).can('update') // alias for hasPermission() method
// or simply call
await user.hasPermission('update')
To check array of permissions
// returns true only if user has all permissions
await Acl.model(user).hasAllPermissions(['update', 'delete'])
// or
await Acl.model(user).canAll(['update', 'delete']) // alias for hasAllPermissions() method
// await user.canAll(['update', 'delete'])
to check if user has any of the permission
// returns true only if user has all permissions
await Acl.model(user).hasAnyPermission(['update', 'delete'])
// or
await Acl.model(user).canAny(['update', 'delete']) // alias for hasAnyPermission() method
// will return true if user has at least one permission
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
// routes.ts
import { middleware } from '#start/kernel'
// routes.ts
router.get('/posts/:id', [ProductsController, 'show']).use(middleware.acl({permission: 'edit'}))
// acl_middleware.ts
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'])
// will remove all assigned roles
await Acl.model(user).flushRoles()
Revoking permissions from the user
await Acl.model(user).revokePermission('update')
await Acl.model(user).revoke('delete') // alias for revokePermission()
// await Acl.model(user).hasPermission('update') will return false
await Acl.model(user).revokeAllPermissions(['update', 'delete'])
await Acl.model(user).revokeAll(['update', 'delete']) // alias for revokeAllPermissions()
// revoke all assigned permissions
await Acl.model(user).flushPermissions()
// revokes all roles and permissions for a user
await Acl.model(user).flush()
Removing permissions from the role
await Acl.role(role).revokePermission('update')
// or
await Acl.role(role).revoke('update') // alias for revokePermission
await Acl.role(role).revokeAllPermissions(['update', 'delete'])
// alias revokeAll(['update', 'delete'])
// remove all assigned permissions
await Acl.role(role).flushPermissions()
// alias flush()
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 andAclModelInterface
on Product class
@MorphMap('products')
export default class Product extends BaseModel implements AclModelInterface {
getModelId(): number {
return this.id
}
// other code
}
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";
// await Acl.model(user).allow('edit', Product)
const productModel1 = Product.find(id1)
const productModel50 = Product.find(id50)
const postModel = Post.find(postId)
await Acl.model(user).hasPermission('edit', productModel1) // true
await Acl.model(user).hasPermission('edit', productModel50) // true
// ... for all Product model instances it will return true
await Acl.model(user).hasPermission('edit', Product) // true
await Acl.model(user).hasPermission('edit', postModel) // false
await Acl.model(user).hasPermission('edit', Post) // false
await Acl.model(user).hasPermission('edit') // false
// containsPermission() method will tell if user has 'edit' permission attached at all
await Acl.model(user).containsPermission('edit') // true
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) // true
await Acl.model(user).hasPermission('edit', product2) // false
await Acl.model(user).hasPermission('edit', Product) // false
await Acl.model(user).hasPermission('edit') // false
await Acl.model(user).containsPermission('edit') // true
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()
// assign role
await Acl.model(user).assignRole(role)
// then if we start checking, result will be same
const product2 = Product.find(2)
await Acl.model(user).hasPermission('edit', product1) // true
await Acl.model(user).hasPermission('edit', product2) // false
await Acl.model(user).hasPermission('edit', Product) // false
await Acl.model(user).hasPermission('edit') // false
await Acl.model(user).containsPermission('edit') // true
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'])
// assigning to the users
await Acl.model(user1).assign('manager')
await Acl.model(user3).assign('manager')
await Acl.model(user3).forbid('delete')
await Acl.model(user1).hasRole('manager') // true
await Acl.model(user1).can('delete') // true
await Acl.model(user3).hasRole('manager') // true
await Acl.model(user3).can('delete') // false
await Acl.model(user3).contains('delete') // true
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') // true
const post1 = Post.find(id1)
await Acl.model(user).allow('edit', Post) // allow for all posts
await Acl.model(user).forbid('edit', post1) // except post1
await Acl.model(user).forbidden('edit', post1) // true
const post7 = Post.find(id7)
await Acl.model(user).forbidden('edit', post7) // false becouse 'edit' action forbidden only for the post1 instance
Unforbidding the permissions
await Acl.model(user3).assignRole('manager')
await Acl.model(user3).forbid('delete')
await Acl.model(user3).forbidden('delete') // true
await Acl.model(user3).can('delete') // false
await Acl.model(user3).can('delete') // true
await Acl.model(user3).unforbid('delete')
await Acl.model(user3).forbidden('delete') // false
await Acl.model(user3).can('delete') // true
Same behaviour applies with roles
await Acl.role(role).assignRole('manager')
await Acl.role(role).forbid('delete')
await Acl.role(role).forbidden('delete') // true
await Acl.role(role).hasPermission('delete') // false
await Acl.role(role).contains('delete') // true
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";
// first assigning permissions
// Global level
await Acl.model(admin).allow('create');
await Acl.model(admin).allow('edit');
await Acl.model(admin).allow('view');
// class level
await Acl.model(manager).allow('create', Post)
// model level
const myPost = await Post.find(id)
await Acl.model(client).allow('view', myPost)
// start checking
// admin
await Acl.model(admin).hasPermission('create') // true
await Acl.model(admin).hasPermission('create', Post) // true
await Acl.model(admin).hasPermission('create', myPost) // true
// manager - assigned class level
await Acl.model(manager).hasPermission('create') // false
await Acl.model(manager).hasPermission('create', Post) // true
await Acl.model(manager).hasPermission('create', myPost) // true
await Acl.model(manager).hasPermission('create', myOtherPost) // true
// assigned model level
await Acl.model(client).hasPermission('create') // false
await Acl.model(client).hasPermission('create', Post) // false
await Acl.model(client).hasPermission('create', myPost) // true
await Acl.model(client).hasPermission('create', myOtherPost) // false
// ... and so on
Same is true when using forbidden
action
// class level
await Acl.model(manager).allow('edit', Post) // allow to edit all posts
await Acl.model(manager).forbid('edit', myPost) // forbid editing ONLY on a myPost
await Acl.model(client).hasPermission('edit', Post) // true
await Acl.model(client).hasPermission('edit', myPost) // false
await Acl.model(client).hasPermission('edit', myOtherPost) // true
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 forcontainsPermission()
Lets in example see this difference
await Acl.model(user).allow('edit'); // assing globally
await Acl.model(user).containsPermission('edit') // true
await Acl.model(user).allow('delete', Post); // assing on resource
await Acl.model(user).containsPermission('delete') // true
await Acl.model(user).forbid('read'); // forbid read action
await Acl.model(user).containsPermission('read') // true
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.
// lets say all users have organization_id attribute
await Acl.model(user).on(user.project_id).allow('edit')
await Acl.model(user).on(user.project_id).allow('delete')
// checking
await Acl.model(user).on(user.project_id).hasPermission('edit') // true
await Acl.model(user).on(user.project_id).hasPermission('delete') // true
// checking without scope
await Acl.model(user).hasPermission('edit') // false - by default scope is equal to 'default'
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:
// acl_middleware
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)
/**
* Call next method in the pipeline and return its output
*/
const output = await next()
return output
}
}
// then on controller you can do
// post_controller.ts
export default class PostController {
async show({acl}: HttpContext){
// will check inside auth.user.account_id scope
await acl.model().hasPermission('view')
// this both will be equal
// await acl.model().on(auth.user.account_id).hasPermission('view')
}
}
Important! If you are using
AclScopeMiddleware
and want to have scope functional per-request then useacl
from thectx
instead of using globalAcl
(NOTE: lower case) object, otherwise changes insideAclScopeMiddleware
will not make effect
Let's see in example
// acl_middleware
export default class AclScopeMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
const scope = new Scope()
scope.set('dashboard') // set scope value to 'dashboard' for current request
ctx.acl = new AclManager().scope(scope)
/**
* Call next method in the pipeline and return its output
*/
const output = await next()
return output
}
}
// post_controller.ts
// global object
import {Acl} from "@holoyan/adonisjs-permissions";
export default class PostController {
async show({acl}: HttpContext){
const scope = acl.getScope()
console.log(scope.get()) // 'dashboard'
// global object
console.log(Acl.getScope()) // 'default'
acl.scope('dashboard_1')// update and set new scope
// for current request it will be 'dashboard_1'
console.log(acl.getScope()) // 'dashboard_1'
Acl.scope('dashboard_2') // Throws error
// check next for the details
await Acl.model(user).on(8).permissions() // get permissions for user on scope 8
}
}
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 passingforceUpdate
param
// global object
import {Acl} from "@holoyan/adonisjs-permissions";
Acl.scope('dashboard_2') // Throws error
// you can force update
let forceUpdate = true;
Acl.scope('dashboard_2', forceUpdate)
// concurent requests will override each other
// request 1
Acl.scope('scope_1', forceUpdate)
// other code - long process
// request 2 - simultaneously
Acl.scope('scope_2', forceUpdate) // will override request 1 scope
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')
// you other code
await trx.commit()
Cheat sheet
Coming soon
TODO
- [X] Scopes (Multitenancy)
- [X] UUID support
- [ ] Events
- [ ] More test coverage
- [ ] Caching
- [ ] Integration with AdonisJs Bouncer
Test
npm run test
Version Map
| AdonisJS Lucid version | Package version | |------------------------|-----------------| | v20.x | 0.8.x | | v21.x | 1.x |
License
MIT