The Problem Statement

Our ABAC v1 model from Post 5 enables context-aware permissions like "Alice can view this document during business hours." But it has a critical limitation: caveats are evaluated per individual user. We can't express organization-wide policies that depend on attribute matching between users and resources.

Real-world systems need attribute-based policies:

"Any user can view HR documents if their department is HR." "Employees can access documents classified at or below their clearance level." "Users can view content licensed for their country."

These aren't permissions for specific users—they're conditional permissions for entire classes of users based on attribute matching.

The core problem: Our current model can't express "any user in namespace X can access resource Y, but only if attribute A matches attribute B."

Why Current Models Don't Work

Let's try to express "any user in HR department can view HR documents" using our existing tools from Posts 1-5.

What We Have So Far (Post 1-5 Recap)

From previous posts, we have two subject types:

1. DirectSubject Grants access to a specific user:

&DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "alice"}}
// Represents: user:alice

2. SubjectSet Grants access to all members of a role:

&SubjectSet{Namespace: "role", ObjectID: "admin", Relation: "member"}
// Represents: role:admin#member (all users who are admin members)

From Post 5, we also have caveats that can add conditions to individual grants:

&DirectSubject{
    Object: &ObjectRef{Namespace: "user", ObjectID: "alice"},
    Caveat: &ContextualizedCaveat{CaveatName: "business_hours"},
}
// Alice can access, but only during business hours

Now let's see why these aren't enough for organization-wide attribute-based policies.

Approach 1: Individual User Tuples with Caveats

// Try to grant every HR employee access to every HR document
for each user in HR_department {
    store.Add(RelationTuple{
        Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "hr_policy", Relation: "viewer"},
        Subject:  &DirectSubject{
            Object: &ObjectRef{Namespace: "user", ObjectID: user.ID},
            Caveat: &ContextualizedCaveat{
                CaveatName: "department_match",
                Context: &CaveatContext{
                    Values: map[string]any{
                        "user_department": user.Department,
                        "required_department": "HR",
                    },
                },
            },
        },
    })
}

Problems:

  • Tuple explosion: 1,000 HR employees × 100 HR documents = 100,000 tuples

  • Maintenance nightmare: Adding a new HR employee requires updating 100 document tuples

  • Stale data: Employee department changes don't automatically update document access

  • No organization-wide policies: Each tuple is independent

Approach 2: Role-Based with SubjectSet

// Create an HR role
store.Add(RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "role", ObjectID: "hr", Relation: "member"},
    Subject:  &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "alice"}},
})
// ... repeat for all 1,000 HR employees

// Grant access to the HR role
store.Add(RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "hr_policy", Relation: "viewer"},
    Subject:  &SubjectSet{Namespace: "role", ObjectID: "hr", Relation: "member"},
})

Problems:

  • Still O(U) tuples: Need 1,000 role membership tuples for 1,000 HR employees

  • Synchronization overhead: When user's department changes, must update role memberships

  • Doesn't express the policy: The rule is "department matches," not "is member of HR role"

  • Maintenance burden: Role memberships must be kept in sync with user attributes

Approach 3: Application-Layer Attribute Checks

// Grant all users access unconditionally
for each user {
    store.Add(RelationTuple{
        Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "hr_policy", Relation: "viewer"},
        Subject:  &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: user.ID}},
    })
}

// Then check department in application code
if user.Department == document.RequiredDepartment {
    // Allow access
}

Problems:

  • Breaks separation of concerns: Authorization logic leaks into application layer

  • Inconsistent enforcement: Different services might implement checks differently

  • No audit trail: Can't see why access was granted/denied from authorization logs

  • Still requires O(U × R) tuples: Doesn't solve the tuple explosion problem

Conclusion: None of our existing subject types (DirectSubject, SubjectSet) can express "any user in namespace X can access resource Y, conditional on attribute matching" efficiently.

The Solution: Wildcard Subjects + Namespace-Based Parameters

We need two new capabilities:

  1. A way to grant access to "all users in a namespace" without enumerating them

  2. A way to compare attributes between users and resources in caveat expressions

The solution combines two concepts:

Part 1: Introducing WildcardSubject (Third Subject Type)

We extend our Subject type hierarchy with a third type that represents all objects in a namespace:

// WildcardSubject: All objects in a namespace
type WildcardSubject struct {
    SubjectNamespace string          // "user" (matches ALL users)
    Caveat           *ContextualizedCaveat  // Condition to filter which users
}

How it works:

// Grant ALL users access (with conditions)
store.Add(RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "hr_policy", Relation: "viewer"},
    Subject:  &WildcardSubject{
        SubjectNamespace: "user",  // Matches user:alice, user:bob, user:*, etc.
        Caveat: &ContextualizedCaveat{CaveatName: "department_match"},
    },
})

