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:

  1. "Owners are editors" (Alice should be able to edit document 1)

  2. "Editors are viewers" (Both Alice and Bob should be able to view document 1)

  3. "Restricted viewers are employees AND editors" (must be both)

  4. "Public viewers are employees OR guests" (either is sufficient)

  5. "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 employee

Problems:

  • 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 relation

  • COMPUTED_SUBJECTS("Q") — include subjects of relation Q on the same object

  • UNION(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)

A ∪ A = A

Adding the same set twice yields the same set

Idempotence (Intersection)

A ∩ A = A

Intersecting a set with itself yields the same set

Exclusion Identity

A \ A = ∅

Subtracting a set from itself yields empty set

Commutativity (Union)

A ∪ B = B ∪ A

Order doesn't matter for union

Commutativity (Intersection)

A ∩ B = B ∩ A

Order doesn't matter for intersection

Non-Commutativity (Exclusion)

A \ B ≠ B \ A

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 as DIRECT(editor) ∪ COMPUTED(owner)

  • public_viewer = UNION(employee, guest) actually evaluates as DIRECT(public_viewer) ∪ UNION(employee, guest)

  • Relations with DirectExpression just get DIRECT(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/INTERSECTION are disallowed in the schema

Evaluating Authorization with Boolean Algebra

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=FALSE

The 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 inheritance

  • restricted_viewer = INTERSECTION(employee, viewer) — must be both

  • external_viewer = EXCLUSION(viewer, employee) — viewers except employee

  • public_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 → viewer

What 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 levels

What 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:

  1. Owners automatically become editors

  2. Editors automatically become viewers

  3. Public documents can be viewed by employees OR guests

  4. HR documents require being an employee AND having explicit HR approval

  5. 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

#editor

TRUE

owner → editor (OP_COMPUTED_SUBJECTS)

Alice

#viewer

TRUE

owner → editor → viewer (chain)

Bob

#viewer

FALSE

viewer derives from editor; employee alone doesn't imply viewer

Bob

#public_viewer

TRUE

employee ∪ guest (OP_UNION)

Bob

#hr_viewer

TRUE

employee ∩ hr_approved (OP_INTERSECTION)

Bob

#confidential_viewer

TRUE

employee - contractor (OP_EXCLUSION)

Charlie

#public_viewer

FALSE

NOT employee, NOT guest

Charlie

#confidential_viewer

FALSE

NOT employee (excluded by left side)

Dave

#public_viewer

TRUE

guest ∪ employee (OP_UNION)

Dave

#hr_viewer

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") returns nil

  • Result: 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 using VisitKey

  • First 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: viewereditoradminowner → 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

  1. 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.

  2. 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.

  3. 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.

  4. 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_DIRECT

  • Role-based permissions (from Post 2): SubjectSet references

  • Computed permissions (new in Post 3): OP_COMPUTED_SUBJECTS

  • Boolean 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.

Keep Reading