Last week I wrote a bit about how I use Guardian and Canary together. This week I want to go into more depth on how I’m using Canary.
Routes
In the previous post, I went over a few of the routes I’m using. Let’s review the routes for building and editing rentals:
scope "/", MyApp do
pipe_through [:browser, :browser_session, :require_login]
resources "/rentals", RentalController do
resources "/addresses", AddressController, only: [:create, :update, :delete]
resources "/images", ImageController, only: [:create]
end
endWe can create or edit rentals as well as create images and addresses for those rentals. That is,
Rentals have many Images and Images belong to a Rental. Similarly,
Rentals have one address and an Address belongs to a Rental.
The Rental Controller
Here’s part of the Rental controller that I’m using
defmodule AgoraBase.RentalController do
use AgoraBase.Web, :controller
alias AgoraBase.Rental
plug :scrub_params, "rental" when action in [:create, :update]
plug :authorize_resource, model: Rental
# Actions follow ...
endI use Canary’s :authorize_resource plug to check if the user is authorized to
modify this rental. But how does Canary know which users are authorized and
which aren’t?
Canary uses the Canada package to do this and Canada defines a protocol that I
can implement for my User type. That protocol includes the function can?/3
which allows Canary to ask if my User can? perform a given action.
I’ve implemented the Canada.Can protocol for my User like this:
defimpl Canada.Can, for: MyApp.User do
alias MyApp.User
alias MyApp.Rental
def can?(user, action, Rental) when action in [:new, :create] do
user.role == "owner"
end
def can?(user, action, rental = %Rental{}) do
if rental.owner_id == user.id, do: true, else: false
end
def can?(subject, action, resource) do
raise """
Unimplemented authorization check for User! To fix see below...
Please implement `can?` for User in #{__ENV__.file}.
The function should match:
subject: #{inspect subject}
action: #{action}
resource: #{inspect resource}
"""
end
endI decided to store this file in web/modules/user_can.ex. And in this file I’ve implemented a
few different cases for can?/3. I also thought that it might be
confusing when things blow up due to cases being unimplemented. To make this
easier to debug I included the final case which raises
an exception with a helpful error message. The message will let me
know which file needs to be updated and what to match on. This has already been
pretty helpful for me as I’ve been implementing the app.
The Image Controller
Here’s a portion of ImageController which is used to create new Image records:
defmodule AgoraBase.ImageController do
use AgoraBase.Web, :controller
alias AgoraBase.Rental
alias AgoraBase.Rental.Image
plug :scrub_params, "image"
plug :authorize_resource, model: Rental, id_name: "rental_id", persisted: true
# Actions follow ...
endIn this case an Image belongs to a Rental so I’ve used Canary’s support for
nested models. I do this with the :authorize_resource plug by setting the
model explicitly to Rental, and specifing the id name as "rental_id". This lets
Canary find the Rental in a route like /rentals/:rental_id/image. Finally, I
set the :persisted option to true to enable the nested resource support.
In this case Canary will invoke can?/3 on the Rental itself. This is different
than checking if the User can create a Rental. In this case Canary will
pass in the Rental struct identified by rental_id. As we can see back in my
implementation of can?/3
def can?(user, action, address = %Rental.Address{}) do
address.rental.owner_id == user.id
endI check that the User is the owner of the rental.
The Address Controller
A Rental also has an address. Here’s the controller I use to set it up:
defmodule MyApp.AddressController do
use MyApp.Web, :controller
alias MyApp.Rental
alias MyApp.Rental.Address
plug :scrub_params, "address" when action in [:create, :update]
plug :load_and_authorize_resource, model: Rental,
id_name: "rental_id",
persisted: true,
preload: :address
def create(conn, %{"address" => address_params}) do
rental = conn.assigns.rental
# ...
end
def update(conn, %{"address" => address_params}) do
rental = conn.assigns.rental
changeset = Address.changeset(rental.address, address_params)
# ....
end
endHere I’ve done things a little differently. Again, I’ve use the nested resource
case. The permissions for creating or updating and Address are basically the
same as for creating an Image. But, I’ve also switched over to using the
:load_and_authorize_resource instead of just :authorize_resource. This
helps me to simplify my controller actions.
I like to think about :load_and_authorize_resource this way: Canary has to load
up the Rental data in order to check the authorization. My controller also
needs the Rental so I should be able to use the Rental copy loaded by Canary.
:load_and_authorize_resource lets me do exactly that. When Canary does the
authorization is stores the Rental in conn.assigns.rental. You can see where
I access this in the create action.
In AddressController I also need the rental.address association preloaded. The :preload
options to :load_and_authorize_resource takes care of this. And you can see
that I use rental.address when building a changelist in the update action.
I should have probably used :load_and_authorize_resource in my
RentalController as well. I may go back and do that later.