AshGrant.Check (AshGrant v0.14.1)

Copy Markdown View Source

SimpleCheck for write and generic actions.

This check integrates with Ash's policy system to provide permission-based authorization for write operations and generic actions. It returns true or false based on whether the actor has the required permission.

For read actions, use AshGrant.FilterCheck instead, which returns a filter expression to limit query results.

Auto-generated Policies

When using default_policies: true in your resource's ash_grant block, this check is automatically configured for write actions and generic actions.

When to Use

Use AshGrant.check/1 for:

  • :create actions
  • :update actions
  • :destroy actions
  • :action (generic actions)

Usage in Policies

policies do
  # For all write actions
  policy action_type([:create, :update, :destroy]) do
    authorize_if AshGrant.check()
  end

  # For generic actions (not included in default_policies)
  policy action_type(:action) do
    authorize_if AshGrant.check()
  end

  # For a specific action
  policy action(:publish) do
    authorize_if AshGrant.check(action: "publish")
  end
end

Generic Actions

Generic actions (Ash actions with type: :action) use Ash.ActionInput instead of Ash.Query or Ash.Changeset. This check correctly extracts tenant from action_input for multi-tenant authorization.

Generic actions must be authorized by their specific action name in the permission string — type wildcards (action*) do not apply because generic actions are individually unique:

# Permission grants access to the specific "ping" action
"service_request:*:ping:always"

# Wildcard (*) grants access to all actions including generic ones
"service_request:*:*:always"

Since generic actions have no target record, only scope :always, true (or other non-record scopes) will pass scope evaluation. Record-based scopes like scope :own, expr(author_id == ^actor(:id)) are not applicable.

default_policies and generic actions

default_policies: true automatically generates a policy for generic actions using AshGrant.Check.

Options

OptionTypeDescription
:actionstringOverride action name for permission matching
:resourcestringOverride resource name for permission matching

How It Works

  1. Resolve permissions: Calls the configured PermissionResolver to get the actor's permissions
  2. Check access: Uses AshGrant.Evaluator.has_access?/3 to verify the actor has a matching permission (deny-wins semantics)
  3. Get scope: Extracts the scope from the matching permission
  4. Verify scope: Uses Ash.Expr.eval/2 to evaluate the scope filter against the target record

Scope Evaluation

Scope filters use Ash.Expr.eval/2 for proper Ash expression handling:

  • Full support for all Ash expression operators
  • Automatic actor template resolution (^actor(:id), etc.)
  • Automatic tenant template resolution (^tenant())
  • Context injection via ^context(:key) for testable scopes
  • Handles nested actor paths

For update/destroy actions:

  • The scope filter is evaluated against the existing record (changeset.data)

For create actions:

  • A "virtual record" is built from the changeset attributes
  • The scope filter is evaluated against this virtual record

Dual Read/Write Scope

This check uses AshGrant.Info.resolve_write_scope_filter/3 for scope resolution, which checks the scope's write: option first, falling back to filter if not set. This allows scopes to provide separate expressions for reads and writes.

The write: option is an explicit override. When omitted, the check automatically chooses the best strategy for the scope expression (see "DB Query Fallback" below).

# Explicit in-memory override (avoids DB round-trip)
scope :same_org, expr(exists(org.users, id == ^actor(:id))),
  write: expr(org_id == ^actor(:org_id))

Set write: false to explicitly deny writes with a scope:

scope :readonly, expr(exists(org.users, id == ^actor(:id))),
  write: false

Relational Scopes and DB Query Fallback

Scopes using exists() or dot-path references cannot be evaluated in-memory. When such a scope has no explicit write: option and the resource has a data layer, the check automatically uses a DB query to verify the scope instead:

write: valueStrategyBehavior
write: falseDenyReturns false immediately
write: trueAllowReturns true immediately
write: expr(...)In-memoryEvaluate custom expression
(omitted, no relationships)In-memoryCurrent behavior
(omitted, has relationships)DB queryQuery DB with read scope

For update/destroy: Queries the DB to check if the existing record matches the read scope expression.

For create: Splits the filter into direct-attribute parts (evaluated in-memory) and relationship parts. Relationship parts are verified by extracting the FK from the changeset and querying the parent resource.

This means scopes like exists(team.memberships, user_id == ^actor(:id)) now work correctly for all action types without requiring a write: option.

Examples

Basic Usage

# Permission: "post:*:update:own"
# Actor can only update their own posts

policy action(:update) do
  authorize_if AshGrant.check()
end

Action Override

# The Ash action is :publish, but we check for "update" permission
policy action(:publish) do
  authorize_if AshGrant.check(action: "update")
end

See Also

Summary

Functions

Creates a check tuple for use in policies.

Determines if two check references conflict (are mutually exclusive).

Determines if one check reference implies another.

Callback implementation for Ash.Policy.Check.init/1.

Simplifies a check reference into a SAT expression of simpler check references.

Callback implementation for Ash.Policy.Check.type/0.

Functions

check(opts \\ [])

Creates a check tuple for use in policies.

Examples

policy always() do
  authorize_if AshGrant.check()
end

policy action(:destroy) do
  authorize_if AshGrant.check(subject: [:status])
end

conflicts?(ref1, ref2, context)

Determines if two check references conflict (are mutually exclusive).

AshGrant checks don't inherently conflict with each other. The deny-wins semantics are handled at permission evaluation time, not at the check level.

Returns false for all cases.

eager_evaluate?()

Callback implementation for Ash.Policy.Check.eager_evaluate?/0.

implies?(ref1, ref2, context)

Determines if one check reference implies another.

Two AshGrant checks imply each other if they have identical options (same action and resource overrides). This helps the SAT solver avoid redundant evaluations.

Examples

# Same check implies itself
implies?({Check, []}, {Check, []}, context) == true

# Different actions don't imply each other
implies?({Check, [action: "read"]}, {Check, [action: "update"]}, context) == false

init(opts)

Callback implementation for Ash.Policy.Check.init/1.

prefer_expanded_description?()

Callback implementation for Ash.Policy.Check.prefer_expanded_description?/0.

requires_original_data?(_, _)

Callback implementation for Ash.Policy.Check.requires_original_data?/2.

simplify(ref, context)

Simplifies a check reference into a SAT expression of simpler check references.

For AshGrant checks, we return the ref unchanged since permissions are resolved dynamically at runtime and cannot be further decomposed statically.

This callback is used by Ash's SAT solver to optimize policy evaluation.

strict_check(actor, context, opts)

Callback implementation for Ash.Policy.Check.strict_check/3.

type()

Callback implementation for Ash.Policy.Check.type/0.