The rules of one resource: an Ecto schema.
Conditions are plain functions. allow/2 and deny/2 take the actor and a
permission and return true, false or a condition on the record, an
Ecto.Query.dynamic/2 whose first binding is the resource:
defmodule MyApp.Blog.PostPolicy do
use Sekisho.Policy, for: MyApp.Blog.Post
import Ecto.Query
permissions [:read, :create, :update, :delete]
@impl true
def allow(%User{admin: true}, _permission), do: true
def allow(%User{id: id}, :read), do: dynamic([p], p.published or p.author_id == ^id)
def allow(%User{id: id}, permission) when permission in [:update, :delete],
do: dynamic([p], p.author_id == ^id)
def allow(%User{}, :create), do: true
def allow(nil, :read), do: dynamic([p], p.published)
@impl true
def deny(_actor, :update), do: dynamic([p], p.locked)
endA permission no clause matches is not allowed: there is no need for a last
clause that returns false. deny/2 wins over everything else.
The same condition answers a check (allowed?/3, on the record in the
database) and a list (where_allowed/3, in SQL), so they never disagree.
A condition about the actor alone (true, false) needs no query.
Roles
Roles are the only part that is declared, because Sekisho checks them:
permissions [:read, :comment, :update, :share, :delete]
role :viewer, permissions: [:read]
role :commenter, permissions: [:comment]
role :editor, permissions: [:update]
role :owner, permissions: [:share, :delete]
derive :commenter, from: :editor
derive :viewer, from: :commenter
derive :editor, from: :owner
derive :editor, from: {:org, :admin}
derive :viewer, from: {:folder, :viewer}
grant_to [:user, :group, :link], link_up_to: :viewerrolenames a set of permissions.derivegives a role from another: one of the same resource (from: :editor: editors are commenters), or one held on a related resource (from: {:org, :admin}: an org's admins are editors of its documents). The relation is the schema's ownbelongs_to, or one declared withrelation/2and written in yourSekisho.Sources. A relation to the same schema (a folder's parent) is followed to any depth, with a recursive query.derivealso gives a permission from one held on a related record:derive :view, from: {:org, :"investigations.view"}(whoever has:"investigations.view"on the org, through a role or a rule, may:viewits investigations), or from a role there (derive :delete, from: {:org, :admin}).grant_tosays who may be granted a role on a record (grant/4of your permissions module): people (:user), groups (:group), and anyone with a link (:link, up to a role).by:names the permission it takes to grant (:shareby default).
Roles held come from grants (Sekisho's table), from your own tables
(Sekisho.Sources.roles/1), or both.
Roles each record defines
custom_roles() lets each record of the schema (an org) define roles of its
own, kept in sekisho_roles, each a name and some of the policy's
permissions:
permissions [:"investigations.view", :"investigations.create", :"team.manage"]
role :owner, permissions: :all
custom_roles()The roles of the policy (:owner) are the same in every org and cannot
be changed; the custom ones are made with create_role/4 of your
permissions module and granted by name (grant(scope, ana, "Support", on: org)). Other resources derive from the permissions they give:
derive :view, from: {:org, :"investigations.view"}Fields
allow_fields/2 says which of a record's fields the actor may see:
:all (the default), a list, or {:except, fields}. redact/2 of your
permissions module sets the others to nil.
Summary
Types
What allow/2 and deny/2 return.
Callbacks
Whether the actor has the permission: everywhere, nowhere, or on the records of a condition.
The fields of a record the actor may see.
Where the actor does not have the permission, whatever else allows it.
Functions
Each record of the schema defines roles of its own (see the module's documentation).
A role given by another: from: :editor (a role of the same record), or
from: {relation, role} (a role held on a related record).
Who may be granted a role on a record: [:user, :group, :link].
The permissions of the resource: every permission its rules and roles name.
A relation that is not a belongs_to of the schema, to the schema given:
its query is Sekisho.Sources.relation/2.
A role and the permissions it gives (:all for every one).
Types
@type condition() :: boolean() | Ecto.Query.DynamicExpr.t()
What allow/2 and deny/2 return.
Callbacks
Whether the actor has the permission: everywhere, nowhere, or on the records of a condition.
@callback allow_fields(actor :: term(), record :: struct()) :: :all | [atom()] | {:except, [atom()]}
The fields of a record the actor may see.
Where the actor does not have the permission, whatever else allows it.
Functions
Each record of the schema defines roles of its own (see the module's documentation).
A role given by another: from: :editor (a role of the same record), or
from: {relation, role} (a role held on a related record).
Who may be granted a role on a record: [:user, :group, :link].
The permissions of the resource: every permission its rules and roles name.
A relation that is not a belongs_to of the schema, to the schema given:
its query is Sekisho.Sources.relation/2.
A role and the permissions it gives (:all for every one).