A rule says who may do what, in a function. It is the first thing to reach for, and many apps need nothing else.
allow and deny
allow/2 takes the actor (a user, or nil for a visitor) and a
permission. It returns one of three things:
true: allowed on every record;false: allowed on none;- a condition on the record, an
Ecto.Query.dynamic/2whose first binding is the resource.
defmodule MyApp.Tasks.TaskPolicy do
use Sekisho.Policy, for: MyApp.Tasks.Task
import Ecto.Query
alias MyApp.Accounts.User
permissions [:read, :create, :update, :complete, :delete]
@impl true
def allow(%User{role: :manager}, _permission), do: true
def allow(%User{}, :create), do: true
def allow(%User{id: id}, :read), do: dynamic([t], t.assignee_id == ^id or t.creator_id == ^id)
def allow(%User{id: id}, :complete), do: dynamic([t], t.assignee_id == ^id)
def allow(%User{id: id}, permission) when permission in [:update, :delete],
do: dynamic([t], t.creator_id == ^id)
@impl true
def deny(_user, permission) when permission in [:update, :complete],
do: dynamic([t], t.archived)
enddeny/2 takes the same arguments and returns the same kinds of answer.
It wins over everything: here a manager may do anything, except change
an archived task.
A permission no clause matches is not allowed. There is no last clause
returning false to write, though you may write one if you like it
explicit.
permissions lists every permission of the resource. Asking for one that
is not there raises, so a typo (:udpate) is an error, not a quiet "no".
Conditions are checked in the database
allowed?(scope, :complete, task) runs the condition against the task's
row, with one query on its primary key. It reads the task as it is in the
database, not the struct in your hands: a change you have not saved does
not count.
A rule about the actor alone (true, false) needs no query at all.
allowed?(scope, :create, Task) takes the schema, since there is no
record yet. Asking about the schema when the answer depends on the record
raises, and says so.
Lists
where_allowed/3 takes a query and keeps the records on which the
permission holds:
Task
|> where([t], t.project_id == ^project.id)
|> Permissions.where_allowed(scope, :read)
|> order_by(:due_on)
|> Repo.all()The condition is the same one allowed?/3 uses. When the answer is
true, the query is left as it is; when it is false, it returns
nothing, without asking the database.
Many records at once
A table of tasks with a Complete and a Delete button on each row would
ask twice per row. with_permissions/3 asks once:
Permissions.with_permissions(tasks, scope, [:complete, :delete])
#=> [{%Task{id: 1}, [:complete]}, {%Task{id: 2}, [:complete, :delete]}, ...]The records keep their order.
What rules see
The actor is whatever actor/1 of your permissions module returns: your
user struct, with whatever it has loaded. Rules match on it as any
Elixir function does:
def allow(%User{plan: :pro}, :export), do: true
def allow(%User{suspended_at: %DateTime{}}, _permission), do: falseFor things that are not the user's (the request's IP, the org being browsed), put them in the scope and return them in the actor. The Attribute rules guide shows how.
When a rule is not enough
Rules are the right tool when "who" is a fact about the actor and the
record: an author, an assignee, a manager. When "who" is a list of people
someone chose (members of an org, people a folder was shared with), use
roles: Role-based access, Sharing and relations.
Rules and roles work together in one policy: either allows, and deny/2
refuses both.