# Dictator
Dictator is a plug-based authorization mechanism.
Dictate what your users can access in fewer than 10 lines of code:
```elixir
# 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](#installation)
- [Usage](#usage)
- [Custom policies](#custom-policies)
- [`Dictator.Policies.EctoSchema`](#dictator.policies.ectoschema)
- [`Dictator.Policies.BelongsTo`](#dictator.policies.belongsto)
- [Plug Options](#plug-options)
- [Limitting the actions to be authorized](#limitting-the-actions-to-be-authorized)
- [Overriding the policy to be used](#overriding-the-policy-to-be-used)
- [Overriding the current user key](#overriding-the-current-user-key)
- [Configuration Options](#configuration-options)
- [Setting a default repo](#setting-a-default-repo)
- [Setting a default user key](#setting-a-default-current-user-key)
- [Setting the unauthorized handler](#setting-the-unauthorized-handler)
- [Contributing](#contributing)
- [Setup](#setup)
- [Other Projects](#other-projects)
- [About](#about)
## Installation
First, you need to add `:dictator` to your list of dependencies on your `mix.exs`:
```elixir
def deps do
[{:dictator, "~> 1.0"}]
end
```
## Usage
To authorize your users, just add in your controller:
```elixir
defmodule ClientWeb.ThingController do
use ClientWeb, :controller
plug Dictator
# ...
end
```
Alternatively, you can also do it at the router level:
```elixir
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`:
```elixir
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:
```elixir
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](https://github.com/subvisual/dictator_demo)
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:
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
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
```elixir
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`.
```elixir
defmodule ClientWeb.ThingController do
use ClientWeb, :controller
plug Dictator, key: :current_organization
# ...
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`:
```elixir
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:
```elixir
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:
```elixir
config :dictator, key: :current_company
```
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:
```elixir
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](https://github.com/subvisual/dictator/blob/master/CODE_OF_CONDUCT.md)
and [Subvisual's guides](https://github.com/subvisual/guides).
## Setup
To clone and setup the repo:
```bash
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:
```bash
bin/server
```
## Other projects
Not your cup of tea? 🍵 Here are some other Elixir alternatives we like:
- [@schrockwell/bodyguard](https://github.com/schrockwell/bodyguard)
- [@jarednorman/canada](https://github.com/jarednorman/canada)
- [@cpjk/canary](https://github.com/cpjk/canary)
- [@boydm/policy_wonk](https://github.com/boydm/policy_wonk)
## About
`Dictator` is maintained by [Subvisual](http://subvisual.com).
[<img alt="Subvisual logo" src="https://raw.githubusercontent.com/subvisual/guides/master/github/templates/subvisual_logo_with_name.png" width="350px" />](https://subvisual.com)