Skip to content

QuantumLabsLtda/adonisjs-cerberus

Repository files navigation

Cerberus

A Hellhound that protects your API Endpoints... 😈

npm version

Cerberus is a library that adds roles, resources and permissions to Auth System from Adonis Framework.

Table of Contents

Concept

Cerberus is a collection of models, commands, migrations and a middleware to help you managing User Access Rights in an Adonis.js API.

Warning: Cerberus doesn't yet manage Controllers and Routes for you. You'll need to create your custom methods using Cerberus Models.

To start using Cerberus properly, you need to understand some basic concepts behind the library. There are 3 main pillars (A.K.A Cerberus Heads), they are:

  1. Roles
  2. Resources
  3. Permissions

A Role is an User responsabilty in your API (e.g., Admin, Moderator, User, Seller, etc.).

A Resource can be a Controller, Model or any resource in your API that you want to protect (e.g., User, Profile, Post, etc.).

And we have Permissions. A Permission is basically the junction of Role and a Resource, with boolean values for each access right that a Role should give to a specific user_id. With Permissions we also have DefaultPermissions, they are Role-specific default values for Permissions boolean accesses. Every new Permission, should inherit from DefaultPermission rights, but they can assume whatever value you want.

The accesses are:

  1. Create
  2. Read
  3. Update
  4. Delete

Then, you can create Roles with DefaultPermissions and give Permissions to specific Resources and Users.

E.g.: You can create an "Admin" Role with a DefaultPermission to "Posts" Resource, and using the Permission model you can create a specific Permission to your User.

Role

{
  "id": 1,
  "name": "Admin",
  "slug": "admin"
}

Resource

{
  "id": 4,
  "name": "Posts",
  "slug": "posts"
}

DefaultPermission

{
  "id": 1,
  "resourceId": 4,
  "roleId": 1,
  "create": true,
  "read": true,
  "update": true,
  "delete" : true
}

Permission

{
  "id": 1,
  "resourceId": 4,
  "roleId": 1,
  "userId": 1,
  "create": true,
  "read": true,
  "update": true,
  "delete" : true
}

The Guard middleware will check values against Permission record, not DefaultPermission values.

Tables diagram

Cerberus

Setup

Install package using npm.

  adonis install @quantumlabs/adonisjs-cerberus

or

  npm i @quantumlabs/adonisjs-cerberus --save

Warning: If you choose npm install instead of adonis install, you'll need to open instructions.md manually.

Instructions

  1. Add Cerberus into providers Array, inside start/app.js file of your project:
  const providers = [
    ...
    '@quantumlabs/adonisjs-cerberus/providers/CerberusProvider',
    ...
  ]
  1. Add Cerberus into aceProviders Array, inside start/app.js file of your project:
  const aceProviders = [
    ...
    '@quantumlabs/adonisjs-cerberus/providers/CommandsProvider'
    ...
  ]
  1. Add Cerberus Middleware into namedMiddleware Array, inside start/kernel.js file of your project:
  const namedMiddleware = [
    ...
    guard: 'Cerberus/Middleware/Guard'
    ...
  ]
  1. Add Cerberus special Traits inside your app/Models/User Model, like this:
class User extends Model {
  static boot () {
    super.boot()

    this.addTrait('@provider:Cerberus/Traits/Role')
    this.addTrait('@provider:Cerberus/Traits/Permission')
  }
...
  1. If you're using Objection.js's Snake case to camel conversion, you should add this property at the end of your project config/database.js file:
  ...
  usingSnakeCaseMappers: true
}
  1. Finally, you need to run:
  adonis cerberus:init

This command will copy all library specific migrations into your project migrations folder.

Warning: Cerberus creates a migration for users table, adding a role_id column, which is a foreing key from roles. You should take a look in the migration before running it in production!

Commands

Init

  adonis cerberus:init

As mentioned before, this command should (and we hope it will) copy all library specific migration into your projects migrations folder. There's no special flags or parameters for this command.

Role

adonis cerberus:role [name] [slug]

Usage:
  cerberus:role <name> [slug] [options]

Arguments:
  name   Name of the role
  slug   Short name for role

This command creates a new role into roles table. You only need to specify role's name, slug argument is optional.

Resource

adonis cerberus:resource [name] [slug] [options]

Arguments:
  name               Name of the resource
  slug               Short name for resource

Options:
  -p, --defaultPermission   Generate default permissions
  -a, --always-ask   Ask which rights give in each Resource once (false by default)
  --from-models      Generate a resource for each app Model
  --from-controllers Generate a resource for each app Controller

This command creates a new resource into resources table. You only need to specify the name of resource, the slug argument is optional. The options are: -p, --defaultPermission - Generate default permissions -a, --always-ask - Ask which rights give in each Resource once (false by default) --from-models - Generate a resource for each app Model --from-controllers - Generate a resource for each app Controller

Default Permission

