Skip to content

feat: v2 evaluation engine — resolver chain, conditions, resource policies#1

Merged
christopherarter merged 33 commits into
mainfrom
feature/v2-evaluation
Apr 9, 2026
Merged

feat: v2 evaluation engine — resolver chain, conditions, resource policies#1
christopherarter merged 33 commits into
mainfrom
feature/v2-evaluation

Conversation

@christopherarter

Copy link
Copy Markdown
Contributor

Summary

Redesigns the evaluation layer around the universal authorization question: given this principal wants to perform this action on this resource in this context, is it allowed?

  • Resolver chain architecture — the fixed v1 pipeline (assignments → roles → permissions → boundaries) is replaced by an ordered list of PolicyResolver implementations. Each resolver produces PolicyStatements; the evaluator collects, filters, and applies deny-wins across all sources.
  • Condition system — first-class conditions on policy statements with 5 built-in evaluators (attribute_equals, attribute_in, time_between, ip_range, environment_equals) and a pluggable registry for custom types.
  • Resource-based policies — policies attached to resources (like S3 bucket policies). New resource_policies table, HasResourcePolicies trait, and ResourcePolicyResolver.
  • Context-aware evaluationPrincipal carries subject attributes, Resource carries target attributes, Context carries scope + environment data. All available to condition evaluators.
  • Zero-disruption DX$user->can('posts.update') works exactly as before. New capabilities (resource, environment, conditions) are opt-in via trailing parameters.

Key changes

  • Evaluator contract collapses to single evaluate(EvaluationRequest): EvaluationResult
  • 4 built-in resolvers: IdentityPolicyResolver, BoundaryPolicyResolver, ResourcePolicyResolver, SanctumPolicyResolver
  • Gate hook auto-detects resource models via HasResourcePolicies trait
  • Policy document format updated to v2 (conditions on roles, resource policies section, keyed format)
  • Config: explain key → trace, new resolvers array

Stats

  • 77 files changed, 5692 insertions, 2887 deletions
  • 502 tests, 1115 assertions, 0 failures, 0 skipped

Test plan

  • Full Pest suite passes (502 tests)
  • Architecture tests pass
  • All existing v1 behavior preserved through IdentityPolicyResolver
  • Boundary enforcement works via BoundaryPolicyResolver
  • Condition filtering verified with integration tests
  • Resource policies: attach/detach/resolve tested
  • Sanctum token scoping works via SanctumPolicyResolver
  • Document v2 format round-trips (export → import → verify)
  • Gate hook handles: no args, scope only, resource model, scope+resource, Resource DTO

Introduces the PolicyResolver contract for resolver-chain evaluation and
implements IdentityPolicyResolver which resolves role-based PolicyStatements
(Allow/Deny) from subject assignments, with support for both global and
scoped contexts.
…ver chain

BoundaryPolicyResolver reads boundaries from BoundaryStore and produces
Deny statements for permissions outside the ceiling, wiring into the
DefaultEvaluator's resolver chain alongside IdentityPolicyResolver.
Unskips all Task 2.1 boundary tests across CacheInvalidationTest,
CachedBoundaryLookupTest, EvaluationPipelineTest, and SecurityTest.
…trait

Introduces ResourcePolicy model, EloquentResourcePolicyStore, ResourcePolicyResolver,
and completes the HasResourcePolicies trait with attachPolicy/detachPolicy methods.
Wires the resolver into the chain and adds 15 feature tests covering attach, detach,
type-level vs instance-level policy retrieval, and condition hydration.
Moves Sanctum personal access token ability checking out of the
evaluator core and into a dedicated SanctumPolicyResolver that produces
Deny statements for permissions not covered by the current token's
abilities. Supports wildcard ability patterns and the '*' full-access
bypass. Unskips and implements all 11 Sanctum-related tests.
- Fix stale error message in ExplainCommand referencing policy-engine.explain (now policy-engine.trace)
- Add Conditions namespace to approved list in ArchitectureTest
- Fix resolver arch test: split into PolicyResolver (4 resolvers) and ScopeResolver (ModelScopeResolver) checks
- Convert consecutive // comment blocks to /* */ block syntax across 4 test files to satisfy arch rule
…ver system

Narrow mixed types from array access and config() calls to satisfy
PHPStan level 9 strict typing. Fixes 45 errors spanning condition
evaluators, document parser/importer, cached evaluator, resolvers,
service provider, and Eloquent stores.
Widen parser method param types from array<string, mixed> to
array<mixed> so PHPStan on PHP 8.4 (where json_decode returns mixed
values) accepts the calls. Add targeted @var annotations for
constructor arguments.
…m source

Widen parser method param types so PHPStan on PHP 8.4 accepts
json_decode output. Rename v1/v2 method names and comments to
indexed/keyed terminology throughout document system.
Pest v3's Laravel plugin doesn't support Laravel 13, causing CI
failures on the Laravel 13 matrix. Upgrade pest, pest-plugin-laravel,
and pest-plugin-mutate from ^3.0 to ^4.0.
The large-scale benchmark (10K users, 500 groups) exhausts MySQL
container memory on CI runners, crashing the server mid-run and
failing all subsequent tests with "MySQL server has gone away".

Benchmarks are now in their own Pest group and excluded from all
CI jobs. Run on-demand with: vendor/bin/pest --group=benchmark
Reduce InnoDB buffer pool to 64M, disable performance schema,
binary logging, and mysqlx. Use tmpfs for datadir to avoid disk
I/O pressure on runners.
MySQL 8.4 OOMs on GitHub Actions runners despite memory tuning.
MariaDB is wire-compatible, lighter on memory, and uses the same
pdo_mysql driver.
MariaDB lacks functional index support needed by migration 0006.
Reverting to MySQL 8.4 with aggressive memory tuning to address
the OOM issue: performance-schema=OFF, 64M buffer pool, no binlog,
tmpfs datadir.
Merge docker-compose.test.yml into docker-compose.yml and remove
the unused Dockerfile. Revert from MariaDB back to MySQL 8.4 with
memory tuning (performance-schema=OFF, 64M buffer pool, no binlog,
tmpfs datadir) to test whether OOM is resolved.
MySQL 8.4 consistently OOMs on GitHub Actions runners. Remove it
from the test matrix, docker-compose, and supported databases.
Bump checkout to v6 and cache to v5 for Node.js 24 support.
@christopherarter
christopherarter merged commit ea60898 into main Apr 9, 2026
10 checks passed
@christopherarter
christopherarter deleted the feature/v2-evaluation branch April 9, 2026 23:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant