Skip to content
ReferenceRust API

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

ExpressionMeaning
authorMatch the subject reached through a relationship on the resource
editorReuse 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.viewerFollow a relationship and evaluate the target's rule
publicEveryone, including anonymous viewers
anyoneViewers with an authenticated subject
adminViewers carrying a trusted administrator claim
nobodyNo viewer
a or bEither expression matches
a and bBoth expressions match
a but not bThe 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: WORKSPACE

These 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

APIUse
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: false

update(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 methodPurpose
system()Explicit privileged operations
system_for(&client)Trusted system access retaining the source's context and transaction
system_in(&scoped).awaitJoin 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.