Key insight: WildcardSubject{SubjectNamespace: "user"} matches any candidate with namespace "user" (e.g., user:alice, user:bob, etc.).

Benefit: One tuple covers all users! No more O(U × R) explosion.

Part 2: Namespace-Based Parameter Naming

But WildcardSubject alone isn't enough. We need caveats that can compare attributes between users and resources. The challenge: how do we organize parameter names to avoid confusion?

The problem with bare parameter names:

// Caveat with bare names - CONFUSING!
CaveatDefinition{
    Name: "department_match",
    Parameters: []CaveatParameter{
        &ScalarParameter{Name: "department", Type: CAV_T_STRING},        // Which department? User's or document's?
        &ScalarParameter{Name: "required_department", Type: CAV_T_STRING}, // Better, but still unclear
    },
    Expression: &BooleanExpr{...},
}

Problems:

  • Collision risk: What if both user and document have a "department" attribute?

  • Unclear ownership: Which entity does each parameter belong to?

  • Doesn't scale: With 20+ parameters, names become arbitrary

The solution: Namespace-based parameter naming

We adopt a convention where parameter names use the namespace as a prefix:

// Caveat with namespace-based names - CLEAR!
CaveatDefinition{
    Name: "department_match",
    Parameters: []CaveatParameter{
        &ScalarParameter{Name: "user.department", Type: CAV_T_STRING},      // User's department
        &ScalarParameter{Name: "document.department", Type: CAV_T_STRING},  // Document's department
    },
    Expression: &BooleanExpr{
        Op: BOOL_PREDICATE,
        Pred: &Predicate{
            Left:  &IdentifierExpr{Name: "user.department"},
            Op:    CMP_EQ,
            Right: &IdentifierExpr{Name: "document.department"},
        },
    },
}

Why namespace-based naming?

  1. Intuitive: user.department clearly refers to the user's department attribute

  2. Consistent with domain model: Matches tuple namespaces (user:alice, document:1)

  3. Self-documenting: The parameter name tells you which entity it belongs to

  4. Prevents collisions: user.department and document.department are distinct

  5. Scales well: As you add more namespaces (service:*, group:*), the pattern remains consistent

Important clarification: The dot notation is purely a naming convention, not a language feature. The system treats "user.department" as a single string key in the context map, not as path traversal.

// At check time, caller provides values for these parameter names
ctx := NewCaveatContext()
ctx.SetValue("user.department", "HR")        // Key is the string "user.department"
ctx.SetValue("document.department", "HR")    // Key is the string "document.department"

Trade-off we accept: Caveats become namespace-specific. A caveat using user.department only works with user:* wildcards. This is intentional — it makes caveats explicit about what they work with, preventing accidental misuse.

Extending the Model: Complete Subject Type Hierarchy

Now let's see how WildcardSubject fits into our existing model:

type Subject interface {
    isSubject()
    Type() SubjectType
    GetCaveat() *ContextualizedCaveat
    Signature() string
    Matches(candidate Subject) bool
    String() string
}

type SubjectType int
const (
    SubjectTypeDirect   SubjectType = iota  // Specific object (user:alice)
    SubjectTypeSet                          // Role members (role:admin#member)
    SubjectTypeWildcard                     // All in namespace (user:*)
)

// DirectSubject: Specific user (from Post 1)
type DirectSubject struct {
    Object *ObjectRef                // user:alice
    Caveat *ContextualizedCaveat     // Optional condition
}

// SubjectSet: All members of a role (from Post 2)
type SubjectSet struct {
    Namespace string                 // role
    ObjectID  string                 // admin
    Relation  string                 // member
    Caveat    *ContextualizedCaveat  // Optional condition
}

// WildcardSubject: All objects in a namespace (NEW in Post 6!)
type WildcardSubject struct {
    SubjectNamespace string          // user (matches ALL users)
    Caveat           *ContextualizedCaveat  // Condition to filter which users
}

How WildcardSubject Matches Candidates

