Hello 👋 Welcome to my corner of the internet. I write here about the different challenges I encounter, and the projects I work on. Find out more about me.
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.
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.
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
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.
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!