Sekisho.Policy behaviour (Sekisho v0.1.0-dev)

Copy Markdown View Source

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)
end

A 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: :viewer
  • role names a set of permissions.
  • derive gives 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 own belongs_to, or one declared with relation/2 and written in your Sekisho.Sources. A relation to the same schema (a folder's parent) is followed to any depth, with a recursive query.
  • derive also 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 :view its investigations), or from a role there (derive :delete, from: {:org, :admin}).
  • grant_to says who may be granted a role on a record (grant/4 of 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 (:share by 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

condition()

@type condition() :: boolean() | Ecto.Query.DynamicExpr.t()

What allow/2 and deny/2 return.

Callbacks

allow(actor, permission)

(optional)
@callback allow(actor :: term(), permission :: atom()) :: condition()

Whether the actor has the permission: everywhere, nowhere, or on the records of a condition.

allow_fields(actor, record)

(optional)
@callback allow_fields(actor :: term(), record :: struct()) ::
  :all | [atom()] | {:except, [atom()]}

The fields of a record the actor may see.

deny(actor, permission)

(optional)
@callback deny(actor :: term(), permission :: atom()) :: condition()

Where the actor does not have the permission, whatever else allows it.

Functions

custom_roles()

(macro)

Each record of the schema defines roles of its own (see the module's documentation).

derive(name, opts)

(macro)

A role given by another: from: :editor (a role of the same record), or from: {relation, role} (a role held on a related record).

grant_to(kinds, opts \\ [])

(macro)

Who may be granted a role on a record: [:user, :group, :link].

permissions(list)

(macro)

The permissions of the resource: every permission its rules and roles name.

relation(name, schema)

(macro)

A relation that is not a belongs_to of the schema, to the schema given: its query is Sekisho.Sources.relation/2.

role(name, opts)

(macro)

A role and the permissions it gives (:all for every one).