Policies allow us to group Bouncer actions together, typically by resource, so that we don’t end up with a giant list of actions within our start/bouncer.ts
file. This will make things easier to maintain and scale as our application grows and ages. Since we have two resources within our project, posts, and comments, we’ll end up extracting our actions into two policies.
Creating Policies
When we configured Bouncer within our project it added a single new Ace CLI command for us. This command is for creating Bouncer Policies, node ace make:policy <name>
.
If we take a look at the options this command contains we’ll see the following.
node ace make:policy -h # Make a new bouncer policy # Usage: make:policy <name> # Arguments # name Name of the policy to create # Flags # --resource-model string Name of the resource model to authorize # --user-model string Name of the user model to be authorized # --actions string[] Actions to implement
Copied!
The make:policy
command takes the name of the policy as its argument. In addition to that, we also have three optional flags at our disposal.
--resource-model
which allows us to specify a model name, for example, Post.--user-model
which allows us to specify what user model should be used, this will default to the default user model.--actions
which we can use to specify a list of actions that should be stubbed.
If we omit the --resource-model
or --user-model
flag, Bouncer will ask us about them after we run the command as well, so if you prefer that approach you can do that instead.
If we provide a resource model, Bouncer does have a default list of actions it’ll ask us about as well and it’ll stub the selected actions within our policy.
Creating Our Policies
First, let's go ahead and create both our policies and inspect the generated files.
node ace make:policy Post --resource-model Post # ❯ Enter the name of the user model to be authorized · User # ❯ Select the actions you want to authorize · viewList, view, create, update, delete # CREATE: app/Policies/PostPolicy.ts
Copied!
Be sure to enter User
as the user model to be authorized, this should be the default. Also, when asked about actions select the following.
viewList
- for authorizing post listsview
- for authorizing viewing a single postcreate
- for authorizing the creation of a postupdate
- for authorizing update a postdelete
- for authorizing deleting a post
This will create our PostPolicy
file at app/Policies/PostPolicy.ts
and our default file will be the below.
import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer' import User from 'App/Models/User' import post from 'App/Models/post' export default class PostPolicy extends BasePolicy { public async viewList(user: User) {} public async view(user: User, post: post) {} public async create(user: User) {} public async update(user: User, post: post) {} public async delete(user: User, post: post) {} }
Copied!
- app
- Policies
- PostPolicy.ts
Our policy as a whole is our class PostPolicy
that extends BasePolicy
from the Bouncer package. Each public method within our class then acts as a Bouncer action.
Note that our policy actions view
, update
, and delete
were stubbed expecting a post record as well since these deal with authorizing a single post record.
Next, let's do that again for our comments.
node ace make:policy Comment --resource-model Comment # ❯ Enter the name of the user model to be authorized · User # ❯ Select the actions you want to authorize · viewList, create, update, delete # CREATE: app/Policies/CommentPolicy.ts
Copied!
Note this time we don’t need the singular view
action since the only display we’ll have is the list of comments on a post show page.
Registering Policies
Now that we’ve created our policies, we next need to register them. So, within our start/bouncer.ts
file scroll down to the bottom and you should see a block like this:
export const { policies } = Bouncer.registerPolicies({})
Copied!
Within the object passed to the registerPolicies
method is where we’ll want to register our policies. We’ll do this almost identically to how named middleware are registered.
export const { policies } = Bouncer.registerPolicies({ PostPolicy: () => import('App/Policies/PostPolicy'), CommentPolicy: () => import('App/Policies/CommentPolicy') })
Copied!
We register the policies with a name, for example PostPolicy
, then import the policy from within our application. With that, our policies are now registered.
Migrating Actions to Policies
Next, let’s migrate our actions out of our start/bouncer.ts
file and into our post and comment policies.
Migrating Post Actions to the PostPolicy
Let’s start with our post actions. To summarize from past lessons here are our actions specific to our post, remember admins are granted access to everything via our before hook (omitted here).
export const { actions } = Bouncer /* POST /***************************************/ .define('createPost', (user: User) => { return user.roleId === Role.EDITOR }) .define('viewPost', (user: User | null, post: Post) => { if (post.userId === user?.id) { return true } if (!post.isPublished) { return Bouncer.deny('This post is not yet published', 404) } return true }, { allowGuest: true }) .define('editPost', (user: User, post: Post) => { return post.userId === user.id }) .define('destroyPost', (user: User, post: Post) => { return post.userId === user.id })
Copied!
- start
- bouncer.ts
To migrate these to our PostPolicy, we’ll first want to identify what each method in our policies each action maps to.
PostPolicy.viewList
will be new, we’ll just be returning true here to allow everyone.PostPolicy.view
will come fromviewPost
PostPolicy.create
will come fromcreatePost
PostPolicy.update
will come fromeditPost
PostPolicy.delete
will come fromdestroyPost
Then, we’ll take the inner contents of our current action’s callback and instead apply it as the inner code of our policy class methods.
For example, for create
we’ll end up moving this:
return user.roleId === Role.EDITOR
Copied!
to here:
import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer' import User from 'App/Models/User' import Post from 'App/Models/Post' export default class PostPolicy extends BasePolicy { public async viewList(user: User) {} public async view(user: User, post: Post) {} // 👇 move inner code of createPost here public async create(user: User) { return user.roleId === Role.EDITOR } public async update(user: User, post: Post) {} public async delete(user: User, post: Post) {} }
Copied!
- app
- Policies
- PostPolicy.ts
So, the remainder of our policy will look like the following.
import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer' import User from 'App/Models/User' import Post from 'App/Models/Post' export default class PostPolicy extends BasePolicy { public async viewList(user: User) { return true // allow all } public async view(user: User, post: Post) { if (post.userId === user?.id) { return true } if (!post.isPublished) { return Bouncer.deny('This post is not yet published', 404) } return true } public async create(user: User) { return user.roleId === Role.EDITOR } public async update(user: User, post: Post) { return post.userId === user.id } public async delete(user: User, post: Post) { return post.userId === user.id } }
Copied!
- app
- Policies
- PostPolicy.ts
Allowing Guests in Policy Actions
Now, our viewPost
action allowed guests, meaning our user could be null. We want to maintain that when we move the action into the policy. We’re also going to want the new viewList
policy action to allow guests as well.
To specify guests are allowed when we’re using policies, we can use the @action()
decorator to apply options for the specific action. So, we can decorate the view
method like so to allow guests.
import { BasePolicy, action } from '@ioc:Adonis/Addons/Bouncer' // 👈 import action decorator import User from 'App/Models/User' import Post from 'App/Models/Post' export default class PostPolicy extends BasePolicy { @action({ allowGuest: true }) // 👈 decorate method public async viewList(user: User) { return true } @action({ allowGuest: true }) // 👈 decorate method public async view(user: User | null, post: Post) { // 👈 allow user to be null if (post.userId === user?.id) { return true } if (!post.isPublished) { return Bouncer.deny('This post is not yet published', 404) } return true } public async create(user: User) {/*...*/} public async update(user: User, post: Post) {/*...*/} public async delete(user: User, post: Post) {/*...*/} }
Copied!
- app
- Policies
- PostPolicy.ts
Migrating Comment Actions to the CommentPolicy
Next, let’s do the same for our comment actions. When we move these actions over into our CommentPolicy, the CommentPolicy will end up looking like so.
import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer' import User from 'App/Models/User' import Comment from 'App/Models/Comment' import Post from 'App/Models/Post' import Role from 'Contracts/enums/Role' export default class CommentPolicy extends BasePolicy { public async viewList(user: User, post: Post) { return post.isPublished } public async create(user: User, post: Post) { return post.isPublished } public async update(user: User, comment: Comment) { return comment.userId === user.id } public async delete(user: User, comment: Comment) { const allowedRoles = [Role.MODERATOR, Role.EDITOR, Role.ADMIN] return comment.userId === user.id || allowedRoles.includes(user.roleId) } }
Copied!
- app
- Policies
- CommentPolicy.ts
Note we’re still utilizing the post for our viewList
and create
actions.
Policy Hooks
Policies contain the same hooks as Bouncer, before and after. These are essentially the same as the main Bouncer hooks except they’ll only be applied before or after the specific policy’s actions. It’s also worth noting the hooks defined within our start/bouncer.ts
file won’t impact our Policy actions either.
So, if we want our administrators to still have access to perform every action within our application, we’ll need to ensure the same before hook is defined within our policies as well.
import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer' import User from 'App/Models/User' import Comment from 'App/Models/Comment' import Post from 'App/Models/Post' import Role from 'Contracts/enums/Role' export default class CommentPolicy extends BasePolicy { public async before(user: User | null) { // allow admins authorization to perform all comment actions if (user?.roleId === Role.ADMIN) { return true } } public async after(user: User | null, actionName, actionResult) { // perform some action after action is completed } public async viewList(user: User, post: Post) {/*...*/} public async create(user: User, post: Post) {/*...*/} public async update(user: User, comment: Comment) {/*...*/} public async delete(user: User, comment: Comment) {/*...*/} }
Copied!
- app
- Policies
- CommentPolicy.ts
Sharing Policy Hooks
Now, we could also define this exact same before hook within our PostPolicy
or, we can create our own BasePolicy
that extends Bouncer’s BasePolicy
. Our BasePolicy
is then where we can define our before hook to provide our admins authorization on all actions.
import { BasePolicy as BouncerBasePolicy } from "@ioc:Adonis/Addons/Bouncer"; import User from "App/Models/User"; import Role from "Contracts/enums/Role"; export default class BasePolicy extends BouncerBasePolicy { public async before(user: User | null) { if (user?.roleId === Role.ADMIN) { return true } } } // app/Policies/CommentPolicy.ts import {action} from '@ioc:Adonis/Addons/Bouncer' import User from 'App/Models/User' import Comment from 'App/Models/Comment' import Post from "App/Models/Post"; import Role from "Contracts/enums/Role"; import BasePolicy from "App/Policies/BasePolicy"; export default class CommentPolicy extends BasePolicy { @action({ allowGuest: true }) public async viewList(_: User, post: Post) { return post.isPublished } public async create(_: User, post: Post) { return post.isPublished } public async update(user: User, comment: Comment) { return comment.userId === user.id } public async delete(user: User, comment: Comment) { const allowedRoles = [Role.MODERATOR, Role.EDITOR] return comment.userId === user.id || allowedRoles.includes(user.roleId) } } // app/Policies/PostPolicy.ts import Bouncer, {action} from '@ioc:Adonis/Addons/Bouncer' import User from 'App/Models/User' import Post from 'App/Models/Post' import Role from "Contracts/enums/Role"; import BasePolicy from "App/Policies/BasePolicy"; export default class PostPolicy extends BasePolicy { @action({ allowGuest: true }) public async viewList(_: User | null) { return true } @action({ allowGuest: true }) public async view(user: User | null, post: Post) { if (post.userId === user?.id) { return true } if (!post.isPublished) { return Bouncer.deny('This post is not yet published', 404) } return true } public async create(user: User) { return user.roleId === Role.EDITOR } public async update(user: User, post: Post) { return post.userId === user.id } public async delete(user: User, post: Post) { return post.userId === user.id } }
Copied!
- app
- Policies
- BasePolicy.ts
Note the only file importing the BasePolicy
from Bouncer is our app/Policies/BasePolicy.ts
file. The PostPolicy
and CommentPolicy
files are importing our BasePolicy
so they contain our before hook giving admins all authorizations.
Using Policy Actions
Now that we have a full understanding of defining actions within policies, let’s finish out our lesson by changing our authorization checks to utilize our policy actions.
Controller Checks
When we’re within controllers, services, or anywhere else that has access to a Bouncer instance within the HttpContext we can specify we want to use a policy action by prefixing our authorize
, allows
, or denies
method calls with with
. The with
method is how we’ll specify to run an authorization check with a specified policy.
So, for example, to check the create
action within our PostPolicy
we can do the following.
await bouncer.with('PostPolicy').authorize('create')
Copied!
Here we’re specifying to Bouncer to use the create
action defined within the PostPolicy
.
So, we’ll want to update all our authorize
, allows
, and denies
checks to use with
and simplify the action name to match the corresponding policy action name.
PostsController
Our updated PostsController
now looks like the following.
export default class PostsController { public async index({ view, bouncer }: HttpContextContract) { await bouncer.with('PostPolicy').authorize('viewList') // 👈 add new check const posts = await Post.query() .preload('user') .where('isPublished', true) return view.render('index', { posts }) } public async create({ view, bouncer }: HttpContextContract) { await bouncer.with('PostPolicy').authorize('create') // 👈 return view.render('posts/createOrEdit') } public async store({ request, response, auth, bouncer }: HttpContextContract) { await bouncer.with('PostPolicy').authorize('create') // 👈 const data = await request.validate(PostValidator) const post = await Post.create({ ...data, userId: auth.user!.id }) return response.redirect().toRoute('posts.show', { id: post.id }) } public async show({ view, params, bouncer }: HttpContextContract) { const post = await Post.query() .preload('user') .where('id', params.id) .firstOrFail() // 👇 update allows check to use CommentPolicy if (await bouncer.with('CommentPolicy').allows('viewList', post)) { await post.load('comments', query => query.preload('user')) } await bouncer.with('PostPolicy').authorize('view', post) // 👈 return view.render('posts/show', { post }) } public async edit({ view, params, bouncer }: HttpContextContract) { const post = await Post.findOrFail(params.id) await bouncer.with('PostPolicy').authorize('update', post) // 👈 return view.render('posts/createOrEdit', { post }) } public async update({ request, response, params, bouncer }: HttpContextContract) { const post = await Post.findOrFail(params.id) await bouncer.with('PostPolicy').authorize('update', post) // 👈 const data = await request.validate(PostValidator) await post.merge(data).save() return response.redirect().toRoute('posts.show', { id: post.id }) } public async destroy({ response, params, bouncer }: HttpContextContract) { const post = await Post.findOrFail(params.id) await bouncer.with('PostPolicy').authorize('delete', post) // 👈 await post.related('comments').query().delete() await post.delete() return response.redirect().toRoute('home') } }
Copied!
- app
- Controllers
- Http
- PostsController.ts
CommentsController
Our updated CommentsController
will end up looking like the following:
export default class CommentsController { public async store({ request, response, auth, params, bouncer }: HttpContextContract) { const post = await Post.findOrFail(params.post_id) await bouncer.with('CommentPolicy').authorize('create', post) // 👈 const data = await request.validate(CommentValidator) await Comment.create({ ...data, userId: auth.user!.id, postId: params.post_id }) return response.redirect().toRoute('posts.show', { id: params.post_id }) } public async update({ request, response, params, bouncer }: HttpContextContract) { const comment = await Comment.findOrFail(params.id) await bouncer.with('CommentPolicy').authorize('update', comment) // 👈 const data = await request.validate(CommentValidator) await comment.merge(data).save() return response.redirect().toRoute('posts.show', { id: comment.postId }) } public async destroy({ response, params, bouncer }: HttpContextContract) { const comment = await Comment.findOrFail(params.id) await bouncer.with('CommentPolicy').authorize('delete', comment) // 👈 await comment.delete() return response.redirect().toRoute('posts.show', { id: comment.postId }) } }
Copied!
- app
- Controllers
- Http
- CommentsController.ts
Edge Checks
When we’re within the Edge Templating Engine, the @can
and @cannot
tags behave a little differently than controllers when it comes to checking policy actions. Instead of calling an additional method, we’ll instead use a dot notation string to specify which policy to use.
So, for example, to check the create
action within our PostPolicy
we can do the following.
@can('PostPolicy.create') <p>Can create posts!</p> @endcan
Copied!
So, throughout our Edge views, all we need to do is update the names provided to the first argument of the @can
and @cannot
tags to include the policy name and the new action name.
{{-- resources/views/layouts/main.edge --}} {{-- Navigation --}} <div class="flex items-center justify-center bg-gray-50 py-3 border-b border-gray-100 space-x-3"> <a href="{{ route('home') }}">Home</a> {{-- 👇 change from createPost to PostPolicy.create --}} @can('PostPolicy.create') <a href="{{ route('posts.create') }}">New Post</a> @endcan @if (auth.user) <a href="{{ route('auth.logout') }}">Logout</a> @else <a href="{{ route('auth.register.show') }}">Register</a> <a href="{{ route('auth.login.show') }}">Login</a> @endif </div>
Copied!
{{-- resources/views/posts/show.edge --}} <div class="flex justify-between space-x-3 items-center mb-6"> <p class="text-gray-400">By {{ post.user.username }}</p> <div class="flex justify-end items-center space-x-3"> {{-- 👇 change from editPost --}} @can('PostPolicy.update', post) <a href="{{ route('posts.edit', { id: post.id }) }}">Edit Post</a> @endcan {{-- 👇 change from destroyPost --}} @can('PostPolicy.delete', post) <form action="{{ route('posts.destroy', { id: post.id }, { qs: { _method: 'DELETE' }}) }}" method="POST"> {{ csrfField() }} <button type="submit" class="text-red-400 hover:text-red-600">Delete Post</a> </form> @endcan </div> </div> <h1 class="text-4xl font-semibold mb-3">{{ post.title }}</h1> <p class="text-lg text-gray-400 mb-3">{{ post.summary }}</p> <p>{{ post.body }}</p> <div class="mt-3 pt-3 border-t border-gray-300"> <h3 class="text-lg font-semibold">Comments</h3> {{-- 👇 change from createComment --}} @can('CommentPolicy.create', post) <form action="{{ route('posts.comments.store', { post_id: post.id }) }}" method="POST" class="mb-3"> {{ csrfField() }} @!input({ type: 'textarea', name: 'body', value: flashMessages.get('body'), errors: flashMessages.get('errors.body') }) <div class="-mt-2 text-right"> <button type="submit">Comment</button> </div> </form> @endcan @each (comment in post.comments) <div class="mb-3"> <p>{{ comment.body }}</p> <div class="flex items-center space-x-3 text-xs "> <p class="text-gray-400">By {{ comment.user.username }}</p> {{-- 👇 change from destroyComment --}} @can('CommentPolicy.delete', comment) <form action="{{ route('posts.comments.destroy', { id: comment.id }, { qs: { _method: 'DELETE' } }) }}" method="POST"> {{ csrfField() }} <button type="submit" class="text-red-400 hover:text-red-600">Delete Comment</button> </form> @endcan </div> </div> @endeach {{-- 👇 change from viewCommentList --}} @cannot('CommentPolicy.viewList', post) <p class="text-gray-600">Comments for this post are turned off</p> @elseif (!post?.comments?.length) <p class="text-gray-400">No comments, be the first to comment!</p> @endcannot </div>
Copied!
Cleaning Up Main Actions
Now that we’ve moved our post and comment actions into policies and updated all authorization checks to use the policy versions, we can now remove all defined actions and hooks from our start/bouncer.ts
file. Remember to leave the exported constant actions
!
export const { actions } = Bouncer
Copied!
- start
- bouncer.ts
Wrapping Up
That’s a wrap! We’ve learned all about Bouncer actions and how to check user authorization using them. We’ve also now learned how we can group our actions into policies allowing for easier management of actions. Lastly, we’ve learned how we can use hooks in our policies and share hooks across policies by creating our own BasePolicy
.
Hopefully, this series has helped provide some insight into how you can perform authorization checks within your AdonisJS application! Thank you all for viewing/reading this series and for your support!
Join The Discussion! (23 Comments)
Please sign in or sign up for free to join in on the dicussion.
meracle
Awesome like always!
Please sign in or sign up for free to reply
meracle
<3
Please sign in or sign up for free to reply
meracle
<3
Please sign in or sign up for free to reply
meracle
<3
Please sign in or sign up for free to reply
meracle
<3
Please sign in or sign up for free to reply
meracle
<3<3<3
Please sign in or sign up for free to reply
meracle
<3<3<3
Please sign in or sign up for free to reply
meracle
<3<3<3
Please sign in or sign up for free to reply
meracle
<3<3<3
Please sign in or sign up for free to reply
meracle
<3<3<3
Please sign in or sign up for free to reply
meracle
<3<3<3
Please sign in or sign up for free to reply
meracle
<3<3<3
Please sign in or sign up for free to reply
tomgobich
Thank you, Dawid!! :)
Please sign in or sign up for free to reply
Anonymous (GorillaAinslie198)
Awesome. could you make or update this post like this link https://laravel-news.com/authorization-gates, I want to know how to implement function hasAccess in the model if use adonisjs
Please sign in or sign up for free to reply
tomgobich
Yeah, I'll add this to my lesson to-do list as this is a different approach than what's discussed in this lesson. Thank you!
Please sign in or sign up for free to reply
shiva-kar
what if a hacker access the database and change the role . How can we make it more secure.
Please sign in or sign up for free to reply
tomgobich
The goal should be to block the hacker from accessing your database in the first place. If they can access your database, what's to stop them from just dumping all the tables? I wouldn't worry about a hacker changing themselves to an admin, but rather focus on blocking the hacker from having access to your database.
This would include ensuring your database is IP/SSH restricted, protected by a strong password, protected by a firewall (your whole app, really), etc. This topic, if you'd like to learn more, falls under "DevOps".
Please sign in or sign up for free to reply
Anonymous (AntelopeMaryanne481)
thanks for this tuts tom
Please sign in or sign up for free to reply
tomgobich
Thanks for watching/reading!!
Please sign in or sign up for free to reply
Anonymous (RoseCornela847)
I am getting this error:
Cannot use "class UserPolicy extends Bouncer_2.BasePolicy {
Please sign in or sign up for free to reply
tomgobich
Hard to tell what it could be without more context. I'd say to check and make sure your BasePolicy is importing from the correct location.
Please sign in or sign up for free to reply
Anonymous (BirdJenine92)
Cannot use "class UserPolicy extends Bouncer_2.BasePolicy {
I am getting this error. I have properly define the policy inside start/bouncer file.
Could you tell what might have gone wrong?
Please sign in or sign up for free to reply
tomgobich
I'd say to double-check and make sure your BasePolicy is importing from the correct location. Sometimes VS Code likes to reach into the build folder instead of using the IoC definitions. It should be:
import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer'
Please sign in or sign up for free to reply