Auth Support Matrix
This document records the current executable CrateStack auth and policy surface. The matrix categories are intentionally modeled after the public ZenStack 2025/2026 access-policy surface described in:https://zenstack.dev/blog/prisma-alternativehttps://zenstack.dev/blog/orm-2026
Current Semantics
Model policies:@@allow(...)and@@deny(...)are supported- action names support
list,detail,read,create,update,delete, andall - deny wins over allow
- if no matching allow rule exists, access is denied
- canonical model-policy literals, predicates, and expressions now live in
cratestack-policy
@allow(...)and@deny(...)are supported- deny wins over allow
- if no allow rule exists, invocation is denied
- canonical procedure-policy literals, predicates, and expressions now live in
cratestack-policy
- create-time
@default(auth().field)is supported - defaults are applied before create policy evaluation
- nested auth paths like
auth().organization.idare supported - defaults still do not allow arbitrary expressions or function calls
Matrix
| Capability | ZenStack-style expectation | CrateStack 2026 status | Notes |
|---|---|---|---|
Model @@allow | Supported | Supported | list, detail, read, create, update, delete |
Model @@deny | Supported | Supported | Deny precedence implemented |
Action alias all | Supported | Supported | Expands to list/detail/create/update/delete |
| Read action split | Supported in richer engines | Supported | list scopes find_many, detail scopes find_unique, read applies to both |
auth() != null | Supported | Supported | Model and procedure policies |
auth() == null | Supported | Supported | Model and procedure policies |
field == literal | Supported | Supported | Boolean, Int, String subset |
field != literal | Supported | Supported | Boolean, Int, String subset |
field == auth().field | Supported | Supported | Model and procedure subset |
field != auth().field | Supported | Supported | Model and procedure subset |
auth().field == modelField | Supported | Supported | Model and procedure subset |
auth().field != modelField | Supported | Supported | Model and procedure subset |
field == otherField | Supported in richer engines | Supported | Procedure policies only |
field != otherField | Supported in richer engines | Supported | Procedure policies only |
auth().field == literal | Supported | Supported | Model and procedure subset |
auth().field != literal | Supported | Supported | Model and procedure subset |
&& / || grouping | Supported | Supported | Parenthesized grouping supported in parser/lowering |
| Row-level read scoping | Supported | Supported | SQL-scoped on find_many / find_unique |
| Row-level update scoping | Supported | Supported | SQL-scoped |
| Row-level delete scoping | Supported | Supported | SQL-scoped |
| Create-time policy checks | Supported | Partial | Scalar/auth checks run in-memory; relation checks use DB lookups when join columns are present in create input/defaults |
| Create-time auth defaults | Supported | Partial | Only @default(auth().field) |
Procedure @allow | Supported | Supported | Runtime wrappers + routes |
Procedure @deny | Supported | Supported | Deny precedence implemented |
| Procedure input field checks | Supported | Supported | Direct args and args.<field> paths, with input/auth/input comparisons |
| DB-backed procedure auth | Supported in richer engines | Partial | @authorize(Model, action, args.path) delegates to model detail/update/delete auth by id |
| Structured principal context | Supported in richer engines | Partial | CoolContext now carries principal.actor/session/tenant/claims plus legacy auth() compatibility |
Relation-based auth like auth() == author | Supported | Supported | Single-column to-one relations that reference id |
Nested auth paths like auth().org.id | Supported in richer engines | Supported | Exact auth keys still win; dotted paths traverse nested auth maps |
| Relation traversal inside policies | Supported in richer engines | Partial | Recursive to-one and quantified to-many traversal are supported across model policies |
| Collection predicates in policies | Supported in richer engines | Partial | Supports dotted some / every / none relation segments inside model policies |
| Built-in policy functions | Supported in richer engines | Partial | hasRole('...') and inTenant('...') are supported as boolean terms in model and procedure policies |
| Arbitrary functions in policies | Supported in richer engines | Not supported | No custom policy function plugin layer beyond the built-in term set |
| Forced server-owned fields | Sometimes supported with richer semantics | Not supported | @default(auth().field) is fallback-only, not override-enforcement |
| Field-level read masking | Sometimes supported in richer stacks | Not supported | Model-level access only |
| Field-level write blocking | Sometimes supported in richer stacks | Not supported | Model-level access only |
| Post-update input-aware policies | Sometimes supported in richer stacks | Partial | Current update/delete checks are row-scoped SQL predicates |
| Durable external auth plugin engine | Sometimes supported via plugin/runtime systems | Not supported | Current engine is built-in and macro/runtime-local |
Supported Examples
Ownership + published read
List/detail split
listapplies tofind_manydetailapplies tofind_uniquereadremains the umbrella action when the same rule should apply to both
Organization scope + role allowlist
Recursive relation-aware read
Moderation with deny override
author.suspendedis a relation-aware boolean denyauthor.email == auth().emailis a relation-aware ownership read rule- deny still overrides matching allow rules
Membership-scoped access
- combines relation-aware read checks with ordinary scalar checks
user.email == auth().emailstays inside the supported recursive relation boundaryupdateremains row-scoped against the current record
Quantified to-many traversal
- supports mixed recursive to-one and quantified to-many segments
somelowers toEXISTS,nonelowers toNOT EXISTS, andeverylowers toNOT EXISTS ... NOT (...)- create-time relation checks work when the traversed root join columns are available from create input/default expansion
Vendor catalog visibility
- useful when ownership lives on the related row rather than the base model row
vendor.contactEmail == auth().emailworks for read and row-scoped update- admin delete stays a plain auth-field check
Procedure allow + deny
Procedure auth with nested input paths
args.<field>works for nested object input checks- procedure policies now support input-vs-auth and input-vs-input equality/inequality
- deny still overrides allow
Nested auth context paths
- nested auth lookups traverse structured auth objects carried in
CoolContext CoolContextnow carries a first-classprincipal.actor/session/tenant/claimsshape internally- canonical policy types are shared through
cratestack-policy; model and procedure auth now lower onto the same runtime policy surface - an exact auth key still wins before dotted traversal, so existing flat claims stay backward compatible
Built-in role and tenant checks
hasRole('...')checks the top-levelroleclaim and falls back toactor.roleinTenant('...')checks the structuredtenant.idclaim- both functions are boolean terms that can participate in grouped
&&/||expressions - only a single string literal argument is supported today
DB-backed procedure delegation
@authorize(Model, action, args.path)performs an extra DB-backed model auth check before invoking the procedure body- current delegated actions are
detail/read,update, anddelete - the delegated check returns forbidden when the referenced row is missing or not visible under the caller context
Not Supported Yet
These should be rejected or treated as future work:Security Notes
Current security posture is intentionally conservative:- unsupported policy shapes fail generation instead of silently degrading
- missing allow rules deny by default
- deny rules override allow rules
- built-in policy functions remain intentionally narrow and deterministic
- unauthenticated creates that depend on required auth-derived defaults fail cleanly as forbidden
- relation-aware model policies support recursive to-one traversal plus dotted
some/every/nonesegments - create-time relation checks only succeed when the root relation join values are known from create input/defaults; otherwise the relation predicate evaluates false and the create is denied
Test Coverage
Current coverage for the supported matrix lives primarily in:cratestack/crates/cratestack/tests/include_schema.rscratestack/crates/cratestack/tests/policy_db.rscratestack/crates/cratestack/tests/policy_db_advanced.rscratestack/crates/cratestack/tests/policy_db_auth_engine.rscratestack/crates/cratestack/tests/policy_db_recursive.rs
- model allow/deny precedence
- procedure allow/deny precedence
- route-level forbidden vs hidden behavior
- auth-derived defaults across direct and HTTP paths
- recursive relation traversal plus dotted
some/every/nonemodel policy segments - create-time DB-backed relation policy checks when root join values are present in the create input
- built-in
hasRole('...')andinTenant('...')checks across direct, SQL-scoped, and procedure authorization paths - non-invocation of denied procedures
Remaining Limits
Current limits that still matter in practice:- create-time relation checks are partial: they only work when the root relation join values are known from create input or auth-derived default expansion
- procedure DB-backed auth is partial:
@authorize(Model, action, args.path)delegates to model auth by referenced id, but there is still no general DB-querying procedure policy language - CoolContext now carries a first-class
principal.actor/session/tenant/claimsmodel, but explicit impersonation, acting-as, and delegated-session semantics are still unsupported - arbitrary policy functions beyond
hasRole('...')andinTenant('...')are still unsupported - field-level read masking and field-level write blocking are still unsupported