Authorization
ReBAC resources, relations, permissions, indexing, and scope.
The authorization module implements relationship-based access control (ReBAC, Zanzibar-style). Subjects (users, teams) link to resources via relation tuples; permissions resolve from those tuples with optional inheritance (owner->read). An internal index (ActorIndex + ObjectIndex) makes can checks fast; tuples are the source of truth.
Apps call the Client API to check the signed-in user's access. Operators define resources, grant tuples, and debug evaluations through the Admin API or MCP at provision time — never from browser code.
Use cases
Multi-tenant B2B
Team-owned records — documents belong to a Team, not just a user
Document sharing
Grant editor/viewer relations without duplicating ownership logic
Scoped creates
New records inherit team context via the scope query param
Runtime permission checks
Ask can this user edit this resource? before showing UI actions
Bulk sharing
Grant one relation to many resources via POST /relations/many
Operator debugging
Trace why access was granted with GET /permissions/evaluate
Capabilities
- Resource definitions (relations + permissions)
- Relation tuple CRUD
- Bulk relation creation (/relations/many)
- Permission inheritance (->)
- scope on database creates
- Client API /authorization/check
- Admin API /permissions/can and /evaluate
- Index reconstruction (soft / hard)
- Built-in Team resource
- Authorized schema integration
- gRPC Can / access-list views
Example: Team-owned documents with scope
Walkthrough
- Enable authorization on the Document schema (conduitOptions.authorization.enabled)
- Ensure the user has edit permission on Team:abc (team member or owner)
- Create a document with scope=Team:abc as a query parameter
- Before PATCH, call GET /authorization/check?action=edit&resource=Document:docId
curl -X POST "http://localhost:3000/database/Document?scope=Team:507f1f77bcf86cd799439011" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Q1 roadmap","body":"Team-owned doc"}'curl "http://localhost:3000/authorization/check?action=edit&resource=Document:DOC_ID" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"How it works
Mental model
ReBAC stores tuples and resolves permissions from a resource definition:
ResourceDefinition "Document"
relations: { owner: [User, Team], editor: [User, Team] }
permissions: { read: [editor, owner, owner->read], edit: [editor, owner] }
Tuple: Team:abc#owner@Document:xyz
Format: subject#relation@resource
Check: can(Team:abc, edit, Document:xyz) → true via team inheritance
Every tuple is persisted as a Relationship document with a unique computedTuple (User:uid#owner@Document:id). Creating or deleting a tuple updates ActorIndex and ObjectIndex rows so can checks avoid graph walks at request time.
Three layers (do not confuse)
| Layer | Controls |
|---|---|
| Document ReBAC | Per-document read/edit/delete on authorized schemas |
| Schema CMS permissions | Who can use Admin Panel CRUD on a schema |
| Route middleware | HTTP route authentication flags on custom endpoints |
This page covers document ReBAC.
Resource definitions
A resource is a named type (e.g. Document, Team) with two JSON maps:
| Field | Shape | Meaning |
|---|---|---|
relations | { relationName: [allowedSubjectTypes] } | Who may hold each relation on instances of this type. Use * to allow any subject type. |
permissions | { actionName: [roles] } | Which relations (or inherited actions) grant each action. Use * for public access; [] for none. |
Example:
{
"name": "Document",
"relations": {
"owner": ["User", "Team"],
"editor": ["User", "Team"]
},
"permissions": {
"read": ["editor", "owner", "owner->read"],
"edit": ["editor", "owner"],
"delete": ["owner"]
},
"version": 0
}
When a resource definition changes, Conduit schedules a re-index for all tuples involving that type. Schemas with conduitOptions.authorization.enabled automatically register a resource definition matching the schema name.
Relation tuples
Grant access by writing tuples — not by copying permission logic into app code.
| Operation | When |
|---|---|
| Create | Share a document, add a team member relation, assign ownership on create |
| Read | Audit who has access; debug Admin Panel |
| Delete | Revoke sharing; cleanup on resource delete |
Validation on create:
subject,relation, andresourcemust beType:ididentifiers- The target resource's definition must declare the
relation - The subject's type must appear in that relation's allowed list (unless
*)
Single-tuple create builds indexes synchronously. Bulk create writes all tuples, then enqueues async index jobs per tuple.
Inheritance (->)
owner->read in a permission rule means: a subject holding owner on this resource inherits the read permission defined on the owner subject's type. For teams, Authentication registers Team with member, owner, and permissions like read, edit, delete — so Team:tid#owner@Document:doc lets team members read via owner->read.
Ownership on create
When scope=Team:tid is set on POST /database/{Schema}:
- Pre-check: user must have
editon the scope resource - Tuple created:
Team:tid#owner@{Schema}:{docId}— not a direct User owner tuple
When scope is omitted, the authenticated user becomes User:uid#owner@{Schema}:{docId}.
Built-in Team resource
Authentication registers Team with relations member, owner, readAll, editAll and permissions read, edit, delete, invite, manageMembers, etc. Use these action names — do not invent new ones without extending the definition.
Permission resolution
can(subject, action, resource) resolves in order:
- Self-access — subject equals resource → allowed
- Direct permission tuple — explicit
subject#action@resourcegrant - Index lookup — ObjectIndex + ActorIndex graph for relation-derived permissions
The Admin /permissions/evaluate endpoint returns the same decision plus the resolution path (assigned direct grant vs inherited index chain) for debugging.
Index reconstruction
Indexes can drift after bulk imports, manual DB edits, or rare worker failures. Operators can rebuild them:
POST ADMIN_BASE_URL/authorization/indexer/reconstruct
Body: { "soft": true | false } // default false
| Mode | Behavior | When to use |
|---|---|---|
soft: true | Does not wipe existing index documents. Enqueues index-build jobs for every stored relation tuple. | First attempt after suspected drift; safer, idempotent |
soft: false (default) | Deletes all ActorIndex and ObjectIndex documents, then enqueues jobs for every relation tuple. | Confirmed corruption; only with explicit operator confirmation |
The endpoint returns "ok" immediately; reconstruction runs in the background (batched, 1000 relations per resource type at a time). During hard rebuild, permission checks may temporarily fail until workers finish — schedule maintenance windows for soft: false.
Surfaces
| Surface | Base | Purpose |
|---|---|---|
| Client API | CLIENT_BASE_URL/authorization/... | Signed-in user checks (/check, /role/:resource) |
| Admin API | ADMIN_BASE_URL/authorization/... | Resource/relation CRUD, bulk grants, indexer, evaluate |
| gRPC | grpcSdk.authorization | Custom modules: can, createRelation, createRelations, access-list views |
| MCP | Admin-backed tools | Provision resources and tuples at deploy time |
Configure
- Enable authorization module in deployment (
patch_config_authorizationor Helm values) - Set
conduitOptions.authorization.enabled: trueon schemas via MCPpatch_database_schemas_id - Define custom resources with
POST /authorization/resourcesor MCPpost_authorization_resources - Grant tuples with
POST /authorization/relations(single) orPOST /authorization/relations/many(bulk)
For team-owned records, always pass scope on database creates and verify access with /authorization/check before destructive UI actions.
Client API
User bearer token required (authMiddleware). The server derives the subject from the authenticated user — callers never pass arbitrary subject on these routes.
| Method | Path | Query / params | Response |
|---|---|---|---|
| GET | /authorization/check | action (required), resource (required), scope (optional) | { allowed: boolean } |
| GET | /authorization/role/:resource | scope (optional) | { roles: string[] } |
/check flow:
- If
scopeis set, verify the user may use that scope (can(User:uid, action, scope)) - Evaluate
can(scope ?? User:uid, action, resource)
/role/:resource flow:
- If
scopeis set, verify the user hasreadon the scope - Return relation names where
subject = scope ?? User:uidandresource = :resource
curl "http://localhost:3000/authorization/check?action=edit&resource=Document:DOC_ID&scope=Team:TEAM_ID" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"curl "http://localhost:3000/authorization/role/Document:DOC_ID" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Never use Client API routes to define resources or create tuples for other users — that is Admin API / provisioning work.
Admin API
Operator credentials on ADMIN_BASE_URL/authorization/... (masterkey, admin JWT, or cdt_ token). Use for provisioning, bulk grants, and debugging — not from app runtime code.
Resources
| Method | Path | Body / query | Notes |
|---|---|---|---|
| POST | /authorization/resources | { name, relations, permissions, version? } | Create definition; returns { status, resourceDefinition } |
| GET | /authorization/resources | search, skip, limit, sort | Paginated list |
| GET | /authorization/resources/:id | — | By MongoDB id or resource name |
| PATCH | /authorization/resources/:id | { relations, permissions, version? } | Triggers re-index for that type |
| DELETE | /authorization/resources/:id | — | Deletes definition and related index data |
curl -X POST http://localhost:3030/authorization/resources \
-H "masterkey: YOUR_MASTERKEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Document",
"relations": { "owner": ["User", "Team"], "editor": ["User", "Team"] },
"permissions": { "read": ["editor", "owner", "owner->read"], "edit": ["editor", "owner"] }
}'Relations
| Method | Path | Body / query | Notes |
|---|---|---|---|
| POST | /authorization/relations | { subject, relation, resource } | Single tuple; indexes built synchronously; idempotent if tuple exists |
| POST | /authorization/relations/many | { subject, relation, resources: string[] } | Bulk grant same relation to many resources; async index jobs; prefer single route for one tuple; batch ≤100 resources when possible |
| GET | /authorization/relations | search, subjectType, resourceType, skip, limit, sort | List / filter tuples |
| GET | /authorization/relations/:id | — | Single tuple by id |
| DELETE | /authorization/relations/:id | — | Removes tuple and index edges |
curl -X POST http://localhost:3030/authorization/relations/many \
-H "masterkey: YOUR_MASTERKEY" \
-H "Content-Type: application/json" \
-d '{
"subject": "User:507f1f77bcf86cd799439011",
"relation": "editor",
"resources": ["Document:aaa", "Document:bbb", "Document:ccc"]
}'Permissions (admin checks)
Unlike Client /check, these accept an explicit subject and use permission (not action) as the query param name.
| Method | Path | Query | Response |
|---|---|---|---|
| GET | /authorization/permissions/can | subject, permission, resource, scope? | { allowed: boolean } |
| GET | /authorization/permissions/evaluate | subject, permission, resource, scope? | { allowed, assigned?, paths?, subjectIndex?, objectIndex? } |
/permissions/can — same boolean result as gRPC Can. When scope is set, verifies the subject has some relation or permission on the scope before evaluating against the target resource.
/permissions/evaluate — debugging aid. Returns whether access was directly assigned (assigned: true) or resolved via the index graph, plus paths (human-readable tuple chain) and raw subjectIndex / objectIndex documents.
curl "http://localhost:3030/authorization/permissions/can?subject=User:UID&permission=edit&resource=Document:DOC_ID" \
-H "masterkey: YOUR_MASTERKEY"curl "http://localhost:3030/authorization/permissions/evaluate?subject=Team:TID&permission=read&resource=Document:DOC_ID" \
-H "masterkey: YOUR_MASTERKEY"Indexer
| Method | Path | Body | Notes |
|---|---|---|---|
| POST | /authorization/indexer/reconstruct | { soft?: boolean } | Rebuild ActorIndex/ObjectIndex; see Index reconstruction |
curl -X POST http://localhost:3030/authorization/indexer/reconstruct \
-H "masterkey: YOUR_MASTERKEY" \
-H "Content-Type: application/json" \
-d '{"soft": true}'Admin vs Client API
| Client API | Admin API | |
|---|---|---|
| Host | Router (:3000) | Core (:3030) |
| Auth | User bearer token | masterkey, admin JWT, or cdt_ token |
| Subject | Implicit (User:{authenticatedId}) | Explicit subject query param |
| Check param | action | permission (same semantics) |
| Define resources | No | POST/PATCH /authorization/resources |
| Manage tuples | No | POST/GET/DELETE /authorization/relations |
| Bulk tuples | No | POST /authorization/relations/many |
| Debug paths | No | GET /authorization/permissions/evaluate |
| Rebuild index | No | POST /authorization/indexer/reconstruct |
Application UI gates actions with Client /check. CI, MCP, and operators use Admin routes. See Client vs Admin API.
MCP
Enable ?modules=authorization in your MCP server URL.
| Tool | Purpose |
|---|---|
get_authorization_resources | List resource definitions |
post_authorization_resources | Define resources, relations, permissions |
post_authorization_relations | Create a single relationship tuple |
get_authorization_permissions_can | Admin-side check (permission, subject, resource) |
MCP wraps Admin API operations — use at deploy time to provision definitions and seed tuples, not from application runtime.