I’m trying to do it like you describe in approach B. I define “policies” for protected resources and call them from the context.
@spec create_transaction(map, by: %User{}) :: {:ok, %Transaction{}} | {:error, Ecto.Changeset.t} | no_return
def create_transaction(attrs, by: %User{id: uid} = user) do
Policy.authorize!(:create_transaction, user)
%Transaction{user_id: uid}
|> Transaction.changeset(attrs)
|> Repo.insert()
end
and then in ledger/policy.ex
I have something like this
defmodule Ledger.Policy do
alias MyApp.Authorization.UnauthorizedError
@typep action :: :create_transaction
@typep actor :: %User{}
@spec authorized?(action, actor) :: boolean
defp authorized?(:create_transaction, %User{role: :admin}), do: true
defp authorized?(:create_transaction, _user}), do: false
# usually I have authorize!(action, target, actor) though
@spec authorize!(action, actor) :: nil | no_return
def authorize!(action, actor) do
unless authorized?(action, actor) do
raise(UnauthorizedError, message: "nah ah")
end
end
end
And the exception has a plug_status
to be rendered correctly on raise
defmodule MyApp.Authorization.UnauthorizedError do
defexception message: "you are not authorized to access this resource :(",
plug_status: 403
end
A lot of boilerplate though … It also causes a stacktrace to be generated which might hurt performance.
Can you show how Approach A would work? I don’t quite get it.
I’m also thinking about how I could move some of the above to the database. Because I don’t know how to handle get
queries like
def get(resource_id, by: _user) do
# authorize somehow
Repo.get(Resource, resource_id)
end
since the result of my authorization depends on the resource being fetched.
btw, the controllers end up looking the same as if there was no authorization
def create(conn, %{"transaction_params"= > params}, current_user) do
case Ledger.create_transaction(params, by: current_user) do
{:ok, %Ledger.Transaction{} = transaction} ->
render(conn, "show.html", transaction: transaction)
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end