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
endAnd that's it! Just like that your users can edit, see and delete their own
Things but not Things belonging to other users.
- Installation
- Usage
- Contributing
- Setup
- Other Projects
- About
First, you need to add :dictator to your list of dependencies on your mix.exs:
def deps do
[{:dictator, "~> 1.1"}]
endFor 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
# ...
endAlternatively, you can also do it at the router level:
defmodule ClientWeb.Router do
pipeline :authorised do
plug Dictator
end
endThat 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
endThis 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
endIMPORTANT: Dictator assumes you have your current user in your
conn.assigns. See our demo app
for an example on integrating with guardian.
Dictator comes bundled with three different types of policies:
Dictator.Policies.EctoSchema: most common behaviour. When youuseit, Dictator will try to call aload_resource/1function by passing the HTTP params. This function is overridable, along withcan?/3Dictator.Policies.BelongsTo: abstraction on top ofDictator.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.useit if you don't want to load resources from the database (e.g to check if a user has anis_adminfield set totrue)
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
endIn 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
endThe following custom options are available:
key: defaults to:id, primary key of the resource being accessed.repo: overrides the repo set by the config.
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
endThis 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
endThe assumptions made are that:
- your resource has a
user_idforeign key (you can change this with the:foreign_keyoption) - your user has an
idprimary key (you can change this with the:owner_idoption)
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
endThe key and repo options supported by Dictator.Policies.EctoSchema are
also supported by Dictator.Policies.BelongsTo.
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.
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]
# ...
endIn 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.
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
# ...
endBy 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
# ...
endBy 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 fromconn.assignsDictator.FetchStrategies.Session- fetches the given key from the session
defmodule ClientWeb.ThingController do
use ClientWeb, :controller
plug Dictator, fetch_strategy: Dictator.FetchStrategies.Session
# ...
endDictator 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:repooption. - key (optional, defaults to
:key) - key to be used to find the current user inconn.assigns. - unauthorized_handler (optional, default:
Dictator.UnauthorizedHandlers.Default) - module to call to handle unauthorisation errors.
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.RepoIf 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
endBy 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_companyThe value set by the key option when plugging Dictator overrides this one.
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 fromconn.assignsDictator.FetchStrategies.Session- fetches the given key from the session
config :dictator, fetch_strategy: Dictator.FetchStrategies.SessionThe value set by the key option when plugging Dictator overrides this one.
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: MyUnauthorizedHandlerFeel 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.
To clone and setup the repo:
git clone git@github.com:subvisual/dictator.git
cd dictator
bin/setupAnd everything should automatically be installed for you.
To run the development server:
bin/serverNot your cup of tea? 🍵 Here are some other Elixir alternatives we like:
Dictator is maintained by Subvisual.
