Last updated 2026-06-29
Check & Expand
This guide is about using the engine: the four
AuthzService RPCs you call at runtime, what each is for, and where
each belongs on (or off) the request hot path.
What you'll learn: when to call Check vs
Expand vs ReadRelationTuples, how to read the
Expand userset tree, and how to batch
WriteRelationTuples for create / share / revoke.
Check — the per-request decision
Check answers one boolean: does this concrete user hold this
relation on this object? Call it at the point where you gate an
action — the same place you'd write an if (authorized).
It evaluates the namespace's rewrite rules transitively (role hierarchy, group
nesting, parent inheritance) and is cycle-safe.
curl -X POST http://localhost:8080/workspace.v1.AuthzService/Check \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" \ -H "Content-Type: application/json" \ -d '{"namespace":"workspace","object_id":"W","relation":"admin","subject_user_id":"bob"}'{ "allowed": true }subject_user_id is the user being tested; it is independent of the
Authorization: Bearer $WS_SERVICE_TOKEN service credential, which
identifies the calling product backend. End-user authentication happens at the
product edge before this service is reached (see
Security & Service Auth).
Latency and usage
- Call it on the gate, once per action. One
Checkper authorization decision — not per row. To filter a list, preferExpand(below) or a denormalized index over fanning out N checks. - Cost grows with rewrite depth. A direct grant is one store hit; deep group nesting and long parent chains add recursion. Keep hierarchies shallow where you can.
- Cache the decision, not the tuples. A
Checkresult is a function of the stored tuples; invalidate it when youWriteRelationTuplesfor that object.
Expand — who has access (debugging / UI)
Expand is the set-valued sibling of Check: instead of
testing one user, it returns the effective userset tree for a
relation. Use it to answer "who can view this?", to render a share dialog, and
to debug why a Check resolved the way it did. It takes no subject.
curl -X POST http://localhost:8080/workspace.v1.AuthzService/Expand \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" \ -H "Content-Type: application/json" \ -d '{"namespace":"resource","object_id":"doc","relation":"viewer"}'Reading the tree
The result is a tree of two node kinds, mirroring the rewrite that produced it:
-
UNION— an internal node with child subtrees; access is the union of its children. A relation built fromunion(this(), computed(...), tupleToUserset(...))expands to a union node with one child per branch. -
LEAF— a terminal node carrying the concreteuser_idsand any nested usersets (sets) stored directly for that branch.
So a document that is shared with dad directly, with the
family group, and inherits from workspace W expands
to a UNION with three leaves: the direct viewers, the
group:family#member userset, and the
workspace:W#member userset. Each userset can be expanded again to
drill into nested groups.
ReadRelationTuples vs Check
Both read the store, but they answer different questions. Choose by whether you want what was written or what it evaluates to.
ReadRelationTuples | Check | |
|---|---|---|
| Returns | Raw stored tuples matching a filter | A single allowed boolean |
| Rewrite evaluation | None — exactly what's stored | Full, transitive |
| Resolves groups / inheritance | No | Yes |
| Use for | Sync, audit, share-management UI | Authorization decisions |
Never make an access decision from ReadRelationTuples: a user can
hold a relation through a group or a parent without any directly stored tuple.
For decisions, always Check.
curl -X POST http://localhost:8080/workspace.v1.AuthzService/ReadRelationTuples \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" \ -H "Content-Type: application/json" \ -d '{"namespace":"resource","object_id":"doc","relation":"viewer","subject_user_id":"dad"}'WriteRelationTuples — create, share, revoke
All grants and revocations go through one write API. The updates
array carries one or more ops, each INSERT or DELETE,
and the whole batch is applied together — so a re-share (revoke one subject,
grant another) is a single atomic call.
curl -X POST http://localhost:8080/workspace.v1.AuthzService/WriteRelationTuples \ -H "Authorization: Bearer $WS_SERVICE_TOKEN" \ -H "Content-Type: application/json" \ -d '{"updates":[ {"op":"INSERT","tuple":{ "namespace":"resource","object_id":"doc","relation":"viewer", "subject":{"user_id":"erin"}}}, {"op":"DELETE","tuple":{ "namespace":"resource","object_id":"doc","relation":"viewer", "subject":{"user_id":"dad"}}} ]}'
A subject is either a concrete user ("user_id") or a userset
("set" with namespace / object_id /
relation) — the latter is how you grant a group or inherit from a
parent. The high-level WorkspaceService and
GroupService RPCs write these tuples for you for the common cases;
drop to WriteRelationTuples for direct resource sharing and custom
namespaces.
Picking the right RPC
| You want to… | Call |
|---|---|
| Gate an action for one user (hot path) | Check |
| List / debug who has access | Expand |
| Read the exact stored grants on an object | ReadRelationTuples |
| Grant, share, or revoke (atomic batch) | WriteRelationTuples |
Where to next
- Authorization Model — how rewrites evaluate, in full
- Sharing Resources — concrete share / revoke flows
- Using the API — protocol, auth, and the live per-RPC reference