When checking Check(document:1#viewer, user:alice, context):

  1. Find tuples where Resource = document:1#viewer

  2. For each tuple, check if tuple.Subject.Matches(user:alice):

Tuple Subject Type

Tuple Subject

Candidate

Matches?

Reason

DirectSubject

user:alice

user:alice

YES

Exact match

DirectSubject

user:bob

user:alice

NO

Different user

SubjectSet

role:admin#member

user:alice

NO

Need to expand role (different mechanism)

WildcardSubject

user:*

user:alice

YES

Namespace matches

WildcardSubject

service:*

user:alice

NO

Different namespace

Key insight: WildcardSubject{SubjectNamespace: "user"} matches any DirectSubject{Namespace: "user", ObjectID: <anything>}.

Important distinction: Wildcard ≠ Role Expansion

These two mechanisms are fundamentally different:

  • SubjectSet (role:admin#member): Requires recursive expansion to check if user is a member

    • Must traverse the graph: Check(user:alice, role:admin#member, context)

    • Can involve multiple hops (user → team → department → role)

    • Complexity: O(depth) recursive calls

    • Example: Checking if Alice is an admin requires walking the membership chain

  • WildcardSubject (user:*): Simple namespace matching with no recursion

    • Direct string comparison: candidate.Namespace == wildcard.SubjectNamespace

    • No graph traversal, no recursive calls

    • Complexity: O(1) constant time

    • Example: Checking if Alice matches user:* is just comparing "user" == "user"

This makes WildcardSubject much more efficient for organization-wide policies—it's a simple namespace check, not a graph traversal.

When to Use Each Subject Type

Subject Type

Use Case

Example

Tuple Count

DirectSubject

Grant access to a specific user

Alice can view document 1

O(U × R) - one per user-resource pair

SubjectSet

Grant access to role members

All admins can view document 1

O(R) tuples + O(U) role memberships

WildcardSubject

Grant conditional access to all users in namespace

All users can view document 1 IF condition

O(R) - one per resource

Scalability: WildcardSubject reduces tuple count from O(U × R) to O(R) when combined with caveats.

Architectural Decision: Caller-Provided Context

Who is responsible for providing attribute values?

Option 1: System-Fetched Attributes (Rejected)

The authorization system could automatically fetch attributes:

// Hypothetical: System fetches attributes automatically
caveat := &ContextualizedCaveat{CaveatName: "department_match"}
// System internally:
// - Looks up user:alice's department from UserService
// - Looks up document:hr_policy's required_department from ResourceService
// - Evaluates caveat

Why we reject this:

  • Tight coupling: Authorization system depends on UserService, ResourceService, etc.

  • No flexibility: Can't use different attribute sources in different contexts

  • Performance issues: System can't batch or cache attribute fetches

  • Testing nightmare: Hard to test with mock attributes

  • Scalability: System becomes responsible for all attribute resolution logic

Option 2: Caller-Provided Context (Chosen)

The application layer provides all context values:

// Application layer fetches attributes and provides them
context := &CaveatContext{
    Values: map[string]any{
        "user.department": "Engineering",
        "document.required_department": "Engineering",
    },
}

result := authzService.Check(resource, subject, context)

Why we choose this:

  • Separation of concerns: Authorization system focuses on policy evaluation, not attribute fetching

  • Flexibility: Application can fetch from any source (database, cache, API, etc.)

  • Performance control: Application can batch fetches, use caching, etc.

  • Testability: Easy to provide mock attributes for testing

  • Determinism: Same inputs always produce same results (no external dependencies)

The Caller's Responsibility

When the authorization system returns REQUIRES_CONTEXT, the caller must:

  1. Detect missing parameters: Check the MissingParams list

  2. Fetch attributes: Use the parameter names to determine what to fetch

  3. Provide context: Set values in CaveatContext

  4. Retry: Call Check again with complete context

This is the "caller-provided context" pattern—a clean separation of concerns.

Implementing Organization-Scoped Conditions

Now that we understand the design, let's see how it works in practice.

Step 1: Define a Caveat with Namespace-Based Parameters

We define a caveat that compares attributes from different entities:

departmentMatchCaveat := &CaveatDefinition{
    Name: "department_match",
    Parameters: []CaveatParameter{
        &ScalarParameter{Name: "user.department", Type: CAV_T_STRING},
        &ScalarParameter{Name: "document.required_department", Type: CAV_T_STRING},
    },
    Expression: &BooleanExpr{
        Op: BOOL_PREDICATE,
        Pred: &Predicate{
            Left:  &IdentifierExpr{Name: "user.department"},
            Op:    CMP_EQ,
            Right: &IdentifierExpr{Name: "document.required_department"},
        },
    },
}

What this means:

  • user.department: The user's department attribute (provided by caller)

  • document.required_department: The document's required department attribute (provided by caller)

  • Expression: Grant access if they match

Step 2: Create a Wildcard Tuple with the Caveat

Instead of creating individual tuples for each user, we create one wildcard tuple:

store.Add(RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "hr_policy", Relation: "viewer"},
    Subject: &WildcardSubject{
        SubjectNamespace: "user",
        Caveat: &ContextualizedCaveat{
            CaveatName: "department_match",
        },
    },
})

What this means:

  • user:*: Any user in the "user" namespace

  • department_match caveat: But only if their department matches the document's required department

  • One tuple handles all users

Step 3: Caller Provides Context at Check Time

When checking access, the application layer provides all attribute values:

// Application layer fetches attributes
userDept := userService.GetDepartment("alice")           // "Engineering"
docRequiredDept := documentService.GetRequiredDept("hr_policy")  // "Engineering"

// Provide context to authorization system
context := &CaveatContext{
    Values: map[string]any{
        "user.department":         userDept,
        "document.required_department": docRequiredDept,
    },
}

// Check access
result := authzService.Check(
    &ObjectAndRelation{Namespace: "document", ObjectID: "hr_policy", Relation: "viewer"},
    &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "alice"}},
    context,
)

