Can
Dead simple, fire and forget authorization kit for the Phoenix framework
Installation
Add Can to your list of dependencies in mix.exs:
def deps do
[{:can, "~>0.0.3"}]
endUsage
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
endStep 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
endStep 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
-
if no argument or
nilis passed -> the policy will be based off the controller's name - if changeset or model struct is passed -> the policy will be based off the model's name
def MyApp.PagePolicy do
def show(conn, page) do
conn.assign.current_user.id == page[:author_id]
end
endAlternative 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
endYou 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.