Permissions reference
For a first introduction, start with Permissions language. This reference covers the syntax and behavior to consult when building a more involved policy.
Enable permissions
access {
subjects = [User]
depth = 16
}subjects lists model types your application can bind as actors. Once enabled, every
ordinary model declares read, create, update, and delete rules. A system model
uses the explicit system-access boundary instead. Without access, ordinary Rent
models need no permission declarations.
Bind a viewer using the complete generated subject identity. A scalar primary key uses its scalar type. A compound key uses the generated ID struct. Tenant identity is not inferred from a delimiter inside an ID string. Your application verifies credentials and supplies trusted subject, tenant, group, and administrator claims.
Rule vocabulary
| Expression | Meaning |
|---|---|
author | Match the subject reached through a relationship on the resource |
editor | Reuse another named rule on the same resource |
[User] | Permit direct assignments to User identities on this named rule |
[Team#member] | Permit assignments to a team's computed member set |
parent.viewer | Follow a relationship and evaluate the target's rule |
public | Everyone, including anonymous viewers |
anyone | Viewers with an authenticated subject |
admin | Viewers carrying a trusted administrator claim |
nobody | No viewer |
a or b | Either expression matches |
a and b | Both expressions match |
a but not b | The first expression matches and the second doesn't |
Organization roles are application-defined. An organization's owner or administrator
is not automatically an admin claim or a system client.
Declare direct grants on named rules, then refer to those rules from operations:
permissions Post {
editor = author or [User]
viewer = editor or [User, Team#member]
read = viewer
create = anyone
update = editor
delete, manage_grants = author
}A bracket grant requires manage_grants on that model. It belongs to the named rule
where it appears: an editor assignment and a viewer assignment are separate data.
All names in the example other than language words refer to your model definitions.
Conditions
Use a relationship filter for related-row values, a subject filter for actor values,
and when for values on the resource:
manager = people(role: MANAGE).principal
reader = manager or people(role: [READ, MANAGE]).principal
read = reader or anyone(kind: PERSON) when visibility: WORKSPACEThese lines assume the corresponding people, principal, role, kind, and visibility
definitions in your schema. If the through model has exactly one subject relationship,
the final subject hop may be omitted. Name it when more than one is possible.
Conditions accept typed scalar literals and lists of alternatives. Use null only
with nullable fields. Dates, times, and timestamps follow their field's declared type.
Instant-valued timestamps use RFC 3339. Mark one instant-valued DateTime field with
@rent.expires when a row's permission evidence should expire.
Precedence, strongest first, is when, and, or, then but not. Add parentheses
when grouping would otherwise be unclear. A rule may continue on a line beginning
with an operator.
Direct grants
The generated resource accessor uses the model's complete key:
client.post_permissions(&post_id)
.grant_viewer(User::subject(user_id.clone()))
.expires_at(deadline)
.save()
.await?;
client.post_permissions(&post_id)
.revoke_viewer(User::subject(user_id))
.await?;For non-Copy ID types, clone an ID when reusing it. Generated grant methods accept only
the subject types allowed by the rule. For usersets, use the generated helper such as
Team::member_subject(team_id).
Both endpoints must be readable and the resource must allow manage_grants. Tenant
and application-policy restrictions remain in force. Repeating a direct assignment
keeps one assignment. Specifying a new expiry replaces its expiry, and omitting expiry
clears it. Revocation removes that assignment, not other routes to access.
Query APIs
| API | Use |
|---|---|
post_permissions(&id).can(PostPermission::Editor) | Check this viewer on one existing resource |
post().can(PostPermission::Editor) | Require a named rule as well as normal read permission |
post().with_permissions() | Return readable rows with permission flags |
post().permissions() | Return flags for the selected readable rows |
user().who_can(PostPermission::Viewer, &id) | List visible stored subjects with access |
post_permissions(&id).explain(PostPermission::Viewer) | Explain the bound viewer's decision |
A resource check returns false for a missing, unreadable, or out-of-tenant resource. Invalid viewer claims and incomplete evaluation return errors. A named query filter doesn't replace the model's read rule. Flags describe query-time access, not permission reserved for a later mutation. See queries.
Relationships and mutations
Each write requires its corresponding operation rule. Related reads keep the related
model's own read rule. Reading a User doesn't reveal all of that user's Posts.
Unreadable related rows are omitted, and an unreadable to-one target loads as None.
Changing a permission-relevant parent relationship also checks the create rule against the resulting resource. Authority can't be manufactured by first changing a relationship in that same operation. Modeled joins retain their own rules in addition to endpoint checks. Nested and bulk writes roll back together when a required check fails.
See moving between parents and bulk creates.
Defaults and database-owned values
Create rules use supplied values and defaults. Computed columns remain database-owned. Set their input fields rather than trying to assign the computed value. A write that produces a forbidden stored result rolls back.
A generated integer ID can be omitted when authorization doesn't need that identity before insertion. Supply an explicit ID when the create policy needs it earlier. Database sequences may contain gaps after rollback. See generated child IDs.
Tenant-local keys
When IDs are local to a tenant, declare the tenant in both primary and foreign keys. Use every key component in generated relationship and permission helpers. An explicit join model supports compound endpoint keys. See compound relationships.
Upserts
An insert branch requires create. A conflict update requires the existing row's read
and update authority, and applies resulting-value restrictions. keep_existing()
still requires authorization for the conflict branch. It isn't a way to discover an
otherwise hidden row. See upsert choices.
Field-level updates
// Inside a Membership permission block.
update(active) = principal
update_to(active) = principal when active: falseupdate(fields) supplies authority for the named mutable scalar fields. Every
assigned field needs authority from a field grant or ordinary whole-row update.
Overlapping grants provide alternatives for a field.
update_to(fields) adds a requirement. It never grants update authority by itself.
It applies when any named field is assigned, and every applicable guard must succeed.
Inline resource when conditions inspect resulting values. Relationship paths,
named rules, relation filters, and subject filters retain their pre-write meaning.
Following an owner relationship therefore refers to the existing owner, not a new
owner being assigned by the same update.
Identities, immutable fields, and relation names aren't mutable scalar targets.
Automatic timestamps, version increments, and database-computed outputs don't require
their own grant just because they accompany an authorized edit. Explicit application
or hook assignments still require authority. Ordinary can(Update) reports whole-row
authority. It doesn't predict acceptance of a particular proposed change.
For protected fields, referential actions that would update rows implicitly must use Restrict/NoAction. Cascades from immutable primary keys remain safe. Schema validation rejects unsupported combinations. Keep application-owned cross-row invariants in an application transaction. See the field tutorial.
Recursive rules and limits
Positive inheritance such as viewer = author or parent.viewer is supported. Recursion
through exclusion (but not) is rejected. The access depth setting defaults to 16
and accepts 1 through 64.
An unfinished required traversal returns a depth/work error rather than partial access. Cycles alone don't grant permission. An explanation may need to inspect branches that a successful ordinary check didn't need. It can therefore encounter a limit where a simple check succeeds. Keep hierarchy depth deliberate and handle errors distinctly from a normal denial.
Transactions and freshness
Permission-aware reads in an existing application transaction require a consistent
snapshot. Choose RepeatableRead or Serializable with TransactionOptions::isolation
on server databases. An insufficient snapshot returns PermissionError::SnapshotRequired.
See transactions.
Revocation visibility follows the database's transaction semantics. A PostgreSQL repeatable-read transaction can retain facts visible in its earlier snapshot. A fresh operation after a committed revocation must no longer rely on the removed assignment. Keep transactions and streams short. Streams hold a connection until completion or drop. A database error may require rolling back an existing transaction before further work.
Delegation
Viewer::on_behalf_of(SubjectClaims::of(subject)) carries a represented subject separately
from the actor. Each has its own group and administrator claims. inherit allows either
party's result. intersect requires both. These combine complete rule decisions, not
matching positions in an explanation tree. Tenant boundaries apply to both parties.
Your application authenticates the claims and decides when delegation is permitted.
Client authority
Binding a verified viewer or context returns ScopedClient. It retains ordinary query
and mutation APIs but can't call system(), replace its viewer, configure shared middleware,
or recover a root executor. Clones and transaction helpers preserve that boundary.
Keep root authority in bootstrap and narrowly trusted adapters:
| Root method | Purpose |
|---|---|
system() | Explicit privileged operations |
system_for(&client) | Trusted system access retaining the source's context and transaction |
system_in(&scoped).await | Join an active scoped transaction as a trusted adapter |
rebind_context(&client, context) | Trusted context rebinding |
configure_for(&client, configure) | Configure while retaining the source and frozen request binding |
root_executor() | Borrow root authority for low-level integration |
For system_in, retain a clone of the same application root. A separately constructed
root—even from the same pool—is another authority source. Non-transaction, closed,
and unrelated-source handles are rejected. See
scoped clients for the request pattern.
A system-only model is not a public directory. With access enabled, ordinary clients
cannot read or mutate it. @rent.reference permits a particular source relationship
to check that a system target exists without exposing its contents. See
private platform references.
Cache behavior
Caching applies only when attached and explicitly requested with PermissionConsistency::Cached.
Fresh remains the default. Ordinary ORM reads, flags, explanations, and mutation authorization
stay fresh. Transaction-bound checks use the transaction snapshot instead of cached decisions.
Custom policies, interceptors, field codecs, application context, and tenant membership that needs a current subject-row lookup can cause checks to bypass caching. Complete claims distinguish entries. Errors aren't cached as permission decisions.
Generated writes invalidate attached caches when they affect permission inputs. External writers must invalidate or notify after commit. With a controller, notifications commit with relevant data writes. Unrelated generated writes add no controller record. Cross-instance notification is eventual, not instantaneous. Follow the caching guide for configuration and writer requirements.
Inspection and errors
who_can returns subjects, complete, and unresolved. It lists subjects as themselves,
without the inspector's extra claims. The inspector must be allowed to read both the
resource and the returned subjects. Check completeness before labeling a sharing UI's
list as exhaustive.
An explanation returns the requested permission, allowed, delegation, an actor
tree, and an optional represented tree. Branches have expression, matched, optional
rule documentation, and ordered children. Related private rows aren't included.
A hidden or missing resource returns None. Local expansion is bounded to 256 nodes.
Exceeding it returns PermissionError::PlanLimit rather than a partial explanation.
Handle a normal denial separately from invalid claims, scope conflicts, snapshot errors, and depth/work limits. See errors and the explanation tutorial.