Key points:

  • The authorization system does NOT fetch attributes

  • The caller provides all context values

  • Parameter names guide the caller on what to fetch

  • The system performs simple string key lookups: context.Values["user.department"]

Step 4: Per-Candidate Evaluation

When the authorization system evaluates the wildcard tuple:

  1. Find the wildcard tuple: (user:*) -> viewer -> (document:hr_policy)

  2. Match the subject: user:alice matches user:* (same namespace)

  3. Evaluate the caveat:

    • Look up "user.department" in context → "Engineering"

    • Look up "document.required_department" in context → "Engineering"

    • Compare: "Engineering" == "Engineering" → TRUE

  4. Grant access: Result is TRUE

This is per-candidate evaluation—the same caveat is evaluated differently for each user, with their specific attributes.

Performance note: Each check evaluates the caveat once with that user's context. If you check 1,000 users against the same document, the caveat is evaluated 1,000 times (once per user). However, resource attributes (like document.required_department) can be fetched once and reused across all checks, making bulk operations efficient.

Testing Organization-Scoped Conditions

Scenario 1: Department-Based Document Access (Happy Path)

Setup:

// Define department matching caveat
schema.AddCaveat(departmentMatchCaveat)

// Grant all users conditional access to HR documents
store.Add(RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "hr_policy", Relation: "viewer"},
    Subject: &WildcardSubject{
        SubjectNamespace: "user",
        Caveat: &ContextualizedCaveat{CaveatName: "department_match"},
    },
})

Check: Alice (HR) accessing HR document

ctx := NewCaveatContext()
ctx.SetValue("user.department", "HR")        // Alice's department
ctx.SetValue("document.required_department", "HR") // Document requirement

result := Check(
    &ObjectAndRelation{Namespace: "document", ObjectID: "hr_policy", Relation: "viewer"},
    &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "alice"}},
    ctx,
)

// Result: Granted=true, CaveatState=CAVEAT_TRUE
// Reason: "HR" == "HR" → TRUE

Check: Bob (Engineering) accessing HR document


ctx := NewCaveatContext()
ctx.SetValue("user.department", "Engineering")  // Bob's department
ctx.SetValue("document.required_department", "HR")   // Document requirement

result := Check(...)

// Result: Granted=false, CaveatState=CAVEAT_FALSE
// Reason: "Engineering" == "HR" → FALSE

Scenario 2: Clearance-Based Access Control

Setup:

// Define clearance level caveat
clearanceCaveat := &CaveatDefinition{
    Name: "clearance_required",
    Parameters: []CaveatParameter{
        &ScalarParameter{Name: "user.clearance_level", Type: CAV_T_INT},
        &ScalarParameter{Name: "document.required_clearance", Type: CAV_T_INT},
    },
    Expression: &BooleanExpr{
        Op: BOOL_PREDICATE,
        Pred: &Predicate{
            Left:  &IdentifierExpr{Name: "user.clearance_level"},
            Op:    CMP_GE,
            Right: &IdentifierExpr{Name: "document.required_clearance"},
        },
    },
}

// Grant all users conditional access based on clearance
store.Add(RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "classified", Relation: "viewer"},
    Subject: &WildcardSubject{
        SubjectNamespace: "user",
        Caveat: &ContextualizedCaveat{CaveatName: "clearance_required"},
    },
})

Check: Alice (clearance 5) accessing level 3 document

ctx := NewCaveatContext()
ctx.SetValue("user.clearance_level", 5)      // Alice's clearance
ctx.SetValue("document.required_clearance", 3)    // Document requirement

result := Check(...)

// Result: Granted=true, CaveatState=CAVEAT_TRUE
// Reason: 5 >= 3 → TRUE

Check: Bob (clearance 2) accessing level 3 document