cerberus:defaultPermission [options]

Options:
  -a, --all               Run default permission creation for each Resource in database
  --resource-name <value> Name of resource

This command creates a new default permission into default_permissions table. You need to specify a Resource name, then, Cerberus creates a default permission record with the specified Resource name.

Usage

Using in routes

After creating your Permissions, you'll be able to start using Guard middleware in your routes.

It's simple to bind a permission into a route, look:

/**
* Get all users
* GET users
*/
Route
  .get('', 'UserController.index')
  .middleware(['auth', 'guard: user.read'])

You can also use multiple permissions in your route:

/**
* Create a new user
* POST users/register
*/
Route
  .post('register', 'UserController.register')
  .middleware(['auth', 'guard: user.create, user.read'])

And it's possible to use multiple resources too:

/**
* Fetch all posts with user profile
* GET posts
*/
Route
  .get('', 'PostController.index')
  .middleware(['auth', 'guard: post.read, user.read'])

Warning: You need to add the default auth middleware before Cerberus guard!

Consuming Models

You should use Cerberus Models to create Roles, Resources and Permissions in your own code. It's also useful for creating seeds or a complete CRUD to manage your Cerberus stuff. It's simple to use:

Roles

  const Role = use('Cerberus/Models/Role')

  // Creating a new Role
  await Role.create({
    name: 'Jedi Master',
    slug: 'jedi'
  })

  // or

  let role = await Role.find(1)

  role.name = 'Wookie'
  role.slug = 'wookie'

  await role.save()

  // You can use any Lucid methods you want

Resources

  const Resource = use('Cerberus/Models/Resource')

  // Creating a new Resource
  await Resource.create({
    name: 'Lightsaber',
    slug: 'lightsaber'
  })

  // or

  let resource = await Resource.find(1)

  resource.name = 'Force'
  resource.slug = 'force'

  await resource.save()

  // You can use any Lucid methods you want

DefaultPermissions

  const DefaultPermission = use('Cerberus/Models/DefaultPermission')

  // Creating a new DefaultPermission
  await Permission.create({
    role_id: role.id,
    resource_id: resource.id,
    create: false,
    read: true,
    update: false,
    delete: false
  })

  // or

  let defaultPermission = await DefaultPermission.find(1)

  defaultPermission.create = true
  defaultPermission.update = true

  await defaultPermission.save()

  // You can use any Lucid methods you want

Permissions

  const Permission = use('Cerberus/Models/Permission')

  // Creating a new Permission
  await Permission.create({
    role_id: role.id,
    user_id: user.id,
    resource_id: resource.id,
    create: false,
    read: true,
    update: false,
    delete: false
  })

  // or

  let permission = await Permission.find(1)

  permission.create = true
  permission.update = true

  await permission.save()

  // You can use any Lucid methods you want

Binding User to Role

You can simply bind an existing User to a Role:

  const User = use('App/Models/User')
  
  // Fetches the current user
  let user = await User.find(1)

  // Set the role Id
  user.role_id = 1

  await user.save()

Inheriting rights from DefaultPermissions

You can inherit rights from DefaultPermissions to Permission:

  const DefaultPermission = use('Cerberus/Models/DefaultPermission')
  const Permission = use('Cerberus/Models/Permission')
  const User = use('App/Models/User')
  
  // Fetches the current user
  let user = await User.find(1)

  // Get the default permission
  let defaultPermission = await DefaultPermission
      .query()
      .where('resourceId', resource.id)
      .orderBy('createdAt', 'desc')
      .limit(1)
      .fetch()

  defaultPermission = defaultPermission.first()

  if (defaultPermission) {
    // Create the default permission
    await Permission.create({
        roleId,
        user_id: user.id,
        resourceId: resource.id,
        create: defaultPermission.create,
        read: defaultPermission.read,
        update: defaultPermission.update,
        delete: defaultPermission.delete
      })
  }

Errors

By default, Cerberus throw 2 types of errors:

  1. CERBERUS_ACCESS_DENIED: This error occours when an User doesn't have the needed permissions for the specified Resource. The error status code is 403.
  2. CERBERUS_RESOURCE_NOT_FOUND: This error occours when you're trying to check for permissions in a Resource that doesn't exist. The error status code is 404.

You can handle this errors manually for custom messages. Check Adonis Framework Official Docs and search for Error Handling.

Tips

SQLite - If you're using SQLite, you may notice a bug when running user migrations. This happens because SQLite doesn't support ALTER TABLE with a constraint. Here you can find info about this issue in SQLite's official docs. I suggest you to create role_id foreign key manually in your user migration.

Todo

  • Add an option in adonis cerberus:permission to run permission for every resource in database
  • Option to create custom permission rights
  • Methods for easy permission, role and resource binding
  • Setting a test utility and write tests for the existing code

Credits

Logo icon made by freepik from flaticon.com. Customized by QuantumLabs.