Bodyguard – Simple, Flexibile Authorization

Bodyguard (previously named Authy) is an authorization library that imposes a simple module naming convention to express authorization.

It supplies some handy functions to DRY up controller actions in Phoenix and other Plug-based apps.

It's inspired by the Ruby gem Pundit, so if you're a fan of Pundit, you'll see where Bodyguard is coming from.

Installation

  1. Add bodyguard to your list of dependencies in mix.exs:
```elixir
def deps do
[{:bodyguard, "~> 1.0.0"}]
end
```
  1. Add imports in web.ex to make convenience functions available.
```elixir
# lib/my_app/web.ex
defmodule MyApp.Web do
def controller do
quote do
import Bodyguard.Controller # <-- new
end
end
def view do
quote do
import Bodyguard.ViewHelpers # <-- new
end
end
end
```
  1. Add an error view case for handling 403 Forbidden, for when authorization fails.
```elixir
defmodule MyApp.ErrorView do
use MyApp.Web, :view
# ...
def render("403.html", _assigns) do # <-- new
"Forbidden"
end
end
```

Policies

Authorization logic is contained in a policy module – one module per resource to be authorized.

To define a policy for a Post, create a module Post.Policy with the authorization logic defined in can?(user, action, term) functions:

defmodule Post.Policy do
# Admin users are god
def can?(%User{role: :admin}, _action, _post), do: true
# Regular users can modify their own posts
def can?(%User{id: user_id, role: :user}, _action, %Post{user_id: post_user_id})
when user_id == post_user_id, do: true
# Other users (including guest user, nil) can only index and view posts
def can?(_user, :index, Post), do: true
def can?(_user, :show, _post), do: true
# Catch-all: deny everything else
def can?(_, _, _), do: false
end

The result of a can?/3 callback is flexibile:

Policy Scopes

Another idea borrowed from Pundit, policy scopes are a way to embed logic about what resources a particular user can see or otherwise access.

It's just another simple naming convention. Define scope(user, action, scope) functions in your policy module to utilize it. Each callback should return a subset of the passed-in scope argument.

defmodule Post.Policy
import Ecto.Query
# ...
# Admin sees all posts
def scope(%User{role: :admin}, _action, scope), do: scope
# User sees their posts only
def scope(%User{role: :user, id: id}, _action, scope),
do: where(scope, user_id: ^id)
# Guest sees published posts only
def scope(nil, _action, scope),
do: where(scope, published: true)
end

The scope argument can be a struct, module name, or Ecto query. If it's something else, you must pass the policy option since the policy cannot be inferred automatically.

If you have upgraded from v0.5.0 or earlier, this is new behavior. The function signature of the scope/3 callback has changed such that opts are no longer used in the callbacks. There will be a warning in the terminal if you try to pass in your own opts.

Permitted Attributes

The policy module can also specify which schema attributes may be modified by a given user. Define permitted_attributes(user, term) and return a list of atoms.

If you are using Ecto, the result can be passed into Ecto.Changeset.cast/3 to whitelist parameters in a changeset.

defmodule Post.Policy
# ...
# Admins can change anything
def permitted_attributes(%User{role: :admin}, _post) do
[:title, :body, :user_id]
end
# Post authors can only change the post body
def permitted_attributes(%User{id: user_id}, %Post{user_id: post_user_id})
when user_id == post_user_id do
[:body]
end
# Otherwise, blacklist everything
def permitted_attributes(_user, _post), do: []
end

Authorizing Controller Actions

The Bodyguard.Controller module contains helper functions designed to provide authorization in controller actions. You should probably import it in web.ex so it is available to all controllers.

The user to authorize is retrieved from conn.assigns[:current_user].

defmodule MyApp.PostController do
use MyApp.Web, :controller
alias MyApp.Post
# Bodyguard.Controller has been imported in web.ex
plug :verify_authorized # <-- is run at the END of each action
def index(conn, _params) do
posts = scope(conn, Post) |> Repo.all # <-- posts in :index are scoped to the current user
conn
|> authorize!(Post) # <-- authorize :index action for Posts in general
|> render("index.html", posts: posts)
end
def show(conn, %{"id" => id}) do
post = scope(conn, Post) |> Repo.get!(id) # <-- scope used for lookup
conn
|> authorize!(post) # <-- authorize the :show action for this post
|> render("show.html", post: post)
end
def new(conn, _params) do
conn = authorize!(conn, Post) # <-- authorize the :new action for posts
changeset = Post.changeset(%Post{})
conn
|> render("new.html", changeset: changeset)
end
def create(conn, %{"post" => post_params}) do
conn = authorize!(conn, Post) # <-- authorize the :create action for posts
changeset = Post.changeset(%Post{}, post_params)
# do insert...
end
def edit(conn, %{"id" => id}) do
post = scope(conn, Post) |> Repo.get!(id) # <-- scope used for lookup
changeset = Post.changeset(post)
conn
|> authorize!(post) # <-- authorize the :edit action for this post
|> render("edit.html", post: post, changeset: changeset)
end
def update(conn, %{"id" => id, "post" => post_params}) do
post = scope(conn, Post) |> Repo.get!(id) # <-- scope used for lookup
conn = authorize!(conn, post) # <-- authorize the :update action for this post
# do update...
end
def delete(conn, %{"id" => id}) do
post = scope(conn, Post) |> Repo.get!(id) # <-- scope used for lookup
conn = authorize!(conn, post) # <-- authorize the :delete action for this post
# do delete...
end
end

