AshGrant.Dsl (AshGrant v0.16.0)

Copy Markdown View Source

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

OptionTypeRequiredDescription
resolvermodule or functionYesResolves permissions for actors
resource_namestringNoResource name for permission matching
default_policiesboolean or atomNoAuto-generate policies (true, :all, :read, :write)
default_field_policiesbooleanNoAuto-generate field_policies from field_group definitions
can_perform_actionslist of atomsNoBatch-generate CanPerform calculations (e.g., [:update, :destroy])
owner_fieldatomNoDeprecated. 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.

ArgumentTypeDescription
nameatomThe scope name (e.g., :all, :own, :published)
filterexpression or booleanThe filter expression or true for no filter
OptionTypeDescription
inheritslist of atomsParent scopes to inherit and combine with
descriptionstringHuman-readable description for explain/4 output
writeexpression, boolean, or nilDeprecated. 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.

ArgumentTypeDescription
actionatomThe action name (e.g., :update, :destroy)
OptionTypeDefaultDescription
nameatom:can_<action>?Custom calculation name
public?booleantrueWhether 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
end

Resolver

The resolver option specifies how to get permissions for an actor. It can be:

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

Inject 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

scope_entity()

Returns the scope entity definition for reuse by AshGrant.Domain.Dsl.

sections()