AbsinthePermission.DSL (AbsinthePermission v1.0.0)

Copy Markdown View Source

Macros that compose the AbsinthePermission DSL.

These are imported automatically by use AbsinthePermission. Don't import this module directly.

Available macros

Condition helpers (used inside when: / unless:)

These are recognised by AbsinthePermission.Compiler at macro time and never need to be imported at runtime:

  • arg(:name) — a GraphQL argument
  • loaded(:name).field.path — a field on a loaded record
  • current_user.field.path or current_user(:field) — context's current_user
  • context.field — arbitrary context path

Summary

Functions

Attach an authorization rule to the enclosing field.

Sugar for the "owner-or-admin" pattern

Declare a piece of data to fetch before rules on this field are evaluated. Subsequent rules can reference it as loaded(:name).

Register a single loader function. Must be called inside a loaders block (or directly at module top level).

Block delimiter for loader registrations. Use at the top level of your schema module

Functions

authorize(perm)

(macro)

Attach an authorization rule to the enclosing field.

When the rule fires (its when: condition holds, or always if none), the caller must hold permission or the operation is denied.

Permission shapes

authorize "edit_todos"                   # single permission
authorize ["admin", "support"]            # any-of
authorize all: ["admin", "verified_2fa"]  # all-of

Conditional firing

authorize "close_tickets",  when: arg(:state) == "CLOSED"
authorize "edit_others",    unless: loaded(:todo).owner_id == current_user.id
authorize "high_priority",  when: arg(:priority) > 5

Behaviour on deny

authorize "view_emails", on_deny: :null   # redact the field, return null
authorize "view_emails", on_deny: :error  # default — return GraphQL error

Custom error message

authorize "edit_todos", error_message: "Only admins may edit todos."

authorize(perm, opts)

(macro)

See authorize/1.

authorize_owner(load_name, opts)

(macro)

Sugar for the "owner-or-admin" pattern:

authorize_owner :todo, by: arg(:id),
  owner_field: :owner_id,
  if_owner:    "edit_own_todo",
  if_other:    "edit_others_todo"

Equivalent to:

load :todo, by: arg(:id)
authorize "edit_own_todo",
  when: loaded(:todo).owner_id == current_user.id
authorize "edit_others_todo",
  when: loaded(:todo).owner_id != current_user.id

Options

  • :by — what to look up (default: arg(:id))
  • :owner_field — record field to compare against current_user.id (default: :owner_id)
  • :if_owner — permission required when caller IS the owner (required)
  • :if_other — permission required when caller is NOT the owner (required)
  • :user_fieldcurrent_user field to compare (default: :id)

load(name, opts \\ [])

(macro)

Declare a piece of data to fetch before rules on this field are evaluated. Subsequent rules can reference it as loaded(:name).

load :todo, by: arg(:id)
load :user, by: arg(:user_id), using: :user_loader
load :todo                       # short for `by: arg(:id), using: :todo`

The loader function is registered via loader/2 inside a loaders block and called as loader.(key, ctx), returning either the record or nil.

loader(name, fun)

(macro)

Register a single loader function. Must be called inside a loaders block (or directly at module top level).

The function receives (key, context) and should return either the loaded record (any term) or nil if not found.

loader :todo, fn id, _ctx -> MyApp.Todos.get(id) end
loader :user, &MyApp.Users.fetch/2

loaders(list)

(macro)

Block delimiter for loader registrations. Use at the top level of your schema module:

loaders do
  loader :todo, fn id, _ctx -> MyApp.Todos.get(id) end
  loader :user, &MyApp.Users.fetch/2
end

The block is otherwise opaque — loader/2 does the real work.