Permission introspection helpers for various use cases.
This module provides functions to query and inspect permissions at runtime, useful for:
- Admin UI: Display what permissions a user has
- Permission Management: List all available permissions for a resource
- Debugging: Check why access is allowed or denied
- API Responses: Return allowed actions to clients
Functions Overview
| Function | Use Case | Returns |
|---|---|---|
actor_permissions/3 | Admin UI | All permissions with status |
available_permissions/1 | Permission management | All possible permissions |
can?/4 | Debugging | :allow or :deny with details |
allowed_actions/3 | API response | List of allowed actions |
permissions_for/3 | Raw access | Permission strings from resolver |
Examples
# Admin UI: What can this user do?
Introspect.actor_permissions(Post, current_user)
# => [%{action: "read", allowed: true, scope: "always", field_groups: []}, ...]
# Permission management: What permissions exist?
Introspect.available_permissions(Post)
# => [%{permission_string: "post:*:read:always", action: "read", scope: "always", field_group: nil}, ...]
# Debugging: Can user do this?
Introspect.can?(Post, :update, user)
# => {:allow, %{scope: "own", instance_ids: nil, field_groups: []}}
# API: What actions are available?
Introspect.allowed_actions(Post, user)
# => [:read, :create, :update]
Summary
Functions
Returns all permissions for a resource with their status for a given actor.
Identifier-based variant of actor_permissions/3.
Returns list of allowed actions for an actor.
Returns all available permissions for a resource.
Simple permission check returning :allow or :deny with details.
Identifier-based variant of can?/4.
Explains an access decision using string/ID inputs.
Resolves a resource key string to its module, if any registered resource declares that name.
Lists Ash domains configured under :ash_domains across all started
applications.
Lists every resource that uses the AshGrant extension.
Returns raw permissions from the resolver for an actor.
Summarizes everything an actor can do across a set of resources.
Types
@type resource_summary() :: %{ resource: module(), resource_key: String.t(), allowed_actions: [atom()], permissions: [permission_status()] }
Functions
@spec actor_permissions(module(), term(), keyword()) :: [permission_status()]
Returns all permissions for a resource with their status for a given actor.
Useful for Admin UI to display what a user can or cannot do.
Options
:context- Additional context to pass to the resolver
Examples
iex> Introspect.actor_permissions(Post, %{role: :editor})
[
%{action: "read", allowed: true, scope: "always", denied: false, instance_ids: nil, field_groups: []},
%{action: "update", allowed: true, scope: "own", denied: false, instance_ids: nil, field_groups: []},
%{action: "destroy", allowed: false, scope: nil, denied: false, instance_ids: nil, field_groups: []}
]
@spec actor_permissions_by_id(term(), String.t(), keyword()) :: {:ok, [permission_status()]} | {:error, :unknown_resource | :actor_loader_not_implemented | :actor_not_found}
Identifier-based variant of actor_permissions/3.
Options
:context- Optional. Additional context map passed to the resolver.
Returns
{:ok, [permission_status()]}{:error, :unknown_resource | :actor_loader_not_implemented | :actor_not_found}
Examples
iex> AshGrant.Introspect.actor_permissions_by_id("user_1", "post")
{:ok, [%{action: "read", allowed: true, ...}, ...]}
Returns list of allowed actions for an actor.
Useful for API responses to tell clients what they can do.
Options
:context- Additional context to pass to the resolver:detailed- Whentrue, returns detailed info instead of just action names
Examples
iex> Introspect.allowed_actions(Post, %{role: :editor})
[:read, :create, :update]
iex> Introspect.allowed_actions(Post, %{role: :editor}, detailed: true)
[
%{action: :read, scope: "always", instance_ids: nil, field_groups: []},
%{action: :create, scope: "always", instance_ids: nil, field_groups: []},
%{action: :update, scope: "own", instance_ids: nil, field_groups: []}
]
@spec available_permissions(module()) :: [available_permission()]
Returns all available permissions for a resource.
Useful for permission management UI to show what permissions can be assigned.
Examples
iex> Introspect.available_permissions(Post)
[
%{permission_string: "post:*:read:always", action: "read", scope: "always", scope_description: nil, field_group: nil},
%{permission_string: "post:*:read:own", action: "read", scope: "own", scope_description: "...", field_group: nil},
...
]
Simple permission check returning :allow or :deny with details.
Useful for debugging authorization issues.
Options
:context- Additional context to pass to the resolver
Examples
iex> Introspect.can?(Post, :read, %{role: :editor})
{:allow, %{scope: "always", instance_ids: nil, field_groups: []}}
iex> Introspect.can?(Post, :destroy, %{role: :viewer})
{:deny, %{reason: :no_permission}}
iex> Introspect.can?(Post, :read, nil)
{:deny, %{reason: :no_actor}}
@spec can_by_identifier(term(), String.t(), atom(), keyword()) :: {:allow, map()} | {:deny, map()} | {:error, :unknown_resource | :actor_loader_not_implemented | :actor_not_found}
Identifier-based variant of can?/4.
Same resolution flow as explain_by_identifier/1 (resource key →
module, actor id → actor), then delegates to can?/4.
Options
:context- Optional. Additional context map passed to the resolver.
Returns
{:allow, map()}/{:deny, map()}- same shape ascan?/4{:error, :unknown_resource | :actor_loader_not_implemented | :actor_not_found}
Examples
iex> AshGrant.Introspect.can_by_identifier("user_1", "post", :read)
{:allow, %{scope: "always", instance_ids: nil, field_groups: []}}
@spec explain_by_identifier(keyword()) :: {:ok, AshGrant.Explanation.t()} | {:error, :unknown_resource | :actor_loader_not_implemented | :actor_not_found}
Explains an access decision using string/ID inputs.
Resolves resource_key to a resource module via find_resource_by_key/1,
then loads the actor by calling load_actor/1 on the resource's
permission resolver module, and finally delegates to
AshGrant.explain/4.
This is the primary entry point for external tools (admin dashboards,
LLM agents, mix ash_grant.explain) that only know string identifiers.
Options (keyword list)
:actor_id- Required. Identifier to pass toresolver.load_actor/1.:resource_key- Required. Resource name (matchesAshGrant.Info.resource_name/1).:action- Required. Action name as an atom.:context- Optional. Additional context map passed to the resolver.
Returns
{:ok, AshGrant.Explanation.t()}{:error, :unknown_resource}- no resource matchedresource_key{:error, :actor_loader_not_implemented}- resolver cannot load actors by ID (either it's an anonymous function or the module doesn't export the optionalload_actor/1callback){:error, :actor_not_found}- resolver'sload_actor/1returned:error
Examples
iex> AshGrant.Introspect.explain_by_identifier(
...> actor_id: "user_1",
...> resource_key: "post",
...> action: :read
...> )
{:ok, %AshGrant.Explanation{decision: :allow, ...}}
Resolves a resource key string to its module, if any registered resource declares that name.
Matching is case-sensitive and uses AshGrant.Info.resource_name/1
(either the explicit resource_name "..." DSL value or the auto-derived
default). This enables external tools to accept resource names as strings
without knowing Elixir module references.
Examples
iex> AshGrant.Introspect.find_resource_by_key("blog")
{:ok, MyApp.Blog.Post}
iex> AshGrant.Introspect.find_resource_by_key("unknown")
:error
@spec list_domains() :: [module()]
Lists Ash domains configured under :ash_domains across all started
applications.
AshGrant reuses the standard Ash convention (config :my_app, ash_domains: [...])
rather than introducing its own configuration key. Any domain registered this
way is a candidate for resource discovery.
Examples
iex> AshGrant.Introspect.list_domains()
[MyApp.Blog, MyApp.Accounts]
Lists every resource that uses the AshGrant extension.
By default, domains are auto-discovered via list_domains/0. Pass
:domains to scope the lookup to a specific set (useful in tests and
multi-tenant setups).
Options
:domains- Explicit list of Ash domain modules to inspect. When omitted, useslist_domains/0.
Examples
iex> AshGrant.Introspect.list_resources()
[MyApp.Blog.Post, MyApp.Blog.Comment, ...]
iex> AshGrant.Introspect.list_resources(domains: [MyApp.Blog])
[MyApp.Blog.Post, MyApp.Blog.Comment]
Returns raw permissions from the resolver for an actor.
Useful when you need direct access to permission strings.
Options
:context- Additional context to pass to the resolver
Examples
iex> Introspect.permissions_for(Post, %{role: :editor})
["post:*:read:always", "post:*:update:own", "post:*:create:always"]
@spec summarize_actor( term(), keyword() ) :: [resource_summary()]
Summarizes everything an actor can do across a set of resources.
Iterates the resource list, calls actor_permissions/3 per resource,
and aggregates the results into a per-resource summary. This is the
"single question" admin dashboards and LLM tools ask to build a
user's global access overview in one call.
Options
:resources- Explicit list of resource modules to summarize. When omitted, useslist_resources/1(optionally scoped by:domains).:domains- Restrict auto-discovery to these Ash domains. Ignored when:resourcesis given.:context- Context map passed through to each resource's resolver.:only_with_access- Whentrue, drops resources whereallowed_actionsis empty. Defaults tofalse.
Returns
A list of maps with:
:resource- the resource module:resource_key- the resource name used in permission strings:allowed_actions- list of action atoms the actor is currently allowed to perform:permissions- the fullactor_permissions/3output for the resource (one entry per action, with allowed/denied/scope details)
Returns [] when actor is nil.
Examples
iex> AshGrant.Introspect.summarize_actor(%{id: "u1", permissions: ["post:*:read:always"]})
[
%{
resource: MyApp.Blog.Post,
resource_key: "post",
allowed_actions: [:read],
permissions: [...]
},
...
]