DSL definition for AshGrant extension.
This module defines the ash_grant DSL section that can be added to
Ash resources to configure permission-based authorization.
DSL Options
| Option | Type | Required | Description |
|---|---|---|---|
resolver | module or function | Yes | Resolves permissions for actors |
resource_name | string | No | Resource name for permission matching |
default_policies | boolean or atom | No | Auto-generate policies (true, :all, :read, :write) |
default_field_policies | boolean | No | Auto-generate field_policies from field_group definitions |
can_perform_actions | list of atoms | No | Batch-generate CanPerform calculations (e.g., [:update, :destroy]) |
owner_field | atom | No | Deprecated. Use scope :own, expr(...) instead |
Scope Entity
The scope entity defines named scopes that translate to Ash filter expressions.
This replaces the need for a separate ScopeResolver module.
| Argument | Type | Description |
|---|---|---|
name | atom | The scope name (e.g., :all, :own, :published) |
filter | expression or boolean | The filter expression or true for no filter |
| Option | Type | Description |
|---|---|---|
inherits | list of atoms | Parent scopes to inherit and combine with |
description | string | Human-readable description for explain/4 output |
write | expression, boolean, or nil | Deprecated. Prefer argument-based scopes + resolve_argument. See below. |
Read vs write semantics
The filter expression is used for read actions (converted to SQL via FilterCheck).
For write actions, Check evaluates the scope — simple scopes use in-memory
evaluation, while scopes with relationship references (exists() or dot-paths)
automatically use a DB query to verify the scope.
For multi-hop authorization ("refund → order → center_id"), prefer
argument-based scopes together with resolve_argument:
scope :at_own_unit, expr(^arg(:center_id) in ^actor(:own_org_unit_ids))
resolve_argument :center_id, from_path: [:order, :center_id]See guides/argument-based-scope.md for the full rationale and examples.
write: option (deprecated)
The write: option was introduced as an escape hatch for relational scopes
whose filter expression could not be evaluated in memory on write actions.
It is deprecated as of 0.14 — prefer argument-based scopes + resolve_argument,
or gate read-only semantics via separate scope names or the policy layer.
Using write: still works but emits a compile-time deprecation warning.
CanPerform Entity
The can_perform entity generates a boolean CanPerform calculation for a
single action. Use can_perform_actions for batch generation.
| Argument | Type | Description |
|---|---|---|
action | atom | The action name (e.g., :update, :destroy) |
| Option | Type | Default | Description |
|---|---|---|---|
name | atom | :can_<action>? | Custom calculation name |
public? | boolean | true | Whether the calculation is public |
Examples
# Batch — generates :can_update? and :can_destroy?
can_perform_actions [:update, :destroy]
# Individual
can_perform :update
# Individual with custom name
can_perform :read, name: :visible?Example
defmodule MyApp.Blog.Post do
use Ash.Resource,
extensions: [AshGrant]
ash_grant do
resolver MyApp.PermissionResolver
resource_name "post"
scope :always, true
scope :own, expr(author_id == ^actor(:id))
scope :published, expr(status == :published)
scope :own_draft, [:own], expr(status == :draft)
# Relational scope — works for reads and writes automatically
scope :team_visible, expr(exists(team.members, user_id == ^actor(:id)))
# UI visibility calculations
can_perform_actions [:update, :destroy]
end
endResolver
The resolver option specifies how to get permissions for an actor.
It can be:
- A module implementing
AshGrant.PermissionResolverbehaviour - A 2-arity function
(actor, context) -> [permissions]
Resource Name
The resource_name option overrides the resource name used in
permission matching. If not specified, it's derived from the module
name (e.g., MyApp.Blog.Post → "post").
Owner Field (Deprecated)
The owner_field option is deprecated and will be removed in v1.0.0.
Use explicit scope expressions instead:
# Instead of owner_field :author_id, use:
scope :own, expr(author_id == ^actor(:id))Context Injection
Scopes can use ^context(:key) for injectable values, enabling deterministic
testing of temporal and parameterized scopes:
ash_grant do
resolver MyApp.PermissionResolver
# Injectable temporal scope
scope :today, expr(fragment("DATE(inserted_at) = ?", ^context(:reference_date)))
# Injectable threshold scope
scope :small_amount, expr(amount < ^context(:max_amount))
endInject values at query/changeset time:
Post
|> Ash.Query.for_read(:read)
|> Ash.Query.set_context(%{reference_date: Date.utc_today()})
|> Ash.read!(actor: actor)This is preferred over database functions like CURRENT_DATE for testability.
Summary
Functions
Returns the scope entity definition for reuse by AshGrant.Domain.Dsl.
Functions
Returns the scope entity definition for reuse by AshGrant.Domain.Dsl.