### Instance Permissions Examples Source: https://context7.com/jhlee111/ash_grant/llms.txt Examples of instance-specific permissions, including full access and DENY rules for a particular resource ID. ```text "blog:post_abc123:read:" # Read specific post "blog:post_abc123:*:`" # Full access to specific post "!blog:post_abc123:delete:" # DENY delete on specific post ``` -------------------------------- ### Instance Permissions Examples Source: https://github.com/jhlee111/ash_grant/blob/main/usage-rules.md Examples of instance-specific permissions, including unconditional access and attribute-based conditions. ```elixir "blog:post_abc123:read:" "blog:post_abc123:*: "!blog:post_abc123:delete:" "doc:doc_123:update:draft" ``` -------------------------------- ### Setup Test Database Source: https://github.com/jhlee111/ash_grant/blob/main/CLAUDE.md Initialize and migrate the test database for Ash Grant. ```bash mix ecto.setup ``` -------------------------------- ### Instance Permissions Examples Source: https://github.com/jhlee111/ash_grant/blob/main/guides/permissions.md Demonstrates how to grant or deny access to specific resource instances using their unique IDs. ```elixir "blog:post_abc123xyz789ab:read:" # Read specific post ``` ```elixir "blog:post_abc123xyz789ab:*:` # Full access to specific post ``` ```elixir "!blog:post_abc123xyz789ab:delete:" # DENY delete on specific post ``` -------------------------------- ### Instance Permission Read Example Source: https://github.com/jhlee111/ash_grant/blob/main/guides/permissions.md Implement a permission resolver to grant instance-specific read access. This example allows users to read only documents explicitly shared with them. ```elixir defmodule MyApp.PermissionResolver do @behaviour AshGrant.PermissionResolver def resolve(%{shared_doc_ids: doc_ids}, _context) when is_list(doc_ids) do # Generate instance permission for each shared document Enum.map(doc_ids, fn doc_id -> "document:#{doc_id}:read:" end) end end ``` ```elixir # User can only read documents explicitly shared with them actor = %{id: "user-1", shared_doc_ids: ["doc_abc", "doc_xyz"]} Document |> Ash.read!(actor: actor) # => Returns only doc_abc and doc_xyz ``` -------------------------------- ### Legacy Permission Format Examples Source: https://github.com/jhlee111/ash_grant/blob/main/guides/permissions.md Examples of legacy permission formats and their recommended full 4-part equivalents. Use the full format to avoid ambiguity. ```elixir # RBAC permissions "blog:*:read:always" # Explicit 4-part format (recommended) "blog:read:always" # Legacy 3-part format (works but discouraged) # Instance permissions "blog:post123:read:" # Explicit instance permission (recommended) ``` -------------------------------- ### Amount-Based Authorization Example in AshGrant Source: https://github.com/jhlee111/ash_grant/blob/main/guides/authorization-patterns.md An example demonstrating transaction limit tiers using scopes for different amount thresholds. This setup defines access levels based on transaction amounts. ```elixir # Resource ash_grant do resolver MyApp.PaymentResolver default_policies true scope :always, true scope :small_amount, expr(amount < 1000) scope :medium_amount, expr(amount < 10_000) scope :large_amount, expr(amount < 100_000) scope :unlimited, true end ``` ```elixir # Resolver defmodule MyApp.PaymentResolver do @behaviour AshGrant.PermissionResolver def resolve(%{role: :clerk}, _ctx), do: ["payment:*:read:small_amount"] def resolve(%{role: :accountant}, _ctx), do: ["payment:*:read:medium_amount"] def resolve(%{role: :finance_manager}, _ctx), do: ["payment:*:read:large_amount"] def resolve(%{role: :cfo}, _ctx), do: ["payment:*:*:unlimited"] def resolve(_, _ctx), do: [] end ``` -------------------------------- ### Field-Level Permissions Examples Source: https://github.com/jhlee111/ash_grant/blob/main/usage-rules.md Examples demonstrating field-level access control using the 5-part permission format. ```elixir "employee:*:read:always:public" "employee:*:read:always:sensitive" "employee:*:read:always:confidential" ``` -------------------------------- ### Update Document Example Source: https://github.com/jhlee111/ash_grant/blob/main/guides/permissions.md Examples of updating a document based on its status and user permissions. The first example succeeds because the document is in draft status and the user has the 'draft' scope. The second fails because the document is published. ```elixir # User has: "doc:doc_123:update:draft" # This succeeds (doc is in draft) Ash.update!(draft_doc, %{title: "New"}, actor: user) # This fails (doc is published, not draft) Ash.update!(published_doc, %{title: "New"}, actor: user) ``` -------------------------------- ### RBAC Permissions Examples Source: https://context7.com/jhlee111/ash_grant/llms.txt Examples of Role-Based Access Control (RBAC) permissions using a wildcard syntax for different levels of access. ```text "blog:*:read:always" # Read all blogs "blog:*:read:published" # Read only published blogs "blog:*:update:own" # Update own blogs only "blog:*:*:always" # All actions on all blogs "*:*:read:always" # Read all resources "blog:*:read*:always" # All read-type actions (type wildcard) "!blog:*:delete:always" # DENY delete on all blogs ``` -------------------------------- ### Deny-Wins Pattern Example Source: https://github.com/jhlee111/ash_grant/blob/main/guides/permissions.md Demonstrates the deny-wins pattern where explicit deny rules take precedence over allow rules. This example denies delete actions on blogs while allowing all others. ```elixir permissions = [ "blog:*:*:always", # Allow all blog actions "!blog:*:delete:always" # Deny delete ] # Result: # - blog:read -> allowed # - blog:update -> allowed ``` -------------------------------- ### Usage of Tenant Context for Creating Posts Source: https://github.com/jhlee111/ash_grant/blob/main/guides/scopes.md Example of creating a post with a specified `tenant_id`. The creation is validated against the tenant scope. ```elixir # Create - validated against tenant scope Ash.create(Post, %{title: "Hello", tenant_id: tenant_id}, actor: user, tenant: tenant_id ) ``` -------------------------------- ### Examples of Permission String Matching Source: https://github.com/jhlee111/ash_grant/blob/main/guides/permissions.md Demonstrates various ways to use wildcards in permission strings for matching resources, actions, and scopes. ```elixir "*:*:read:always" # All resources, read action (exact name match) ``` ```elixir "blog:*:read*:always" # All :read-type actions on blog (type match) ``` ```elixir "blog:*:read:always" # Only the action named "read" on blog (exact match) ``` -------------------------------- ### RBAC Permissions Examples Source: https://github.com/jhlee111/ash_grant/blob/main/usage-rules.md Examples of Role-Based Access Control (RBAC) permissions using the '*' wildcard for instance_id. ```elixir "blog:*:read:always" # Read all blogs "blog:*:read:published" # Read only published blogs "blog:*:update:own" # Update own blogs only "blog:*:*:always" # All actions on all blogs "*:*:read:always" # Read all resources "blog:*:read*:always" # All read-type actions (read, read_all, etc.) "!blog:*:delete:always" # DENY delete on all blogs ``` -------------------------------- ### Hand-rolled Argument Resolution Example Source: https://github.com/jhlee111/ash_grant/blob/main/guides/argument-based-scope.md An example of manually wiring argument resolution, useful for customized variants. This demonstrates the underlying mechanism of the DSL sugar. ```elixir end ``` -------------------------------- ### RBAC Permissions Examples Source: https://github.com/jhlee111/ash_grant/blob/main/guides/permissions.md Illustrates various Role-Based Access Control (RBAC) permission strings for different resources, actions, and scopes. ```elixir "blog:*:read:always" # Read all blogs ``` ```elixir "blog:*:read:published" # Read only published blogs ``` ```elixir "blog:*:update:own" # Update own blogs only ``` ```elixir "blog:*:*:always" # All actions on all blogs ``` ```elixir "":*:read:always" # Read all resources ``` ```elixir "blog:*:read*:always" # All read-type actions ``` ```elixir "!blog:*:delete:always" # DENY delete on all blogs ``` -------------------------------- ### Get Available Permissions for a Resource Source: https://context7.com/jhlee111/ash_grant/llms.txt List all possible permissions that can be applied to a given resource. Useful for understanding the permission schema. ```elixir # What permissions exist for a resource? AshGrant.Introspect.available_permissions(Post) # [ # %{permission_string: "post:*:read:always", action: "read", scope: "always", ...}, # %{permission_string: "post:*:read:own", action: "read", scope: "own", ...}, # ... # ] ``` -------------------------------- ### RBAC Resource Setup with AshGrant Source: https://github.com/jhlee111/ash_grant/blob/main/guides/authorization-patterns.md Defines an Ash.Resource with AshGrant extensions, setting up default policies and scopes for RBAC. ```elixir defmodule MyApp.Blog.Post do use Ash.Resource, domain: MyApp.Blog, authorizers: [Ash.Policy.Authorizer], extensions: [AshGrant] ash_grant do resolver MyApp.PermissionResolver default_policies true scope :always, true scope :own, expr(author_id == ^actor(:id)) scope :published, expr(status == :published) end end ``` -------------------------------- ### Usage of Tenant Context for Updating Posts Source: https://github.com/jhlee111/ash_grant/blob/main/guides/scopes.md Example of updating a post, requiring it to match both the tenant scope and ownership scope (`own_in_tenant`). ```elixir # Update - must match both tenant AND ownership for own_in_tenant scope Ash.update(post, %{title: "Updated"}, actor: user, tenant: tenant_id) ``` -------------------------------- ### Scope DSL with Inheritance and Multi-Tenancy Source: https://context7.com/jhlee111/ash_grant/llms.txt Demonstrates defining scopes with expressions, inheritance, and multi-tenant support using Ash Grant's DSL. Includes examples of relational and context-injected scopes. ```elixir ash_grant do resolver MyApp.PermissionResolver default_policies true # Boolean scope - no filtering scope :always, true # Expression scopes scope :own, expr(author_id == ^actor(:id)) scope :published, expr(status == :published) # Multi-tenancy using ^tenant() scope :same_tenant, expr(tenant_id == ^tenant()) # Scope inheritance - combines with AND scope :own_in_tenant, [:same_tenant], expr(author_id == ^actor(:id)) # Result: tenant_id == ^tenant() AND author_id == ^actor(:id) scope :own_draft, [:own], expr(status == :draft) # Result: author_id == ^actor(:id) AND status == :draft # Relational scopes scope :team_member, expr(exists(team.memberships, user_id == ^actor(:id))) scope :same_center, expr(order.center_id == ^actor(:center_id)) # Context injection for testable scopes scope :recent, expr(inserted_at > ^context(:cutoff)) scope :within_limit, expr(amount <= ^context(:max_amount)) end # Usage with tenant context posts = Post |> Ash.read!(actor: user, tenant: "acme_corp") # Usage with injected context Post |> Ash.Query.for_read(:read) |> Ash.Query.set_context(%{cutoff: DateTime.add(DateTime.utc_now(), -7, :day)}) |> Ash.read!(actor: actor) ``` -------------------------------- ### RBAC Permission Resolver Example Source: https://github.com/jhlee111/ash_grant/blob/main/guides/authorization-patterns.md Maps roles to permission strings for RBAC. Each role defines specific access levels to resources. ```elixir defmodule MyApp.PermissionResolver do @behaviour AshGrant.PermissionResolver @impl true def resolve(%{role: :admin}, _context) do ["post:*:*:always"] # Full access end def resolve(%{role: :editor}, _context) do ["post:*:read:always", "post:*:update:always"] # Read + update all end def resolve(%{role: :author}, _context) do ["post:*:read:always", "post:*:update:own"] # Read all, update own end def resolve(%{role: :viewer}, _context) do ["post:*:read:published"] # Published only end def resolve(_, _context), do: [] end ``` -------------------------------- ### Load Per-Record Calculations for UI Source: https://github.com/jhlee111/ash_grant/blob/main/guides/checks-and-policies.md Example of loading per-record calculations like `:can_update?` and `:can_destroy?` for UI elements within a LiveView or controller. This avoids N+1 queries. ```elixir members = Member |> Ash.Query.load([:can_update?, :can_destroy?]) |> Ash.read!(actor: current_user) ``` -------------------------------- ### Usage of Tenant Context for Reading Posts Source: https://github.com/jhlee111/ash_grant/blob/main/guides/scopes.md Example of reading posts within a specific tenant context. The `tenant_id` is passed via query options. ```elixir # Read - only returns posts from the specified tenant posts = Post |> Ash.read!(actor: user, tenant: tenant_id) ``` -------------------------------- ### Basic Resource Setup with Inline Resolver Source: https://context7.com/jhlee111/ash_grant/llms.txt Configure a resource with AshGrant using an inline resolver and default policies. The resolver maps actor roles to permission strings, and `default_policies true` auto-generates read/write policies. ```elixir defmodule MyApp.Blog.Post do use Ash.Resource, domain: MyApp.Blog, authorizers: [Ash.Policy.Authorizer], extensions: [AshGrant] ash_grant do # Inline resolver converts actor to permission strings resolver fn actor, _context -> case actor do %{role: :admin} -> ["post:*:*:always"] %{role: :editor} -> [ "post:*:read:always", "post:*:create:always", "post:*:update:own" ] %{role: :viewer} -> ["post:*:read:published"] _ -> [] end end default_policies true # Auto-generates read/write policies scope :always, true scope :own, expr(author_id == ^actor(:id)) scope :published, expr(status == :published) end attributes do uuid_primary_key :id attribute :title, :string attribute :status, :atom, constraints: [one_of: [:draft, :published]] attribute :author_id, :uuid end actions do defaults [:read, :create, :update, :destroy] end end # Usage editor = %{id: "user_123", role: :editor} Post |> Ash.read!(actor: editor) # Returns all posts Ash.update!(post, %{title: "New"}, actor: editor) # Succeeds only if post.author_id == editor.id ``` -------------------------------- ### Summarize Actor Permissions Across Resources Source: https://context7.com/jhlee111/ash_grant/llms.txt Get a summary of all allowed actions for an actor across all resources they have access to. Useful for a high-level overview. ```elixir # Summarize actor across all resources AshGrant.Introspect.summarize_actor(actor, only_with_access: true) # [ # %{resource: MyApp.Blog.Post, resource_key: "post", allowed_actions: [:read, :update], ...}, # %{resource: MyApp.Blog.Comment, resource_key: "comment", allowed_actions: [:read], ...} # ] ``` -------------------------------- ### Module-Based Permission Resolver Source: https://context7.com/jhlee111/ash_grant/llms.txt Implement a production-ready permission resolver module that fetches permissions from a database. This example demonstrates fetching role permissions and adding context-aware instance permissions for specific actions. ```elixir defmodule MyApp.PermissionResolver do @behaviour AshGrant.PermissionResolver @impl true def resolve(nil, _context), do: [] @impl true def resolve(actor, context) do base_permissions = get_role_permissions(actor) # Add context-aware instance permissions 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 defp get_role_permissions(actor) do actor |> MyApp.Accounts.get_user_roles() |> Enum.flat_map(& &1.permissions) end defp get_shared_document_ids(actor) do MyApp.Sharing.get_shared_with(actor.id) |> Enum.map(& &1.document_id) end end # Reference in resource ash_grant do resolver MyApp.PermissionResolver default_policies true scope :always, true end ``` -------------------------------- ### Scope Inheritance Example Source: https://github.com/jhlee111/ash_grant/blob/main/guides/scopes.md Demonstrates how scopes can inherit from parent scopes, combining filters using AND logic. The `base` scope defines a tenant filter, and `active` inherits from it. ```elixir scope :base, expr(tenant_id == ^actor(:tenant_id)) scope :active, [:base], expr(status == :active) # Result: tenant_id == actor.tenant_id AND status == :active ``` -------------------------------- ### Explain Authorization Decisions Source: https://github.com/jhlee111/ash_grant/blob/main/usage-rules.md Use `AshGrant.explain/4` to get a detailed `AshGrant.Explanation` struct for any authorization decision. This includes matching permissions, evaluation reasons, scope information, and the final decision. ```elixir explanation = AshGrant.explain(MyApp.Post, :read, actor) # Print human-readable output explanation |> AshGrant.Explanation.to_string() |> IO.puts() ``` -------------------------------- ### Generate Multiple `can_perform` Calculations with `can_perform_actions` Source: https://github.com/jhlee111/ash_grant/blob/main/CHANGELOG.md The `can_perform_actions` batch option generates multiple `CanPerform` calculations at once. This example generates `:can_update?` and `:can_destroy?`. ```elixir can_perform_actions [:update, :destroy] ``` -------------------------------- ### Explain Authorization Decisions with `explain/4` Source: https://github.com/jhlee111/ash_grant/blob/main/guides/debugging-and-introspection.md Use `AshGrant.explain/4` to get a detailed breakdown of why an authorization decision was made. Inspect the decision, matching permissions, and reasons for non-matches. Pipe the result to `AshGrant.Explanation.to_string/1` for human-readable output. ```elixir # Get detailed explanation result = AshGrant.explain(MyApp.Post, :read, actor) # Check the decision result.decision # => :allow or :deny # See matching permissions with metadata result.matching_permissions # => [%{permission: "post:*:read:always", description: "Read all posts", source: "editor_role", ...}] # See why permissions didn't match result.evaluated_permissions # => [%{permission: "post:*:update:own", matched: false, reason: "Action mismatch"}, ...] # Print human-readable output result |> AshGrant.Explanation.to_string() |> IO.puts() ``` ```text ═══════════════════════════════════════════════════════════════════ Authorization Explanation for MyApp.Blog.Post ═══════════════════════════════════════════════════════════════════ Action: read Decision: ✓ ALLOW Actor: %{id: "user-1", role: :editor} Matching Permissions: • post:*:read:always [scope: always - All records without restriction] (from: editor_role) └─ Read all posts Scope Filter: true (no filtering) ─────────────────────────────────────────────────────────────────── ``` -------------------------------- ### Combine RBAC, ABAC, and ReBAC Patterns in AshGrant Source: https://github.com/jhlee111/ash_grant/blob/main/guides/authorization-patterns.md Use this example to define RBAC, ABAC, and ReBAC scopes, along with field-level restrictions and UI visibility rules, within a single AshGrant configuration. Ensure the `MyApp.PermissionResolver` is correctly configured. ```elixir ash_grant do resolver MyApp.PermissionResolver default_policies true default_field_policies true # RBAC scopes scope :always, true scope :own, expr(author_id == ^actor(:id)) # ABAC scopes scope :same_tenant, expr(tenant_id == ^actor(:tenant_id)) scope :active, expr(status == :active) scope :tenant_own, [:same_tenant], expr(author_id == ^actor(:id)) # ReBAC scope :team_member, expr(exists(team.memberships, user_id == ^actor(:id))) scope_through :feed # Field-level field_group :public, [:title, :body] field_group :internal, [:notes, :score], inherits: [:public] # UI visibility can_perform_actions [:update, :destroy] end ``` -------------------------------- ### Granting Access to Specific Generic Actions Source: https://github.com/jhlee111/ash_grant/blob/main/guides/permissions.md Provides examples of how to grant access to specific generic actions using their exact names in a permission list. ```elixir # Grant access to specific generic actions ["service:*:ping:always", "service:*:check_status:always"] ``` -------------------------------- ### Explain Resource by Identifier Source: https://context7.com/jhlee111/ash_grant/llms.txt Use this function to get a detailed explanation of permissions for a specific actor, resource, and action, often used in admin dashboards or CLI tools. ```elixir {:ok, explanation} = AshGrant.Introspect.explain_by_identifier( actor_id: "user_1", resource_key: "post", action: :read ) ``` -------------------------------- ### Match Instance Permissions with `instance_key` Source: https://github.com/jhlee111/ash_grant/blob/main/CHANGELOG.md Use the `instance_key` DSL option to match instance permission IDs against a field other than `:id`. This example shows how to match against `feed_id`. ```elixir instance_key :feed_id ``` -------------------------------- ### Deny-Wins Semantics Example Source: https://github.com/jhlee111/ash_grant/blob/main/usage-rules.md Demonstrates deny-wins semantics where a deny rule overrides an allow rule. When both allow and deny rules match, the deny rule takes precedence. ```elixir permissions = [ "blog:*:*:always", # Allow all blog actions "!blog:*:delete:always" # Deny delete ] # Result: read ✓, update ✓, delete ✗ (deny wins) ``` -------------------------------- ### Elixir DSL Policy Test Example Source: https://github.com/jhlee111/ash_grant/blob/main/guides/policy-testing.md Use this Elixir DSL to define and test policy configurations for your resources. It requires importing `AshGrant.PolicyTest` and defining actors and test cases. ```elixir defmodule MyApp.PolicyTests.PostPolicyTest do use AshGrant.PolicyTest resource MyApp.Post actor :admin, %{role: :admin} actor :editor, %{role: :editor, id: "editor_001"} actor :viewer, %{role: :viewer} describe "read access" do test "editor can read all posts" do assert_can :editor, :read end test "viewer can read published posts" do assert_can :viewer, :read, %{status: :published} end test "viewer cannot read drafts" do assert_cannot :viewer, :read, %{status: :draft} end end describe "write access" do test "editor can update own posts" do assert_can :editor, :update, %{author_id: "editor_001"} end test "editor cannot update others posts" do assert_cannot :editor, :update, %{author_id: "other_user"} end test "viewer cannot update any posts" do assert_cannot :viewer, :update end end end ``` -------------------------------- ### Generate `CanPerform` Calculations with DSL Sugar Source: https://github.com/jhlee111/ash_grant/blob/main/guides/checks-and-policies.md Recommended approach for generating per-record UI visibility calculations. This example batches generation for `:update` and `:destroy` actions and defines a custom `:visible?` calculation for read actions. ```elixir ash_grant do resolver MyApp.PermissionResolver scope :always, true scope :own, expr(author_id == ^actor(:id)) # Batch — generates :can_update? and :can_destroy? can_perform_actions [:update, :destroy] # Individual with custom name can_perform :read, name: :visible? end ``` -------------------------------- ### Configure Instance Key for Permissions Source: https://github.com/jhlee111/ash_grant/blob/main/usage-rules.md Use `instance_key` to match permissions against a different field than the resource's default ID. This example maps 'feed:feed_abc:read:' to a WHERE clause on `feed_id`. ```elixir ash_grant do resolver MyApp.PermissionResolver instance_key :feed_id # "feed:feed_abc:read:" → WHERE feed_id IN ('feed_abc') scope :always, true end ``` -------------------------------- ### YAML Policy Test Example Source: https://github.com/jhlee111/ash_grant/blob/main/guides/policy-testing.md Define policy tests in YAML format for non-developers or interchange. This format mirrors the Elixir DSL structure with resources, actors, and test cases. ```yaml resource: MyApp.Post actors: editor: role: editor id: "editor_001" viewer: role: viewer tests: - name: "editor can read all posts" assert_can: actor: editor action: read - name: "viewer can read published posts" assert_can: actor: viewer action: read record: status: published - name: "viewer cannot read drafts" assert_cannot: actor: viewer action: read record: status: draft - name: "editor can update own posts" assert_can: actor: editor action: update record: author_id: "editor_001" - name: "editor cannot update others posts" assert_cannot: actor: editor action: update record: author_id: "other_user" ``` -------------------------------- ### Generate Documentation Source: https://github.com/jhlee111/ash_grant/blob/main/CLAUDE.md Generate project documentation using Mix. ```bash mix docs ``` -------------------------------- ### Mix Task: `mix ash_grant.explain` Source: https://github.com/jhlee111/ash_grant/blob/main/guides/public-api-contract.md Command-line interface for explaining permission resolutions. ```APIDOC ## Mix Task: `mix ash_grant.explain` (Stable) ### Description A stable CLI tool that wraps `Introspect.explain_by_identifier/1` to provide permission explanations. ### Usage ```bash mix ash_grant.explain --actor USER_ID --resource RESOURCE_KEY --action ACTION [ --format text|json ] [ --context '' ] ``` ### Options - `--actor USER_ID`: The ID of the actor. - `--resource RESOURCE_KEY`: The key of the resource. - `--action ACTION`: The action to check. - `--format text|json` (Optional): The output format. Defaults to `text`. - `--context ''` (Optional): A JSON string representing the context. ### Exit Codes - **`0`**: Explanation produced successfully. - **`1`**: Lookup failure (e.g., `unknown_resource`, `actor_not_found`, `actor_loader_not_implemented`). - **`2`**: Usage error (e.g., missing option, invalid `--context`, unknown `--format`). ### JSON Output When `--format json` is used, the output is the `Jason.encode!/1` representation of the `Explanation.t()` struct. ``` -------------------------------- ### CLI: `mix ash_grant.explain` Source: https://github.com/jhlee111/ash_grant/blob/main/guides/debugging-and-introspection.md A command-line interface tool for explaining permissions. ```APIDOC ## CLI: `mix ash_grant.explain` ### Description A command-line wrapper around `explain_by_identifier/1` for shell scripts and CI checks. ### Usage ```bash mix ash_grant.explain --actor USER_ID --resource RESOURCE_KEY --action ACTION \ [--format text|json] [--context ''] ``` ### Exit Codes - `0`: Explanation produced (allow or deny). - `1`: Lookup failure (unknown resource, actor loader, or actor not found). - `2`: Usage error (bad flag, malformed `--context`). ``` -------------------------------- ### `mask_with/2` Function Signature and Example Source: https://github.com/jhlee111/ash_grant/blob/main/guides/field-level-permissions.md The `mask_with/2` function receives the field's value and name. This example demonstrates per-field logic, masking phone numbers with a partial reveal and emails with a masked prefix, while other fields are masked with `"***"`. ```elixir # @spec mask_with(value :: term(), field :: atom()) :: term() # # - `value` — the attribute's current value on the record. May be `nil`. # - `field` — the attribute name as an atom. Useful for one function that # masks multiple fields differently. mask_with: fn value, :phone when is_binary(value) -> "***-****-" <> String.slice(value, -4..-1) value, :email when is_binary(value) -> [_, domain] = String.split(value, "@", parts: 2) "***@" <> domain _value, _field -> "***" end ``` -------------------------------- ### Combined Scopes Example Source: https://github.com/jhlee111/ash_grant/blob/main/guides/argument-based-scope.md Combines an argument-based scope with another condition. This allows for more complex authorization rules. ```elixir scope :at_own_unit_and_small, [:at_own_unit], expr(total_amount <= 100) ``` -------------------------------- ### AshGrant.Explanation Module Source: https://github.com/jhlee111/ash_grant/blob/main/CHANGELOG.md Tools for debugging authorization decisions with detailed explanations. ```APIDOC ## AshGrant.Explanation and AshGrant.Explainer ### Description These modules facilitate detailed debugging of authorization decisions, providing insights into how permissions are evaluated. ### Key Functions and Features - **`AshGrant.explain/4`** - Description: Generates a detailed `AshGrant.Explanation` struct for authorization decisions. - Output: Includes matching permissions with metadata (description, source), evaluated permissions with reasons, and scope information. - **`AshGrant.Explanation` Struct** - Description: A data structure holding detailed information about an authorization decision. - **`AshGrant.Explainer` Module** - Description: Responsible for building the `AshGrant.Explanation` struct. - **`AshGrant.Explanation.to_string/2`** - Description: Converts the `AshGrant.Explanation` struct into a human-readable string, with optional ANSI color support. ### Scope Descriptions - **`description` field for scopes**: - Allows adding human-readable descriptions to scopes defined in the DSL. - Example: `scope :own, [], expr(author_id == ^actor(:id)), description: "Records owned by the current user"` - **`AshGrant.Info.scope_description/2`** - Description: Retrieves the description for a given scope programmatically. - Usage: Enhances debugging and UI display of scope information. ``` -------------------------------- ### CLI for Permission Explanation Source: https://github.com/jhlee111/ash_grant/blob/main/guides/debugging-and-introspection.md The `mix ash_grant.explain` command provides a CLI interface for `explain_by_identifier/1`, suitable for shell scripts and CI checks. It supports various output formats and context options. ```bash mix ash_grant.explain --actor USER_ID --resource RESOURCE_KEY --action ACTION \ [--format text|json] [--context ''] ``` -------------------------------- ### Get Actor Permissions for a Resource Source: https://context7.com/jhlee111/ash_grant/llms.txt Query all permissions an actor has on a specific resource. This is useful for displaying user capabilities in UIs. ```elixir # What can this user do on a resource? AshGrant.Introspect.actor_permissions(Post, current_user) # [ # %{action: "read", allowed: true, scope: "always", denied: false, ...}, # %{action: "update", allowed: true, scope: "own", denied: false, ...}, # %{action: "destroy", allowed: false, scope: nil, denied: false, ...} # ] ``` -------------------------------- ### Explain Permissions by Identifier Source: https://github.com/jhlee111/ash_grant/blob/main/guides/debugging-and-introspection.md Use `explain_by_identifier/1` to resolve resources and load actors by their identifiers, then delegate to `AshGrant.explain/4`. It returns an explanation of the permission decision. ```elixir AshGrant.Introspect.explain_by_identifier( actor_id: "user_1", resource_key: "post", # matches AshGrant.Info.resource_name/1 action: :read, context: %{tenant: "acme"} # optional ) # => {:ok, %AshGrant.Explanation{decision: :allow, ...}} ``` -------------------------------- ### Field Group Permissions Source: https://context7.com/jhlee111/ash_grant/llms.txt Examples of 5-part format permissions for controlling access to different field groups (public, sensitive, confidential). ```text "employee:*:read:always:public" # See public fields only "employee:*:read:always:sensitive" # See public + sensitive fields "employee:*:read:always:confidential" # See all fields ``` -------------------------------- ### Instance Key Configuration Source: https://github.com/jhlee111/ash_grant/blob/main/guides/permissions.md Configure Ash Grant to match instance permissions against a field other than the primary key. This example uses `feed_id`. ```elixir ash_grant do resolver MyApp.PermissionResolver instance_key :feed_id # Match against feed_id instead of id scope :always, true end ``` -------------------------------- ### Admin Access with Universal Wildcard Source: https://github.com/jhlee111/ash_grant/blob/main/guides/permissions.md Shows how to use the universal wildcard ('*:*:*:always') to grant administrative access to all actions on all services. ```elixir # Or use the universal wildcard for admin access ["service:*:*:always"] ``` -------------------------------- ### Relational Scope Example Source: https://github.com/jhlee111/ash_grant/blob/main/guides/argument-based-scope.md A relational scope that traverses a relationship directly. This works for read actions but can have rough edges for write actions. ```elixir scope :at_own_unit, expr(order.center_id in ^actor(:own_org_unit_ids)) ``` -------------------------------- ### Get Raw Permissions from Resolver Source: https://context7.com/jhlee111/ash_grant/llms.txt Retrieve the raw permission strings as defined by the resource's resolvers. Useful for debugging or advanced introspection. ```elixir # Raw permissions from resolver AshGrant.Introspect.permissions_for(Post, user) # ["post:*:read:always", "post:*:update:own"] ``` -------------------------------- ### Debug Authorization Decisions with `AshGrant.explain/4` Source: https://context7.com/jhlee111/ash_grant/llms.txt Use `AshGrant.explain/4` to inspect authorization decisions, understanding why access was allowed or denied. It provides details on matching permissions and the overall decision. ```elixir # Basic usage explanation = AshGrant.explain(MyApp.Post, :read, actor) # %AshGrant.Explanation{ # decision: :allow, # matching_permissions: [%{permission: "post:*:read:always", ...}], # ... # } # With context explanation = AshGrant.explain(MyApp.Post, :read, actor, %{tenant: "acme"}) ``` ```elixir AshGrant.explain(MyApp.Post, :read, actor) |> AshGrant.Explanation.to_string() |> IO.puts() # ═══════════════════════════════════════════════════════════════════ # Authorization Explanation for MyApp.Post # ═══════════════════════════════════════════════════════════════════ # Action: read # Decision: ✓ ALLOW # ... ``` -------------------------------- ### Get Actor Permissions with Context Source: https://github.com/jhlee111/ash_grant/blob/main/guides/debugging-and-introspection.md Pass a `:context` option to `actor_permissions/3` to include additional resolver context, such as tenant IDs, in permission checks. ```elixir AshGrant.Introspect.actor_permissions(Post, user, context: %{tenant: tenant_id}) ``` -------------------------------- ### Scope Through with Action Filtering Source: https://github.com/jhlee111/ash_grant/blob/main/CHANGELOG.md The `scope_through` entity supports action filtering using the `actions:` option. This example limits the scope propagation to specific actions. ```elixir scope_through :feed, actions: [:read] ``` -------------------------------- ### Mix Task: Run Policy Tests Source: https://github.com/jhlee111/ash_grant/blob/main/guides/policy-testing.md Execute policy tests using the `mix ash_grant.verify` task. Specify the path to your DSL or YAML test files. Use the `--verbose` flag for detailed output. ```bash mix ash_grant.verify test/policy_tests/ mix ash_grant.verify priv/policy_tests/document.yaml mix ash_grant.verify test/policy_tests/ --verbose ``` -------------------------------- ### Date-Based Scopes Source: https://github.com/jhlee111/ash_grant/blob/main/guides/scopes.md Define scopes for temporal filtering using SQL fragments. Examples include filtering records created today and combining with ownership. ```elixir # Records created today only scope :today, expr(fragment("DATE(inserted_at) = CURRENT_DATE")) # Combined with ownership scope :own_today, [:own], expr(fragment("DATE(inserted_at) = CURRENT_DATE")) ``` -------------------------------- ### Instance Permission Read Support Source: https://github.com/jhlee111/ash_grant/blob/main/CHANGELOG.md Instance permissions now support read actions through `AshGrant.Evaluator.get_matching_instance_ids/3`. This enables fine-grained access control for specific resources, similar to Google Docs sharing. ```elixir AshGrant.Evaluator.get_matching_instance_ids(actor, Ash.Data.Resource.name(Post), :read) ``` -------------------------------- ### Define Custom Permission Resolver Source: https://github.com/jhlee111/ash_grant/blob/main/guides/getting-started.md Implement the `AshGrant.PermissionResolver` behaviour to define how permissions are fetched for an actor. This example fetches role permissions for a given actor. ```elixir defmodule MyApp.PermissionResolver do @behaviour AshGrant.PermissionResolver @impl true def resolve(actor, _context) do MyApp.Accounts.get_role_permissions(actor) end end ``` -------------------------------- ### Permissions with Metadata using PermissionInput Source: https://github.com/jhlee111/ash_grant/blob/main/guides/getting-started.md Return AshGrant.PermissionInput structs from your resolver to provide descriptions and sources for permissions. This enhances debugging and the explain/4 functionality. ```elixir 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 end ``` -------------------------------- ### Get Matching Scope for a Permission Source: https://context7.com/jhlee111/ash_grant/llms.txt Find the specific scope that matches a given resource and action from a list of permissions. Returns the first matching scope. ```elixir # Get matching scope AshGrant.Evaluator.get_scope(permissions, "blog", "read") # "always" AshGrant.Evaluator.get_all_scopes(permissions, "blog", "read") # ["always", "own"] ``` -------------------------------- ### Explain Authorization Decision Source: https://github.com/jhlee111/ash_grant/blob/main/guides/public-api-contract.md Use this function to get a detailed explanation of an authorization decision for a given resource, action, and actor. It returns an AshGrant.Explanation struct. ```elixir AshGrant.explain(MyApp.Post, :read, actor, %{}) ``` -------------------------------- ### Verify Policy Configuration with Mix Task Source: https://github.com/jhlee111/ash_grant/blob/main/usage-rules.md Run policy configuration tests using the `mix ash_grant.verify` command. This task helps ensure that your defined policies are correctly configured and behave as expected. ```bash mix ash_grant.verify ``` -------------------------------- ### Argument-Based Scope Example Source: https://github.com/jhlee111/ash_grant/blob/main/guides/scope-naming-convention.md When the scoped attribute is not on the record itself, write the expression against an action argument and declare a `resolve_argument`. The scope name remains consistent with the sentence test. ```elixir scope :at_own_unit, expr(^arg(:center_id) in ^actor(:own_org_unit_ids)) resolve_argument :center_id, from_path: [:order, :center_id] ``` -------------------------------- ### Manual Field Policies using AshGrant.field_check Source: https://github.com/jhlee111/ash_grant/blob/main/guides/field-level-permissions.md Write Ash `field_policies` manually using `AshGrant.field_check/1`. This example defines policies for `:confidential`, `:sensitive`, and a catch-all `:*` policy. ```elixir field_policies do field_policy [:salary, :email] do authorize_if AshGrant.field_check(:confidential) end field_policy [:phone, :address] do authorize_if AshGrant.field_check(:sensitive) end field_policy :* do authorize_if always() end end ``` -------------------------------- ### Get Actor Permissions by ID Source: https://github.com/jhlee111/ash_grant/blob/main/guides/debugging-and-introspection.md Use `actor_permissions_by_id/2` to retrieve a list of permissions for an actor, identified by ID, on a specific resource. Returns a list of permission details. ```elixir AshGrant.Introspect.actor_permissions_by_id("user_1", "post") # => {:ok, [%{action: "read", allowed: true, ...}, ...]}} ``` -------------------------------- ### AshGrant.explain/4 Source: https://github.com/jhlee111/ash_grant/blob/main/guides/public-api-contract.md Provides a rich explanation of authorization decisions for a given resource, action, and actor. ```APIDOC ## AshGrant.explain/4 ### Description This is the top-level entry point for generating a detailed authorization explanation. It returns an `AshGrant.Explanation.t()` struct. ### Method `AshGrant.explain/4` ### Parameters - `resource` (module() | String.t()) - The Ash resource module or its string key. - `action` (atom()) - The action to explain. - `actor` (term()) - The actor for whom the action is being checked. - `context` (map()) - Additional context for the explanation. ### Request Example ```elixir AshGrant.explain(MyApp.Post, :read, actor, %{}) ``` ### Response Returns an `AshGrant.Explanation.t()` struct. The fields of this struct are part of the public contract. #### Success Response (200) - `:resource` (module()) - The Ash resource module. - `:action` (atom()) - Action name. - `:actor` (term()) - The actor passed in. - `:decision` (:allow | :deny) - The final decision. - `:reason` (atom()) - Low-level reason (internal-shaped). - `:reason_code` (:allow_matched | :deny_rule_matched | :no_matching_permission | nil) - Stable branching code. *(Provisional)* - `:summary` (String.t()) - Human/LLM-readable one-liner. *(Provisional)* - `:matching_permissions` ([map()]) - Permissions that contributed to the decision. - `:evaluated_permissions` ([map()]) - All evaluated permissions with per-permission reasons. - `:deny_rule` (map() | nil) - The deny rule that won, if any. - `:scope_filter` (Ash.Expr.t() | nil) - Raw scope filter expression. - `:scope_filter_string` (String.t() | nil) - Human/LLM-readable stringification of `scope_filter`. *(Provisional)* ### Response Example ```elixir %{ resource: MyApp.Post, action: :read, actor: %MyApp.User{id: 123}, decision: :allow, reason: :allow_matched, reason_code: :allow_matched, summary: "User is allowed to read posts.", matching_permissions: ["read_own_posts"], evaluated_permissions: [...], deny_rule: nil, scope_filter: nil, scope_filter_string: nil } ``` ### Stable Functions - `AshGrant.Explanation.to_string/1` - Used for terminal output. ``` -------------------------------- ### Instance Permissions with Scopes (ABAC) Source: https://context7.com/jhlee111/ash_grant/llms.txt Examples of Attribute-Based Access Control (ABAC) permissions using scopes to define conditions like document status or business hours. ```text "doc:doc_123:update:draft" # Update only when document is in draft status "doc:doc_123:read:business_hours" # Read only during business hours ``` -------------------------------- ### Explainer Prefix Matching with Action Types Source: https://github.com/jhlee111/ash_grant/blob/main/CHANGELOG.md The `Explainer.matches_action?/2` function now uses `Permission.matches_action?/3` to support full prefix and action-type matching, aligning with the updated action prefix behavior. ```elixir Explainer.matches_action?/2 ``` -------------------------------- ### Verify All YAML Policy Tests Source: https://github.com/jhlee111/ash_grant/blob/main/CLAUDE.md Run all policy verification tests located in the specified directory. ```bash mix ash_grant.verify priv/policy_tests/ ``` -------------------------------- ### Resolver Context Usage Example Source: https://github.com/jhlee111/ash_grant/blob/main/guides/getting-started.md Access the context parameter in your resolver to dynamically determine permissions based on the resource, action, and other contextual information. This allows for instance-level permissions. ```elixir 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 end ``` -------------------------------- ### AshGrant with Default Policies Source: https://github.com/jhlee111/ash_grant/blob/main/usage-rules.md Configuring AshGrant with `default_policies: true` to automatically generate read and write policies, eliminating the need for a manual policies block. ```elixir ash_grant do resolver MyApp.PermissionResolver default_policies true # Generates read + write policies automatically scope :always, true scope :own, expr(author_id == ^actor(:id)) end # No policies block needed! ``` -------------------------------- ### Get Raw Permissions for a Resource Source: https://github.com/jhlee111/ash_grant/blob/main/guides/debugging-and-introspection.md Use `permissions_for/2` to retrieve all raw permissions for a given resource and actor. This is useful for understanding the full scope of an actor's access. ```elixir AshGrant.Introspect.permissions_for(Post, user) # => ["post:*:read:always", "post:*:update:own", "post:*:create:always"] ``` -------------------------------- ### Get Allowed Actions for an Actor Source: https://github.com/jhlee111/ash_grant/blob/main/guides/debugging-and-introspection.md Use `AshGrant.Introspect.allowed_actions/2` or `AshGrant.Introspect.allowed_actions/3` to retrieve a list of actions an actor is permitted to perform on a resource. The `detailed: true` option provides more information. ```elixir # Simple list AshGrant.Introspect.allowed_actions(Post, user) # => [:read, :create, :update] # With details AshGrant.Introspect.allowed_actions(Post, user, detailed: true) # => [ ```