Sekisho

Sekisho — Permissions for Phoenix

Permissions for Phoenix: who may do what, from one rule on a post to roles each customer defines and documents shared like Google Docs.

An author edits their own posts. An org's admins manage its members, with roles the org makes for itself. A folder is shared with a person, a team, or anyone with the link, and everything inside it comes along. You write each rule once, in Elixir, and the same rule answers "may Ana edit this?" and "which documents may Ana see?", the second one in your database.

Sekisho (関所) were the checkpoints on the roads of Edo Japan, where guards checked each traveller's pass. Sekisho keeps its grants in your own database, next to your data, and needs no other service.

Install

You need Elixir 1.18+, Ecto 3.13 on PostgreSQL, and Phoenix 1.7.14 or 1.8. LiveView is optional.

With Igniter, one command adds Sekisho and sets it up. It shows each change and asks before it writes:

mix igniter.install sekisho
mix ecto.migrate
Without Igniter

Add {:sekisho, "~> 0.1"} to the deps in mix.exs, then run:

mix deps.get
mix sekisho.gen.migration
mix ecto.migrate

Write your permissions module in lib/my_app/permissions.ex:

defmodule MyApp.Permissions do
  use Sekisho,
    repo: MyApp.Repo,
    pubsub: MyApp.PubSub,
    policies: []

  @impl Sekisho
  def actor(%MyApp.Accounts.Scope{user: user}), do: user
  def actor(nil), do: nil
end

Import allowed?/3 in the html_helpers of lib/my_app_web.ex, for your templates:

import MyApp.Permissions, only: [allowed?: 3]

And add :sekisho to the import_deps of .formatter.exs, so policies read role :viewer, permissions: [:read].

Your first policy

A policy holds the rules of one schema. Each rule is a function that takes the person asking and a permission, and returns true, false, or a condition on the record, written with Ecto.Query.dynamic/2:

defmodule MyApp.Blog.PostPolicy do
  use Sekisho.Policy, for: MyApp.Blog.Post
  import Ecto.Query

  alias MyApp.Accounts.User

  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}, :update), 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(_user, :update), do: dynamic([p], p.locked)
end

Add it to the policies: of your permissions module. Then ask, with your scope first:

def update_post(scope, post, attrs) do
  with :ok <- Permissions.authorize(scope, :update, post) do
    post |> Post.changeset(attrs) |> Repo.update()
  end
end

def list_posts(scope) do
  Post |> Permissions.where_allowed(scope, :read) |> Repo.all()
end

And in a template:

<.link :if={allowed?(@current_scope, :update, @post)} navigate={~p"/posts/#{@post}/edit"}>
  Edit
</.link>

authorize/3 returns {:error, %Sekisho.Denied{}}, and authorize!/3 raises it. Phoenix answers that with a 403.

The Quickstart goes on from here, with roles and a LiveView.

What you get

Guides

Quickstart · Rules · Role-based access · Sharing and relations · Attribute rules · Mixing models · Your own tables · Phoenix · Testing and explanations · How it queries

Development

You need PostgreSQL. The tests make their own database:

mix deps.get
mix test          # DATABASE_URL, or postgres://postgres:postgres@localhost/sekisho_test
mix credo --strict

License

MIT. See LICENSE.