Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Authorization with Bouncer

Authentication answers "who is this", and authorization answers "what are they allowed to do". For example, only its author may edit a post. In AdonisJS the Bouncer package checks permissions: rules are described as functions, and a check in the controller takes one line. Laravel does the same with Gates and policies.

Installing

node ace add @adonisjs/bouncer

The command creates app/abilities/main.ts and app/policies/main.ts and registers a middleware that adds a bouncer object to the request context. For the example, assume a post has a user_id column and a userId property — the post's author.

An ability

An ability is a function that receives the user and a record and returns true or false:

// app/abilities/main.ts
import { Bouncer } from '@adonisjs/bouncer'
import User from '#models/user'
import Post from '#models/post'
 
export const editPost = Bouncer.ability((user: User, post: Post) => {
  return user.id === post.userId
})

Bouncer passes the current user itself, you pass only the post.

Checking in a controller

// app/controllers/posts_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
import Post from '#models/post'
import { editPost } from '#abilities/main'
 
export default class PostsController {
  async edit({ params, bouncer, view }: HttpContext) {
    const post = await Post.findOrFail(params.id)
    await bouncer.authorize(editPost, post)
    return view.render('pages/posts/edit', { post })
  }
}

If the ability returns false, authorize throws an exception and the visitor gets a 403 response. When you do not want to stop the request but just get the answer, use await bouncer.allows(editPost, post) or denies.

Checking in a template

Show the "Edit" button only to those who are allowed:

{{-- resources/views/pages/posts/show.edge --}}
@can('editPost', post)
  <a href="{{ route('posts.edit', { id: post.id }) }}">Edit</a>
@end

The @can tag takes the ability name as a string. For the opposite check there is @cannot.

Policies

When one model gets many rules, they are collected into a policy — a class with a method for each action:

node ace make:policy post
// app/policies/post_policy.ts
import { BasePolicy } from '@adonisjs/bouncer'
import User from '#models/user'
import Post from '#models/post'
 
export default class PostPolicy extends BasePolicy {
  edit(user: User, post: Post) {
    return user.id === post.userId
  }
 
  delete(user: User, post: Post) {
    return user.id === post.userId
  }
}
// app/controllers/posts_controller.ts
import PostPolicy from '#policies/post_policy'
 
await bouncer.with(PostPolicy).authorize('delete', post)

In a template the policy is referenced with a dot: @can('PostPolicy.edit', post).

Common mistake

The button is hidden with @can, but no check is written in the controller. Then any user can open /posts/5/edit by hand and change someone else's post. A template only hides elements; the check in the controller is what protects, and it is always needed.

Thanks for reading!

Donate to the site — we work hard for you.