ctx := NewCaveatContext()
ctx.SetValue("user.clearance_level", 2)      // Bob's clearance
ctx.SetValue("document.required_clearance", 3)    // Document requirement

result := Check(...)

// Result: Granted=false, CaveatState=CAVEAT_FALSE
// Reason: 2 >= 3 → FALSE

Scenario 3: Geographic Content Licensing

Setup:

// Define geo-restriction caveat
geoCaveat := &CaveatDefinition{
    Name: "geo_restriction",
    Parameters: []CaveatParameter{
        &ScalarParameter{Name: "user.country", Type: CAV_T_STRING},
        &ListParameter{
            Name:        "content.licensed_countries",
            ElementType: CAV_T_STRING,  // List of country codes
        },
    },
    Expression: &BooleanExpr{
        Op: BOOL_PREDICATE,
        Pred: &Predicate{
            Left:  &IdentifierExpr{Name: "user.country"},
            Op:    CMP_IN,
            Right: &IdentifierExpr{Name: "content.licensed_countries"},
        },
    },
}

// Grant all users conditional access based on geography
store.Add(RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "content", ObjectID: "movie_123", Relation: "viewer"},
    Subject: &WildcardSubject{
        SubjectNamespace: "user",
        Caveat: &ContextualizedCaveat{CaveatName: "geo_restriction"},
    },
})

Check: Alice (US) accessing US/CA licensed content

ctx := NewCaveatContext()
ctx.SetValue("user.country", "US")
ctx.SetValue("content.licensed_countries", []string{"US", "CA", "GB"})

result := Check(...)

// Result: Granted=true, CaveatState=CAVEAT_TRUE
// Reason: "US" in ["US", "CA", "GB"] → TRUE

Edge Case 1: Missing User Attributes (REQUIRES_CONTEXT)

Setup: Same as Scenario 1.

Check without user.department:

ctx := NewCaveatContext()
ctx.SetValue("document.required_department", "HR")  // Document requirement
// Missing user.department

result := Check(...)

// Result: Granted=false, CaveatState=CAVEAT_REQUIRES_CONTEXT
// MissingParams: ["user.department"]
// Reason: Can't evaluate without user's department

Caller's responsibility: The application layer should:

  • Fetch Alice's department from the user service

  • Provide it as context.SetValue("user.department", aliceDept)

  • Retry the check with complete context

  • Or deny access (fail-safe default)

Edge Case 2: Wrong Namespace Wildcard

Setup:

// Accidentally use wrong namespace
store.Add(RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "hr_policy", Relation: "viewer"},
    Subject: &WildcardSubject{
        SubjectNamespace: "service",  // Wrong! Should be "user"
        Caveat: &ContextualizedCaveat{CaveatName: "department_match"},
    },
})

Check: Alice (user:alice) accessing document

result := Check(
    &ObjectAndRelation{Namespace: "document", ObjectID: "hr_policy", Relation: "viewer"},
    &DirectSubject{Object: &ObjectRef{Namespace: "user", ObjectID: "alice"}},
    ctx,
)

// Result: Granted=false
// Reason: user:alice doesn't match service:* wildcard

Why this matters: Namespace mismatches are caught automatically. You can't accidentally grant user permissions to service accounts or vice versa.

Edge Case 3: Multiple Wildcard Tuples with Tri-State Logic

Setup:

// Multiple wildcard tuples with different conditions
store.Add(RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "shared", Relation: "viewer"},
    Subject: &WildcardSubject{
        SubjectNamespace: "user",
        Caveat: &ContextualizedCaveat{CaveatName: "department_match"},
    },
})

store.Add(RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "shared", Relation: "viewer"},
    Subject: &WildcardSubject{
        SubjectNamespace: "user",
        Caveat: &ContextualizedCaveat{CaveatName: "clearance_required"},
    },
})

Check: Alice accessing shared document

Tri-state OR logic:

  • If ANY caveat returns TRUE → Result is TRUE (access granted)

  • If ALL caveats return FALSE → Result is FALSE (access denied)

  • If NO caveat returns TRUE AND ANY returns REQUIRES_CONTEXT → Result is REQUIRES_CONTEXT

Example outcomes:

// Case 1: One caveat passes
// department_match: TRUE (Alice in HR, document requires HR)
// clearance_required: FALSE (Alice clearance 2, document requires 3)
// Result: TRUE (at least one grant succeeded)

// Case 2: Both caveats need context
// department_match: REQUIRES_CONTEXT (missing user.department)
// clearance_required: REQUIRES_CONTEXT (missing user.clearance_level)
// Result: REQUIRES_CONTEXT (union of missing params)

