Joseph Kain bio photo

Joseph Kain

Professional Software Engineer learning Elixir.

Twitter LinkedIn Github

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.