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 }
The subject is data, not the caller. 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 Check per authorization decision — not per row. To filter a list, prefer Expand (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 Check result is a function of the stored tuples; invalidate it when you WriteRelationTuples for 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 from union(this(), computed(...), tupleToUserset(...)) expands to a union node with one child per branch.
  • LEAF — a terminal node carrying the concrete user_ids and 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