Today we’ll be looking at handling a simple authorization scenario with Bodyguard.
In the case of a full-fledged permission and roles system, authorization can get pretty complicated, it could look something like:
As you can imagine this can get pretty involved and requires a lot of supporting infrastructure in the form of database tables and forms for setting up and defining permissions and roles; and then assigning roles to users.
For the purpose of demonstrating Bodyguard
, we’ll stick with a simple scenario where we have the concept of Admin
users, and only certain actions can be performed by admins.
Getting started
If you’d rather grab the source code directly rather than follow along, it’s available on GitHub at https://github.com/riebeekn/phx-authorization-with-bodyguard, otherwise let’s get started!
We’ll use the same application we created in our Authentication posts as a starting point for today. So if you’ve been following along with the Authentication posts, you can continue with the code from there, otherwise you can grab the starting point code from GitHub.
Clone the Repo:
If cloning:
Terminal
Let’s create a branch for today’s work:
Terminal
And make sure we have our dependencies and database set-up.
Terminal
Terminal
Note: before running ecto.setup
you may need to update the username / password database settings in dev.config
to match your local postgres settings, i.e.
/config/dev.config …line 69
Terminal
With that out of the way… let’s see where we are starting from.
Terminal
If we navigate to http://localhost:4000/ we’ll see our application.
So we have the basic application you get when running mix phx.new
with some scaffolding added to represent a very simple warehouse inventory system.
If we create a user and login, we’ll see we have access to our products page.
Currently any signed in user can delete a product. We want to switch things up so that only admin
users can delete a product.
Adding an admin field to users
The first step is going to be adding a new field to users. We’ll add an is_admin
field to indicate whether a user is an admin.
We’ll start by updating the database.
Updating the database
We need to create a migration to add the new column to the users table.
Terminal
/priv/repo/timestamp
_add_is_admin_to_users.exs
Pretty simple, all we’ve done is alter the table to include the new field, by default we’ll set the value of is_admin
to false
.
Now we need to run the migration.
Terminal
And that’s it for the database changes!
Updating the user schema
Next up is adding our new field to user.ex
.
/lib/warehouse/users/user.ex
We’ve added the new field via “field :is_admin, :boolean
” and also updated the changeset to include the is_admin
field.
Finally we’ll update our sign in page to allow user’s to register as admins
… obviously this is not something you would do in a real application, but for our example application it will provide a convenient way of creating admin
users.
Updating the registration page
We’ll add a checkbox to the registration page that can be used to toggle whether a user is an admin or not.
/lib/warehouse_web/templates/pow/registration/new.html.eex …line 22
Sweet, that’s it for our preliminary steps, let’s create a new user that we’ll use to test out our admin functionality.
And now we’re ready to get into some authorization with Bodyguard
.
Bodyguard
The first step is to install Bodyguard
.
Installation
Installation is simple, we just need to add Bodyguard
to our dependencies and run mix deps.get
.
/mix.exs …line 34
Terminal
The mix
output will indicate we’ve added Bodyguard
.
Setting up the authorization rules / policies
Bodyguard refers to authorization rules as policies. These policies can be defined directly in our context modules or can be placed in seperate policy modules. Although today we’re only going to be writing a single policy, I still like the idea of having policies defined in a seperate module so that’s the approach we will take.
So let’s create our policy!
Terminal
/lib/warehouse/inventory/policy.ex
Any policy module needs to include the Bodyguard.Policy
behaviour and an authorize/3
function. The authorize
function is (surprise!) the function that will be called to determine whether an action is authorized or not.
In our case the authorize
function is very simple. All we are doing is indicating in the case of a delete action, only a user who is an admin
is allowed to perform the action. As per the Bodyguard
README
, a policy can return one of the following values:
- :ok or true to permit an action
- :error, {:error, reason}, or false to deny an action
This works perfect for us as we can just return the user.is_admin
boolean. Note that true/false return values are converted to :ok
and {:error, reason}
respectively when Bodyguard
consumes our authorize functions. In this manner a consistent set of return values are always returned from Bodyguard
.
Now we need to make use of our new policy.
Using our policy
Bodyguard
is agnostic in terms of where policy checks occur. We could perform our check at the controller level, but I’ve opted to perform the check in the context.
We first need to delegate the call to our policy file.
/lib/warehouse/inventory/inventory.ex …line 6
We also added an alias for our User module.
Next we need to update the delete function.
/lib/warehouse/inventory/inventory.ex …line 91
Nothing complicated, we are calling into Bodyguard.permit/4
and only proceeding with the deletion when we receive back an :ok
atom.
The final piece of the puzzle is to update the call to Inventory.delete_product
in the controller. We just need to pass thru the current user as we now require that in our context function.
/lib/warehouse_web/controllers/product_controller.ex …line 54
Let’s restart the server and see where we are at.
Terminal
We’ve created an admin
user betty
… if she deletes a product, all is good.
If we logout, sign in as bob
, re-create our Widget
product and then attempt a delete, we get:
A match error in our controller… which makes sense since we aren’t matching for the unauthorized
scenario. Let’s see how we can use a fallback controller to avoid having to explicitly match on {:error, :unauthorized}
.
Creating a fallback controller to handle unauthorized actions
One of the functions available to us on a controller is an action_fallback. We can make use of this to handle unauthorized actions.
Let’s create a fallback controller for this purpose.
Terminal
/lib/warehouse_web/controllers/fallback_controller.ex
Here we are matching on {:error, :unauthorized}
and rendering out to the ErrorView
.
We then need to specify this as the action_fallback
in the controller.
/lib/warehouse_web/controllers/product_controller.ex …line 5
And we then need to update the delete
function to use with
.
/lib/warehouse_web/controllers/product_controller.ex …line 56
With the above in place we now get the following when an unauthorized delete is attempted.
So that is pretty much it, everything seems to be working!
It probably makes sense to hide the Delete
link for users who aren’t authorized to perform a delete however. So before wrapping up for the day, let’s fix that up.
All we need to do is wrap the link in an if
statement.
/lib/warehouse_web/templates/product/index.html.eex …line 18
We’ve made use of the Bodyguard.permit?/4
function and only show the link in cases where the value returned is true
.
Bob
now no longer sees the link in the UI.
And that’s it for our simple authorization scenario!
A quick word about tests
I haven’t been sure how to handle testing for the purposes of this post. Testing is obviously very important but at the same time can require a fair bit of explanation which can add significantly to the length of a post and distract from the core subject.
Initially I was going to completely ignore testing, and indeed if you run mix test
you’ll see a number of tests will fail with the updates we’ve made to the code base.
This felt a little janky… so as a compromise I’ve updated the existing tests in the master branch of the GitHub repo so they pass; but there is no discussion of the tests (as you’ve no doubt noticed) and I haven’t added any new tests. Needless to say, with a real application this is not the strategy you would want to follow!
Summary
Similar to how Pow is not necessary for performing authentication in Phoenix, Bodyguard isn’t necessary for performing authorization. The README
even includes a link to a roll your own implementation. However, both of these packages are easy to use, flexible, and quick to set up. In my opinion they provide great solutions to implementing authentication and authorization in Phoenix; hopefully you’ll find them useful in your own applications!
Thanks for reading and I hope you enjoyed the post!