Can

Dead simple, fire and forget authorization kit for the Phoenix framework

Build Status

Installation

Add Can to your list of dependencies in mix.exs:

def deps do
  [{:can, "~>0.0.3"}]
end

Usage

Generally, there are two things you need to explicitly implement in your application.

For this controller and action

defmodule MyApp.PageController do
  def show(conn, %{"id" => id}) do
    page = Repo.get(Page, id)
    render(conn, "show.html", page: page)
  end
end

Step 1: Use the can macro and add an unauthorized_handler

defmodule MyApp.PageController do
  use MyApp.Web, :controller
  use Can, :unauthorized_handler

  def show(conn, %{"id" => id}) do
    page = Repo.get(Page, id)

    can(conn, page) do
      render(conn, "show.html", page: page)
    end
  end

  def unauthorized(conn, resource, policy) do
    conn
    |> put_flash(:error, "You are unauthorized because #{policy} did not return true for author id #{resource[:author_id]}")
    |> render("show.html", page: resource)
  end
end

Step 2: Add the policy module and function

Can will try to find the policy based on the second argument and the following pattern, therefore we need to adhere to a convention set by Phoenix

def MyApp.PagePolicy do
  def show(conn, page) do
    conn.assign.current_user.id == page[:author_id]
  end
end

Alternative Handler

The unauthorized handler can also be done in a separate module if you wish so.

This effectively separates the handler and the controller, and makes pattern matching against the policy clean, readable and reusable.

defmodule MyApp.PageController do
  use MyApp.Web, :controller
  use Can, {MyApp.UnauthorizedHandler, :unauthorized}

  def show(conn, %{"id" => id}) do
    page = Repo.get(Page, id)

    can(conn, page) do
      render(conn, "show.html", page: page)
    end
  end
end

defmodule MyApp.UnauthorizedHandler do
  import Phoenix.Controller

  def unauthorized(conn, resource, PagePolicy) do
    conn
    |> put_flash(:error, "You are unauthorized because #{policy} did not return true for author id #{resource[:author_id]}")
    |> render("show.html", page: resource)
  end

  # wildcard
  def unauthorized(conn, _resource, policy) do
    conn
    |> put_flash(:error, "You are unauthorized because #{policy} did not return true")
    |> render(MyApp.ErrorView, "401.html")
  end
end

You can write your own authorization logic as complex or as simple as you wish. It is necessary however, at the end of your authorization logic, it has to return a boolean value.

In the case when the authorization logic returns true, the connection proceeds normally. If the authorization logic return false, the unauthorized handler will be called instead.

Documentation

See documentation on hexdocs for API reference and usage details.