Handling "Not Found"

Note that if Repo.get! fails due to an invalid ID, the action will raise an exception and render a 404 Not Found page, which is the desired behavior in most cases.

nil data will not defer to any policy module, and will fail authorization by default. If the :policy option is explicitly specified, then that policy module will be used, passing nil as the term.

Handling authorize!/3 Errors

For Phoenix apps, presenting error views in MyApp.ErrorView is often enough.

For further customization, or for plain Plug apps, authorize!/3 raises directly to the router, so you can use Plug.ErrorHandler to catch errors caused by Bodyguard.

defmodule MyApp.Router do
use MyApp.Web, :router
use Plug.ErrorHandler # <-- new
defp handle_errors(conn, %{reason: %Bodyguard.NotAuthorizedError{}}) do
# redirect or do whatever you want
end
end

Controller-Wide Authorization

For more sensitive controllers (e.g. admin control panels), you may not want to leak the details of a particular resource's existence. In that case, you can pre-authorize before even attempting to fetch the record, additionally authorizing that particular resource once it has been retrieved from the database.

To lock down an entire controller using this technique, use authorize! as a plug. Keep in mind you will have to implement can?/3 callbacks where term is the module name, even for member actions like :show and :edit.

defmodule MyApp.ManageUserController do
plug :authorize!, User # <-- pre-authorize all actions
# ...
end

If you have options that are the same throughout an entire controller, there's a plug for that:

defmodule MyApp.DraftController do
plug :put_bodyguard_options, policy: Post.DraftPolicy
# All actions will use Post.DraftPolicy unless overridden
end

Nested Resources

To authenticate a nested resource, it is common to authorize the parent resource before performing the child resource's action. This can also be accomplished via a controller plug.

If the authorization check consists of a simple foreign key comparison (e.g. current_user can only modify a resource if its user_id equals current_user.id), then the resource struct can be constructed in memory without requiring a round-trip to the database.

# router.ex
resources "/companies", CompanyController do
resources "/users", UserController
end
# user_controller.ex
defmodule MyApp.UserController do
plug :authorize_company!
defp authorize_company!(%{params: %{"company_id" => company_id}} = conn) do
# Create this Company in-memory since we only care about its ID
company = %Company{id: company_id}
# Authorize the :update action of the parent company as a generic
# policy for this company's user actions
authorize!(conn, company, action: :update)
end
end

Additional Options

View Helpers

Authorization may be performed in views via the Bodyguard.ViewHelpers.can?/4 function, which you should import into your view modules.

<%= if can?(@conn, :delete, post) do %>
<%= link "Delete", to: post_path(@conn, :delete, post), method: :delete %>
<% end %>

The first argument can be either a Plug.Conn or a user model. The :policy option may be provided to override the default policy.

Authorization Outside of Controllers

Policies are just plain old modules, so you can call them directly:

Post.Policy.can?(user, :edit, post) # <-- returns boolean
Post.Policy.scope(user, :index) # <-- returns query for posts

Or you can use the core Bodyguard module to determine the policy module automatically.

Bodyguard.authorized?(user, :edit, post) # <-- defers to Post.Policy.can?/3
Bodyguard.scoped(user, :index, Post) # <-- defers to Post.Policy.scope/3

Common Patterns

Policy Helpers

Consider creating a generic policy helper to collect authorization logic that is common to many different parts of your application. Reuse it by importing it into more specific policies.

defmodule MyApp.PolicyHelper do
# common functions here
end
defmodule MyApp.Post.Policy do
import MyApp.PolicyHelper
# ...
end

Controller Policies

What if you have a Phoenix controller that doesn't correspond to one particular resource? Or, maybe you just want to customize how that controller's actions are locked down.

Try creating a policy for the controller itself. MyApp.FooController.Policy is completely acceptable.

Not What You're Looking For?

Check out these other libraries:

License

MIT License, Copyright (c) 2016 Rockwell Schrock

Acknowledgements

Thanks to Ben Cates for helping maintain and mature this library.