CHANGELOG.md

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.14.1] - 2026-04-13

Two fixes that make `resolve_argument` (introduced in 0.14.0) actually
usable outside `AshGrant.PolicyTest` — both bugs rendered the DSL sugar
a silent no-op in production. Also bumps the hard Ash floor to 3.19 for
compatibility-CI parity.

### Fixed

- **`resolve_argument` was a silent no-op for non-plain-map actors** (#101). The `needs_resolution?/3` optimization read permissions straight off `actor.permissions` and returned `[]` for any actor that was not a literal map with a `:permissions` key. Real Ash resource structs carry no such field — permissions come from the configured `PermissionResolver` — so the change never ran in production, the argument stayed `nil`, and argument-based scopes always denied. `AshGrant.Changes.ResolveArgument` now routes through the resource's configured resolver (same source as `AshGrant.Check`/`FilterCheck`) and conservatively resolves the argument when the resolver is absent or raises, rather than skipping.
- **`resolve_argument` silently failed on CREATE for attribute-multitenant targets** (#99). `AshGrant.Changes.ResolveArgument` did not forward the changeset's tenant to `Ash.get!`/`Ash.load!`, so whenever any hop in `from_path` pointed to a resource with `multitenancy strategy: :attribute`, the fetch raised, the rescue returned `nil`, and the argument-based scope evaluated to `false` — denying the action. The change now passes `tenant: changeset.tenant` to both the create-path `safe_get/3` and the update/destroy-path `safe_load/3`.

### Changed

- **Ash floor bumped from `~> 3.7` to `~> 3.19`** (#98). Aligns the declared minimum with the version the compatibility CI matrix already exercises.

### Documentation

- ExDoc now surfaces the `scope-naming-convention.md` and `argument-based-scope.md` guides in its extras list (previously authored but unlinked in `mix.exs`).
- `AshGrant.ArgumentAnalyzer` `@moduledoc` no longer references a removed helper on `AshGrant.Check`.

## [0.14.0] - 2026-04-13

This release is centred on a new **argument-based scope pattern** for
multi-hop authorization, fixes a related security bypass in composite
scope routing, and begins deprecating the `write:` scope option in favour
of the new pattern.

### Added

- **`resolve_argument` DSL entity** for argument-based scopes (#90). Declare a scope that compares an action argument (`^arg(:name)`) to actor attributes, and let the resource populate the argument from its own relationships:

  ```elixir
  ash_grant do
    scope :at_own_unit, expr(^arg(:center_id) in ^actor(:own_org_unit_ids))
    resolve_argument :center_id, from_path: [:order, :center_id]
  end
  ```

  The transformer validates the path, detects dead declarations, and auto-injects an argument + a lazy change on every write action. The change only performs the DB load when an in-play permission uses a scope that actually references the argument — direct-attribute scopes pay zero cost. Multi-hop paths (e.g., `[:order, :customer, :organization_id]`) are supported. See the new [Argument-Based Scope guide](guides/argument-based-scope.md).
- **`^arg(:name)` templates in scope expressions** now resolve correctly at strict-check time (#88). Previously `AshGrant.Check` never forwarded `changeset.arguments` to `Ash.Expr.fill_template`, so any scope using `^arg(...)` crashed with `BadMapError` before this release.
- **Explain surface for `resolve_argument`** (#93). `AshGrant.Explanation` gains a `:resolve_arguments` field populated by `Explainer.explain/4`, and `Explanation.to_string/2` renders a new "Argument Resolution" section showing each declared resolver, its path, and which scopes trigger it.
- **PolicyTest support for action arguments** (#93). `assert_can`/`assert_cannot` accept a keyword-list third argument — `[record: ..., arguments: ...]` — and `YamlParser` parses an `arguments:` field on each test. `DslGenerator` emits the keyword-list form when converting YAML→DSL.

### Fixed

- **Composite scope on create security bypass** (#87, closes #83). `should_use_db_query?/3` now inspects the resolved filter (with inheritance applied) instead of the child scope's raw `scope_def.filter`. Composite scopes inheriting a relational parent (`exists()` or dot-path) no longer skip the DB query fallback on create actions. Before this fix, `:at_own_unit_and_small` (inheriting a relational `:at_own_unit` parent) could silently allow unauthorized creates when the parent's `exists()` condition evaluated truthy against a virtual record.
- **Detector coverage** for function-wrapped relational references and lists (#87). `contains_relationship_reference?/1` now descends into `%{__function__?: true, arguments: ...}` structs, list RHS of `in` operators, and handles `nil` explicitly.

### Deprecated

- **`write:` option on `scope`** (#91). The option was introduced as an escape hatch for relational scopes that couldn't be evaluated in memory on writes. With `resolve_argument` now providing a first-class way to express multi-hop authorization via in-memory-evaluable scopes, `write:` is redundant for the common case. Using it still works but emits a compile-time deprecation warning pointing at the replacement pattern. A regression test (#92) asserts the warning emits with the correct message.

### Changed

- **Scope naming**: the unrestricted scope is renamed from `:all` to `:always` across the codebase, guides, and test fixtures (#84). `"all"` is still accepted as a boolean-true scope for backward compatibility; new code should use `:always`. A [Scope Naming Convention guide](guides/scope-naming-convention.md) documents the "sentence test" that motivated the rename.

### Documentation

- New [Argument-Based Scope guide](guides/argument-based-scope.md) with full Refund → Order → center_id example, hand-rolled "under the hood" section, safety analysis, and gotchas (#89).
- Cross-links from `authorization-patterns.md`, `checks-and-policies.md`, `policy-testing.md`, `scope-naming-convention.md`, `debugging-and-introspection.md`, `scopes.md`, `getting-started.md` (#94).
- `usage-rules.md` (the AI-agent-facing rulebook) rewritten to recommend `resolve_argument`, flag `write:` as deprecated, and document the new DSL entity (#95).
- `/pr` and `/release` skill doc gates extended to check `usage-rules.md` and `guides/*.md` — structural fix so these files aren't silently missed in future PRs (#95).
- New tip in `guides/scopes.md` preferring direct FK column (`is_nil(team_id)`) over relationship traversal (`is_nil(team.id)`) when the check is really about the FK itself (#96).

### Follow-up / related

- [#86](https://github.com/jhlee111/ash_grant/issues/86) (DB-query fallback limitations on function-wrapped relational refs during create) was closed as "not planned" — the argument-based pattern covers the motivating cases cleanly, and the fallback path is no longer the recommended route for authorization involving relationships.

## [0.13.5] - 2026-04-08

### Changed

- **Remove noisy `:all`/`:always` scope warning**: The compile-time warning from `ValidateScopes` that fired for every resource without a universal scope has been removed. This was a best-practice hint rather than a real validation, and produced excessive noise in projects with many resources — especially with `--warnings-as-errors`. (#81)

## [0.13.4] - 2026-04-06

### Fixed

- **Generic action authorization**: `AshGrant.Check` now correctly extracts tenant from `action_input` for generic actions (type `:action`). Previously, `get_tenant/1` only handled `query` and `changeset`, causing tenant-aware resolvers to return empty permissions for generic actions. The same fix is applied to `FilterCheck` and `FieldCheck`. (#76)
- **`default_policies` now covers generic actions**: `AddDefaultPolicies` transformer generates a policy for `action_type(:action)` using `AshGrant.Check`. Previously, resources with `default_policies true` and generic actions had no matching policy, resulting in forbidden. (#76)
- **Write scope evaluation**: Use `fill_template` and pass `resource:` option to `Ash.Expr.eval` for correct template resolution and attribute hydration in write scope checks. (#75)

## [0.13.2] - 2026-03-29

### Changed

- **Action type wildcard (`read*`) no longer matches by string prefix.** Previously, `read*` matched both actions starting with "read" (e.g., `read_all`) and actions with `:read` action type (e.g., `list`). Now `read*` matches **only by action type** — use `read` (exact) for the action named "read". This prevents false matches where an action named `read_something` could be a non-read type. (#72)

## [0.13.1] - 2026-03-29

### Added

- **Authorization Patterns guide**: New ExDoc guide covering RBAC, ABAC, ReBAC, and additional patterns (deny-wins, multi-tenancy, field-level access, domain inheritance, CanPerform). Includes practical scope DSL examples for each pattern and a comparison table. (#70)
- **Usage rules**: Document `instance_key` and `scope_through` in policy test fixtures. (#69)

## [0.13.0] - 2026-03-24

### Added

- **`instance_key` DSL option**: Match instance permission IDs against a field other than `:id`. For example, `instance_key :feed_id` makes `"feed:feed_abc:read:"` generate `WHERE feed_id IN ('feed_abc')` instead of `WHERE id IN ('feed_abc')`. Works with FilterCheck, Check, and CanPerform. (#62)
- **`scope_through` entity**: Propagate parent resource instance permissions to child resources via `belongs_to` relationships. When a user has `"feed:feed_abc:read:"`, adding `scope_through :feed` to the child resource grants access to all child records where `feed_id == "feed_abc"`. Supports action filtering with `actions:` option. (#62)
- **`ValidateScopeThroughs` transformer**: Compile-time validation that `scope_through` references valid `belongs_to` relationships.

### Changed

- **Documentation refactored into ExDoc guides**: README trimmed from 1,687 to 166 lines. Content split into 7 focused guides (Getting Started, Permissions, Scopes, Field-Level Permissions, Checks & Policies, Debugging & Introspection, Policy Testing) with ExDoc sidebar grouping under "Guides". (#65)
- **Issue #65 documentation improvements**: Action wildcard type clarification (`read*` matches action type, not string prefix), instance permission boundary note, per-action `default_policies` subsection, RBAC + instance OR combination example, relational scopes tip in Getting Started.

## [0.12.0] - 2026-03-16

### Added

- **`CanPerform` calculation for per-record UI visibility**: New `AshGrant.Calculation.CanPerform` module produces per-record boolean values (e.g., `:can_update?`, `:can_destroy?`) that compile to SQL via `expression/2` — no N+1 queries. Supports RBAC scopes, instance permissions, deny-wins, and multi-scope OR combination. (#58)
- **`can_perform` DSL entity**: Declare individual CanPerform calculations inline in the `ash_grant` block with optional custom naming (e.g., `can_perform :read, name: :visible?`). The transformer auto-detects the resource module.
- **`can_perform_actions` batch option**: Generate multiple CanPerform calculations at once (e.g., `can_perform_actions [:update, :destroy]` generates `:can_update?` and `:can_destroy?`).
- **Compile-time action name validation**: `can_perform` and `can_perform_actions` now raise `Spark.Error.DslError` at compile time if a referenced action does not exist on the resource, preventing typos from silently producing always-false calculations. (#60)
- **`AshGrant.Info.can_perform_actions/1`**: Introspection helper to query configured batch actions.

## [0.11.1] - 2026-03-15

### Changed

- **Documentation**: Add domain-level DSL section to README with usage guidance, inheritance rules, and examples. Update installation version, feature list, and DSL Configuration table.
- **Developer tooling**: Add `/pr` and `/release` slash commands for streamlined PR and release workflows with built-in documentation gates. Update CLAUDE.md architecture section.

## [0.11.0] - 2026-03-15

### Added

- **Domain-level DSL (`AshGrant.Domain`)**: Define shared `resolver` and `scope` at the Ash Domain level. Resources using the `AshGrant` extension automatically inherit domain config, eliminating repeated `ash_grant do` blocks across resources. Resource-level settings take precedence (resolver override, same-name scope override). Cross-boundary scope inheritance is supported (resource scope can inherit from a domain-defined parent). (#54)

## [0.10.3] - 2026-03-15

### Fixed

- **`default_field_policies` includes PK/timestamps, fails on OTP 28**: When `field_group :admin, :all` is used with `default_field_policies true`, generated field policies now exclude primary keys and non-public attributes (e.g. `created_at`, `updated_at`). Previously these invalid fields caused `Spark.Error.DslError` on OTP 28 due to transformer ordering differences. (#51)

### Changed

- **Resolve all credo issues**: Fixed 82 credo warnings including `length/1` comparisons, `Enum.map_join` usage, negated conditions, nesting depth, and cyclomatic complexity. `mix credo` now reports zero issues.

## [0.10.2] - 2026-03-13

### Fixed

- **Overlapping field_policies with `:all` field_groups**: When multiple `field_group` definitions use `:all` (or `:all, except:`), resolved fields overlapped across policies. Since Ash requires ALL matching `field_policy` entries to pass, fields in multiple groups were denied even when the actor had the correct permission. `AddFieldPolicies` now deduplicates fields across groups so each field appears in exactly one policy. (#48)

## [0.10.1] - 2026-03-12

### Fixed

- **Action prefix patterns now match by Ash action type**: `read*` matches any `:read`-type action (e.g., `list_published`, `by_slug`, `search`), not just actions whose name starts with `"read"`. Same for `create*`, `update*`, `destroy*`. (#46)
- **Explainer prefix matching**: `Explainer.matches_action?/2` only did exact match — now uses `Permission.matches_action?/3` with full prefix and action-type support.

## [0.10.0] - 2026-03-12

### Changed (BREAKING)

- **`field_group` DSL redesign** (#40): Removed positional argument ambiguity between `inherits` and `fields`
  - **BREAKING: `inherits` is now keyword-only**: `field_group :name, [:parents], [:fields]` no longer works. Use `field_group :name, [:fields], inherits: [:parents]` instead.
  - **BREAKING: `[:*]` replaced by `:all`**: Use `field_group :name, :all` instead of `field_group :name, [:*]` (deprecated `[:*]` still works with a warning, will be removed in v1.0.0)
  - **New `:all` syntax**: `field_group :admin, :all` — all resource attributes
  - **Blacklist mode**: `field_group :public, :all, except: [:salary, :ssn]`
  - **Combined**: `field_group :editor, :all, except: [:admin_notes], inherits: [:base]`
  - **Most common pattern unchanged**: `field_group :public, [:name, :department]` works as before

#### Migration Guide

```elixir
# Before (v0.9.0)
field_group :public, [], [:*], except: [:salary, :ssn]
field_group :sensitive, [:public], [:phone, :address]
field_group :confidential, [:sensitive], [:salary, :email]

# After (v0.10.0)
field_group :public, :all, except: [:salary, :ssn]
field_group :sensitive, [:phone, :address], inherits: [:public]
field_group :confidential, [:salary, :email], inherits: [:sensitive]
```

### Deprecated

- **`[:*]` wildcard syntax**: Use `:all` instead. `[:*]` still works but emits a compile-time deprecation warning. Will be removed in v1.0.0.

## [0.9.0] - 2026-03-12

### Added

- **`field_group` `except` option (blacklist mode)**: Use `[:*]` wildcard with `except` to exclude specific fields instead of listing all visible ones. Useful for resources with many attributes where only a few are sensitive. (#36)
  - `field_group :public, [], [:*], except: [:salary, :ssn]` — all attributes except salary and ssn
  - `[:*]` without `except` expands to all resource attributes
  - Compile-time validations: `except` requires `[:*]`, except fields must exist, masked fields cannot be in `except`
  - New transformer `AshGrant.Transformers.ResolveFieldGroupExcept` resolves wildcards before downstream validation

## [0.8.1] - 2026-03-11

### Fixed

- **DB query fallback now handles `Ash.Query.Call` (dot-path expressions)**: Expressions like `expr(order.center_id in ^actor(:ids))` are now correctly detected as relationship references and trigger the DB query fallback for write actions. Previously only `exists()` expressions were detected. (#33)
- **Pass `tenant:` to `Ash.exists?/2` calls**: Multitenanted resources no longer fail silently during DB query fallback write checks. (#33)

## [0.8.0] - 2026-03-11

### Added

- **DB query fallback for relational write scopes**: Scopes using `exists()` or dot-path references now work correctly for write actions (create, update, destroy) without requiring a `write:` option. When a scope has relationship references and no explicit `write:` override, `AshGrant.Check` automatically queries the database using the read scope expression. (#28)
  - **Update/destroy**: Queries DB to check if the existing record matches the read scope
  - **Create**: Splits the filter — direct-attribute conditions are evaluated in-memory, relationship conditions are verified via DB query on parent resources
  - `write:` option still works as an explicit override (backward compatible)
  - Resources without a data layer fall back to in-memory evaluation (existing behavior)

### Removed

- **Compile-time warning for relationship scopes without `write:`**: The warning is no longer needed since the DB query fallback handles these cases automatically. (#28)

## [0.7.0] - 2026-03-11

### Added

- **Dual read/write scope (`write:` option)**: Scopes can now provide a separate expression for write actions via the `write:` option. This solves the authorization bypass where `exists()` and dot-path scopes were silently replaced with `true` during in-memory evaluation for write actions. (#26)
  - `write: expr(...)` — direct-field expression for in-memory evaluation
  - `write: false` — explicitly deny writes with this scope
  - `write: true` — allow all writes with this scope (no filtering)
  - Falls back to `filter` when `write:` is omitted (backward compatible)
  - Inheritance support: child scopes inherit parent's `write:` expression; `write: false` propagates to children
  - New `AshGrant.Info.resolve_write_scope_filter/3` function for write scope resolution

### Changed

- **Compile-time warning for relationship scopes**: The warning for `exists()`/dot-path scopes now fires regardless of `default_policies` setting and also detects dot-path references (not just `exists()`). The warning is suppressed when a `write:` option is provided. (#26)
- **`AshGrant.Check` uses write scope resolution**: Write actions now resolve scopes via `resolve_write_scope_filter/3` instead of `resolve_scope_filter/3`, using the `write:` expression when available. (#26)

## [0.6.1] - 2026-03-01

### Fixed

- **Bulk operations crash with `exists()` scopes**: `Ash.bulk_create/4` (and bulk_update/bulk_destroy) crashed with `nil.persisted(:relationships_by_name)` when the resource had an `exists()` scope expression. The fix replaces `exists()` nodes with `true` before in-memory evaluation. Attribute-based conditions in the same scope are still enforced. (#23)

### Added

- **Compile-time warning for `exists()` scopes**: Resources with `default_policies` including write actions now emit a warning when scopes contain `exists()`, informing users that the relational condition is not enforced for writes
- **Documentation**: Added "Relational Scopes" section to README and moduledocs explaining the `exists()` limitation for write actions

## [0.6.0] - 2026-02-19

### Added

- **Field-Level Permissions**: Column-level read authorization via field groups
  - `field_group` DSL entity with inheritance (DAG-based) and masking support
  - 5-part permission format: `resource:instance:action:scope:field_group` (backward compatible with 4-part)
  - `AshGrant.FieldCheck` - SimpleCheck for Ash `field_policies` integration
  - `AshGrant.field_check/1` - Public API for use in manual `field_policies` (Mode A)
  - `default_field_policies: true` - Auto-generate `field_policies` from `field_group` definitions (Mode B)
  - Field group inheritance: child groups include all parent fields
  - Field masking with allow-wins semantics via `mask` and `mask_with` options

- **New Modules**:
  - `AshGrant.Dsl.FieldGroup` - Struct and DSL entity for field group definitions
  - `AshGrant.FieldCheck` - SimpleCheck for field-level authorization
  - `AshGrant.Preparations.ApplyMasking` - Runtime masking via `after_action` hook
  - `AshGrant.Transformers.AddFieldPolicies` - Auto-generates `field_policies` from field groups
  - `AshGrant.Transformers.AddMaskingPreparation` - Auto-registers masking preparation
  - `AshGrant.Transformers.ValidateFieldGroups` - Compile-time validation (duplicates, cycles, missing parents)

- **New Evaluator Functions**:
  - `get_field_group/3` - Get first matching field group from permissions
  - `get_all_field_groups/3` - Get all matching field groups (union for field access)

- **New Info Functions**:
  - `field_groups/1` - Get all field group definitions for a resource
  - `get_field_group/2` - Get a specific field group by name
  - `resolve_field_group/2` - Resolve a field group with inheritance
  - `default_field_policies/1` - Get the `default_field_policies` setting
  - `fetch_permissions/3` - Shared permission resolution helper

- **Introspect Updates**: `actor_permissions`, `available_permissions`, `can?`, and `allowed_actions` now include `field_groups` / `field_group` in their responses

- **Explainer & PolicyExport**: Field group information in `explain/4` output, Markdown tables, and Mermaid diagrams

### Changed

- **Permission format**: Extended from 4-part to optional 5-part with `field_group`
- **Transformer ordering**: `ValidateFieldGroups` now explicitly runs before `AddFieldPolicies` and `AddMaskingPreparation`

## [0.5.0] - 2026-01-21

### Added

- **Policy Configuration Testing**: DSL-based testing framework for verifying policy configurations without a database
  - `AshGrant.PolicyTest` - Main module with `use` macro for defining policy tests
  - `assert_can/2,3` and `assert_cannot/2,3` assertion macros
  - `resource/1`, `actor/2`, `describe/2`, `test/2` DSL macros
  - `AshGrant.PolicyTest.Runner` - Test execution with summary statistics
  - `AshGrant.PolicyTest.Result` - Result struct with timing information

- **YAML Policy Tests**: Alternative format for non-Elixir developers
  - `AshGrant.PolicyTest.YamlParser` - Parse and run YAML test files
  - `AshGrant.PolicyTest.YamlExporter` - Export DSL tests to YAML
  - `AshGrant.PolicyTest.DslGenerator` - Generate DSL code from YAML

- **Policy Export**: Export policy configurations to documentation formats
  - `AshGrant.PolicyExport.Mermaid` - Generate Mermaid flowchart diagrams
  - `AshGrant.PolicyExport.Markdown` - Generate Markdown documentation

- **Mix Tasks**: CLI tools for policy testing and export
  - `mix ash_grant.verify` - Run policy configuration tests
  - `mix ash_grant.export` - Export policies to YAML/Mermaid/Markdown
  - `mix ash_grant.import` - Convert YAML to Elixir DSL

### Dependencies

- Added `yaml_elixir ~> 2.9` as optional dependency for YAML support

## [0.4.1] - 2026-01-21

### Added

- **SAT Solver Optimization Callbacks**: Implements `Ash.Policy.Check` optional callbacks for smarter authorization decisions
  - `simplify/2` - Returns ref unchanged (permissions are runtime-resolved)
  - `implies?/3` - Returns `true` when check refs have identical module and options
  - `conflicts?/3` - Returns `false` (deny-wins is handled at evaluation time)
  - Enables the authorizer to reach decisions with fewer variables in conditions
  - Suggested by Jonatan Männchen (Ash contributor)

### Changed

- **Version management**: Removed hardcoded version numbers from documentation
  - README.md now references GitHub without specific tag (always uses latest)
  - `@moduledoc` installation section now links to README
  - Single source of truth: `mix.exs @version`

- **Deprecation timeline**: Extended `owner_field` deprecation from v0.3.0 to v1.0.0
  - Affects: `dsl.ex`, `info.ex`, `validate_scopes.ex`, `CHANGELOG.md`

## [0.4.0] - 2026-01-05

### Added

- **Permission Introspection Module**: New `AshGrant.Introspect` module for runtime permission queries
  - `actor_permissions/3` - Admin UI: Display all permissions with their status for an actor
  - `available_permissions/1` - Permission management: List all possible permission combinations
  - `can?/4` - Debugging: Simple check returning `:allow` or `:deny` with details
  - `allowed_actions/3` - API response: List allowed actions (with optional `:detailed` mode)
  - `permissions_for/3` - Raw access to permission strings from resolver
  - All functions support `:context` option for resolver context

- **Instance Permission Read Support**: Instance permissions now work with read actions (`filter_check/1`)
  - `AshGrant.Evaluator.get_matching_instance_ids/3` extracts instance IDs from permissions
  - `FilterCheck` combines RBAC scopes with instance ID filters using OR logic
  - Enables Google Docs-style sharing where specific resources are shared with specific users
  - Example: `"doc:doc_abc123:read:"` allows reading the specific document

## [0.3.1] - 2025-01-05

### Added

- **Scope Descriptions**: Optional `description` field for scopes in the DSL
  - `scope :own, [], expr(author_id == ^actor(:id)), description: "Records owned by the current user"`
  - `AshGrant.Info.scope_description/2` to retrieve scope descriptions programmatically
  - Descriptions are displayed in `explain/4` output for better debugging

- **Authorization Debugging with `explain/4`**: New `AshGrant.explain/4` function for debugging authorization decisions
  - Returns `AshGrant.Explanation` struct with detailed decision info
  - Shows matching permissions with metadata (description, source)
  - Shows all evaluated permissions with match/no-match reasons
  - Includes scope information from both permissions and DSL definitions
  - `AshGrant.Explanation.to_string/2` for human-readable output with ANSI colors

- **New Modules**:
  - `AshGrant.Explanation` - Struct for authorization decision explanations
  - `AshGrant.Explainer` - Builds detailed authorization explanations

## [0.3.0] - 2025-01-04

### Added

- **Permission Metadata**: `AshGrant.PermissionInput` struct for permissions with metadata
  - `description` - Human-readable description of the permission
  - `source` - Where the permission came from (e.g., "role:admin")
  - `metadata` - Additional arbitrary metadata as a map

- **Permissionable Protocol**: `AshGrant.Permissionable` protocol for converting custom structs to permissions
  - Implement for your own structs to return them directly from resolvers
  - Default implementations for `BitString`, `PermissionInput`, and `Permission`

- **Instance Permissions with Scopes (ABAC)**: Instance permissions now support scope conditions
  - `doc:doc_123:update:draft` - Update only when document is in draft status
  - `doc:doc_123:read:business_hours` - Access only during business hours
  - `invoice:inv_456:approve:small_amount` - Approve only below threshold
  - Scopes are now treated as "authorization conditions" rather than just "record filters"
  - Empty scopes (trailing colon) remain backward compatible ("no conditions")

- **New Evaluator Functions**:
  - `get_instance_scope/3` - Get the scope from a matching instance permission
  - `get_all_instance_scopes/3` - Get all scopes from matching instance permissions

- **Context Injection for Testable Scopes**: Scopes can now use `^context(:key)` for injectable values
  - `scope :today_injectable, expr(fragment("DATE(inserted_at) = ?", ^context(:reference_date)))`
  - `scope :threshold, expr(amount < ^context(:max_amount))`
  - Enables deterministic testing of temporal and parameterized scopes
  - Values are passed via `Ash.Query.set_context(%{reference_date: ~D[2025-01-15]})`

### Changed

- **Documentation**: Clarified that scope represents an "authorization condition" that can apply
  to both RBAC and instance permissions, enabling full ABAC (Attribute-Based Access Control)

## [0.2.2] - 2025-01-02

### Fixed

- **Documentation**: Removed deprecated `owner_field` from README examples
- **Documentation**: Added note that instance permissions currently only work with write actions (`check/1`)

### Changed

- **Tests**: Enabled previously skipped "own" scope update tests that now pass

## [0.2.1] - 2025-01-01

### Added

- **Multi-tenancy Support**: Full support for Ash's `^tenant()` template in scope expressions
  - `scope :same_tenant, expr(tenant_id == ^tenant())` now works correctly
  - Tenant context is passed through to `Ash.Expr.eval/2`
  - Smart fallback evaluation when Ash.Expr.eval returns `:unknown`
- **TenantPost test resource**: Demonstrates multi-tenancy scope patterns

### Deprecated

- **`owner_field` DSL option**: This option is deprecated and will be removed in v1.0.0.
  Use explicit scope expressions instead:
  ```elixir
  # Instead of: owner_field :author_id
  # Use: scope :own, expr(author_id == ^actor(:id))
  ```
  The fallback evaluation now extracts the field from the scope expression directly.

### Improved

- **Fallback expression evaluation**: Smarter handling when `Ash.Expr.eval` can't evaluate
  - Analyzes filter to detect `^tenant()` and `^actor()` references
  - Automatically extracts actor field from the filter expression (no `owner_field` needed)
  - Proper tenant isolation for write actions

## [0.2.0] - 2025-01-01

### Added

- **Default Policies**: New `default_policies` DSL option to auto-generate standard policies
  - `default_policies true` or `:all` - Generate both read and write policies
  - `default_policies :read` - Only generate filter_check policy for read actions
  - `default_policies :write` - Only generate check policy for write actions
  - Eliminates boilerplate policy declarations for common use cases
- **Transformer**: `AshGrant.Transformers.AddDefaultPolicies` generates policies at compile time
- **Info helper**: `AshGrant.Info.default_policies/1` to query the setting

### Improved

- **Expression evaluation**: Now uses `Ash.Expr.eval/2` for proper Ash expression handling
  - Full support for all Ash expression operators (not just `==` and `in`)
  - Proper actor template resolution (`^actor(:id)`, `^actor(:tenant_id)`, etc.)
  - Proper tenant template resolution (`^tenant()`)
  - Handles nested actor paths automatically
- **Code quality**: Removed ~60 lines of custom expression handling in favor of Ash built-ins

### DSL Configuration (Updated)

```elixir
ash_grant do
  resolver MyApp.PermissionResolver       # Required
  default_policies true                   # NEW: auto-generate policies
  resource_name "custom_name"             # Optional

  scope :all, true
  scope :own, expr(author_id == ^actor(:id))
  scope :published, expr(status == :published)
end
```

## [0.1.0] - 2025-01-01

### Added

- **Unified Permission Format**: New 4-part permission syntax `resource:instance_id:action:scope`
  - RBAC permissions: `blog:*:read:all` (instance_id = `*`)
  - Instance permissions: `blog:post_abc123:read:` (specific instance)
  - Backward compatible with legacy 2-part and 3-part formats
- **Scope DSL**: Define scopes inline within resources using the `scope` entity
  - `scope :all, true`
  - `scope :own, expr(author_id == ^actor(:id))`
  - `scope :published, expr(status == :published)`
  - Scope inheritance with `scope :own_draft, [:own], expr(status == :draft)`
- **Deny-wins semantics**: Deny rules always override allow rules
- **Wildcard matching**: `*` for resources/actions, `read*` for action prefixes
- **Two check types**:
  - `AshGrant.filter_check/1` for read actions (returns filter expression)
  - `AshGrant.check/1` for write actions (returns true/false)
- **Property-based testing**: 34 property tests for edge case discovery
- **Comprehensive test coverage**: 211 total tests (19 doctests + 34 properties + 158 unit tests)

### DSL Configuration

```elixir
ash_grant do
  resolver MyApp.PermissionResolver       # Required
  resource_name "custom_name"             # Optional

  # Inline scope definitions (new!)
  scope :all, true
  scope :own, expr(author_id == ^actor(:id))
  scope :published, expr(status == :published)
end
```

### Behaviours

- `AshGrant.PermissionResolver` - Resolves permissions for actors
- `AshGrant.ScopeResolver` - Legacy: translates scopes to Ash filters (deprecated in favor of scope DSL)

### Modules

| Module | Description |
|--------|-------------|
| `AshGrant` | Main extension with `check/1` and `filter_check/1` |
| `AshGrant.Permission` | Permission parsing and matching |
| `AshGrant.Evaluator` | Deny-wins permission evaluation |
| `AshGrant.Info` | DSL introspection helpers |
| `AshGrant.Check` | SimpleCheck for write actions |
| `AshGrant.FilterCheck` | FilterCheck for read actions |

[Unreleased]: https://github.com/jhlee111/ash_grant/compare/v0.10.2...HEAD
[0.10.2]: https://github.com/jhlee111/ash_grant/compare/v0.10.1...v0.10.2
[0.10.1]: https://github.com/jhlee111/ash_grant/compare/v0.10.0...v0.10.1
[0.10.0]: https://github.com/jhlee111/ash_grant/compare/v0.9.0...v0.10.0
[0.9.0]: https://github.com/jhlee111/ash_grant/compare/v0.8.1...v0.9.0
[0.8.1]: https://github.com/jhlee111/ash_grant/compare/v0.8.0...v0.8.1
[0.8.0]: https://github.com/jhlee111/ash_grant/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/jhlee111/ash_grant/compare/v0.6.1...v0.7.0
[0.6.1]: https://github.com/jhlee111/ash_grant/compare/v0.6.0...v0.6.1
[0.6.0]: https://github.com/jhlee111/ash_grant/releases/tag/v0.6.0
[0.5.0]: https://github.com/jhlee111/ash_grant/releases/tag/v0.5.0
[0.4.1]: https://github.com/jhlee111/ash_grant/releases/tag/v0.4.1
[0.4.0]: https://github.com/jhlee111/ash_grant/releases/tag/v0.4.0
[0.3.1]: https://github.com/jhlee111/ash_grant/releases/tag/v0.3.1
[0.3.0]: https://github.com/jhlee111/ash_grant/releases/tag/v0.3.0
[0.2.2]: https://github.com/jhlee111/ash_grant/releases/tag/v0.2.2
[0.2.1]: https://github.com/jhlee111/ash_grant/releases/tag/v0.2.1
[0.2.0]: https://github.com/jhlee111/ash_grant/releases/tag/v0.2.0
[0.1.0]: https://github.com/jhlee111/ash_grant/releases/tag/v0.1.0