A few months ago, Miguel and I got together and reimplemented much of Dictator.
If you don't know, Dictator is a tiny library that authorises users based on policies. Here's how you would authorise users creating and updating their own posts and reading posts by other users:
# config/config.exs
config :dictator, repo: Client.Repo
# lib/client_web/controllers/post_controller.ex
defmodule ClientWeb.PostController do
  use ClientWeb, :controller
  plug Dictator
  # ...
end
# lib/client_web/policies/post.ex
defmodule ClientWeb.Policies.Post do
  alias Client.Context.Post
  use Dictator.Policies.BelongsTo, for: Post
endThat's it. That's all that's needed. There are policies that allow for more or less granularity but the complexity is as little as what's shown here.
A new perspective
After using it in our projects for a while, we got together to look at it under new shades. The reasoning for that was based on three main ideas:
- We wanted a clearer API.
- We wanted less magic and more maintainability.
- We wanted to accommodate requests that do not load resources.
The latter is one of the biggest proponents for change. At the time, Dictator
tried to load the source being accessed. But what if we wanted to check if the
current_user is an admin? There's no need for resource loading. In theory, we
could plug Dictator into a router pipeline and it would attempt to authorise
admins only. However, due to the implementation, it would fail attempting to
load resources.
At the time, the separation of concerns regarding what was a policy and what was resource loading wasn't that clear. With that in mind, we created two separate concepts regarding policies:
- A policy is a policy. There are no resources. It implements a can?/3function.
- A resource loading policy is a different policy. It's an augmentation of the
latter. Resource loading has its peculiarities in every system, so it should
be easily extended. Besides a can?/3function, a resource loading policy also implements aload_resource/1function.
As a result, dictator has 3 policies implemented:
- Dictator.Policy
- Dictator.Policies.EctoSchema
- Dictator.Policies.BelongsTo
Policies are policies
The concept is simply that. Imagine you want to ensure only admins could access a certain component of the system. The policy would be akin to this:
defmodule MyApp.Policies.Admin do
  use Dictator.Policy
  def can?(%User{admin: true}, _action, _params), do: true
endIf you want to add granularity and ensure that only admins can write, you would dig deep into the allowed actions:
defmodule MyApp.Policies.Admin do
  use Dictator.Policy
  def can?(%User{admin: true}, action, _params)
    when action in [:create, :update],
    do: true
  def can?(_, _, _), do: false
endYou can also be granular to the point of matching on the request params. The
final argument of the can?/3 function is a map with three keys:
- :resource- the resource being loaded,- nilin this scenario.
- :opts- the options passed to the- plug.
- :params- the request params.
This allows you to be as specific as you want when setting rules:
defmodule MyApp.Policies.Admin do
  use Dictator.Policy
  def can?(%User{admin: true}, action, _params)
    when action in [:create, :update],
    do: true
  def can?(_user, _action, %{params: params}),
    do: params["id"] == "1"
  def can?(_, _, _), do: false
endBy keeping these policies not bound to a resource, we are able to not only plug
dictator on a controller level but to keep the logic bound to a namespace. Both
of these are valid options to authorise your users against an AdminPolicy.
defmodule MyAppWeb.Admin.PostController do
  use MyAppWeb, :controller
  plug Dictator, policy: MyAppWeb.Policies.Admin
  # ...
end
#
# or, if you prefer router level authorisation
#
defmodule MyAppWeb.Router do
  pipeline :admin do
   plug Dictator, policy: MyAppWeb.Policies.Admin
  end
endResource loading is something different
Policies that load resources are still policies and the can?/3 API is still in
effect. However, these policies require an integration with Ecto. Since
different resources have different peculiarities, these policies allow for some
sensible overrides. Let's start with the most basic one,
Dictator.Policies.EctoSchema.
This policy always requires you to provide the schema you are attempting to authorise against:
defmodule MyAppWeb.Policies.Post do
  alias MyApp.Content.Post
  use Dictator.Policies.EctoSchema, for: Post
