AshGrant.Introspect (AshGrant v0.17.0)

Copy Markdown View Source

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

FunctionUse CaseReturns
actor_permissions/3Admin UIAll permissions with status
available_permissions/1Permission managementAll possible permissions
can?/4Debugging:allow or :deny with details
allowed_actions/3API responseList of allowed actions
permissions_for/3Raw accessPermission 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", scopes: ["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.

Returns list of allowed actions for an actor.

Returns all available permissions for a resource.

Simple permission check returning :allow or :deny with details.

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

available_permission()

@type available_permission() :: %{
  permission_string: String.t(),
  action: String.t(),
  scope: String.t(),
  scope_description: String.t() | nil,
  field_group: String.t() | nil
}

permission_status()

@type permission_status() :: %{
  action: String.t(),
  allowed: boolean(),
  denied: boolean(),
  scope: String.t() | nil,
  instance_ids: [String.t()] | nil,
  field_groups: [String.t()]
}

resource_summary()

@type resource_summary() :: %{
  resource: module(),
  resource_key: String.t(),
  allowed_actions: [atom()],
  permissions: [permission_status()]
}

Functions

actor_permissions(resource, actor, opts \\ [])

@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: []}
]

actor_permissions_by_id(actor_id, resource_key, opts \\ [])

@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, ...}, ...]}

allowed_actions(resource, actor, opts \\ [])

@spec allowed_actions(module(), term(), keyword()) :: [atom()] | [map()]

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 - When true, 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: []}
]

available_permissions(resource)

@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},
  ...
]

can?(resource, action, actor, opts \\ [])

@spec can?(module(), atom(), term(), keyword()) :: {:allow, map()} | {:deny, map()}

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", scopes: ["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}}

can_by_identifier(actor_id, resource_key, action, opts \\ [])

@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 as can?/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", scopes: ["always"], instance_ids: nil, field_groups: []}}

explain_by_identifier(opts)

@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 to resolver.load_actor/1.
  • :resource_key - Required. Resource name (matches AshGrant.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 matched resource_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 optional load_actor/1 callback)
  • {:error, :actor_not_found} - resolver's load_actor/1 returned :error

Examples

iex> AshGrant.Introspect.explain_by_identifier(
...>   actor_id: "user_1",
...>   resource_key: "post",
...>   action: :read
...> )
{:ok, %AshGrant.Explanation{decision: :allow, ...}}

find_resource_by_key(key)

@spec find_resource_by_key(String.t()) :: {:ok, module()} | :error

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

list_domains()

@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]

list_resources(opts \\ [])

@spec list_resources(keyword()) :: [module()]

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, uses list_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]

permissions_for(resource, actor, opts \\ [])

@spec permissions_for(module(), term(), keyword()) :: [String.t()]

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"]

summarize_actor(actor, opts \\ [])

@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, uses list_resources/1 (optionally scoped by :domains).
  • :domains - Restrict auto-discovery to these Ash domains. Ignored when :resources is given.
  • :context - Context map passed through to each resource's resolver.
  • :only_with_access - When true, drops resources where allowed_actions is empty. Defaults to false.

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 full actor_permissions/3 output 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: [...]
  },
  ...
]