In general, authorization is the definition of access policies and scoping. For example, if you are building a blogging content management system, you could define which posts a user is allowed to edit, which posts a user is allowed to see etc. If you are coming from the Rails world, you might have been familiar with some libraries that do this, e.g. cancancan and pundit.

Libraries

While it is definitely possible to roll out something by hand, it usually makes sense to not re-invent the wheel if there are well maintained and tested libraries available. canada and bodyguard are two of the more popular ones that I have seen around in the community. I especially like the API of Bodyguard that has been able to provide a lot of complex policy scoping options as compared to Pundit, so I will be focusing mainly on Bodyguard in this post.

Bodyguard

There are two parts that we need to keep in mind for authorization.

  1. Policy - Is this user allowed to perform this operation on this object?
  2. Scope - Which objects is this user allowed to see?

Let us see each of these in detail in the next sections.

Policy

Let’s start with a point about how to implement a policy from the official documentation. Add @behaviour Bodyguard.Policy to a context, then define authorize(action, user, params) callbacks, which must return:

  • :ok or true to permit an action.
  • :error, {:error, reason}, or false to deny an action.

Although it is possible to do it directly on the context, I prefer doing it in a separate module to keep the context clean. This is how it looks in practice:

defmodule MyApp.Blog.Policy do
  @behaviour Bodyguard.Policy

  alias MyApp.Accounts.User

  # Super Admins can do anything
  def authorize(_action, %User{role: :super_admin}, _params), do: true

  # Admin users can list/get/create anything
  def authorize(action, %User{role: :admin}, _params) when action in ~w[list_posts get_post]a, do: true

  # Admin users can update own organization's posts
  def authorize(action, %User{role: :admin} = user, %{organization_id: organization_id})
    when action in ~w[update_post create_post]a
  do
    MyApp.Accounts.can_user_access_organization?(user.id, organization_id)
  end

   # Default blacklist
  def authorize(_action, _user, _params), do: false
end

defmodule MyApp.Blog do
  # --------------------
  # The context code ...
  # --------------------

  defdelegate authorize(action, user, params), to: MyApp.Blog.Policy
end

After this is done, it is easy to authorize actions by calling Bodyguard.permit(MyApp.Blog, :list_posts, user) (with an optional fourth argument passing in the actual resource). This can be done in your controller or, if you are writing a GraphQL API, in the resolver. Here is how the tests for the above policy look like:

test "allows admins to list posts", %{organization: organization} do
  admin = MyApp.Accounts.Fixtures.fixture(:user_admin, %{organization_ids: [organization.id]})
  assert :ok = Bodyguard.permit(MyApp.Blog, :list_posts, admin)
end

test "does not allow customers to list/update/delete/create posts", %{user: user, organization: organization} do
  post = post_fixture(organization)
  assert {:error, :unauthorized} = Bodyguard.permit(MyApp.Blog, :list_posts, user)
  assert {:error, :unauthorized} = Bodyguard.permit(MyApp.Blog, :create_post, user)
  assert {:error, :unauthorized} = Bodyguard.permit(MyApp.Blog, :update_post, user, post)
end

test "allows admin to update/delete posts of own organization", %{organization: organization} do
  post = post_fixture(organization)
  user = MyApp.Accounts.Fixtures.fixture(:user_admin, %{organization_ids: [organization.id]})
  assert :ok = Bodyguard.permit(MyApp.Blog, :update_post, user, post)
end

test "allows admin to create posts", %{organization: organization} do
  user = MyApp.Accounts.Fixtures.fixture(:user_admin, %{organization_ids: [organization.id]})
  assert :ok = Bodyguard.permit(MyApp.Blog, :create_post, user, %{organization_id: organization.id})
end

test "does not allow admin to update/delete posts of another organization", %{organization: organization} do
  user = MyApp.Accounts.Fixtures.fixture(:user_admin, %{organization_ids: [organization.id]})

  organization2 = MyApp.Accounts.Fixtures.fixture(:organization, %{name: "Organization 2"})
  post = post_fixture(organization2)

  assert {:error, :unauthorized} = Bodyguard.permit(MyApp.Blog, :update_post, user, post)
end

test "allows super_admin to do anything", %{organization: organization} do
  post = post_fixture(organization)
  user = MyApp.Accounts.Fixtures.fixture(:user_super_admin)
  assert :ok = Bodyguard.permit(MyApp.Blog, :create_post, user)
  assert :ok = Bodyguard.permit(MyApp.Blog, :update_post, user, post)
  assert :ok = Bodyguard.permit(MyApp.Blog, :list_posts, user)
end

Scope

In addition to policies, Bodyguard also provides a way to provide default scoping for querying items that a user can access. This can be done by implementing scope(query, user, params) inside an Ecto.Schema from the @behaviour Bodyguard.Schema. The function should filter the query down to include only the objects the user is allowed to access. You can also pass custom params when invoking the scoping to provide further filtering.

Here is how it looks in practice:

defmodule MyApp.Blog.Post do
  use Ecto.Schema

  def scope(query, user, %{organization: organization}) do
    query
    |> where(organization_id: organization.id)
    |> scope_published(user, params)
  end

  defp scope_published(query, %MyApp.Accounts.User{role: :admin}, _params), do: query
  defp scope_published(query, %MyApp.Accounts.User{role: :super_admin}, _params), do: query
  defp scope_published(query, _user, _params), do: query |> where(is_published: true)
end

All this does is defines a scope that says that users can only view posts of a particular organization (which is being sent in the params) and with additional filtering by the is_published status based on the user’s role.

This can then be used inside your context while querying. For example:

defmodule MyApp.Blog do
  def list_posts(user, organization) do
    Post
    |> Bodyguard.scope(user, %{organization: organization})
    |> Repo.all()
  end
end

Conclusion

In this post, we saw how easy it is to use Bodyguard to include policy and scoping based authorization to the app. This is by no means a task that should require an external library, so if you don’t want to, it is very easy to roll your own solution. But I have been using Bodyguard in a production app for a while now and I can vouch for the customizability that it offers in terms of authorization and stays out of the way when we don’t want it. It also grants a clarity to these operations that would otherwise have been scattered inside the context methods.