endYou are always advised to override the can?/3 function, as the default
implementation only returns false. To authorise users to only update and
delete their own posts we could do the following:
defmodule MyAppWeb.Policies.Post do
  alias MyApp.Content.Post
  alias MyApp.Accounts.User
  use Dictator.Policies.EctoSchema, for: Post
  def can?(_, action, _) when action in ~w[index show new create]a,
    do: true
  def can?(%User{id: id}, action, %{resource: %Post{user_id: id})
    when action in ~w[edit update delete]a,
    do: true
  def can?(_, _, _), do: false
endThis scenario is the most common we have found in our projects. It represents a
basic belongs_to association. The owner is allowed write access and everyone
is allowed read access. Consequently, this is abstracted into a different
policy: Dictator.Policies.BelongsTo. There a few caveats and custom options
but we will get there.
In the meantime, we should focus on how the Post resource of the aforementioned
example is loaded. The EctoSchema policy implements a load_resource/1
function that receives the HTTP request parameters as the one argument and
attempts to do Repo.get(params["id"]). This simple line has two assumptions:
- You have somehow provided us the repo.
- The request has an idparameter.
These are very strong assumptions to make. The first one is managed by having
you configure the repo. On your config.exs file:
config :dictator, repo: MyApp.RepoOf course, you might have multiple repos and want a specific repo for a specific policy. This configuration can be overriden on a per-policy basis by doing:
use Dictator.Policies.EctoSchema, for: Post, repo: MyApp.OtherRepoAs for the second assumption, that the request has an id parameter, while true
for the majority of the cases, it must be configurable as well. If your Post
resource is identified by a uuid instead, you may do:
use Dictator.Policies.EctoSchema, for: Post, key: :uuidThere is a final underlying assumption, a much larger one, hidden by these
concepts: that all resources are loaded by calling Repo.get_by(resource, key:
params[value]).
Loading a resource varies with application requirements and there must be
flexibility when doing so. load_resource/1 does provide a default
implementation but it can be overridden. If your resource loading is a complex
operation, you can do the following:
defmodule MyAppWeb.Policies.Post do
  alias MyApp.Content.Post
  alias MyApp.Accounts.User
  use Dictator.Policies.EctoSchema, for: Post
  # can?/3 definitions here...
    def load_resource(%{"email" => email, "team_id" => team_id}) do
      Repo.get_by(email: email, team_id: team_id)
    end
endBelongsTo as an extension
As I mentioned before, belongs_to associations tend to fall under a specific pattern:
- The parent resource (e.g. User) has anidkey.
- The children (e.g. Post) has auser_idkey.
- The parent resource has write access to their own children resources.
- All parent resources can have read access to any child resource.
Dictator.Policies.BelongsTo is a specification of the EctoSchema policy for
those scenarios. Doing use Dictator.Policies.BelongsTo, for: Post generates
the following code:
@impl Policy
def can?(_, action, _)
  when action in [:index, :show, :new, :create],
  do: true
@impl Policy
def can?(%{id: owner_id}, action, %{
      resource: %Post{user_id: owner_id}
    })
    when action in [:edit, :update, :delete],
    do: true
@impl Policy
def can?(_user, _action, _params), do: falsePoints 1 and 2 I mentioned above are, again, assumptions that do not hold for
every scenario. With that in mind, you can configure 1. through the owner_key
option and 2. through the foreign_key option.
Imagining the User resource had a uuid primary key and the corresponding
foreign key on the Post resource was user_uuid, to implement a policy you
would do the following:
defmodule MyAppWeb.Policies.Post do
  alias MyApp.Content.Post
  use Dictator.Policies.BelongsTo,
    for: Post,
    owner_key: :uuid,
    foreign_key: :user_uuid
endCurrent User
As with the previous version, it is assumed that the currently logged in user is
available in conn.assigns. By default, the key it is under is current_user.
However, this is configurable on a project-wide basis or on a plug basis:
# config/config.exs
# configure for the whole project
config :dictator, key: :current_resource
# configure for a single controller
# overrides the previous option
plug Dictator, key: :current_ownerUnauthorised Handlers
One final assumption that the first version of dictator took was what to send
back to the user. At the time, it was a simple 401 with the body set to
you're not authorised to do that. This, of course, becomes cumbersome when
developers are writing APIs to be compliant with certain standards, such as
JSON:API. With extensibility in mind, we introduced the concept of unauthorised
handlers.
When the user is not authorised to access a certain resource, an unauthorised
handler is called. This is a module that implements the Plug behaviour. There
are two available: Default and JsonApi. The former is the already existing
behaviour of dictator and the latter is a JSON:API compliant implementation.
You can choose which one to use by setting:
config :dictator, unauthorised_handler: Dictator.UnauthorizedHandlers.JsonApiThis gives the flexibility for you to add your own custom handler, as well. As
long as it implements the Plug behaviour, it should work by just configuring
:dictator to use your module.
In Summary
We gave this version a lot more thought than the initial one. We removed some of the magic (for example, attempting to guess what Ecto repo your application was using) and decided to bet on extensibility and readibility. It's preferable to be explicit than to rely on automagic.
The project aims to keep sane defaults but allow you to easily override and make your own implementation. The API is much clearer, well defined and allows for generic scenarios which it was very much lacking in the initial version.
The 1.0 version is up, we've been using it internally for quite some time and
we're very happy with the results. As always, we're open to pull requests and
issues. We're very receptive to new ideas and suggestions, as well,
feel free to reach out on Twitter.
Set {:dictator, "~> 1.0.0"} on your mix.exs and go and get 'em tiger!
Ta-ta,
Mendes

