Any particular reason you are using Plug instead of Phoenix? There would be no notable difference for your use case as long as you uniquely serve an api on the same Plug app, as otherwise you might need Phoenix’ pipelines to run specific plugs on a specific scope - thus being able to serve anything else without interfering.
On to your question, there surely are many ways of doing this but you pretty much always end up with the need of creating custom plug-ables. How I do this is, I have a module with useful functions that can be plugged to perform specific actions - as this way you have things more compact as opposed to having a module for each plug you might need.
defmodule Something.Plugs do
import Ecto.Query
alias Something.{Repo, User}
def fetch(conn, params) do
id = conn.params["id"] # /api/posts/:id
schema = params[:schema] # Post
...
cond do
post ->
conn
|> assign(:post, post) # :post would instead be based off the schema's module name
true ->
error :not_found
end
end
def authorize(conn, params) do
cond do
user = token_valid?(conn) ->
route = conn.private[:phoenix_controller]
action = get_action(conn)
cond do
apply route, :can?, [user, action, conn.assigns] -> # Route.can?
conn
|> assign(:user, user)
true ->
error :forbidden
end
true ->
error :unauthorized
end
end
end
defp get_action(conn) do
conn.private[:phoenix_action]
end
defp token_valid?(conn) do
conn
|> get_token
|> get_user
end
defp get_token(conn) do
conn
|> get_req_header("authorization")
|> List.first
end
defp get_user(token) do
cond do
token ->
User
|> where(token: ^token)
|> Repo.one
true ->
false
end
end
defexception plug_status: 500
defp error(atom) do
raise __MODULE__, plug_status: atom |> Plug.Conn.Status.code
end
end
This example may seem a bit off or complicated, but it’s really simple and may serve you as a reference. How this works is, you do plug :authorize
and this would check whether a request has an Authorization header with a valid token and fetch whichever user it’s linked to - this obviously may differ depending on how you do tokens and store your data. Once token_valid?
returns, if the value is false (meaning there’s no user for the input token or there’s no token at all) it raises an :unauthorized
error, and if the value is indeed an user it grabs the current action (:get, :post, …) and calls a can?
function, which altogether follows a Canada-like behaviour, on your current route that allows you to decide whether a specific user is allowed or not to perform an action on a fetched resource. This function is provided with conn.assigns
, which contains all the resources that the :fetch
plug would have stored.
Say the following was your route, which for reference may be configured like on this example.
defmodule Something.Route do
use Phoenix.Controller
plug :fetch, schema: Post # query a Post with the given id param
plug :authorize
def show(conn, params) do
# we are authorized!
# post = conn.assigns[:post]
end
def can?(user, action, resources) do
# action = :get
post = resources[:post]
post.owner == user.id
end
end
Concluding, keep in mind Repo is an Ecto repository and both User and Post are Ecto schemas. I would also need to mention that what error :unauthorized
does is raising an error which then Phoenix catches and stops going further down the pipeline. There are many ways of doing this, but I found this to be more intuitive - you may find an explanation down here.
Hope you at least get a good idea on how to handle this kind of functionality! It was a bit hard to keep this short and compact, and so I will be expecting questions in case there’s any. I have thought in the past about publishing a set of useful plugs like these just so beginners have an example to work off, and might be releasing it soon.