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/bouncerThe 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>
@endThe @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.