Fix compile cycle with domain code_interface + append default policies#1
Closed
barnabasJ wants to merge 2 commits intoteam-alembic:mainfrom
Closed
Fix compile cycle with domain code_interface + append default policies#1barnabasJ wants to merge 2 commits intoteam-alembic:mainfrom
barnabasJ wants to merge 2 commits intoteam-alembic:mainfrom
Conversation
Ash bypass policies only short-circuit policies that come after them. add_entity defaults to prepend, which placed AshGrant's generated policies before user-defined bypass policies, making bypasses ineffective. Switch to type: :append so user-defined bypass policies are evaluated first and can properly short-circuit AshGrant's permission checks.
…face A resource using `AshGrant` plus a domain that uses `AshGrant.Domain` AND declares a `code_interface do define … end` block deadlocked on compile: the resource's `MergeDomainConfig` transformer forced the domain to compile, while the domain's code_interface transformer was waiting for the resource. Move domain config merging from a compile-time transformer to runtime `AshGrant.Info` calls (`resolver/1` falls back to the domain, `scopes/1` merges resource + domain scopes with resource precedence). Convert the compile-time presence checks to Spark verifiers, which run post-compile and can safely reach into the domain. Spark surfaces verifier errors as compile warnings, so `AshGrant.Check.resolve_permissions/3` also raises a clear `ArgumentError` when no resolver is configured at authorization time. Adds a regression test (`code_interface_cycle_test.exs`) that pairs `AshGrant` on a resource with `AshGrant.Domain` + `code_interface` on the domain. This case deadlocked on the previous code.
C-Sinclair
added a commit
that referenced
this pull request
Apr 21, 2026
Addressing review items: #1 (Important): GrantsResolver predicate eval no longer rescues silently. Any error is now logged at :warning with the resource and grant name; the grant is still treated as non-matching (fail-closed preserved). #6 (Important): The @grants section DSL example had stale function-form predicates. Updated to use `expr(^actor(:role) == :admin)`, matching what the feature actually ships. #7 (Important): Documented the known deny+sibling-allow limitation on `AshGrant.Dsl.Permission.@moduledoc`. The resolver emits the `!` prefix correctly and Evaluator honors deny-wins for broad allow + narrow deny, but narrow allow (:own) + narrow deny (:published) on the same action does not always surface in Check. Recommended workaround inline. jhlee111#8 (Minor): Pulled through caller-provided `context.context` into `Ash.Expr.fill_template/2` so grant predicates can reference `^context(:key)` the same way scopes can. jhlee111#10 (Minor): Tightened the `instance:` schema type from `:any` to `{:or, [{:in, [:*]}, :atom, :string]}`. Random structs and integers now fail at compile time instead of producing malformed permission strings at runtime. Added a DSL test covering context threading.
3 tasks
|
Superseded by #4 |
C-Sinclair
added a commit
that referenced
this pull request
Apr 21, 2026
* feat: declarative grants DSL with compile-time verification
Adds a `grants` block to the `ash_grant` DSL so permissions can be
declared as data rather than returned from an imperative resolver
function. Each grant pairs an actor predicate with a set of named,
structured permissions and optional compliance-purpose metadata.
- `grant :name, predicate do ... end` — predicate is a 1-arity function
over the actor; matching grants contribute their permissions to the
synthesized resolver output
- `permission :name, :action, :scope` — positional mirrors the string
form; `on:`, `instance:`, `purpose(s):`, `deny:`, `description:` as
keyword options
- Extension-level `purposes [...]` declares the compliance vocabulary;
unknown purpose atoms fail compilation
- `NormalizeGrants` transformer injects the enclosing resource as the
default `on:` and rejects declaring both `grants` and an explicit
`resolver`
- `ValidateGrantReferences` verifier checks that each permission's
`on:` is an Ash.Resource, that `action:` exists on it (or is `:*`),
and that `scope:` is defined in its `ash_grant` block
- `SynthesizeGrantsResolver` wires `AshGrant.GrantsResolver` as the
resource's resolver when grants are declared — no other runtime
machinery changes; Check/FilterCheck/Explainer pick it up unchanged
- `Info.grants/1`, `permissions/1`, `permissions_for_purpose/2`,
`effective_purposes/2`, `declared_purposes/1` for compliance
introspection
* refactor grants DSL: Ash.Expr predicates, drop purposes, DB integration tests
Following review feedback:
- Grant predicates are now `Ash.Expr` expressions evaluated via
`Ash.Expr.eval/2` with `Ash.Expr.fill_template/2` for `^actor(:key)`
substitution. The function form was inconsistent with how scopes and
other AshGrant expressions already work.
grant :admin, expr(^actor(:role) == :admin) do
permission :manage_all, :*, :always
end
- Removed `purpose` / `purposes` entirely from the DSL, schema, Info,
NormalizeGrants, and tests. Compliance-purpose metadata needs deeper
design before shipping.
- Added end-to-end integration test (`grants_integration_test.exs`) that
exercises the full pipeline against AshPostgres and Ash.Policy.Authorizer:
read filtering via FilterCheck, create/update enforcement via Check,
:own scope narrowing, compound predicates, and actor/context edge cases.
- Added `AshGrant.Test.GrantsPost` test resource driven purely by the
synthesized resolver, backed by a real `grants_posts` table.
- DSL test suite rewritten with `expr()` predicates; added compound and
missing-field coverage.
* Address code-review feedback on grants DSL
Addressing review items:
#1 (Important): GrantsResolver predicate eval no longer rescues silently.
Any error is now logged at :warning with the resource and grant name;
the grant is still treated as non-matching (fail-closed preserved).
#6 (Important): The @grants section DSL example had stale function-form
predicates. Updated to use `expr(^actor(:role) == :admin)`, matching
what the feature actually ships.
#7 (Important): Documented the known deny+sibling-allow limitation on
`AshGrant.Dsl.Permission.@moduledoc`. The resolver emits the `!` prefix
correctly and Evaluator honors deny-wins for broad allow + narrow deny,
but narrow allow (:own) + narrow deny (:published) on the same action
does not always surface in Check. Recommended workaround inline.
jhlee111#8 (Minor): Pulled through caller-provided `context.context` into
`Ash.Expr.fill_template/2` so grant predicates can reference
`^context(:key)` the same way scopes can.
jhlee111#10 (Minor): Tightened the `instance:` schema type from `:any` to
`{:or, [{:in, [:*]}, :atom, :string]}`. Random structs and integers
now fail at compile time instead of producing malformed permission
strings at runtime.
Added a DSL test covering context threading.
* mix format
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two independent fixes surfaced while adopting
ash_grantin a real codebase(ARCC Center — Ash 3.x, ~57 resources, domain-level
code_interfaceblocks).1.
cfce2b4— append default policies after user-defined onesAshGrant.Transformers.AddDefaultPolicieswas prepending its generatedpolicies via
Ash.Resource.Builder.add_policy/3(which defaults totype: :prepend). Ashbypasspolicies only short-circuit policies thatcome after them, so user-defined
bypass action_type(:create)blocks(e.g. "allow public registration for
nilactor") never fired — ash_grant'spermission check ran first and denied.
Change: flip the builder call to
type: :appendso user-defined policies(including bypasses) are evaluated first and can properly short-circuit
ash_grant's generated checks. 6-line diff.
2.
e56b814— break domain↔resource compile cycle with domain-levelcode_interfaceCombining
AshGranton a resource withAshGrant.Domain+ acode_interface do define … endblock on the domain deadlocked at compile:MergeDomainConfigtransformer forced the domain to compile(to read domain-level
ash_grantDSL).code_interfacetransformer was waiting for the resource tofinish compiling.
Fix:
MergeDomainConfigtransformer. Move resolver/scope mergingfrom compile-time into runtime
AshGrant.Infocalls:resolver/1now falls back to the domain;scopes/1merges resource +domain scopes with resource precedence.
ValidateResolverPresentandValidateScopesbecome verifiers — theyrun post-compile and can safely reach into the domain without creating a
cycle. Spark surfaces verifier errors as compile warnings, so
AshGrant.Check.resolve_permissions/3also raises a clearArgumentErrorat authorization time if no resolver is configured.
test/ash_grant/code_interface_cycle_test.exsthat pairs
AshGranton a resource withAshGrant.Domain+code_interfaceon the domain — this case deadlocked on the previous code.Both fixes are authored against
jhlee111/ash_grant@32c607d(the committeam-alembic/ash_grant:maincurrently points at), so this PR appliescleanly.
Test plan
mix testpasses locally (includes the newcode_interface_cycle_test.exs).domain-level
code_interfaceacross 11 domains,bypasspolicies forpublic registration on User, system-actor + superadmin wildcard grants).
Full
mix check(format, credo, compile --warnings-as-errors, 687tests) green with these two commits applied.
Notes
independent. Kept them together here because both were blockers for the
same adoption effort.
e56b814deleteslib/ash_grant/transformers/merge_domain_config.ex,lib/ash_grant/transformers/validate_resolver_present.ex, andlib/ash_grant/transformers/validate_scopes.ex; the replacement verifierslive under
lib/ash_grant/verifiers/.