This guide walks you through setting up AshGrant beyond the basics covered in the README's Quick Start.
Module-Based Resolver (Production)
For production, extract the resolver to a module:
defmodule MyApp.PermissionResolver do
@behaviour AshGrant.PermissionResolver
@impl true
def resolve(nil, _context), do: []
@impl true
def resolve(actor, _context) do
# Load permissions from database
actor
|> MyApp.Accounts.get_user_permissions()
|> Enum.map(& &1.permission_string)
end
endThen reference it in your resource:
ash_grant do
resolver MyApp.PermissionResolver
# ...
endExplicit Policies (Full Control)
For more control, disable default_policies and define policies explicitly:
ash_grant do
resolver MyApp.PermissionResolver
# default_policies false (default)
scope :always, true
scope :own, expr(author_id == ^actor(:id))
end
policies do
# Admin bypass
bypass actor_attribute_equals(:role, :admin) do
authorize_if always()
end
# Read actions: use filter_check (returns filtered results)
policy action_type(:read) do
authorize_if AshGrant.filter_check()
end
# Write actions: use check (returns true/false)
policy action_type([:create, :update, :destroy]) do
authorize_if AshGrant.check()
end
endResolver Context
The context parameter passed to your resolver contains:
| Key | Type | Description |
|---|---|---|
:actor | term | The actor performing the action |
:resource | module | The Ash resource module |
:action | Ash.Action.t | The action struct |
:tenant | term | nil | Current tenant (from query/changeset) |
:changeset | Ash.Changeset.t | nil | For write actions |
:query | Ash.Query.t | nil | For read actions |
Example usage:
defmodule MyApp.PermissionResolver do
@behaviour AshGrant.PermissionResolver
@impl true
def resolve(actor, context) do
base_permissions = get_role_permissions(actor)
# Add instance permissions based on context
case context do
%{resource: MyApp.Document, action: %{name: :read}} ->
shared_docs = get_shared_document_ids(actor)
instance_perms = Enum.map(shared_docs, &"document:#{&1}:read:")
base_permissions ++ instance_perms
_ ->
base_permissions
end
end
endDomain-Level DSL
When multiple resources share the same resolver and scopes, define them once at the domain level instead of repeating the same ash_grant do block in every resource.
When to use:
- 3+ resources in a domain share the same resolver and common scopes (
:always,:own, etc.) - You want a single place to change the resolver or add a scope for all resources
When NOT to use:
- Resources in a domain have very different resolvers or scope logic
- You only have 1–2 resources in the domain
Setup
defmodule MyApp.Blog do
use Ash.Domain,
extensions: [AshGrant.Domain]
ash_grant do
resolver MyApp.PermissionResolver
scope :always, true
scope :own, expr(author_id == ^actor(:id))
end
resources do
resource MyApp.Blog.Post
resource MyApp.Blog.Comment
end
endResources inherit the domain's resolver and scopes automatically:
defmodule MyApp.Blog.Post do
use Ash.Resource,
domain: MyApp.Blog,
authorizers: [Ash.Policy.Authorizer],
extensions: [AshGrant]
ash_grant do
default_policies true
# No resolver needed — inherited from domain
# :always and :own scopes inherited from domain
scope :published, expr(status == :published) # Add resource-specific scopes
end
# ...
endInheritance Rules
| Config | Resource defines it | Domain defines it | Result |
|---|---|---|---|
| resolver | Yes | Yes | Resource wins |
| resolver | No | Yes | Domain's resolver used |
| scope (same name) | Yes | Yes | Resource wins (override) |
| scope | No | Yes | Domain scope inherited |
Resource scopes can inherit from domain-defined parent scopes:
# Domain defines :own scope
# Resource adds :own_draft that inherits from domain's :own
ash_grant do
scope :own_draft, [:own], expr(status == :draft)
endA compile error is raised if no resolver is found from either the resource or the domain.
Resolver Patterns
Permissions with Metadata
Return AshGrant.PermissionInput structs for enhanced debugging and explain/4:
defmodule MyApp.PermissionResolver do
@behaviour AshGrant.PermissionResolver
@impl true
def resolve(actor, _context) do
actor
|> get_roles()
|> Enum.flat_map(fn role ->
Enum.map(role.permissions, fn perm ->
%AshGrant.PermissionInput{
string: perm,
description: "From role permissions",
source: "role:#{role.name}"
}
end)
end)
end
endCustom Structs with Permissionable Protocol
Implement the AshGrant.Permissionable protocol for your custom structs:
defmodule MyApp.RolePermission do
defstruct [:permission_string, :label, :role_name]
end
defimpl AshGrant.Permissionable, for: MyApp.RolePermission do
def to_permission_input(%MyApp.RolePermission{} = rp) do
%AshGrant.PermissionInput{
string: rp.permission_string,
description: rp.label,
source: "role:#{rp.role_name}"
}
end
end
# Then just return your structs from the resolver
defmodule MyApp.PermissionResolver do
@behaviour AshGrant.PermissionResolver
@impl true
def resolve(actor, _context) do
MyApp.Accounts.get_role_permissions(actor)
end
endTip: Relational Scopes
Once you're comfortable with the basics, AshGrant supports relationship-based scopes
using exists() and dot-path references. These work for both read and write actions:
ash_grant do
scope :team_member, expr(exists(team.memberships, user_id == ^actor(:id)))
scope :same_center, expr(order.center_id == ^actor(:center_id))
endFor read actions, these compile to SQL (EXISTS subquery or JOIN). For simple write actions, AshGrant uses a DB query fallback that handles most cases automatically. For multi-hop authorization, composite inheritance, or scopes that wrap relationship references inside functions, prefer the argument-based pattern — see the Argument-Based Scope guide. The Scopes guide covers relational scopes in detail.