The Problem Statement
Our RBAC model from Post 2 works great for global roles like "admin" or "editor." But your product manager returns with increasingly complex requirements that break the global role paradigm:
"Document owners should automatically be editors. Editors should automatically be viewers. And:
Restricted documents that only employees can view (not contractors)
Public documents that guests OR employees can view
Confidential documents that employees can view EXCEPT those in the 'restricted' group"
Let's see what this means with concrete examples:
// Alice owns document 1
RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "1", Relation: "owner"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "alice"}},
}
// Bob is both an employee and has editor access to document 1
RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "1", Relation: "employee"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "bob"}},
}
RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "1", Relation: "editor"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "bob"}},
}The human rules we need to express:
"Owners are editors" (Alice should be able to edit document 1)
"Editors are viewers" (Both Alice and Bob should be able to view document 1)
"Restricted viewers are employees AND editors" (must be both)
"Public viewers are employees OR guests" (either is sufficient)
"External viewers are all viewers EXCEPT employees" (set subtraction)
But with our current model, we can only check direct tuples. We need boolean algebra for relationships—the ability to combine, intersect, and subtract sets of users based on their relationships to the same object.
Why Global Roles Don't Work Here
Let's see what goes wrong if we try to solve this with global roles:
Approach 1: Global Role Explosion
// Try to model complex document permissions as global roles
RelationTuple{Resource: role:document-1-owner#member, Subject: user:alice}
RelationTuple{Resource: role:document-1-employee-editor#member, Subject: user:bob} // employee AND editor
RelationTuple{Resource: role:document-1-public-viewer#member, Subject: user:charlie} // employee OR guest
RelationTuple{Resource: role:document-1-external-viewer#member, Subject: user:dave} // viewer EXCEPT employeeProblems:
Combinatorial explosion: Need roles for every document × every boolean combination
Redundancy: Must manually create all implied relationships and intersections
Maintenance nightmare: When relationships change, must update dozens of role memberships
Can't express boolean logic: No way to say "employees AND editors" or "viewers EXCEPT employees"
Approach 2: Application-Layer Boolean Logic
func Check(user, resource string) bool {
// Check direct permission
if tupleStore.Contains(user, resource) {
return true
}
// Application logic for boolean combinations - gets complex fast!
if strings.HasSuffix(resource, "#restricted_viewer") {
// Must be BOTH employee AND editor
isEmployee := tupleStore.Contains(user, strings.Replace(resource, "#restricted_viewer", "#employee", 1))
isEditor := tupleStore.Contains(user, strings.Replace(resource, "#restricted_viewer", "#editor", 1))
return isEmployee && isEditor
}
if strings.HasSuffix(resource, "#public_viewer") {
// Must be employee OR guest
isEmployee := tupleStore.Contains(user, strings.Replace(resource, "#public_viewer", "#employee", 1))
isGuest := tupleStore.Contains(user, strings.Replace(resource, "#public_viewer", "#guest", 1))
return isEmployee || isGuest
}
if strings.HasSuffix(resource, "#external_viewer") {
// Must be viewer but NOT employee
isViewer := tupleStore.Contains(user, strings.Replace(resource, "#external_viewer", "#viewer", 1))
isEmployee := tupleStore.Contains(user, strings.Replace(resource, "#external_viewer", "#employee", 1))
return isViewer && !isEmployee
}
return false
}Problems:
Scattered logic: Boolean combinations live outside the tuple store
Hardcoded combinations: Every new boolean rule requires code changes
No schema: The relationship rules are hardcoded in application logic
Performance: Multiple tuple store queries for each boolean operation
The Core Insight
We need to model computed relationships with boolean algebra where relations are defined in terms of boolean combinations of other relations on the same object.
Scope Note: In this post, "employee/contractor/guest" are relations on the same object (usersets). No cross-object traversal; OP_EDGE arrives in Post 4. No ABAC predicates yet.
Instead of storing every possible permission explicitly, we want to define rules that compute permissions from existing relationships using set operations:
"The editors of document X are: all users who have direct editor relation on document X, UNION all users who have owner relation on document X."
"The restricted viewers of document X are: all users who have employee relation on document X, INTERSECT all users who have viewer relation on document X."
"The external viewers of document X are: all users who have viewer relation on document X, EXCEPT all users who have employee relation on document X."
This is our step into ReBAC with boolean algebra—where permissions are computed from boolean combinations of relationships, giving us the full expressive power of set theory.
Extending the Model: Boolean Algebra for Relations
The solution is to introduce computed relations with boolean operations that can combine other relations on the same object using set theory operations: union (∪), intersection (∩), and exclusion (−).
Step 1: Schema-Defined Relationships
Instead of hardcoding inheritance in application logic, we define it in a schema using the existing domain model:
type NamespaceDefinition struct {
Name string
Relations map[string]*NamespaceRelationDefinition
}
type NamespaceRelationDefinition struct {
Name string
SubjectsComputationExpression SubjectsComputationExpression
}
// Operations for computing subjects from same-object relations
// (Ordering is unspecified; treat as symbolic kinds, not wire values)
type SubjectsComputationOp int32
const (
OP_UNION SubjectsComputationOp = iota
OP_INTERSECTION
OP_EXCLUSION
OP_COMPUTED_SUBJECTS // Same-object inheritance
OP_DIRECT // Direct tuples only
)
// Note: numeric order is for readability only; actual storage should use symbolic names.
// The key interface for defining how subjects are computed
type SubjectsComputationExpression interface {
isSubjectsComputationExpression()
Operation() SubjectsComputationOp
Validate() error
String() string
}Step 2: All SubjectsComputationExpression Types
The domain model provides five key expression types for boolean algebra:
// Direct relations: only explicit tuples
type DirectExpression struct {}
func (e *DirectExpression) Operation() SubjectsComputationOp { return OP_DIRECT }
// Computed relations: include subjects from another relation (same-object inheritance)
type ComputedSubjectsExpression struct {
ComputedRelation string // "owner" (on the same object)
}
func (e *ComputedSubjectsExpression) Operation() SubjectsComputationOp { return OP_COMPUTED_SUBJECTS }
// Union: combine multiple sets (OR logic)
type UnionExpression struct {
Children []SubjectsComputationExpression
}
func (e *UnionExpression) Operation() SubjectsComputationOp { return OP_UNION }
// Intersection: users in ALL sets (AND logic)
type IntersectionExpression struct {
Children []SubjectsComputationExpression
}
func (e *IntersectionExpression) Operation() SubjectsComputationOp { return OP_INTERSECTION }
// Exclusion: subtract sets (A - B logic)
type ExclusionExpression struct {
Left SubjectsComputationExpression
Right SubjectsComputationExpression
}
func (e *ExclusionExpression) Operation() SubjectsComputationOp { return OP_EXCLUSION }Boolean Algebra Semantics (Same-Object)
DIRECT— direct tuples of the current relationCOMPUTED_SUBJECTS("Q")— include subjects of relationQon the same objectUNION(A, …)— set OR (n-ary; associative, commutative, idempotent)INTERSECTION(A, …)— set AND (n-ary; associative, commutative, idempotent)EXCLUSION(A, B)— set difference (left-biased, not commutative)
Algebraic Laws:
Law | Formula | Property |
|---|---|---|
Idempotence (Union) |
| Adding the same set twice yields the same set |
Idempotence (Intersection) |
| Intersecting a set with itself yields the same set |
Exclusion Identity |
| Subtracting a set from itself yields empty set |
Commutativity (Union) |
| Order doesn't matter for union |
Commutativity (Intersection) |
| Order doesn't matter for intersection |
Non-Commutativity (Exclusion) |
| Exclusion is left-biased; order matters |
Step 3: Complete Document Schema Example
// Schema for document permissions using boolean algebra
documentNamespace := &NamespaceDefinition{
Name: "document",
Relations: map[string]*NamespaceRelationDefinition{
// Base relations (direct tuples only)
"owner": {
Name: "owner",
SubjectsComputationExpression: &DirectExpression{},
},
"employee": {
Name: "employee",
SubjectsComputationExpression: &DirectExpression{},
},
"guest": {
Name: "guest",
SubjectsComputationExpression: &DirectExpression{},
},
// Computed relations using inheritance
"editor": {
Name: "editor",
SubjectsComputationExpression: &ComputedSubjectsExpression{
ComputedRelation: "owner", // owners are automatically editors
},
},
"viewer": {
Name: "viewer",
SubjectsComputationExpression: &ComputedSubjectsExpression{
ComputedRelation: "editor", // editors are automatically viewers
},
},
// Boolean combinations (note: employee/guest are relations on the same document object)
"public_viewer": {
Name: "public_viewer",
SubjectsComputationExpression: &UnionExpression{
Children: []SubjectsComputationExpression{
&ComputedSubjectsExpression{ComputedRelation: "employee"},
&ComputedSubjectsExpression{ComputedRelation: "guest"},
},
},
// public_viewer = employee ∪ guest (direct grants included automatically)
},
"restricted_viewer": {
Name: "restricted_viewer",
SubjectsComputationExpression: &IntersectionExpression{
Children: []SubjectsComputationExpression{
&ComputedSubjectsExpression{ComputedRelation: "employee"},
&ComputedSubjectsExpression{ComputedRelation: "viewer"},
},
},
// restricted_viewer = employee ∩ viewer (must be both)
},
"external_viewer": {
Name: "external_viewer",
SubjectsComputationExpression: &ExclusionExpression{
Left: &ComputedSubjectsExpression{ComputedRelation: "viewer"},
Right: &ComputedSubjectsExpression{ComputedRelation: "employee"},
},
// external_viewer = viewer - employee (viewers who aren't employees)
},
},
}
This schema demonstrates all five operation types:
document:X#owner: Direct tuples only (OP_DIRECT)document:X#employee: Direct tuples only (OP_DIRECT)document:X#guest: Direct tuples only (OP_DIRECT)document:X#editor: Direct + owners (OP_COMPUTED_SUBJECTS)document:X#viewer: Direct + editors (which includes owners) (OP_COMPUTED_SUBJECTS)document:X#public_viewer: Direct + employees + guests (OP_UNION)document:X#restricted_viewer: Employees who are also viewers (OP_INTERSECTION)document:X#external_viewer: Viewers who are not employees (OP_EXCLUSION)
Note on DirectExpression: Base relations like owner, employee, guest use DirectExpression; computed and boolean relations omit it because DIRECT(R) is always included by the relation-level rule (see Step 4 for uniform semantics).
Step 4: Relation-Level Semantics
All relations follow a uniform evaluation rule:
FinalSubjects(R) = DIRECT(R) ∪ Eval(Expression(R))
Every relation automatically includes its direct tuples, then adds subjects computed by its expression.
This means:
editor = COMPUTED(owner)actually evaluates asDIRECT(editor) ∪ COMPUTED(owner)public_viewer = UNION(employee, guest)actually evaluates asDIRECT(public_viewer) ∪ UNION(employee, guest)Relations with
DirectExpressionjust getDIRECT(R) ∪ ∅ = DIRECT(R)
Important: DirectExpression contributes direct tuples only when it's the top-level expression of a relation; when nested inside other operators (like UNION, INTERSECTION, or EXCLUSION), it evaluates to ∅. This is why composite expressions should reference other relations using ComputedSubjectsExpression rather than embedding DirectExpression.
(See Step 4 for the complete uniform semantics rule.)
Schema Well-Formedness Rules
Valid schemas must satisfy these compile-time constraints:
Closure: Every referenced relation must exist in the same namespace
Acyclic: The relation-reference graph formed by all expressions must be cycle-free
0-ary ops: Empty
UNION/INTERSECTIONare disallowed in the schema
Now authorization becomes boolean evaluation that follows the schema definitions:
ALGORITHM: Check(user, resource) - Core Logic
1. Parse inputs:
- user = "user:alice"
- resource = "document:1#restricted_viewer"
2. Look up relation definition:
- Find schema for "document" namespace
- Get definition for "restricted_viewer" relation
- If not found → DENY
3. Evaluate relation using uniform semantics:
FinalSubjects(R) = DIRECT(R) ∪ Eval(Expression(R))
a) Check direct grants: (user:alice, document:1#restricted_viewer)
If found → ALLOW
b) Evaluate expression based on type:
IF expression is DirectExpression:
- No additional subjects (expression evaluates to ∅)
IF expression is ComputedSubjectsExpression:
- Recursively check: Check(user:alice, document:1#{ComputedRelation})
IF expression is UnionExpression:
- Evaluate each child expression
- Return TRUE if ANY child returns TRUE
IF expression is IntersectionExpression:
- Evaluate each child expression
- Return TRUE if ALL children return TRUE
IF expression is ExclusionExpression:
- Evaluate Left and Right expressions
- Return TRUE if Left=TRUE AND Right=FALSEThe key insight: Instead of storing every possible permission combination, we compute permissions using boolean algebra. Complex access patterns like "employees who are viewers but not contractors" become simple schema definitions rather than application logic.
Minimal Operation Examples
Core examples showing each operation:
editor = COMPUTED(owner)— same-object inheritancerestricted_viewer = INTERSECTION(employee, viewer)— must be bothexternal_viewer = EXCLUSION(viewer, employee)— viewers except employeepublic_viewer = UNION(employee, guest)— either employee or guest
Implementation Notes: Safety and Depth Control
Why We Need Safety Measures
The recursive nature of computed relations introduces potential problems that we must handle:
Problem 1: Infinite Recursion from Cycles
// Bad schema: editor → viewer → editor
"editor": &ComputedSubjectsExpression{ComputedRelation: "viewer"}
"viewer": &ComputedSubjectsExpression{ComputedRelation: "editor"}
What happens: Check(alice, document:1#editor) → Check(alice, document:1#viewer) → Check(alice, document:1#editor) → infinite loop!
Why it's dangerous: Stack overflow crashes the authorization service.
Problem 2: Deep Inheritance Chains
// Very deep chain: owner → admin → manager → editor → contributor → viewerWhat happens: A single authorization check might traverse 6+ levels deep, making hundreds of database queries.
Why it's dangerous: Performance degradation and potential DoS attacks.
Problem 3: Malicious Schema Design
// Intentionally complex schema designed to consume resources
"relation1": &ComputedSubjectsExpression{ComputedRelation: "relation2"}
"relation2": &ComputedSubjectsExpression{ComputedRelation: "relation3"}
// ... 50 more levelsWhat happens: Authorization becomes extremely expensive to compute.
Why it's dangerous: Resource exhaustion attacks.
Optional Implementation Note — Safety in Production Evaluators
ALGORITHM: Check(user, resource) - Production Ready
1. Initialize safety tracking:
- visited = empty set
- depth = 0
- maxDepth = 50 (configurable, see below)
2. Parse inputs and validate
3. Check safety limits:
- IF (user, resource) in visited → DENY (cycle detected)
- IF depth > maxDepth → DENY (too deep)
- Add (user, resource) to visited
- Increment depth
4. Evaluate based on expression type:
[Same core logic as above]
5. Cleanup:
- Remove (user, resource) from visited
- Decrement depth
- Return result
Choosing the Right maxDepth Value
Based on production authorization systems worldwide:
System | Max Depth | Rationale |
|---|---|---|
SpiceDB (Google Zanzibar implementation) | 50 | Default limit for nested authorization calls |
Google Cloud Firestore Security Rules | 0 | No recursive calls permitted (different use case) |
AWS IAM | ~20-30 | Estimated based on policy evaluation complexity |
Most Enterprise Systems | 10-25 | Balance between flexibility and safety |
Recommended Value: 50
This matches SpiceDB's default and provides enough depth for complex real-world scenarios:
Simple RBAC: 2-3 levels (user → role → permission)
Hierarchical Organizations: 5-10 levels (user → team → department → division → company)
Complex Document Systems: 10-20 levels (user → group → folder → subfolder → document)
Pathological Cases: Caught before causing system damage
The production reality: Authorization systems must be safe by default. The safety measures aren't optional—they're essential for preventing system failures and attacks.
Testing the Extended Model
We'll test all five operations with real-world scenarios that demonstrate how the model solves actual permission problems.
Scenario 1: OP_DIRECT — Base Relations (Direct Tuples Only)
Real-world use case: "Only Explicitly Granted Users Can Access the HR Database"
Setup:
// Schema: HR database with only direct access (no inheritance)
hrNamespace := &NamespaceDefinition{
Name: "hr_database",
Relations: map[string]*NamespaceRelationDefinition{
"admin": {
Name: "admin",
SubjectsComputationExpression: &DirectExpression{},
},
},
}
// Only Alice and Bob are explicitly granted admin access
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "hr_database", ObjectID: "prod", Relation: "admin"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "alice"}},
})
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "hr_database", ObjectID: "prod", Relation: "admin"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "bob"}},
})Checks:
Check("user:alice", "hr_database:prod#admin")→ TRUE (explicitly granted)Check("user:bob", "hr_database:prod#admin")→ TRUE (explicitly granted)Check("user:charlie", "hr_database:prod#admin")→ FALSE (not explicitly granted)
Some resources require explicit approval with no automatic inheritance. OP_DIRECT ensures strict access control.
Scenario 2: OP_COMPUTED_SUBJECTS — Same-Object Inheritance
Real-world use case: "Document Owners Should Automatically Be Editors, and Editors Should Automatically Be Viewers"
Setup:
// Schema: Document permissions with inheritance chain
documentNamespace := &NamespaceDefinition{
Name: "document",
Relations: map[string]*NamespaceRelationDefinition{
"owner": {
Name: "owner",
SubjectsComputationExpression: &DirectExpression{},
},
"editor": {
Name: "editor",
SubjectsComputationExpression: &ComputedSubjectsExpression{
ComputedRelation: "owner", // editors include owners
},
},
"viewer": {
Name: "viewer",
SubjectsComputationExpression: &ComputedSubjectsExpression{
ComputedRelation: "editor", // viewers include editors (and transitively, owners)
},
},
},
}
// Alice owns document 1
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "1", Relation: "owner"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "alice"}},
})
// Bob is an explicit editor
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "1", Relation: "editor"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "bob"}},
})
// Charlie is an explicit viewer
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "1", Relation: "viewer"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "charlie"}},
})Checks:
Check("user:alice", "document:1#editor")→ TRUE (owner → editor)Check("user:alice", "document:1#viewer")→ TRUE (owner → editor → viewer)Check("user:bob", "document:1#viewer")→ TRUE (editor → viewer)Check("user:charlie", "document:1#editor")→ FALSE (viewer ≠ editor)
Inheritance eliminates redundant tuples. Alice doesn't need separate editor and viewer tuples; they're computed automatically.
Scenario 3: OP_UNION — Combining Sets (OR Logic)
Real-world use case: "Public Documents Can Be Viewed by Employees OR Guests (Either Is Sufficient)"
Setup:
// Schema: Public document with union-based access
publicDocNamespace := &NamespaceDefinition{
Name: "document",
Relations: map[string]*NamespaceRelationDefinition{
"employee": {
Name: "employee",
SubjectsComputationExpression: &DirectExpression{},
},
"guest": {
Name: "guest",
SubjectsComputationExpression: &DirectExpression{},
},
"public_viewer": {
Name: "public_viewer",
SubjectsComputationExpression: &UnionExpression{
Children: []SubjectsComputationExpression{
&ComputedSubjectsExpression{ComputedRelation: "employee"},
&ComputedSubjectsExpression{ComputedRelation: "guest"},
},
},
// public_viewer = employee ∪ guest
},
},
}
// Alice is an employee
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "public-doc", Relation: "employee"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "alice"}},
})
// Bob is a guest
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "public-doc", Relation: "guest"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "bob"}},
})
// Charlie is neither
Checks:
Check("user:alice", "document:public-doc#public_viewer")→ TRUE (employee ✓)Check("user:bob", "document:public-doc#public_viewer")→ TRUE (guest ✓)Check("user:charlie", "document:public-doc#public_viewer")→ FALSE (neither employee nor guest)
Union eliminates the need to store redundant tuples. Instead of adding Alice to both "employee" and "public_viewer", we compute public_viewer as the union of employees and guests.
Scenario 4: OP_INTERSECTION — Requiring Multiple Conditions (AND Logic)
Real-world use case: "HR Documents Can Only Be Viewed by Employees Who Have Explicit HR Access (Must Be BOTH)"
Setup:
// Schema: HR document with intersection-based access
hrDocNamespace := &NamespaceDefinition{
Name: "document",
Relations: map[string]*NamespaceRelationDefinition{
"employee": {
Name: "employee",
SubjectsComputationExpression: &DirectExpression{},
},
"hr_approved": {
Name: "hr_approved",
SubjectsComputationExpression: &DirectExpression{},
},
"hr_viewer": {
Name: "hr_viewer",
SubjectsComputationExpression: &IntersectionExpression{
Children: []SubjectsComputationExpression{
&ComputedSubjectsExpression{ComputedRelation: "employee"},
&ComputedSubjectsExpression{ComputedRelation: "hr_approved"},
},
},
// hr_viewer = employee ∩ hr_approved (must be both)
},
},
}
// Alice is an employee AND has HR approval
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "hr-doc", Relation: "employee"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "alice"}},
})
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "hr-doc", Relation: "hr_approved"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "alice"}},
})
// Bob is an employee but NOT HR approved
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "hr-doc", Relation: "employee"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "bob"}},
})
// Charlie is HR approved but NOT an employee
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "hr-doc", Relation: "hr_approved"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "charlie"}},
})Checks:
Check("user:alice", "document:hr-doc#hr_viewer")→ TRUE (employee ✓ AND hr_approved ✓)Check("user:bob", "document:hr-doc#hr_viewer")→ FALSE (employee ✓ but hr_approved ✗)Check("user:charlie", "document:hr-doc#hr_viewer")→ FALSE (hr_approved ✓ but employee ✗)
Intersection enforces multi-factor requirements. Alice must satisfy BOTH conditions; neither alone is sufficient.
Scenario 5: OP_EXCLUSION — Subtracting Sets (NOT Logic)
Real-world use case: "Confidential Documents Can Be Viewed by Employees EXCEPT Contractors"
Setup:
// Schema: Confidential document with exclusion-based access
confDocNamespace := &NamespaceDefinition{
Name: "document",
Relations: map[string]*NamespaceRelationDefinition{
"employee": {
Name: "employee",
SubjectsComputationExpression: &DirectExpression{},
},
"contractor": {
Name: "contractor",
SubjectsComputationExpression: &DirectExpression{},
},
"confidential_viewer": {
Name: "confidential_viewer",
SubjectsComputationExpression: &ExclusionExpression{
Left: &ComputedSubjectsExpression{ComputedRelation: "employee"},
Right: &ComputedSubjectsExpression{ComputedRelation: "contractor"},
},
// confidential_viewer = employee - contractor (employees who aren't contractors)
},
},
}
// Alice is an employee but NOT a contractor
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "conf-doc", Relation: "employee"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "alice"}},
})
// Bob is BOTH an employee AND a contractor
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "conf-doc", Relation: "employee"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "bob"}},
})
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "conf-doc", Relation: "contractor"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "bob"}},
})
// Charlie is a contractor but NOT an employee
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "conf-doc", Relation: "contractor"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "charlie"}},
})Checks:
Check("user:alice", "document:conf-doc#confidential_viewer")→ TRUE (employee ✓ and NOT contractor ✓)Check("user:bob", "document:conf-doc#confidential_viewer")→ FALSE (employee ✓ but IS contractor ✗)Check("user:charlie", "document:conf-doc#confidential_viewer")→ FALSE (NOT employee ✗)
Exclusion handles negative permissions. Bob is excluded even though he's an employee, because he's also a contractor.
Scenario 6: Complex Real-World Example — All Operations Combined
Real-world use case: "A SaaS Platform with Complex Document Access Policies"
Business rules:
Owners automatically become editors
Editors automatically become viewers
Public documents can be viewed by employees OR guests
HR documents require being an employee AND having explicit HR approval
Confidential documents can be viewed by employees EXCEPT contractors
Setup:
// Complete schema using all five operations
saasNamespace := &NamespaceDefinition{
Name: "document",
Relations: map[string]*NamespaceRelationDefinition{
// Base relations (OP_DIRECT)
"owner": {
Name: "owner",
SubjectsComputationExpression: &DirectExpression{},
},
"employee": {
Name: "employee",
SubjectsComputationExpression: &DirectExpression{},
},
"contractor": {
Name: "contractor",
SubjectsComputationExpression: &DirectExpression{},
},
"guest": {
Name: "guest",
SubjectsComputationExpression: &DirectExpression{},
},
"hr_approved": {
Name: "hr_approved",
SubjectsComputationExpression: &DirectExpression{},
},
// Inheritance chain (OP_COMPUTED_SUBJECTS)
"editor": {
Name: "editor",
SubjectsComputationExpression: &ComputedSubjectsExpression{
ComputedRelation: "owner",
},
},
"viewer": {
Name: "viewer",
SubjectsComputationExpression: &ComputedSubjectsExpression{
ComputedRelation: "editor",
},
},
// Public access (OP_UNION)
"public_viewer": {
Name: "public_viewer",
SubjectsComputationExpression: &UnionExpression{
Children: []SubjectsComputationExpression{
&ComputedSubjectsExpression{ComputedRelation: "employee"},
&ComputedSubjectsExpression{ComputedRelation: "guest"},
},
},
},
// HR access (OP_INTERSECTION)
"hr_viewer": {
Name: "hr_viewer",
SubjectsComputationExpression: &IntersectionExpression{
Children: []SubjectsComputationExpression{
&ComputedSubjectsExpression{ComputedRelation: "employee"},
&ComputedSubjectsExpression{ComputedRelation: "hr_approved"},
},
},
},
// Confidential access (OP_EXCLUSION)
"confidential_viewer": {
Name: "confidential_viewer",
SubjectsComputationExpression: &ExclusionExpression{
Left: &ComputedSubjectsExpression{ComputedRelation: "employee"},
Right: &ComputedSubjectsExpression{ComputedRelation: "contractor"},
},
},
},
}
// Setup: Alice (owner), Bob (employee), Charlie (contractor), Dave (guest)
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "doc-1", Relation: "owner"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "alice"}},
})
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "doc-1", Relation: "employee"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "bob"}},
})
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "doc-1", Relation: "contractor"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "charlie"}},
})
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "doc-1", Relation: "guest"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "dave"}},
})
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "doc-1", Relation: "hr_approved"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "bob"}},
})Comprehensive checks:
User | Relation | Result | Reason |
|---|---|---|---|
Alice |
| ✅ TRUE | owner → editor (OP_COMPUTED_SUBJECTS) |
Alice |
| ✅ TRUE | owner → editor → viewer (chain) |
Bob |
| ❌ FALSE | viewer derives from editor; employee alone doesn't imply viewer |
Bob |
| ✅ TRUE | employee ∪ guest (OP_UNION) |
Bob |
| ✅ TRUE | employee ∩ hr_approved (OP_INTERSECTION) |
Bob |
| ✅ TRUE | employee - contractor (OP_EXCLUSION) |
Charlie |
| ❌ FALSE | NOT employee, NOT guest |
Charlie |
| ❌ FALSE | NOT employee (excluded by left side) |
Dave |
| ✅ TRUE | guest ∪ employee (OP_UNION) |
Dave |
| ❌ FALSE | NOT employee (fails OP_INTERSECTION) |
What this demonstrates:
✅ OP_DIRECT: Base relations (owner, employee, contractor, guest, hr_approved) store explicit tuples
✅ OP_COMPUTED_SUBJECTS: Inheritance chain (owner → editor → viewer) eliminates redundant tuples
✅ OP_UNION: Public access combines employees and guests
✅ OP_INTERSECTION: HR access requires both employee AND hr_approved
✅ OP_EXCLUSION: Confidential access excludes contractors from employees
The power of this model: All five business rules are expressed declaratively in the schema. No application logic needed. Authorization becomes pure boolean evaluation.
Edge Case 1: Missing Relation Definition
Setup:
// Try to check a relation not defined in schema
Check("user:alice", "document:1#admin")Behavior:
schema.GetRelation("document", "admin")returnsnilResult: FALSE (unknown relations are denied)
This prevents access through undefined or mistyped relations.
Edge Case 2: Circular Relation Dependencies
Setup:
// Bad schema: editor includes viewer, viewer includes editor
badNamespace := &NamespaceDefinition{
Name: "document",
Relations: map[string]*NamespaceRelationDefinition{
"editor": {
Name: "editor",
SubjectsComputationExpression: &ComputedSubjectsExpression{
ComputedRelation: "viewer",
},
},
"viewer": {
Name: "viewer",
SubjectsComputationExpression: &ComputedSubjectsExpression{
ComputedRelation: "editor",
},
},
},
}
Behavior:
The
EvaluationState.IsVisited()prevents infinite recursion usingVisitKeyFirst cycle detection returns FALSE
Result: Cycles are safely handled without stack overflow
Note: We validate closure + acyclicity at schema load. The schema compilation process would actually detect this cycle during compilation and mark it in RelationMetadata.IsCyclic, preventing deployment of invalid schemas.
Edge Case 3: Deep Inheritance Chains
Setup:
// Chain: owner → admin → editor → viewer
deepNamespace := &NamespaceDefinition{
Name: "document",
Relations: map[string]*NamespaceRelationDefinition{
"owner": {
Name: "owner",
SubjectsComputationExpression: &DirectExpression{},
},
"admin": {
Name: "admin",
SubjectsComputationExpression: &ComputedSubjectsExpression{
ComputedRelation: "owner",
},
},
"editor": {
Name: "editor",
SubjectsComputationExpression: &ComputedSubjectsExpression{
ComputedRelation: "admin",
},
},
"viewer": {
Name: "viewer",
SubjectsComputationExpression: &ComputedSubjectsExpression{
ComputedRelation: "editor",
},
},
},
}
// Alice owns document 1
store.Add(RelationTuple{
Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "1", Relation: "owner"},
Subject: &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "alice"}},
})
Check: "Can Alice view document 1?"
Traversal:
viewer→editor→admin→owner→ Found ✅Result: TRUE (deep chains work correctly)
Important: The EvaluationState tracks depth and node limits to prevent runaway evaluation. The RelationMetadata.MaxDepth field would show this chain has depth 4.
Real-World Context
This pattern with boolean algebra appears in every system with complex object-specific permissions:
GitHub Repository Permissions (with Teams)
(Abbreviated schema — identical patterns as above.)
// Repository schema with boolean operations for team-based access
repoNamespace := &NamespaceDefinition{
Name: "repo",
Relations: map[string]*NamespaceRelationDefinition{
// Base relations
"owner": {
Name: "owner",
SubjectsComputationExpression: &DirectExpression{},
},
"team_member": {
Name: "team_member",
SubjectsComputationExpression: &DirectExpression{},
},
"external_collaborator": {
Name: "external_collaborator",
SubjectsComputationExpression: &DirectExpression{},
},
// Computed relations with inheritance
"admin": {
Name: "admin",
SubjectsComputationExpression: &ComputedSubjectsExpression{
ComputedRelation: "owner", // owners are admins
},
},
// Boolean combinations
"write": {
Name: "write",
SubjectsComputationExpression: &UnionExpression{
Children: []SubjectsComputationExpression{
&ComputedSubjectsExpression{ComputedRelation: "admin"},
&ComputedSubjectsExpression{ComputedRelation: "team_member"},
},
},
// write = admin ∪ team_member
},
"read": {
Name: "read",
SubjectsComputationExpression: &UnionExpression{
Children: []SubjectsComputationExpression{
&ComputedSubjectsExpression{ComputedRelation: "write"},
&ComputedSubjectsExpression{ComputedRelation: "external_collaborator"},
},
},
// read = write ∪ external_collaborator
},
"internal_read": {
Name: "internal_read",
SubjectsComputationExpression: &ExclusionExpression{
Left: &ComputedSubjectsExpression{ComputedRelation: "read"},
Right: &ComputedSubjectsExpression{ComputedRelation: "external_collaborator"},
},
// internal_read = read - external_collaborator (only internal users)
},
},
}Result: Complex team-based permissions with inheritance and exclusions, all defined declaratively in the schema.
Enterprise Document Management
(Abbreviated schema — identical patterns as above.)
// Enterprise document schema with complex access controls
enterpriseNamespace := &NamespaceDefinition{
Name: "document",
Relations: map[string]*NamespaceRelationDefinition{
// Base relations
"owner": {
Name: "owner",
SubjectsComputationExpression: &DirectExpression{},
},
"employee": {
Name: "employee",
SubjectsComputationExpression: &DirectExpression{},
},
"contractor": {
Name: "contractor",
SubjectsComputationExpression: &DirectExpression{},
},
"guest": {
Name: "guest",
SubjectsComputationExpression: &DirectExpression{},
},
"hr_approved": {
Name: "hr_approved",
SubjectsComputationExpression: &DirectExpression{},
},
// Complex boolean combinations for enterprise policies
"hr_viewer": {
Name: "hr_viewer",
SubjectsComputationExpression: &IntersectionExpression{
Children: []SubjectsComputationExpression{
&ComputedSubjectsExpression{ComputedRelation: "employee"},
&ComputedSubjectsExpression{ComputedRelation: "hr_approved"},
},
},
// hr_viewer = employee ∩ hr_approved (employees with explicit HR access)
},
"public_viewer": {
Name: "public_viewer",
SubjectsComputationExpression: &UnionExpression{
Children: []SubjectsComputationExpression{
&ComputedSubjectsExpression{ComputedRelation: "employee"},
&ComputedSubjectsExpression{ComputedRelation: "guest"},
},
},
// public_viewer = employee ∪ guest (anyone internal or invited)
},
"confidential_viewer": {
Name: "confidential_viewer",
SubjectsComputationExpression: &ExclusionExpression{
Left: &ComputedSubjectsExpression{ComputedRelation: "employee"},
Right: &ComputedSubjectsExpression{ComputedRelation: "contractor"},
},
// confidential_viewer = employee - contractor (employees who aren't contractors)
},
},
}Result: Complex enterprise access policies expressed declaratively: HR documents for employees with explicit access, public documents for employees or guests, confidential documents for employees but not contractors.
Healthcare System Access
(Abbreviated schema — identical patterns as above.)
// Medical record schema with strict access controls
medicalNamespace := &NamespaceDefinition{
Name: "medical_record",
Relations: map[string]*NamespaceRelationDefinition{
"patient": {
Name: "patient",
SubjectsComputationExpression: &DirectExpression{},
},
"doctor": {
Name: "doctor",
SubjectsComputationExpression: &DirectExpression{},
},
"nurse": {
Name: "nurse",
SubjectsComputationExpression: &DirectExpression{},
},
"emergency_staff": {
Name: "emergency_staff",
SubjectsComputationExpression: &DirectExpression{},
},
// Complex medical access rules
"full_access": {
Name: "full_access",
SubjectsComputationExpression: &UnionExpression{
Children: []SubjectsComputationExpression{
&ComputedSubjectsExpression{ComputedRelation: "patient"},
&ComputedSubjectsExpression{ComputedRelation: "doctor"},
},
},
// full_access = patient ∪ doctor
},
"emergency_access": {
Name: "emergency_access",
SubjectsComputationExpression: &UnionExpression{
Children: []SubjectsComputationExpression{
&ComputedSubjectsExpression{ComputedRelation: "full_access"},
&ComputedSubjectsExpression{ComputedRelation: "emergency_staff"},
},
},
// emergency_access = full_access ∪ emergency_staff
},
"non_emergency_access": {
Name: "non_emergency_access",
SubjectsComputationExpression: &ExclusionExpression{
Left: &ComputedSubjectsExpression{ComputedRelation: "full_access"},
Right: &ComputedSubjectsExpression{ComputedRelation: "emergency_staff"},
},
// non_emergency_access = full_access - emergency_staff (regular access only)
},
},
}
Result: Healthcare access policies that handle normal care (patient + doctor), emergency situations (+ emergency staff), and audit trails (regular vs emergency access).
These examples show that ReBAC v1 captures nearly every practical same-object rule — from ownership inheritance to exclusion-based confidentiality.
Takeaways
Boolean algebra eliminates permission explosion: Instead of creating separate roles for every combination (employee-viewer, contractor-editor, etc.), you define base relations and combine them with union, intersection, and exclusion.
Schema-driven boolean logic keeps complexity manageable: Complex access patterns like "employees who are viewers but not contractors" become simple schema definitions rather than scattered application logic.
Set theory provides expressive power: Union (∪) for "either/or", intersection (∩) for "both/and", and exclusion (−) for "except" give you the full power of set operations on user relationships.
Relation traversal with boolean evaluation enables rich ReBAC: By combining relationship inheritance with boolean operations, we can express virtually any object-specific permission pattern declaratively.
What We've Built
We now have ReBAC v1 with Boolean Algebra:
Direct permissions (from Post 1):
OP_DIRECTRole-based permissions (from Post 2):
SubjectSetreferencesComputed permissions (new in Post 3):
OP_COMPUTED_SUBJECTSBoolean combinations (new in Post 3):
OP_UNION,OP_INTERSECTION,OP_EXCLUSION
The authorization check now performs boolean evaluation on relationship graphs, enabling complex permission patterns without combinatorial explosion of roles or tuples.
Important scope note: Boolean operations here operate only over relations on the same object; cross-object composition arrives with OP_EDGE in Post 4.
Limitations (What's Still Missing)
Our model now handles complex object-specific permission combinations, but we still can't express:
Cross-object inheritance: "Documents inherit permissions from their parent folder" (requires
OP_EDGE)Conditional access: "Alice can only access during business hours"
Attribute-based conditions: "Users can access if department matches document classification"
These require edge traversal and conditional evaluation that we'll tackle in the next posts.
Up next (Post 4): Hierarchies and Inheritance (ReBAC v2) We'll add cross-object relationships with OP_EDGE so the model can say "documents inherit viewer permissions from their parent folder." This introduces edge traversal across object boundaries and the foundation for hierarchical permissions.