// Case 3: Mixed results
// department_match: REQUIRES_CONTEXT (missing user.department)
// clearance_required: FALSE (Alice clearance 2, document requires 3)
// Result: REQUIRES_CONTEXT (might grant if context provided)

Why this matters: Multiple wildcard tuples create alternative authorization paths. The system tries all paths and grants access if any path succeeds.

Authorization Algorithm: Extending Post 5 with Wildcard Support

In Post 5, we had the Check algorithm that evaluates caveats on DirectSubject and SubjectSet tuples with tri-state logic. In Post 6, we extend this to support WildcardSubject for organization-wide policies.

What's New in Post 6

From Post 5, we had:

  • Check algorithm with caveat evaluation on DirectSubject and SubjectSet

  • Tri-state output: TRUE, FALSE, or REQUIRES_CONTEXT

  • Context parameter for runtime values

  • Kleene logic for combining results

In Post 6, we add:

  • WildcardSubject matching: Match all objects in a namespace (e.g., user:*)

  • Per-candidate evaluation: Same caveat evaluated differently for each user

  • Namespace-based parameters: Parameters like user.department, document.classification

  • Multiple tuple paths: Combine DirectSubject, SubjectSet, and WildcardSubject results

Check Algorithm with Wildcard Support

ALGORITHM: Check(subject, resource#relation, context) - With Wildcard Support

Input:
  - subject: DirectSubject (e.g., "user:alice")
  - resource: ObjectAndRelation (e.g., "document:hr_policy#viewer")
  - context: CaveatContext (runtime values with namespace-based keys like "user.department")

Output:
  - CheckResult {
      Granted: bool
      CaveatState: CAVEAT_TRUE | CAVEAT_FALSE | CAVEAT_REQUIRES_CONTEXT
      MissingParams: []string  // Namespaced as "caveat_name.param"
      ErrorCode: *ErrorCode    // Optional: ERR_TYPE_MISMATCH, ERR_UNKNOWN_CAVEAT, etc.
    }

Steps:

1. Look up the relation definition in schema:
   - Find NamespaceDefinition for resource.namespace
   - Get NamespaceRelationDefinition for relation
   - Extract schema-level required caveat (if any) from SubjectConstraints

2. Find all tuples that grant access to resource:
   - Direct tuples: (subject, resource#relation) where subject matches exactly
   - SubjectSet tuples: (role:X#member, resource#relation) where subject is member of role:X
   - Wildcard tuples: (namespace:*, resource#relation) where namespace matches subject.namespace (NEW!)

3. For each matching tuple, evaluate with caveats:

   a) If DirectSubject tuple (from Post 5):
      - Check if subject matches exactly
      - Get effective caveat (schema-level AND tuple-level)
      - result = EvaluateCaveat(effectiveCaveat, context, schema)
      - If result.State == CAVEAT_TRUE  add to candidateResults
      - If result.State == CAVEAT_REQUIRES_CONTEXT  collect MissingParams
      - If result.State == CAVEAT_FALSE  skip this tuple

   b) If SubjectSet tuple (from Post 5):
      - Recursively expand: Check(subject, role:X#member, context)
      - If expansion succeeds, evaluate tuple caveat
      - Add result to candidateResults

   c) If WildcardSubject tuple (NEW in Post 6!):
      - Check if subject.namespace matches wildcard.SubjectNamespace
        Example: WildcardSubject{SubjectNamespace: "user"} matches DirectSubject{Namespace: "user", ObjectID: "alice"}
      - Get effective caveat (schema-level AND wildcard caveat)
      - Evaluate wildcard caveat with per-candidate context:
        * Context contains namespace-based parameters:
          - "user.department" (subject's department)
          - "document.department" (resource's required department)
          - "now_utc" (UTC timestamp)
          - "tz" (IANA timezone)
          - "request.method" (request metadata)
        * result = EvaluateCaveat(effectiveCaveat, context, schema)
      - If result.State == CAVEAT_TRUE  add to candidateResults
      - If result.State == CAVEAT_REQUIRES_CONTEXT  collect MissingParams
      - If result.State == CAVEAT_FALSE  skip this tuple

4. Evaluate SubjectsComputationExpression (from Post 4-5):
   - Expression can be: DIRECT, COMPUTED_SUBJECTS, UNION, INTERSECTION, EXCLUSION, EDGE
   - For each operation, recursively call Check(subject, computed_resource#computed_relation, context)
   - Add results to candidateResults

5. Combine all results using Kleene tri-state OR logic:
   - If ANY result.CaveatState == CAVEAT_TRUE  return {Granted: true, CaveatState: CAVEAT_TRUE}
   - If NO result is TRUE AND ANY result.CaveatState == CAVEAT_REQUIRES_CONTEXT:
      return {Granted: false, CaveatState: CAVEAT_REQUIRES_CONTEXT, MissingParams: union of all missing params}
   - If ALL results.CaveatState == CAVEAT_FALSE  return {Granted: false, CaveatState: CAVEAT_FALSE}

6. Return final CheckResult

Real-World Example: Healthcare Patient Records System

Let's see how this works in a complete, realistic healthcare system with multiple organization-wide policies.

System Requirements

Business rules:

  1. Doctors can view patient records in their department

  2. Nurses can view records for patients they're assigned to

  3. Administrators can view all records during business hours

  4. Emergency access: Anyone with emergency role can view any record (audit logged separately)

Schema Definition

// Caveat 1: Department matching
departmentMatchCaveat := &CaveatDefinition{
    Name: "department_match",
    Parameters: []CaveatParameter{
        &ScalarParameter{Name: "doctor.department", Type: CAV_T_STRING},
        &ScalarParameter{Name: "patient_record.department", Type: CAV_T_STRING},
    },
    Expression: &BooleanExpr{
        Op: BOOL_PREDICATE,
        Pred: &Predicate{
            Left:  &IdentifierExpr{Name: "doctor.department"},
            Op:    CMP_EQ,
            Right: &IdentifierExpr{Name: "patient_record.department"},
        },
    },
}

// Caveat 2: Nurse assignment
nurseAssignmentCaveat := &CaveatDefinition{
    Name: "nurse_assignment",
    Parameters: []CaveatParameter{
        &ListParameter{
            Name:        "nurse.assigned_patients",
            ElementType: CAV_T_STRING,  // List of patient IDs (strings)
        },
        &ScalarParameter{Name: "patient_record.patient_id", Type: CAV_T_STRING},
    },
    Expression: &BooleanExpr{
        Op: BOOL_PREDICATE,
        Pred: &Predicate{
            Left:  &IdentifierExpr{Name: "patient_record.patient_id"},
            Op:    CMP_IN,
            Right: &IdentifierExpr{Name: "nurse.assigned_patients"},
        },
    },
}

// Caveat 3: Business hours for admins
businessHoursCaveat := &CaveatDefinition{
    Name: "business_hours",
    Parameters: []CaveatParameter{
        &ScalarParameter{Name: "now_utc", Type: CAV_T_TIMESTAMP},  // UTC timestamp
        &ScalarParameter{Name: "tz", Type: CAV_T_STRING},          // IANA timezone
    },
    Expression: &BooleanExpr{
        Op: BOOL_AND,
        Children: []*BooleanExpr{
            {
                Op: BOOL_PREDICATE,
                Pred: &Predicate{
                    Left: &CallExpr{
                        FnName: "local_hour",
                        Args: []CaveatExpr{
                            &IdentifierExpr{Name: "now_utc"},
                            &IdentifierExpr{Name: "tz"},
                        },
                    },
                    Op:    CMP_GE,
                    Right: &LiteralExpr{Value: 8, Type: CAV_T_INT},
                },
            },
            {
                Op: BOOL_PREDICATE,
                Pred: &Predicate{
                    Left: &CallExpr{
                        FnName: "local_hour",
                        Args: []CaveatExpr{
                            &IdentifierExpr{Name: "now_utc"},
                            &IdentifierExpr{Name: "tz"},
                        },
                    },
                    Op:    CMP_LT,
                    Right: &LiteralExpr{Value: 18, Type: CAV_T_INT},
                },
            },
        },
    },
}

Tuple Configuration

// Rule 1: Doctors can view records in their department
store.Add(RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "patient_record", ObjectID: "record_123", Relation: "viewer"},
    Subject: &WildcardSubject{
        SubjectNamespace: "doctor",
        Caveat: &ContextualizedCaveat{CaveatName: "department_match"},
    },
})

// Rule 2: Nurses can view assigned patient records
store.Add(RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "patient_record", ObjectID: "record_123", Relation: "viewer"},
    Subject: &WildcardSubject{
        SubjectNamespace: "nurse",
        Caveat: &ContextualizedCaveat{CaveatName: "nurse_assignment"},
    },
})

// Rule 3: Admins can view during business hours
store.Add(RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "patient_record", ObjectID: "record_123", Relation: "viewer"},
    Subject: &WildcardSubject{
        SubjectNamespace: "admin",
        Caveat: &ContextualizedCaveat{CaveatName: "business_hours"},
    },
})

