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:
:createactions:updateactions:destroyactions: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
endGeneric 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
| Option | Type | Description |
|---|---|---|
:action | string | Override action name for permission matching |
:resource | string | Override resource name for permission matching |
How It Works
- Resolve permissions: Calls the configured
PermissionResolverto get the actor's permissions - Check access: Uses
AshGrant.Evaluator.has_access?/3to verify the actor has a matching permission (deny-wins semantics) - Get scope: Extracts the scope from the matching permission
- Verify scope: Uses
Ash.Expr.eval/2to 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: falseRelational 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: value | Strategy | Behavior |
|---|---|---|
write: false | Deny | Returns false immediately |
write: true | Allow | Returns true immediately |
write: expr(...) | In-memory | Evaluate custom expression |
| (omitted, no relationships) | In-memory | Current behavior |
| (omitted, has relationships) | DB query | Query 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()
endAction Override
# The Ash action is :publish, but we check for "update" permission
policy action(:publish) do
authorize_if AshGrant.check(action: "update")
endSee Also
AshGrant.FilterCheck- For read actionsAshGrant.Evaluator- Permission evaluation logicAshGrant.Info- DSL introspection helpers
Summary
Functions
Creates a check tuple for use in policies.
Determines if two check references conflict (are mutually exclusive).
Callback implementation for Ash.Policy.Check.eager_evaluate?/0.
Determines if one check reference implies another.
Callback implementation for Ash.Policy.Check.init/1.
Callback implementation for Ash.Policy.Check.prefer_expanded_description?/0.
Callback implementation for Ash.Policy.Check.requires_original_data?/2.
Simplifies a check reference into a SAT expression of simpler check references.
Callback implementation for Ash.Policy.Check.strict_check/3.
Callback implementation for Ash.Policy.Check.type/0.
Functions
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
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.
Callback implementation for Ash.Policy.Check.eager_evaluate?/0.
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
Callback implementation for Ash.Policy.Check.init/1.
Callback implementation for Ash.Policy.Check.prefer_expanded_description?/0.
Callback implementation for Ash.Policy.Check.requires_original_data?/2.
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.
Callback implementation for Ash.Policy.Check.strict_check/3.
Callback implementation for Ash.Policy.Check.type/0.