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
end
We 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 ...
end
I 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
end
I 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 ...
end
In 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
end
I 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
end
Here 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.