/dictator

Dictates what your users see. Plug-based authorization.

Primary LanguageElixirISC LicenseISC

Dictator

Dictator is a plug-based authorization mechanism.

Dictate what your users can access in fewer than 10 lines of code:

# config/config.exs
config :dictator, repo: Client.Repo

# lib/client_web/controllers/thing_controller.ex
defmodule ClientWeb.ThingController do
  use ClientWeb, :controller

  plug Dictator

  # ...
end

# lib/client_web/policies/thing.ex
defmodule ClientWeb.Policies.Thing do
  alias Client.Context.Thing

  use Dictator.Policies.BelongsTo, for: Thing
end

And that's it! Just like that your users can edit, see and delete their own Things but not Things belonging to other users.


Installation

First, you need to add :dictator to your list of dependencies on your mix.exs:

def deps do
  [{:dictator, "~> 1.1"}]
end

Usage

For in-depth usage, refer to this blog post.

To authorize your users, just add in your controller:

defmodule ClientWeb.ThingController do
  use ClientWeb, :controller

  plug Dictator

  # ...
end

Alternatively, you can also do it at the router level:

defmodule ClientWeb.Router do
  pipeline :authorised do
    plug Dictator
  end
end

That plug will automatically look for a ClientWeb.Policies.Thing module, which should use Dictator.Policy. It is a simple module that should implement can?/3. It receives the current user, the action it is trying to perform and a map containing the conn.params, the resource being acccessed and any options passed when plug-ing Dictator.

In lib/client_web/policies/thing.ex:

defmodule ClientWeb.Policies.Thing do
  alias Client.Context.Thing

  use Dictator.Policies.EctoSchema, for: Thing

  # User can edit, update, delete and show their own things
  def can?(%User{id: user_id}, action, %{resource: %Thing{user_id: user_id}})
    when action in [:edit, :update, :delete, :show], do: true

  # Any user can index, new and create things
  def can?(_, action, _) when action in [:index, :new, :create], do: true

  # Users can't do anything else (users editing, updating, deleting and showing)
  # on things they don't own
  def can?(_, _, _), do: false
end

This exact scenario is, in fact, so common that already comes bundled as Dictator.Policies.BelongsTo. This is equivalent to the previous definition:

defmodule ClientWeb.Policies.Thing do
  alias Client.Context.Thing

  use Dictator.Policies.BelongsTo, for: Thing
end

IMPORTANT: Dictator assumes you have your current user in your conn.assigns. See our demo app for an example on integrating with guardian.


Custom Policies

Dictator comes bundled with three different types of policies:

  • Dictator.Policies.EctoSchema: most common behaviour. When you use it, Dictator will try to call a load_resource/1 function by passing the HTTP params. This function is overridable, along with can?/3
  • Dictator.Policies.BelongsTo: abstraction on top of Dictator.Policies.EctoSchema, for the most common use case: when a user wants to read and write resources they own, but read access is provided to everyone else. This policy makes some assumptions regarding your implementation, all of those highly customisable.
  • Dictator.Policy: most basic policy possible. use it if you don't want to load resources from the database (e.g to check if a user has an is_admin field set to true)

Dictator.Policies.EctoSchema

Most common behaviour. When you use it, Dictator will try to call a load_resource/1 function by passing the HTTP params. This allows you to access the resource in the third parameter of can/3?. The load_resource/1 function is overridable, along with can?/3.

Take the following example:

defmodule ClientWeb.Policies.Thing do
  alias Client.Context.Thing

  use Dictator.Policies.EctoSchema, for: Thing

  # User can edit, update, delete and show their own things
  def can?(%User{id: user_id}, action, %{resource: %Thing{user_id: user_id}})
    when action in [:edit, :update, :delete, :show], do: true

  # Any user can index, new and create things
  def can?(_, action, _) when action in [:index, :new, :create], do: true

  # Users can't do anything else (users editing, updating, deleting and showing)
  # on things they don't own
  def can?(_, _, _), do: false
end

In the example above, Dictator takes care of loading the Thing resource through the HTTP params. However, you might want to customise the way the resource is loaded. To do that, you should override the load_resource/1 function.

As an example:

defmodule ClientWeb.Policies.Thing do
  alias Client.Context.Thing

  use Dictator.Policies.EctoSchema, for: Thing

  def load_resource(%{"owner_id" => owner_id, "uuid" => uuid}) do
    ClientWeb.Repo.get_by(Thing, owner_id: owner_id, uuid: uuid)
  end

  def can?(_, action, _) when action in [:index, :show, :new, :create], do: true

  def can?(%{id: owner_id}, action, %{resource: %Thing{owner_id: owner_id}})
    when action in [:edit, :update, :delete],
    do: true

  def can?(_user, _action, _params), do: false
end

The following custom options are available:

  • key: defaults to :id, primary key of the resource being accessed.
  • repo: overrides the repo set by the config.

Dictator.Policies.BelongsTo

Policy definition commonly used in typical belongs_to associations. It is an abstraction on top of Dictator.Policies.EctoSchema.

This policy assumes the users can read (:show, :index, :new, :create) any information but only write (:edit, :update, :delete) their own.

As an example, in a typical Twitter-like application, a user has_many posts and a post belongs_to a user. You can define a policy to let users manage their own posts but read all others by doing the following:

defmodule MyAppWeb.Policies.Post do
  alias MyApp.{Post, User}

  use Dictator.Policies.EctoSchema, for: Post

  def can?(_, action, _) when action in [:index, :show, :new, :create], do: true

  def can?(%User{id: id}, action, %{resource: %Post{user_id: id}})
      when action in [:edit, :update, :delete],
      do: true

  def can?(_, _, _), do: false
