dictator
dictator copied to clipboard
Dictates what your users see. Plug-based authorization.
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
- Usage
- Custom policies
Dictator.Policies.EctoSchemaDictator.Policies.BelongsTo
- Plug Options
- Limitting the actions to be authorized
- Overriding the policy to be used
- Overriding the current user key
- Overriding the current user fetch strategy
- Configuration Options
- Setting a default repo
- Setting a default user key
- Setting the fetch strategy
- Setting the unauthorized handler
- Custom policies
- Contributing
- Setup
- Other Projects
- About
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 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)
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_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
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 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
# ...
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: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.
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 fromconn.assignsDictator.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 [email protected]: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.
