ReBAC: Relationship-Based Access Control
ReBAC (Relationship-Based Access Control) is the third and most powerful layer in coomia-dip's three-layer permission model. It derives access permissions from graph relationships between objects, naturally aligning with an ontology-driven platform architecture. This article covers the relationship graph model, Zanzibar-style tuple storage, relationship traversal algorithms, permission inheritance and negation, performance optimization strategies, and coordination with the RBAC/ABAC layers.
“Series: S6 Platform Engineering · Article 4 | Level: Advanced | Reading Time: 18 min
ReBAC: Relationship-Based Access Control
#TL;DR
ReBAC (Relationship-Based Access Control) is the third and most powerful layer in coomia-dip's three-layer permission model. It derives access permissions from graph relationships between objects, naturally aligning with an ontology-driven platform architecture. This article covers the relationship graph model, Zanzibar-style tuple storage, relationship traversal algorithms, permission inheritance and negation, performance optimization strategies, and coordination with the RBAC/ABAC layers.
#1. Why ReBAC Is Needed
#1.1 Limitations of RBAC and ABAC
In large-scale multi-tenant platforms, RBAC and ABAC each have blind spots:
| Scenario | RBAC Capability | ABAC Capability | ReBAC Capability |
|---|---|---|---|
| User A owns folder X | Requires dedicated role | Attribute matching | Direct relationship query |
| User A belongs to group G, G has access to project P | Requires role transitivity | Not supported | Relationship chain traversal |
| Project P nested in org O, O's admin can manage P | Requires hierarchical roles | Not supported | Hierarchical inheritance |
| File F in folder D, D under project P | Role per file | Attribute maintenance | Automatic hierarchy inheritance |
| Shared link grants read-only to specific user | Dedicated role | Dedicated policy | Direct relationship |
The core insight of ReBAC: permissions are fundamentally relationships between objects, and an ontology platform naturally maintains rich object relationship graphs.
#1.2 Natural Fit with Ontology
coomia-dip's Ontology already defines object types and relation types. ReBAC reuses these relationships to derive permissions:
Ontology Layer Defines:
ObjectType: Project
ObjectType: Dataset
RelationType: Project --contains--> Dataset
ReBAC Permission Semantics:
Project:proj1 --editor--> User:alice
Project:proj1 --contains--> Dataset:ds1
=> User:alice has editor permission on Dataset:ds1 (via relationship inheritance)
#1.3 Inspiration from Google Zanzibar
The Google Zanzibar paper (2019) defined the industrial standard for ReBAC implementation. coomia-dip adopts its core ideas:
- Tuple storage:
(object, relation, user)triples - Namespace configuration: Schema defining types and relations
- Check API: Determine if a user has permission
- Expand API: Expand all users for a given relation
- Watch API: Subscribe to permission changes
#2. Relationship Graph Model Design
#2.1 Core Data Structures
The foundation of ReBAC is the relationship tuple:
// Relationship tuple definition
message RelationshipTuple {
ObjectReference resource = 1; // Resource object
string relation = 2; // Relation name
SubjectReference subject = 3; // Subject (user or userset)
}
message ObjectReference {
string type = 1; // Object type, e.g., "project", "dataset"
string id = 2; // Object ID
}
message SubjectReference {
ObjectReference object = 1; // Subject object
string optional_relation = 2; // Optional relation qualifier
}
Key Design: SubjectReference's optional_relation
This allows expressing "usersets" as subjects, for example:
(document:readme, viewer, project:alpha#member)
Meaning: all members of project:alpha are viewers of document:readme.
#2.2 Namespace Configuration (Type Definition)
Each object type needs to define its relation schema:
# Namespace configuration — similar to Zanzibar's namespace config
type_definitions:
- type: "organization"
relations:
admin:
description: "Organization administrator"
directly_related_types:
- type: "user"
member:
description: "Organization member"
union:
- directly_related: { type: "user" }
- computed_relation: "admin" # admin is also a member
- type: "project"
relations:
parent_org:
description: "Parent organization"
directly_related_types:
- type: "organization"
admin:
description: "Project administrator"
union:
- directly_related: { type: "user" }
- tuple_to_userset:
tupleset: "parent_org"
computed_relation: "admin" # org admin is also project admin
editor:
description: "Project editor"
union:
- directly_related: { type: "user" }
- directly_related: { type: "team", relation: "member" }
- computed_relation: "admin" # admin is also editor
viewer:
description: "Project viewer"
union:
- directly_related: { type: "user" }
- computed_relation: "editor" # editor is also viewer
- type: "dataset"
relations:
parent_project:
description: "Parent project"
directly_related_types:
- type: "project"
owner:
description: "Dataset owner"
directly_related: { type: "user" }
editor:
union:
- directly_related: { type: "user" }
- computed_relation: "owner"
- tuple_to_userset:
tupleset: "parent_project"
computed_relation: "editor"
viewer:
union:
- directly_related: { type: "user" }
- computed_relation: "editor"
- tuple_to_userset:
tupleset: "parent_project"
computed_relation: "viewer"
#2.3 Three Modes of Relationship Derivation
The coomia-dip ReBAC engine supports three derivation modes:
1. Direct Relation
┌─────────┐ viewer ┌──────────┐
│ User:bob │──────────>│ Doc:file1 │
└─────────┘ └──────────┘
2. Computed Relation (Implied)
Definition: viewer includes editor
┌───────────┐ editor ┌──────────┐
│ User:alice │──────────>│ Doc:file1 │
└───────────┘ └──────────┘
=> alice automatically gains viewer permission
3. Tuple-to-Userset
┌──────────┐ parent ┌──────────────┐
│ Doc:file1 │──────────>│ Project:proj1 │
└──────────┘ └──────────────┘
│ editor
▼
┌───────────┐
│ User:carol │
└───────────┘
=> carol inherits editor permission on file1 through proj1
#3. Tuple Storage Engine
#3.1 Storage Layer Design
Relationship tuple storage requires high-throughput reads/writes and fast querying:
┌─────────────────────────────────────────────────────────┐
│ Tuple Store Layer │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ Write Path │ │ Read Path │ │ Watch Path │ │
│ │ (gRPC API) │ │ (gRPC API) │ │ (Stream API) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌──────▼──────────────────▼──────────────────▼───────┐ │
│ │ Changelog (WAL) │ │
│ │ - Zookie/Token per write for consistency │ │
│ │ - Ordered sequence of tuple changes │ │
│ └──────┬────────────────────────────────────────────┘ │
│ │ │
│ ┌──────▼────────────────────────────────────────────┐ │
│ │ Primary Store (PostgreSQL) │ │
│ │ - relation_tuples table │ │
│ │ - Indexed: (resource_type, resource_id, relation)│ │
│ │ - Indexed: (subject_type, subject_id) │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Cache Layer (Redis) │ │
│ │ - Check result cache with Zookie invalidation │ │
│ │ - Expand result cache │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
#3.2 Database Schema
-- Core relationship tuples table
CREATE TABLE relation_tuples (
id BIGSERIAL PRIMARY KEY,
resource_type VARCHAR(128) NOT NULL,
resource_id VARCHAR(256) NOT NULL,
relation VARCHAR(64) NOT NULL,
subject_type VARCHAR(128) NOT NULL,
subject_id VARCHAR(256) NOT NULL,
subject_relation VARCHAR(64), -- NULL means direct user
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE, -- Soft delete
zookie BIGINT NOT NULL, -- Consistency token
UNIQUE (resource_type, resource_id, relation,
subject_type, subject_id, subject_relation)
);
-- High-frequency query indexes
CREATE INDEX idx_tuples_resource ON relation_tuples
(resource_type, resource_id, relation)
WHERE deleted_at IS NULL;
CREATE INDEX idx_tuples_subject ON relation_tuples
(subject_type, subject_id)
WHERE deleted_at IS NULL;
CREATE INDEX idx_tuples_zookie ON relation_tuples (zookie);
-- Changelog table (for Watch API and consistency)
CREATE TABLE changelog (
zookie BIGSERIAL PRIMARY KEY,
operation VARCHAR(16) NOT NULL, -- TOUCH / DELETE
resource_type VARCHAR(128) NOT NULL,
resource_id VARCHAR(256) NOT NULL,
relation VARCHAR(64) NOT NULL,
subject_type VARCHAR(128) NOT NULL,
subject_id VARCHAR(256) NOT NULL,
subject_relation VARCHAR(64),
timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
#3.3 Consistency Model: Zookies
Zanzibar introduced Zookies to solve the "new enemy" problem (permission just revoked but cache not yet updated):
Write Flow:
1. Client writes tuple -> receives zookie Z1
2. Subsequent Check requests carry zookie Z1
3. Server ensures evaluation sees Z1 and all prior writes
Consistency Levels:
- minimize_latency: Uses cache, may read stale data
- at_least_as_fresh(zookie): At least as fresh as specified zookie
- fully_consistent: Reads latest snapshot, slowest but safest
class ZookieManager:
"""Zookie consistency token manager"""
def __init__(self, store: TupleStore):
self._store = store
self._current_zookie = AtomicCounter()
def next_zookie(self) -> int:
"""Generate next globally incrementing zookie"""
return self._current_zookie.increment()
def snapshot_at(self, zookie: int) -> TupleSnapshot:
"""Get tuple snapshot at specified zookie"""
return self._store.snapshot(zookie)
def is_at_least_as_fresh(self, requested: int, current: int) -> bool:
"""Check if current snapshot satisfies freshness requirement"""
return current >= requested
#4. Permission Check Algorithm
#4.1 Check API Implementation
The Check API is the core of ReBAC: determining whether (user, permission, resource) holds.
class ReBACheckEngine:
"""ReBAC permission check engine"""
def __init__(self, store: TupleStore, type_system: TypeSystem):
self._store = store
self._type_system = type_system
self._cache = CheckCache()
async def check(
self,
resource: ObjectRef,
permission: str,
subject: SubjectRef,
consistency: Consistency = Consistency.MINIMIZE_LATENCY,
) -> CheckResult:
"""
Check if subject has the specified permission on resource.
Algorithm:
1. Get relation definition for the resource type
2. Recursively expand based on derivation rules
3. Search for subject reachability during expansion
"""
cache_key = (resource, permission, subject)
if cached := self._cache.get(cache_key, consistency):
return cached
type_def = self._type_system.get_type(resource.type)
relation_def = type_def.get_relation(permission)
result = await self._evaluate_rewrite(
resource, relation_def.rewrite, subject,
visited=set(), depth=0
)
self._cache.set(cache_key, result)
return result
async def _evaluate_rewrite(
self,
resource: ObjectRef,
rewrite: RewriteRule,
subject: SubjectRef,
visited: set,
depth: int,
) -> CheckResult:
"""Recursively evaluate rewrite rules"""
if depth > MAX_DEPTH:
raise MaxDepthExceededError()
cycle_key = (resource, rewrite, subject)
if cycle_key in visited:
return CheckResult.DENIED # Prevent cycles
visited.add(cycle_key)
match rewrite:
case DirectRelation():
return await self._check_direct(resource, rewrite.relation, subject)
case ComputedRelation(relation=rel):
# Permission implication: e.g., editor implies viewer
rel_def = self._type_system.get_relation(resource.type, rel)
return await self._evaluate_rewrite(
resource, rel_def.rewrite, subject, visited, depth + 1
)
case TupleToUserset(tupleset=ts, computed=comp):
# Find all parent objects satisfying the tupleset relation
parents = await self._store.query_tuples(
resource_type=resource.type,
resource_id=resource.id,
relation=ts,
)
for parent_tuple in parents:
parent_ref = ObjectRef(
type=parent_tuple.subject_type,
id=parent_tuple.subject_id
)
parent_type_def = self._type_system.get_type(parent_ref.type)
parent_rel_def = parent_type_def.get_relation(comp)
result = await self._evaluate_rewrite(
parent_ref, parent_rel_def.rewrite, subject,
visited, depth + 1
)
if result.allowed:
return result
return CheckResult.DENIED
case Union(children=children):
# Any child rule allowing means allowed
tasks = [
self._evaluate_rewrite(resource, child, subject, visited.copy(), depth + 1)
for child in children
]
results = await asyncio.gather(*tasks)
return CheckResult.ALLOWED if any(r.allowed for r in results) else CheckResult.DENIED
case Intersection(children=children):
# All child rules must allow
tasks = [
self._evaluate_rewrite(resource, child, subject, visited.copy(), depth + 1)
for child in children
]
results = await asyncio.gather(*tasks)
return CheckResult.ALLOWED if all(r.allowed for r in results) else CheckResult.DENIED
case Exclusion(base=base, subtract=sub):
# base allows AND subtract does not allow
base_result = await self._evaluate_rewrite(
resource, base, subject, visited.copy(), depth + 1
)
if not base_result.allowed:
return CheckResult.DENIED
sub_result = await self._evaluate_rewrite(
resource, sub, subject, visited.copy(), depth + 1
)
return CheckResult.ALLOWED if not sub_result.allowed else CheckResult.DENIED
#4.2 Expand API Implementation
The Expand API expands all subjects for a given relation, useful for debugging and auditing:
async def expand(
self,
resource: ObjectRef,
relation: str,
) -> UsersetTree:
"""
Expand the complete userset tree for a resource and relation.
Returns a tree structure with leaf nodes being concrete users.
"""
type_def = self._type_system.get_type(resource.type)
rel_def = type_def.get_relation(relation)
return await self._expand_rewrite(resource, rel_def.rewrite, visited=set())
async def _expand_rewrite(
self,
resource: ObjectRef,
rewrite: RewriteRule,
visited: set,
) -> UsersetTree:
match rewrite:
case DirectRelation():
tuples = await self._store.query_tuples(
resource_type=resource.type,
resource_id=resource.id,
relation=rewrite.relation,
)
leaf_users = []
intermediate_nodes = []
for t in tuples:
if t.subject_relation:
child_tree = await self._expand_rewrite(
ObjectRef(t.subject_type, t.subject_id),
DirectRelation(t.subject_relation),
visited,
)
intermediate_nodes.append(child_tree)
else:
leaf_users.append(SubjectRef(t.subject_type, t.subject_id))
return UsersetTree(
type="leaf" if not intermediate_nodes else "union",
users=leaf_users,
children=intermediate_nodes,
)
case Union(children=children):
child_trees = []
for child in children:
tree = await self._expand_rewrite(resource, child, visited)
child_trees.append(tree)
return UsersetTree(type="union", children=child_trees)
#4.3 Parallel Evaluation with Short-Circuit Optimization
class ParallelCheckOptimizer:
"""Parallel evaluation optimizer"""
async def check_with_early_exit(
self,
resource: ObjectRef,
union_rules: list[RewriteRule],
subject: SubjectRef,
) -> CheckResult:
"""
Use asyncio concurrency for Union rules,
short-circuit exit on first ALLOWED result.
"""
done_event = asyncio.Event()
result_holder = {"result": CheckResult.DENIED}
async def check_branch(rule: RewriteRule):
r = await self._evaluate_rewrite(resource, rule, subject)
if r.allowed:
result_holder["result"] = CheckResult.ALLOWED
done_event.set()
tasks = [asyncio.create_task(check_branch(r)) for r in union_rules]
wait_task = asyncio.create_task(done_event.wait())
all_done = asyncio.gather(*tasks, return_exceptions=True)
await asyncio.wait(
[wait_task, all_done],
return_when=asyncio.FIRST_COMPLETED,
)
for t in tasks:
if not t.done():
t.cancel()
return result_holder["result"]
#5. Relationship Inheritance and Negation
#5.1 Hierarchical Relationship Inheritance
In coomia-dip, the object hierarchy determines permission inheritance paths:
Organization: acme
│ admin: [alice]
│ member: [bob, charlie]
│
├── Project: alpha
│ │ parent_org: acme
│ │ admin: [inherited from org:acme#admin -> alice]
│ │ editor: [dave]
│ │ viewer: [eve]
│ │
│ ├── Dataset: sales_2024
│ │ parent_project: alpha
│ │ owner: [frank]
│ │ editor: [inherited from project:alpha#editor -> dave]
│ │ viewer: [inherited from project:alpha#viewer -> eve]
│ │
│ └── Dataset: customer_info
│ parent_project: alpha
│ owner: [grace]
│ editor: [inherited]
│ viewer: [inherited]
│
└── Project: beta
│ parent_org: acme
│ admin: [inherited from org:acme#admin -> alice]
│ editor: [heidi]
#5.2 Negation (Exclusion)
Sometimes you need to exclude specific users from inherited permissions:
# Sensitive dataset: inherits project permissions but excludes contractors
type: "sensitive_dataset"
relations:
parent_project:
directly_related_types: [{ type: "project" }]
blocked:
directly_related_types: [{ type: "user" }]
viewer:
exclusion:
base:
tuple_to_userset:
tupleset: "parent_project"
computed_relation: "viewer"
subtract:
direct_relation: "blocked"
#5.3 Contextual Tuples
Some relationships only hold in specific contexts:
class ContextualTuple:
"""Context-aware relationship tuple"""
resource: ObjectRef
relation: str
subject: SubjectRef
condition: dict # Condition expression
def evaluate_condition(self, context: dict) -> bool:
"""Evaluate whether condition is satisfied"""
for key, expected in self.condition.items():
actual = context.get(key)
if actual != expected:
return False
return True
# Example: editor permission only during business hours
contextual_tuple = ContextualTuple(
resource=ObjectRef("project", "alpha"),
relation="editor",
subject=SubjectRef("user", "bob"),
condition={"time_range": "business_hours", "network": "internal"},
)
#6. Coordination with RBAC/ABAC
#6.1 Three-Layer Decision Pipeline
coomia-dip's permission decision follows a three-layer pipeline:
Request Arrives
│
▼
┌─────────────────────────────────┐
│ Layer 1: RBAC Coarse Filtering │
│ - Check user role includes perm │
│ - Fast reject on no role match │
│ Result: ALLOW / DENY / CONTINUE │
└──────────────┬──────────────────┘
│ CONTINUE
▼
┌─────────────────────────────────┐
│ Layer 2: ABAC Attribute Eval │
│ - Evaluate subject/resource/env │
│ - Check data classification │
│ Result: ALLOW / DENY / CONTINUE │
└──────────────┬──────────────────┘
│ CONTINUE
▼
┌─────────────────────────────────┐
│ Layer 3: ReBAC Graph Traversal │
│ - Check direct and indirect rels │
│ - Handle inheritance, negation │
│ Result: ALLOW / DENY │
└─────────────────────────────────┘
#6.2 Decision Merging Strategy
class UnifiedPolicyEngine:
"""Unified policy engine: merges RBAC + ABAC + ReBAC"""
async def authorize(self, request: AuthzRequest) -> AuthzDecision:
# Phase 1: RBAC
rbac_result = await self.rbac_engine.check(
user=request.subject,
permission=request.permission,
)
if rbac_result == Decision.EXPLICIT_DENY:
return AuthzDecision.DENIED
if rbac_result == Decision.EXPLICIT_ALLOW and not request.needs_fine_grained:
return AuthzDecision.ALLOWED
# Phase 2: ABAC
abac_result = await self.abac_engine.evaluate(
subject_attrs=request.subject_attributes,
resource_attrs=request.resource_attributes,
action=request.action,
environment=request.environment,
)
if abac_result == Decision.EXPLICIT_DENY:
return AuthzDecision.DENIED
# Phase 3: ReBAC
rebac_result = await self.rebac_engine.check(
resource=request.resource,
permission=request.permission,
subject=request.subject_ref,
)
# Merge decisions
return self._merge_decisions(rbac_result, abac_result, rebac_result)
def _merge_decisions(self, rbac, abac, rebac) -> AuthzDecision:
"""
Merge strategy:
- Any EXPLICIT_DENY -> DENIED (deny takes precedence)
- ReBAC ALLOW and ABAC no deny -> ALLOWED
- RBAC ALLOW and ABAC ALLOW -> ALLOWED
- Default -> DENIED (default deny)
"""
if any(d == Decision.EXPLICIT_DENY for d in [rbac, abac, rebac]):
return AuthzDecision.DENIED
if rebac.allowed and abac != Decision.EXPLICIT_DENY:
return AuthzDecision.ALLOWED
if rbac == Decision.EXPLICIT_ALLOW and abac == Decision.EXPLICIT_ALLOW:
return AuthzDecision.ALLOWED
return AuthzDecision.DENIED
#7. Performance Optimization
#7.1 Caching Strategy
ReBAC graph traversal is expensive; effective caching is essential:
class ReBACheckCache:
"""Layered caching system"""
def __init__(self):
self.l1_local = LRUCache(max_size=10000) # In-process cache
self.l2_redis = RedisCache(ttl_seconds=300) # Distributed cache
def get(self, key: CheckCacheKey, consistency: Consistency) -> Optional[CheckResult]:
if consistency == Consistency.FULLY_CONSISTENT:
return None # Skip cache
if consistency == Consistency.AT_LEAST_AS_FRESH:
cached = self.l1_local.get(key)
if cached and cached.zookie >= consistency.min_zookie:
return cached.result
cached = self.l2_redis.get(key)
if cached and cached.zookie >= consistency.min_zookie:
self.l1_local.set(key, cached)
return cached.result
return None
# minimize_latency: return cache directly
if result := self.l1_local.get(key):
return result.result
if result := self.l2_redis.get(key):
self.l1_local.set(key, result)
return result.result
return None
def invalidate_for_resource(self, resource: ObjectRef):
"""Invalidate cache when resource relations change"""
pattern = f"check:{resource.type}:{resource.id}:*"
self.l1_local.invalidate_pattern(pattern)
self.l2_redis.invalidate_pattern(pattern)
#7.2 Graph Traversal Depth Limits
# Global configuration
MAX_TRAVERSAL_DEPTH = 15 # Maximum recursion depth
MAX_CONCURRENT_BRANCHES = 50 # Maximum concurrent branches
CHECK_TIMEOUT_MS = 500 # Single Check timeout
class DepthLimiter:
"""Depth limiter to prevent infinite recursion"""
def __init__(self, max_depth: int = MAX_TRAVERSAL_DEPTH):
self.max_depth = max_depth
self._metrics = MetricsCollector()
def check_depth(self, current_depth: int, context: str):
if current_depth > self.max_depth:
self._metrics.increment("rebac.depth_exceeded", tags={"context": context})
raise MaxDepthExceededError(
f"ReBAC traversal exceeded max depth {self.max_depth} at {context}"
)
#7.3 Batch Check Optimization
class BatchCheckOptimizer:
"""Batch permission check optimization"""
async def batch_check(
self,
checks: list[CheckRequest],
) -> list[CheckResult]:
"""
Batch check optimization strategies:
1. Deduplication: same (resource, permission, subject) checked once
2. Prefetching: bulk load related tuples into cache
3. Shared traversal: reuse intermediate traversal results
"""
# Deduplicate
unique_checks = {c.cache_key: c for c in checks}
# Prefetch tuples for all involved resources
resources = {(c.resource.type, c.resource.id) for c in unique_checks.values()}
await self._prefetch_tuples(resources)
# Concurrent execution of deduplicated checks
tasks = {
key: asyncio.create_task(self.engine.check(c.resource, c.permission, c.subject))
for key, c in unique_checks.items()
}
results = {key: await task for key, task in tasks.items()}
# Map back to original request order
return [results[c.cache_key] for c in checks]
async def _prefetch_tuples(self, resources: set[tuple[str, str]]):
"""Batch prefetch tuples into cache"""
tasks = [
self._store.prefetch(resource_type=rt, resource_id=ri)
for rt, ri in resources
]
await asyncio.gather(*tasks)
#8. gRPC API Design
#8.1 Service Definition
service ReBAAuthorizationService {
// Permission check
rpc Check(CheckRequest) returns (CheckResponse);
rpc BatchCheck(BatchCheckRequest) returns (BatchCheckResponse);
// Relationship expansion
rpc Expand(ExpandRequest) returns (ExpandResponse);
// Relationship management
rpc WriteTuples(WriteTuplesRequest) returns (WriteTuplesResponse);
rpc DeleteTuples(DeleteTuplesRequest) returns (DeleteTuplesResponse);
rpc ReadTuples(ReadTuplesRequest) returns (ReadTuplesResponse);
// Change watching
rpc Watch(WatchRequest) returns (stream WatchResponse);
// Reverse queries: what resources can a user access
rpc LookupResources(LookupResourcesRequest) returns (stream LookupResourcesResponse);
rpc LookupSubjects(LookupSubjectsRequest) returns (stream LookupSubjectsResponse);
}
message CheckRequest {
ObjectReference resource = 1;
string permission = 2;
SubjectReference subject = 3;
Consistency consistency = 4;
repeated RelationshipTuple contextual_tuples = 5;
}
message CheckResponse {
bool allowed = 1;
int64 checked_at_zookie = 2;
DebugInfo debug = 3; // Optional debug info
}
#8.2 LookupResources: Reverse Query
async def lookup_resources(
self,
resource_type: str,
permission: str,
subject: SubjectRef,
) -> AsyncIterator[ObjectRef]:
"""
Find which resources of resource_type the subject has permission on.
This is the reverse of Check, used to list accessible resources.
"""
# Strategy 1: Forward traversal from subject
direct_tuples = await self._store.query_by_subject(
subject_type=subject.type,
subject_id=subject.id,
)
seen = set()
for tuple in direct_tuples:
if tuple.resource_type == resource_type:
if await self._relation_implies(
resource_type, tuple.relation, permission
):
if tuple.resource_id not in seen:
seen.add(tuple.resource_id)
yield ObjectRef(resource_type, tuple.resource_id)
# Strategy 2: Consider indirect relations through intermediate objects
for tuple in direct_tuples:
children = await self._store.query_tuples(
subject_type=tuple.resource_type,
subject_id=tuple.resource_id,
)
for child in children:
if child.resource_type == resource_type and child.resource_id not in seen:
result = await self.check(
ObjectRef(child.resource_type, child.resource_id),
permission,
subject,
)
if result.allowed:
seen.add(child.resource_id)
yield ObjectRef(child.resource_type, child.resource_id)
#9. Testing and Debugging
#9.1 Relationship Graph Visualization
class ReBAGraphVisualizer:
"""Relationship graph visualization tool for debugging permission inheritance"""
async def visualize_permissions(
self,
resource: ObjectRef,
max_depth: int = 5,
) -> str:
"""Generate Mermaid-format relationship graph"""
lines = ["graph TD"]
visited = set()
async def traverse(obj: ObjectRef, depth: int):
if depth > max_depth or obj in visited:
return
visited.add(obj)
node_id = f"{obj.type}_{obj.id}"
tuples = await self._store.query_tuples(
resource_type=obj.type,
resource_id=obj.id,
)
for t in tuples:
subject_id = f"{t.subject_type}_{t.subject_id}"
label = t.relation
if t.subject_relation:
label += f" (via #{t.subject_relation})"
lines.append(f" {subject_id} -->|{label}| {node_id}")
if t.subject_type != "user":
await traverse(
ObjectRef(t.subject_type, t.subject_id),
depth + 1,
)
await traverse(resource, 0)
return "\n".join(lines)
#9.2 Permission Explainer
class PermissionExplainer:
"""Permission decision explainer"""
async def explain(
self,
resource: ObjectRef,
permission: str,
subject: SubjectRef,
) -> ExplanationTree:
"""
Explain why subject does/does not have permission on resource.
Returns a complete decision tree with evaluation results at each step.
"""
root = ExplanationNode(
description=f"Check: {subject} -> {permission} -> {resource}",
)
result = await self._check_with_explanation(
resource, permission, subject, root
)
root.result = result
return ExplanationTree(root=root)
#10. Production Deployment Considerations
#10.1 Multi-Region Deployment
┌─────────────────────────────────────────────┐
│ Region A (Primary) │
│ ┌────────────┐ ┌────────────────────┐ │
│ │ ReBAC API │────>│ PostgreSQL Primary │ │
│ │ (gRPC) │ │ (Write + Read) │ │
│ └────────────┘ └────────────────────┘ │
│ │ │ │
│ │ WAL Replication │
│ │ │ │
└────────│─────────────────────│───────────────┘
│ │
│ ┌──────▼───────────────┐
│ │ Region B (Replica) │
│ │ ┌────────────────┐ │
│ │ │ PostgreSQL │ │
│ │ │ (Read Replica) │ │
│ │ └────────────────┘ │
│ │ ┌────────────────┐ │
│ │ │ ReBAC API │ │
│ │ │ (Read-only) │ │
│ │ └────────────────┘ │
│ └──────────────────────┘
#10.2 Monitoring Metrics
REBAC_METRICS = {
"rebac_check_total": Counter("Total check count"),
"rebac_check_latency": Histogram("Check latency", buckets=[1, 5, 10, 50, 100, 500]),
"rebac_check_allowed": Counter("Allowed count"),
"rebac_check_denied": Counter("Denied count"),
"rebac_traversal_depth": Histogram("Traversal depth", buckets=[1, 2, 3, 5, 8, 15]),
"rebac_cache_hit_rate": Gauge("Cache hit rate"),
"rebac_tuple_count": Gauge("Total tuple count"),
"rebac_expand_latency": Histogram("Expand latency"),
"rebac_batch_size": Histogram("Batch check size"),
}
#Key Takeaways
- ReBAC models permissions as object relationship graphs, naturally aligning with ontology-driven platform architecture
- Three derivation modes (direct, computed, tuple-to-userset) cover all permission inheritance scenarios
- Zanzibar-style Zookie mechanism solves consistency challenges in distributed environments
- Parallel evaluation + short-circuit optimization + layered caching ensures low-latency Check API
- Three-layer pipeline with RBAC/ABAC provides coarse-to-fine authorization coverage
- LookupResources reverse query efficiently answers "what can this user see"
- Expand API and permission explainer provide full observability for auditing and debugging
#Next Article
The next article S6-05 Policy as Query Rewrite dives into transforming permission policies into database query conditions, enabling row-level and column-level data access control without application-layer filtering.
#rebac #zanzibar #relationship-based-access-control #authorization #graph-traversal #coomia-dip #platform-engineering