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
Thing
s but not Thing
s 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"}]
end
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.
Dictator comes bundled with three different types of policies:
Dictator.Policies.EctoSchema
: most common behaviour. When youuse
it, Dictator will try to call aload_resource/1
function by passing the HTTP params. This function is overridable, along withcan?/3
Dictator.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.use
it if you don't want to load resources from the database (e.g to check if a user has anis_admin
field 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
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.
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 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]
# ...
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.
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
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
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 fromconn.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
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 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.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
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.
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.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.
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
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.
To clone and setup the repo:
git clone [email protected]:subvisual/dictator.git
cd dictator
bin/setup
And everything should automatically be installed for you.
To run the development server:
bin/server
Not your cup of tea? 🍵 Here are some other Elixir alternatives we like:
Dictator
is maintained by Subvisual.