Per-controller resource authorization in elixir phoenix

by Glenn square thumb Glenn Jones, published on 19/08/16

In a current project for our webstudio Nebulae2016, I have the need to authorize resources on a per-controller basis. In Rails (my background) this was made easy by the communities at Cancancan and Pundit. That’s role based authentication.

My authorization is single-role only (user), for now. The basic need is to check that the current user is the owner of the resource it is trying to access.

Asking what would be the best way to go about this on the elixir Slack, I was recommended to write a simple plug. I learned more about plugs here and how to create them here.

What libs I’m using

How it would be ideally implemented

I was after the following syntax:

plug ProjectName.AuthorizeResource, %{resource: ProjectName.Form} when action in [:show, :edit, :update, :delete]

(this assumes I have a model called ProjectName.Form).

Rails-like, I would like to place the plug at the top of my controller. With this ‘structure’, I’m able to define the resource to be authorized and for which actions. Plugs are commonly used in the routes section (in pipelines) but they can also be used in controllers.

Code

defmodule ProjectName.AuthorizeResource do
  @moduledoc "Validates that a resource belongs to a user. Must provide the attribute 'resource' as option. Implemented for Form."
  require Logger

  use Phoenix.Controller

  import Plug.Conn
  import Addict.Helper

  alias ProjectName.Form
  alias ProjectName.Repo

  def init(opts) do
    opts
  end

  def call(conn, opts \\ %{}) do
    # get resource type, get its id, get current user
    resource = Map.fetch!(opts, :resource)
    %Plug.Conn{params: %{"id" => resource_id}} = conn
    user_id = current_user(conn).id
    Logger.debug("Authorizing usage of resource: #{resource} with id: #{resource_id} by user with id: #{user_id}")

    # depending on the resource type, do validation
    case resource do
      Elixir.ProjectName.Form ->
        form = Form |> Form.by_id(resource_id) |> Repo.all |> List.first
        case user_id == form.user_id do
          true ->
            Logger.debug("Resource authorization succesfull.")
            conn
          false ->
            Logger.debug("Resource authorization unsuccesfull.")
            conn
            |> put_flash(:error, "Oh no, that form does not seem to belong to you.")
            |> redirect(to: "/forms")
            |> halt
        end
      _ ->
        raise("No resource type provided")
    end
  end
end

Because the plug handles the conn, we can extract the current_user.id by using the current_user(conn) helper as provided by addict. I’m sure most other authentication libraries however also provide a way to identify the current user from the conn.

Afterwards I use the method Form.by_id(resource_id), which is a way to chain ecto query methods. I saw and interpreted that from Drew Olson. Below you’ll find the method in it’s larger Ecto Model context.

defmodule ProjectName.Form do
  use ProjectName.Web, :model
  use Ecto.Schema

  schema "forms" do
    belongs_to :user, ProjectName.User
    has_many :responses, ProjectName.Response
    field :key, :string
    field :domain, :string
    field :name, :string
    field :description, :string

    timestamps()
  end

  @doc "Select form by its id"
  def by_id(query, form_id) do
    from f in query,
    where: f.id == ^form_id,
    select: %{id: f.id, user_id: f.user_id, key: f.key, domain: f.domain, name: f.name, description: f.description}
  end

end

Handling the redirect if authorization fails

false ->
    Logger.debug("Resource authorization unsuccesfull.")
    conn
    |> put_flash(:error, "Oh no, that form does not seem to belong to you.")
    |> redirect(to: "/forms")
    |> halt

Firstly, I place a debug message. I’ve found that over time, it pays to have useful debug messages in all steps of the application. Afterwards, I place a flash message on the connection to explain the user as to why he is being redirected and the I redirect to the forms/index, in this case /forms. Afterwards I had to call halt/1, to stop getting an (Plug.Conn.AlreadySentError) the response was already sent error. I found this out here.

Considerations

This is a fairly provisional solution. It works, but it won’t scale. For example, once a user can possess many different resources, this type of authorization becomes very verbose.

Also, as said earlier, this solution does not incorporate roles. That could be incorporated in the above plug, keeping the same structure where you provide a resource + role in the controller to do the authorization. That however makes the solution even more verbose, especially when there is many more resources the user can possess.

All in all this is a good provisional solution for me (it works!) but I can’t see it scale very well. If you have any suggestions on libraries that exist for this need or how the problem of adding ‘scaleability’ can be solved, please let me know (glenn@gpjones.org).

Hope this was useful!

>> Default arguments in an elixir plug

<< Workflow for setting up elixir phoenix channel

Back to glennjon.es

comments powered by Disqus