end

This scenario is so common, it is abstracted completely through this module and you can simply use Dictator.Policies.BelongsTo, for: Post to make use of it. The following example is equivalent to the previous one:

defmodule MyAppWeb.Policies.Post do
  use Dictator.Policies.BelongsTo, for: MyApp.Post
end

The assumptions made are that:

  • your resource has a user_id foreign key (you can change this with the :foreign_key option)
  • your user has an id primary key (you can change this with the :owner_id option)

If your user has a uuid primary key and the post identifies the user through a :poster_id foreign key, you can do the following:

defmodule MyAppWeb.Policies.Post do
  use Dictator.Policies.BelongsTo, for: MyApp.Post,
    foreign_key: :poster_id, owner_id: :uuid
end

The key and repo options supported by Dictator.Policies.EctoSchema are also supported by Dictator.Policies.BelongsTo.

Plug Options

plug Dictator supports 3 options:

  • only/except: (optional) - actions subject to authorization.
  • policy: (optional, infers the policy) - policy to be used
  • resource_key: (optional, default: :current_user) - key to use in the conn.assigns to load the currently logged in resource.

Limitting the actions to be authorized

If you want to only limit authorization to a few actions you can use the :only or :except options when calling the plug in your controller:

defmodule ClientWeb.ThingController do
  use ClientWeb, :controller

  plug Dictator, only: [:create, :update, :delete]
  # plug Dictator, except: [:show, :index, :new, :edit]

  # ...
end

In both cases, all other actions will not go through the authorization plug and the policy will only be enforced for the create,update and delete actions.

Overriding the policy to be used

By default, the plug will automatically infer the policy to be used. MyWebApp.UserController would mean a MyWebApp.Policies.User policy to use.

However, by using the :policy option, that can be overriden

defmodule ClientWeb.ThingController do
  use ClientWeb, :controller

  plug Dictator, policy: MyPolicy

  # ...
end

Overriding the current user key

By default, the plug will automatically search for a current_user in the conn.assigns. You can change this behaviour by using the key option in the plug call. This will override the key option set in config.exs.

defmodule ClientWeb.ThingController do
  use ClientWeb, :controller

  plug Dictator, key: :current_organization

  # ...
end

Overriding the current user fetch strategy

By default, the plug will assume you want to search for the key set in the previous option in the conn.assigns. However, you may have it set in the session or want to use a custom strategy. You can change this behaviour by using the fetch_strategy option in the plug call. This will override the fetch_strategy option set in config.exs.

There are two strategies available by default:

  • Dictator.FetchStrategies.Assigns - fetches the given key from conn.assigns
  • Dictator.FetchStrategies.Session - fetches the given key from the session
defmodule ClientWeb.ThingController do
  use ClientWeb, :controller

  plug Dictator, fetch_strategy: Dictator.FetchStrategies.Session

  # ...
end

Configuration Options

Dictator supports three options to be placed in config/config.exs:

  • repo - default repo to be used by Dictator.Policies.EctoSchema. If not set, you need to define what repo to use in the policy through the :repo option.
  • key (optional, defaults to :key) - key to be used to find the current user in conn.assigns.
  • unauthorized_handler (optional, default: Dictator.UnauthorizedHandlers.Default) - module to call to handle unauthorisation errors.

Setting a default repo

Dictator.Policies.EctoSchema requires a repo to be set to load resource from.

It is recommended that you set it in config/config.exs:

config :dictator, repo: Client.Repo

If not configured, it must be provided in each policy. The repo option when use-ing the policy takes precedence. So you can also set a custom repo for certain resources:

defmodule ClientWeb.Policies.Thing do
  alias Client.Context.Thing
  alias Client.FunkyRepoForThings

  use Dictator.Policies.BelongsTo, for: Thing, repo: FunkyRepoForThings
end

Setting a default current user key

By default, the plug will automatically search for a current_user in the conn.assigns. The default value is :current_user but this can be overriden by changing the config:

config :dictator, key: :current_company

The value set by the key option when plugging Dictator overrides this one.

Setting the fetch strategy

By default, the plug will assume you want to search for the key set in the previous option in the conn.assigns. However, you may have it set in the session or want to use a custom strategy. You can change this behaviour across the whole application by setting the fetch_strategy key in the config.

There are two strategies available by default:

  • Dictator.FetchStrategies.Assigns - fetches the given key from conn.assigns
  • Dictator.FetchStrategies.Session - fetches the given key from the session
config :dictator, fetch_strategy: Dictator.FetchStrategies.Session

The value set by the key option when plugging Dictator overrides this one.

Setting the unauthorized handler

When a user does not have access to a given resource, an unauthorized handler is called. By default this is Dictator.UnauthorizedHandlers.Default which sends a simple 401 with the body set to "you are not authorized to do that".

You can also make use of the JSON API compatible Dictator.UnauthorizedHandlers.JsonApi or provide your own:

config :dictator, unauthorized_handler: MyUnauthorizedHandler

Contributing

Feel free to contribute.

If you found a bug, open an issue. You can also open a PR for bugs or new features. Your PRs will be reviewed and subject to our style guide and linters.

All contributions must follow the Code of Conduct and Subvisual's guides.

Setup

To clone and setup the repo:

git clone git@github.com:subvisual/dictator.git
cd dictator
bin/setup

And everything should automatically be installed for you.

To run the development server:

bin/server

Other projects

Not your cup of tea? 🍵 Here are some other Elixir alternatives we like:

About

Dictator is maintained by Subvisual.

Subvisual logo