// Rule 4: Emergency role has unconditional access (no caveat)
store.Add(RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "patient_record", ObjectID: "record_123", Relation: "viewer"},
    Subject: &WildcardSubject{
        SubjectNamespace: "emergency_staff",
        // No caveat = unconditional access for this namespace
    },
})

Access Check Examples

Example 1: Dr. Smith (Cardiology) accessing cardiology patient record

ctx := NewCaveatContext()
ctx.SetValue("doctor.department", "Cardiology")
ctx.SetValue("patient_record.department", "Cardiology")

result := Check(
    &ObjectAndRelation{Namespace: "patient_record", ObjectID: "record_123", Relation: "viewer"},
    &DirectSubject{Object: &ObjectRef{Namespace: "doctor", ObjectID: "dr_smith"}},
    ctx,
)
// Result: TRUE (department_match caveat passes)

Example 2: Nurse Johnson accessing assigned patient

ctx := NewCaveatContext()
ctx.SetValue("nurse.assigned_patients", []string{"patient_456", "patient_789"})
ctx.SetValue("patient_record.patient_id", "patient_456")

result := Check(...)
// Result: TRUE (nurse_assignment caveat passes)

Example 3: Admin accessing record at 2 PM

ctx := NewCaveatContext()
ctx.SetValue("env.current_hour", 14)  // 2 PM

