Authorization and Policy Scopes for Phoenix Apps
Tags: elixir
Published: 2021-11-16 15:09:20

Authorization (not to be confused with authentication) is vital to every application but often isn't given much thought before implementation. The [IETF Site Security Handbook](https://datatracker.ietf.org/doc/html/rfc2196) defines authorization as:

> The process of granting privileges to processes and, ultimately, users. This differs from authentication in that authentication is the process used to identify a user. Once identified (reliably), the privileges, rights, property, and permissible actions of the user are determined by authorization.

So, in short, authorization is about defining access policies and scoping.

For example, consider a platform like Github.
In very simple terms, it must handle which repositories:

- a particular user is allowed to read (this is policy scoping)
- the user is allowed to write to (authorization)

If you come from the Rails world, you might be familiar with some gems that provide APIs to handle this, the most popular ones being [cancancan](https://github.com/CanCanCommunity/cancancan) and [pundit](https://github.com/varvet/pundit).

In today's post, we'll take a close look at the two critical components of authorization — access policies and scoping. I'll show you how you can roll out your own solution for each in Phoenix and how to leverage the Bodyguard library for quick and easy authorization.

## Authorization in Phoenix

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

1. Access policy — Is *this user* allowed to perform *this* operation on *this resource*?
2. Policy scope — *Which resources* is *this user* allowed to see?

While it is definitely possible to roll out something by hand, it usually makes sense not to reinvent the wheel if well-maintained and tested libraries are available.
[Canada](https://github.com/jarednorman/canada) and [Bodyguard](https://hexdocs.pm/bodyguard/Bodyguard.html) are two of the more popular ones that I have seen in the community.

Let's see what implementation might look like for our own solution and also with Bodyguard. We will use a CMS example similar to what the [official Phoenix Context guide](https://hexdocs.pm/phoenix/contexts.html) uses. This CMS allows users to create pages and share them with everyone. Only the author should be able to edit, update, or delete a page once created, but everyone else should see the page.

## Implementing Access Policies

Getting back to our CMS example — when the user is on a page, we need an access policy that decides if the user is allowed to perform an action (say, `edit`) on the page.

### Roll Your Own Access Policy

The following is what the official Phoenix guide suggests. This is also what most of us would do, were we rolling out our own authorization solution:

```elixir
defmodule Hello.CMS.PageController do
  plug :authorize_page when action in [:edit, :update, :delete]

  defp authorize_page(conn, _) do
    page = CMS.get_page!(conn.params["id"])

    if conn.assigns.current_author.id == page.author_id do
      assign(conn, :page, page)
    else
      conn
      |> put_flash(:error, "You can't modify that page")
      |> redirect(to: Routes.cms_page_path(conn, :index))
      |> halt()
    end
  end
end
```

The implementation is straightforward. We use the `:authorize_page` plug for edit, update, or delete actions. In that plug, we allow the action only if the page's author is the same as the current user. Otherwise, we redirect to an index page that shows an error.

### Use Bodyguard
We can also implement an access policy using `Bodyguard.Policy` behavior. Depending on the level of access scoping you need, this behavior could be placed on the controller or directly on the underlying context. I usually like to define a separate `Policy` module to handle this and then delegate the methods from the behavior's target:

```elixir
defmodule Hello.CMS.Policy do
  @behaviour Bodyguard.Policy

  alias Hello.Accounts.User

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

  # Users can list/get/create anything
  def authorize(action, %User{role: :user}, _params) when action in ~w[index show create]a, do: true

  # Users can edit/update/delete own pages
  def authorize(action, %User{id: id, role: :user} = user, %{author_id: ^id})
    when action in ~w[update edit delete]a,
    do: true

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

Here, we see that we defined `authorize(action, user, params)` that returns `true`/`false` to permit (or not) the action on the resource. You can also get additional control on the error messages by returning  `{:error, reason}` instead of just `false`.

The `Bodyguard.Policy` behavior expects the callbacks to return:

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

Then, on the context or the controller, all you need is to delegate the `authorize` method to our `Policy` module and then call `Bodyguard.permit`:

```elixir
defmodule Hello.CMS.PageController do
  plug :authorize_page

  defp authorize_page(conn, _) do
    page = CMS.show(conn.params["id"])

    case Bodyguard.permit(__MODULE__, action, user, page) do
      :ok ->
        assign(conn, :page, page)
      {:error, _reason} ->
        conn
        |> put_flash(:error, "You can't access that page")
        |> redirect(to: Routes.cms_page_path(conn, :index))
        |> halt()
    end
  end

  defdelegate authorize(action, user, params), to: Hello.CMS.Policy
end
```

To authorize an action, we can call `Bodyguard.permit(Hello.CMS.PageController, action, user)`.
`Bodyguard.permit/4` also accepts passing a fourth argument's authorization in the actual resource.

This form can be used to authorize actions performed on a single resource (for example, `update` or `delete`).

Under the hood, `Bodyguard.permit` calls `authorize` on the module we provided as the first argument.

The return value is then normalized into `:ok` or `{:error, reason}` (regardless of whether we were returning `true`/`:ok` or `false`/`:error`/`{:error, reason}` from that method).

While delegating `authorize` from the controller works well, there might be cases when you need similar access control in multiple places. For example, the same resource could be accessed from your controller and through an [Absinthe Resolver](https://hexdocs.pm/absinthe/schemas.html) exposed through the GraphQL API.

For such cases, I suggest delegating `authorize/3` on the Phoenix context module:

```elixir
defmodule Hello.CMS do
  # ...
  defdelegate authorize(action, user, params), to: Hello.CMS.Policy
end
```

Then, when you need to call `Bodyguard.permit` from your controller (or the Absinthe Resolver), pass in a first argument of that context module:

```elixir
defmodule Hello.CMS.PageController do
  defp authorize_page(conn, _) do
    page = CMS.show(conn.params["id"])
    case Bodyguard.permit(CMS, action, user, page) do
      :ok ->
        # OK. Render page
      {:error, reason} ->
        # Error. Show flash and redirect
    end
  end
end
```

It is also very easy to write tests for the above policy. Here's what the tests might look like:

```elixir
test "allows users to list pages" do
  user = Hello.Accounts.Fixtures.fixture(:user)
  assert :ok = Bodyguard.permit(Hello.CMS, :index, user)
end

test "allows user to update/delete own pages" do
  user = Hello.Accounts.Fixtures.fixture(:user)
  page = page_fixture(user)
  assert :ok = Bodyguard.permit(Hello.CMS, :update, user, page)
end

test "allows user to create pages" do
  user = Hello.Accounts.Fixtures.fixture(:user)
  assert :ok = Bodyguard.permit(Hello.CMS, :create, user)
end

test "does not allow user to update/delete pages of someone else" do
  user1 = Hello.Accounts.Fixtures.fixture(:user)
  user2 = Hello.Accounts.Fixtures.fixture(:user)

  page = page_fixture(user2)

  assert {:error, :unauthorized} = Bodyguard.permit(Hello.CMS, :update, user1, page)
end

test "allows super_admin to do anything" do
  super_admin = Hello.Accounts.Fixtures.fixture(:user_super_admin)
  user = Hello.Accounts.Fixtures.fixture(:user)
  page = page_fixture(user)
  assert :ok = Bodyguard.permit(Hello.CMS, :index, super_admin)
  assert :ok = Bodyguard.permit(Hello.CMS, :create, super_admin)
  assert :ok = Bodyguard.permit(Hello.CMS, :update, super_admin, page)
end
```

## Implementing Policy Scopes

We've now allowed users to access resources based on some attributes. The next part of the puzzle is to handle the listing of resources. As you might have noticed above, we are allowing users to list everything.

But you might have specific business requirements on what a user can and can't list. For example, in the Github example, users can only see public repositories and the repositories that they have access to (e.g., through their organization or team). Similarly, you could set a restriction that users see all published posts but only their own drafts in a CMS.

### Roll Your Own Policy Scoping

This just boils down to using the correct queries based on the user and their access rights. For example, a simple implementation inside a context could look like this:

```elixir
defmodule Hello.CMS do
  def list_pages(%{id: id} = user) do
    Page
    |> where(author_id: ^id)
    |> or_where(state: :published)
    |> Repo.all()
  end
end
```

It is definitely possible to do this with simple Ecto queries on the accessor methods. However, it usually makes much more sense to centralize these kinds of domain requirements so that future developers don't forget to include one of the requirements while adding a new feature.

### Use Bodyguard

With Bodyguard, we can provide default scoping to query items that a user can access. Implement a `scope/3` function inside an `Ecto.Schema` module from the `@behaviour Bodyguard.Schema`. The function should filter the `query` down to only include the resources the user is allowed to access. You can also pass custom params when invoking the scoping to provide further filtering.

Here's how it looks in practice:

```elixir
defmodule Hello.CMS.Page do
  use Ecto.Schema

  alias Hello.Accounts.User

  def scope(query, user, params) do
    scope_published(query, user, params)
  end

  # Signed in users can access published posts or their own posts (in any state)
  defp scope_published(query, %User{role: :user, id: id}, _params) do
    query
    |> where(author_id: ^id)
    |> or_where(state: :published)
  end

  # Super admins can access anything
  defp scope_published(query, %User{role: :super_admin}, _params), do: query

  # Anonymous users can access only published posts
  defp scope_published(query, _user, _params), do: query |> where(state: :published)
end
```

Now, this can then be used inside your context when querying. For example:

```elixir
defmodule Hello.CMS do
  def list_pages(user) do
    Page
    |> Bodyguard.scope(user) # <-- defers to Page.scope/3
    |> Repo.all()
  end
end
```

If we need to add another listing of the pages later, we can simply use the same `Bodyguard.scope(query, user)` call and know that everything will be appropriately scoped.

## Authorization in Phoenix: Further Reading

In this post, we saw how to implement authorization in your Phoenix apps.

We focused on a simple CMS example to explore authorization with — and without — external libraries.

I recommend [Dockyard's Authorization Considerations For Phoenix Contexts blog post](https://dockyard.com/blog/2017/08/01/authorization-for-phoenix-contexts) for a more detailed look at rolling out your authorization solution.

If you are looking for external libraries, [check out Bodyguard](https://hexdocs.pm/bodyguard/Bodyguard.html).
I have been using it in production for a while and can vouch for its customizability in authorization. It stays out of the way when we don't want it and also clarifies operations that would otherwise be scattered inside the context and controller methods.

I hope you've found this a useful guide that's inspired you to dive into policy scoping and authorization in Phoenix apps.

Until next time, happy coding!

**P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, [subscribe to our Elixir Alchemy newsletter and never miss a single post!](https://blog.appsignal.com/category/elixir-alchemy.html#elixir-alchemy)**

*Sapan Diwakar is a full-stack developer. He writes about his interests [on his blog](https://sapandiwakar.in/) and is a big fan of keeping things simple, in life and in code. When he’s not working with technology, he loves to spend time in the garden, hiking around forests, and playing outdoor sports.*