result := Check(...)
// Result: TRUE (business_hours caveat passes: 8 <= 14 < 18)

Example 4: Admin accessing record at 10 PM

ctx := NewCaveatContext()
ctx.SetValue("env.current_hour", 22)  // 10 PM

result := Check(...)
// Result: FALSE (business_hours caveat fails: 22 >= 18)

Example 5: Emergency staff accessing any record

result := Check(
    &ObjectAndRelation{Namespace: "patient_record", ObjectID: "record_123", Relation: "viewer"},
    &DirectSubject{Object: &ObjectRef{Namespace: "emergency_staff", ObjectID: "emt_jones"}},
    nil,  // No context needed - unconditional access
)
// Result: TRUE (no caveat to evaluate)

How Multiple Policies Compose

When Dr. Smith (who is also an admin) checks access:

  1. Doctor wildcard: Evaluates department_match → TRUE (same department)

  2. Admin wildcard: Evaluates business_hours → TRUE (during business hours)

  3. Combined result: TRUE (OR logic: at least one path grants access)

Key insight: Multiple wildcard tuples create alternative authorization paths. Access is granted if ANY path succeeds, providing flexibility while maintaining security.

Industry Context

This pattern appears in:

  • Healthcare: HIPAA-compliant patient record access (shown above)

  • Finance: SOX-compliant document access based on role and department

  • Government: Classified document access based on clearance level and need-to-know

  • SaaS: Multi-tenant systems with organization-scoped permissions

Takeaways

  1. Wildcard subjects enable organization-wide policies: Instead of creating individual user tuples, define conditional policies that apply to entire user namespaces.

  2. Namespace-based parameters enable attribute matching: Parameters like user.department and document.classification allow caveats to compare attributes between different entities in a self-documenting way.

  3. Per-candidate evaluation scales efficiently: One wildcard tuple can handle thousands of users, with conditions evaluated individually for each user at O(1) space complexity.

  4. Caller-provided context maintains separation of concerns: The authorization system focuses on policy evaluation while the application layer handles attribute fetching, caching, and batching.

  5. Tri-state logic handles missing context gracefully: The system can distinguish between "access denied" (FALSE), "access granted" (TRUE), and "need more information" (REQUIRES_CONTEXT).

What We've Built

We now have Organization-Scoped ABAC:

  • Direct permissions (Post 1): OP_DIRECT

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

  • Boolean combinations (Post 3): OP_UNION, OP_INTERSECTION, OP_EXCLUSION, OP_COMPUTED_SUBJECTS

  • Cross-object inheritance (Post 4): OP_EDGE

  • Context-aware conditions (Post 5): CaveatDefinition + ContextualizedCaveat

  • Organization-wide policies (new in Post 6): WildcardSubject + namespace-based parameters

The authorization check now supports both individual user permissions and organization-wide policies with attribute-based conditions.

Limitations (What's Still Missing)

Our model now handles organization-scoped conditions, but we still can't express:

  • Complex boolean logic in caveats: Multi-predicate conditions with AND/OR/NOT

  • Function calls in caveats: Dynamic computations like now(), contains()

  • Schema-level constraints: "All users must have business_hours caveat"

  • Determinism guarantees: Ensuring two evaluators always agree

Up next (Post 7): Complex Boolean Logic (ABAC v2) We'll add boolean algebra to caveats so they can express multi-predicate conditions like "(employee OR contractor) AND NOT suspended AND clearance >= 3." This introduces function calls and the full power of attribute-based access control.

Keep Reading