The Problem Statement

Our model from Posts 1–7 handles flexible, contextual access. But real systems also need non-negotiable rules mandatory policies that apply everywhere, regardless of individual tuple configuration. Without schema-level enforcement, we risk:

  • Policy violations: Tuples created without required security checks

  • Compliance failures: Missing mandatory caveats for regulated data

  • Security gaps: Legacy tuples that bypass new security requirements

  • Inconsistent enforcement: Some tuples have caveats, others don't

Real-world examples from production systems:

Looking at how major systems enforce mandatory policies reveals critical requirements:

  • Google Workspace: All external sharing requires admin approval (schema-level policy)

  • AWS S3: Bucket policies can enforce encryption for all objects (mandatory requirement)

  • GitHub Enterprise: Can require 2FA for all organization members (org-level policy)

  • Salesforce: Field-level security applies to all records (schema-level constraint)

Common patterns that require schema-level enforcement:

  1. Compliance: "All access to HIPAA data must check business_hours caveat"

  2. Security: "All external users must have ip_restriction caveat"

  3. Audit: "All admin access must have mfa_verified caveat"

  4. Data residency: "All EU data access must check gdpr_consent caveat"

What we can't enforce today:

"ALL users accessing classified documents must satisfy the security_clearance caveat—no exceptions."

Our current model allows:

  • Creating tuples without required caveats

  • Legacy tuples that bypass new security requirements

  • Inconsistent caveat application across tuples

  • No way to enforce mandatory policies at schema level

The core problem: We need to enforce mandatory caveats at the schema level, ensuring all tuples for a relation satisfy required policies, while gracefully handling legacy data.

Human Rule

"All users accessing patient records must obey the business_hours caveat—no exceptions, regardless of when the access tuple was created."

This single sentence captures the requirement: a schema-level policy that applies universally to all tuples, old and new, without requiring data migration.

Why Current Models Don't Work

Let's try to enforce "all viewers must access during business hours" using our existing tools from Posts 1-7.

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

From previous posts, we have:

From Post 5: Tuple-level caveats:

RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "1", Relation: "viewer"},
    Subject: &DirectSubject{
        Object: &ObjectRef{Namespace: "user", ObjectID: "alice"},
        Caveat: &ContextualizedCaveat{CaveatName: "business_hours"},  // ← Optional!
    },
}

From Post 3: Subject constraints (type restrictions):

SubjectConstraints: &SubjectConstraints{
    AllowedSubjectTypes: []AllowedSubjectType{
        &AllowedDirectSubject{Namespace: "user"},  // ← Only type restriction
    },
}

What's missing: No way to enforce that ALL tuples must have a specific caveat!

Approach 1: Manual Caveat on Every Tuple (Error-Prone)

// Attempt: Add business_hours caveat to every tuple manually

// Tuple 1: ✅ Has caveat
RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "1", Relation: "viewer"},
    Subject: &DirectSubject{
        Object: &ObjectRef{Namespace: "user", ObjectID: "alice"},
        Caveat: &ContextualizedCaveat{CaveatName: "business_hours"},
    },
}

// Tuple 2: ❌ Forgot caveat!
RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "2", Relation: "viewer"},
    Subject: &DirectSubject{
        Object: &ObjectRef{Namespace: "user", ObjectID: "bob"},
        // Missing business_hours caveat!
    },
}

// Tuple 3: ✅ Has caveat
RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "3", Relation: "viewer"},
    Subject: &DirectSubject{
        Object: &ObjectRef{Namespace: "user", ObjectID: "charlie"},
        Caveat: &ContextualizedCaveat{CaveatName: "business_hours"},
    },
}

Problems:

  • Human error: Easy to forget caveat when creating tuples

  • No enforcement: System doesn't prevent missing caveats

  • Inconsistent: Some tuples have caveat, others don't

  • Audit nightmare: Must scan all tuples to verify compliance

Example failure:

// Bob can access document:2 at ANY time (no caveat!)
Check(document:2#viewer, user:bob, context={current_hour: 23})
// → TRUE (should be FALSE outside business hours!)

Approach 2: Application-Layer Validation (Brittle)

// Attempt: Validate caveats in application code before creating tuples

func CreateViewerTuple(documentID, userID string) error {
    tuple := &RelationTuple{
        Resource: &ObjectAndRelation{Namespace: "document", ObjectID: documentID, Relation: "viewer"},
        Subject: &DirectSubject{
            Object: &ObjectRef{Namespace: "user", ObjectID: userID},
            Caveat: &ContextualizedCaveat{CaveatName: "business_hours"},  // ← Hardcoded!
        },
    }
    
    return tupleStore.Write(tuple)
}

Problems:

  • Scattered logic: Policy enforcement in application code, not authorization system

  • Bypass risk: Direct tuple writes bypass validation

  • Version skew: Different app versions may have different policies

  • No schema evolution: Can't update policy without redeploying all apps

Example failure:

// Legacy app version doesn't know about business_hours requirement
legacyApp.WriteTuple(document:4#viewer, user:dave)  // ← No caveat!

Approach 3: Composite Caveat (Doesn't Scale)

// Attempt: Create a composite caveat that includes business_hours

// Define composite caveat
&CaveatDefinition{
    Name: "viewer_with_business_hours",
    Expression: &BooleanExpr{
        Op: BOOL_AND,
        Children: []BooleanExpr{
            // Original viewer caveat (if any)
            &BooleanExpr{Op: BOOL_PREDICATE, Predicate: ...},
            // Mandatory business_hours check
            &BooleanExpr{Op: BOOL_PREDICATE, Predicate: ...},
        },
    },
}

// Use composite caveat on every tuple
RelationTuple{
    Subject: &DirectSubject{
        Caveat: &ContextualizedCaveat{CaveatName: "viewer_with_business_hours"},
    },
}

Problems:

  • Caveat explosion: Need separate composite for every combination

  • Still manual: Must remember to use composite caveat

  • Can't evolve: Adding new mandatory check requires new composite caveats

  • No enforcement: Nothing prevents using wrong caveat

Example failure:

// Developer uses wrong caveat
RelationTuple{
    Subject: &DirectSubject{
        Caveat: &ContextualizedCaveat{CaveatName: "ip_restriction"},  // ← Wrong caveat!
    },
}

The Solution: RequiredCaveat in SubjectConstraints

We extend the SubjectConstraints model (introduced in Post 3) to support required caveats that apply to all subjects of a given type.

Part 1: RequiredCaveat Field

Already defined in our model (Post 3, now fully utilized):

type AllowedDirectSubject struct {
    Namespace      string
    RequiredCaveat string  // ← NEW: Mandatory caveat for all direct subjects
}

type AllowedSubjectSet struct {
    Namespace      string
    Relation       string
    RequiredCaveat string  // ← NEW: Mandatory caveat for all subject sets
}

type AllowedWildcardSubject struct {
    Namespace      string
    RequiredCaveat string  // ← NEW: Mandatory caveat for all wildcard subjects
}

Semantic meaning:

If RequiredCaveat is set, ALL tuples with subjects of this type MUST satisfy the caveat during evaluation.

Key insight: The caveat is enforced at evaluation time, not write time. This allows:

  • Graceful schema evolution: Old tuples still work

  • No data migration: Don't need to rewrite existing tuples

  • Consistent enforcement: Policy applies to all tuples, old and new

Part 2: Effective Caveat Combination

Rule: When evaluating a tuple, the effective caveat is the AND of:

  1. Schema-level RequiredCaveat (from SubjectConstraints)

  2. Tuple-level Caveat (from Subject.Caveat)

Formula:

EffectiveCaveat = RequiredCaveat AND TupleCaveat

Truth table (tri-state logic):

RequiredCaveat

TupleCaveat

EffectiveCaveat

Meaning

nil

nil

nil

No caveats (always TRUE)

business_hours

nil

business_hours

Only schema caveat applies

nil

ip_restriction

ip_restriction

Only tuple caveat applies

business_hours

ip_restriction

business_hours AND ip_restriction

Both caveats must pass

Example:

// Schema: All viewers must access during business hours
SubjectConstraints: &SubjectConstraints{
    AllowedSubjectTypes: []AllowedSubjectType{
        &AllowedDirectSubject{
            Namespace:      "user",
            RequiredCaveat: "business_hours",  // ← Schema-level requirement
        },
    },
}

// Tuple 1: No tuple-level caveat
RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "1", Relation: "viewer"},
    Subject: &DirectSubject{
        Object: &ObjectRef{Namespace: "user", ObjectID: "alice"},
        Caveat: nil,  // ← No tuple caveat
    },
}
// `EffectiveCaveat = business_hours` (from schema)

// Tuple 2: Has tuple-level caveat
RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "2", Relation: "viewer"},
    Subject: &DirectSubject{
        Object: &ObjectRef{Namespace: "user", ObjectID: "bob"},
        Caveat: &ContextualizedCaveat{CaveatName: "ip_restriction"},
    },
}
// `EffectiveCaveat = business_hours AND ip_restriction` (both must pass)

Evaluation:

// Check at 2 PM from allowed IP
Check(document:1#viewer, user:alice, context={current_hour: 14, ip: "10.0.0.1"})
// → Evaluate business_hours → TRUE ✅

// Check at 11 PM from allowed IP
Check(document:1#viewer, user:alice, context={current_hour: 23, ip: "10.0.0.1"})
// → Evaluate business_hours → FALSE ❌

// Check at 2 PM from disallowed IP
Check(document:2#viewer, user:bob, context={current_hour: 14, ip: "192.168.1.1"})
// → Evaluate (business_hours AND ip_restriction)
// → business_hours: TRUE, ip_restriction: FALSE
// → Result: FALSE ❌

Implementation Details

Algorithm: Computing Effective Caveat Expression

The effective caveat is a BooleanExpr, not a named caveat. This avoids the "__composite__" naming hack.

function GetEffectiveCaveatExpr(tuple, schema, context) -> BooleanExpr:
    // Step 1: Get tuple-level caveat expression
    tupleCaveat = tuple.Subject.GetCaveat()
    tupleExpr = nil
    if tupleCaveat != nil:
        tupleExpr = ResolveCaveat(tupleCaveat.CaveatName).Expression

    // Step 2: Get schema-level required caveat expression
    relation = schema.GetRelation(tuple.Resource.Namespace, tuple.Resource.Relation)
    requiredCaveatName = relation.GetRequiredCaveatForSubject(tuple.Subject)
    schemaExpr = nil
    if requiredCaveatName != "":
        schemaExpr = ResolveCaveat(requiredCaveatName).Expression

    // Step 3: Combine expressions
    if schemaExpr == nil and tupleExpr == nil:
        return nil  # No caveats

    if schemaExpr == nil:
        return tupleExpr  # Only tuple caveat

    if tupleExpr == nil:
        return schemaExpr  # Only schema caveat

    # Both present: AND them together
    return &BooleanExpr{
        Op: BOOL_AND,
        Children: [schemaExpr, tupleExpr],
    }


function EvaluateEffectiveCaveat(tuple, schema, context) -> CaveatEvalResult:
    effectiveExpr = GetEffectiveCaveatExpr(tuple, schema, context)

    if effectiveExpr == nil:
        return CAV_TRUE  # No caveats

    return EvaluateBooleanExpr(effectiveExpr, context)

Key implementation notes:

  1. Direct BooleanExpr composition: No intermediate "__composite__" caveat name; directly build AND expression

  2. Lookup by subject type: Schema matches subject type to find correct RequiredCaveat name

  3. Evaluation order: Schema expression is left child (evaluated first), tuple expression is right child

  4. Caching: Effective expression can be cached per (schema_version, tuple_signature)

Real-World Example: HIPAA Compliance System

Let's build a complete example for a healthcare system that must comply with HIPAA regulations.

Requirements

Compliance Policy:

"All access to patient health records must:

  1. Occur during business hours (9 AM - 5 PM) — MANDATORY for all users

  2. Doctors must have valid medical license — tuple-specific

  3. Nurses must be assigned to patient's department — tuple-specific"

Schema Definition

// Define mandatory business_hours caveat
businessHours := &CaveatDefinition{
    Name: "business_hours",
    Parameters: []CaveatParameter{
        &ScalarParameter{Name: "env.current_hour", Type: CAV_T_INT},
    },
    Expression: &BooleanExpr{
        Op: BOOL_AND,
        Children: []*BooleanExpr{
            {
                Op: BOOL_PREDICATE,
                Pred: &Predicate{
                    Left:  &IdentifierExpr{Name: "env.current_hour"},
                    Op:    CMP_GE,
                    Right: &LiteralExpr{Value: 9, Type: CAV_T_INT},
                },
            },
            {
                Op: BOOL_PREDICATE,
                Pred: &Predicate{
                    Left:  &IdentifierExpr{Name: "env.current_hour"},
                    Op:    CMP_LT,
                    Right: &LiteralExpr{Value: 17, Type: CAV_T_INT},
                },
            },
        },
    },
}

// Define doctor-specific caveat
validLicense := &CaveatDefinition{
    Name: "valid_medical_license",
    Parameters: []CaveatParameter{
        &ScalarParameter{Name: "user.license_expiry", Type: CAV_T_TIMESTAMP},
        &ScalarParameter{Name: "env.now_utc", Type: CAV_T_TIMESTAMP},
    },
    Expression: &BooleanExpr{
        Op: BOOL_PREDICATE,
        Pred: &Predicate{
            Left:  &IdentifierExpr{Name: "user.license_expiry"},
            Op:    CMP_GT,
            Right: &IdentifierExpr{Name: "env.now_utc"},
        },
    },
}

// Define nurse-specific caveat
departmentMatch := &CaveatDefinition{
    Name: "department_match",
    Parameters: []CaveatParameter{
        &ScalarParameter{Name: "user.department", Type: CAV_T_STRING},
        &ScalarParameter{Name: "patient.department", Type: CAV_T_STRING},
    },
    Expression: &BooleanExpr{
        Op: BOOL_PREDICATE,
        Pred: &Predicate{
            Left:  &IdentifierExpr{Name: "user.department"},
            Op:    CMP_EQ,
            Right: &IdentifierExpr{Name: "patient.department"},
        },
    },
}

// Schema with RequiredCaveat enforcement
patientRecordSchema := &NamespaceDefinition{
    Name: "patient_record",
    Relations: map[string]*NamespaceRelationDefinition{
        "viewer": {
            Name: "viewer",
            SubjectsComputationExpression: &DirectExpression{},
            SubjectConstraints: &SubjectConstraints{
                AllowedSubjectTypes: []AllowedSubjectType{
                    // Doctors: business_hours is MANDATORY
                    &AllowedDirectSubject{
                        Namespace:      "doctor",
                        RequiredCaveat: "business_hours",  // ← Schema-level enforcement
                    },
                    // Nurses: business_hours is MANDATORY
                    &AllowedDirectSubject{
                        Namespace:      "nurse",
                        RequiredCaveat: "business_hours",  // ← Schema-level enforcement
                    },
                },
            },
        },
    },
}

Tuple Creation

// Tuple 1: Doctor with license check (tuple-level caveat)
doctorTuple := &RelationTuple{
    Resource: &ObjectAndRelation{
        Namespace: "patient_record",
        ObjectID:  "patient-12345",
        Relation:  "viewer",
    },
    Subject: &DirectSubject{
        Object: &ObjectRef{Namespace: "doctor", ObjectID: "dr-smith"},
        Caveat: &ContextualizedCaveat{
            CaveatName: "valid_medical_license",
            Context: &CaveatContext{
                Values: map[string]any{
                    "user.license_expiry": int64(1735689600), // 2025-01-01
                },
            },
        },
    },
}
// `EffectiveCaveat = business_hours AND valid_medical_license`

// Tuple 2: Nurse with department check (tuple-level caveat)
nurseTuple := &RelationTuple{
    Resource: &ObjectAndRelation{
        Namespace: "patient_record",
        ObjectID:  "patient-12345",
        Relation:  "viewer",
    },
    Subject: &DirectSubject{
        Object: &ObjectRef{Namespace: "nurse", ObjectID: "nurse-jones"},
        Caveat: &ContextualizedCaveat{
            CaveatName: "department_match",
            Context: &CaveatContext{
                Values: map[string]any{
                    "patient.department": "Cardiology",
                },
            },
        },
    },
}
// `EffectiveCaveat = business_hours AND department_match`

// Tuple 3: Legacy doctor tuple (no tuple-level caveat)
legacyDoctorTuple := &RelationTuple{
    Resource: &ObjectAndRelation{
        Namespace: "patient_record",
        ObjectID:  "patient-67890",
        Relation:  "viewer",
    },
    Subject: &DirectSubject{
        Object: &ObjectRef{Namespace: "doctor", ObjectID: "dr-brown"},
        Caveat: nil,  // ← No tuple caveat (legacy data)
    },
}
// `EffectiveCaveat = business_hours` (schema enforces it!)

Evaluation Examples

Scenario 1: Doctor during business hours with valid license

ctx := NewCaveatContext()
ctx.SetValue("env.current_hour", 14)  // 2 PM
ctx.SetValue("env.now_utc", int64(1704067200))  // 2024-01-01

Check(patient_record:patient-12345#viewer, doctor:dr-smith, ctx)
// → Evaluate (business_hours AND valid_medical_license)
// → business_hours: 14 >= 9 AND 14 < 17 → TRUE
// → valid_medical_license: 1735689600 > 1704067200 → TRUE
// → Result: TRUE ✅

Scenario 2: Doctor outside business hours (HIPAA violation prevented!)

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

Check(patient_record:patient-12345#viewer, doctor:dr-smith, ctx)
// → Evaluate (business_hours AND valid_medical_license)
// → business_hours: 22 >= 9 AND 22 < 17 → FALSE ❌
// → Result: FALSE (short-circuit, don't check license)

Scenario 3: Nurse with wrong department

ctx := NewCaveatContext()
ctx.SetValue("env.current_hour", 10)  // 10 AM
ctx.SetValue("user.department", "Neurology")

Check(patient_record:patient-12345#viewer, nurse:nurse-jones, ctx)
// → Evaluate (business_hours AND department_match)
// → business_hours: 10 >= 9 AND 10 < 17 → TRUE
// → department_match: "Neurology" == "Cardiology" → FALSE
// → Result: FALSE ❌

Scenario 4: Legacy doctor tuple (schema caveat still enforced!)

ctx := NewCaveatContext()
ctx.SetValue("env.current_hour", 23)  // 11 PM

Check(patient_record:patient-67890#viewer, doctor:dr-brown, ctx)
// → Evaluate business_hours (from schema, even though tuple has no caveat!)
// → business_hours: 23 >= 9 AND 23 < 17 → FALSE ❌
// → Result: FALSE (legacy tuple protected by schema policy!)

Key insight: Even legacy tuples created before the business_hours requirement are automatically protected by the schema-level policy. No data migration needed!

Design Decisions

Decision 1: Evaluation-Time vs Write-Time Enforcement

Question: Should we enforce RequiredCaveat when writing tuples or when evaluating them?

Answer: Evaluation-time enforcement for graceful schema evolution.

Rationale:

Write-time enforcement (rejected):

// ❌ Reject tuple writes that don't have required caveat
func WriteTuple(tuple *RelationTuple) error {
    requiredCaveat := schema.GetRequiredCaveat(tuple)
    if requiredCaveat != "" && tuple.Subject.Caveat == nil {
        return errors.New("missing required caveat")
    }
    return store.Write(tuple)
}

Problems:

  • Breaking change: Existing tuples become invalid when schema adds RequiredCaveat

  • Data migration: Must rewrite all existing tuples to add caveat

  • Downtime: Can't deploy schema change without migrating data first

  • Rollback complexity: Can't easily revert schema change

Evaluation-time enforcement (chosen):

// ✅ Apply required caveat during evaluation
func Check(resource, subject, context) bool {
    tuple := FindTuple(resource, subject)
    effectiveCaveat := GetEffectiveCaveat(tuple, schema)
    return EvaluateCaveat(effectiveCaveat, context)
}

Benefits:

  • Non-breaking, zero-downtime deployment: Old tuples continue to work; schema change takes effect immediately without data migration; easy rollback

Trade-off: Tuples don't "know" about schema-level caveats, but this is acceptable because:

  1. Schema is the source of truth for policy

  2. Evaluation always uses latest schema

  3. Audit logs capture effective caveat used

Decision 2: AND Semantics (Not OR)

Question: Should RequiredCaveat AND or OR with tuple caveat?

Answer: AND semantics for additive security.

Rationale:

OR semantics (rejected):

EffectiveCaveat = RequiredCaveat OR TupleCaveat

Problem: Tuple caveat could bypass schema requirement!

Example failure:

// Schema requires business_hours
RequiredCaveat: "business_hours"

// Tuple has different caveat
TupleCaveat: "ip_restriction"

// With OR: business_hours OR ip_restriction
// → If ip_restriction passes, access granted even outside business hours!
// → Schema requirement bypassed! ❌

AND semantics (chosen):

EffectiveCaveat = RequiredCaveat AND TupleCaveat

Benefits:

  • Additive security: Schema requirement always enforced

  • No bypass: Tuple caveat can only add restrictions, not remove them

  • Compliance: Mandatory policies cannot be circumvented

  • Intuitive: "Required" means "must pass"

Example:

// Schema requires business_hours
RequiredCaveat: "business_hours"

// Tuple has additional restriction
TupleCaveat: "ip_restriction"

// With AND: business_hours AND ip_restriction
// → Both must pass
// → Schema requirement always enforced ✅

Decision 3: Subject Type Specificity

Question: Should RequiredCaveat apply to all subjects or specific subject types?

Answer: Per subject type for fine-grained control.

Rationale:

Global requirement (rejected):

// ❌ All subjects must satisfy same caveat
&NamespaceRelationDefinition{
    Name: "viewer",
    RequiredCaveat: "business_hours",  // Applies to ALL subject types
}

Problems:

  • Too coarse: Can't have different requirements for different subject types

  • Inflexible: Doctors and nurses might need different checks

  • Workaround needed: Would need separate relations for each subject type

Per-type requirement (chosen):

// ✅ Different requirements for different subject types
SubjectConstraints: &SubjectConstraints{
    AllowedSubjectTypes: []AllowedSubjectType{
        &AllowedDirectSubject{
            Namespace:      "doctor",
            RequiredCaveat: "business_hours",  // Doctors: business hours only
        },
        &AllowedDirectSubject{
            Namespace:      "admin",
            RequiredCaveat: "mfa_verified",    // Admins: MFA required
        },
        &AllowedDirectSubject{
            Namespace:      "system",
            RequiredCaveat: "",                // Systems: no restriction
        },
    },
}

Benefits:

  • Fine-grained: Different policies for different subject types

  • Flexible: Can exempt certain subjects (e.g., system accounts)

  • Realistic: Matches real-world compliance requirements

  • Composable: Can combine with tuple-level caveats

Edge Cases and Safety

Edge Case 1: Unknown Required Caveat

Problem: What if schema references a caveat that doesn't exist?

Solution: Treat as FALSE (fail-safe).

// Schema requires non-existent caveat
RequiredCaveat: "unknown_caveat"

// Evaluation
effectiveCaveat := GetEffectiveCaveat(tuple, schema)
caveatDef := caveatRegistry.Get(effectiveCaveat.CaveatName)
if caveatDef == nil {
    // Unknown caveat → deny access (fail-safe)
    return FALSE
}

Rationale:

  • Fail-safe: Unknown policy → deny access

  • Prevents bypass: Can't grant access with missing caveat

  • Detectable: Monitoring can alert on unknown caveats

  • Schema validation: Compilation should catch this earlier

Edge Case 2: Pre-Bound Context in RequiredCaveat

Problem: Should RequiredCaveat allow pre-binding context values?

Solution: Disallow pre-binding in RequiredCaveat only caveat name is allowed.

// ✅ VALID: RequiredCaveat references caveat by name only
&AllowedDirectSubject{
    Namespace:      "user",
    RequiredCaveat: "business_hours",  // ← Name only, no pre-bound context
}

// ❌ INVALID: RequiredCaveat with pre-bound context
&AllowedDirectSubject{
    Namespace:      "user",
    RequiredCaveat: &ContextualizedCaveat{  // ← Not allowed!
        CaveatName: "business_hours",
        Context: &CaveatContext{
            Values: map[string]any{"env.current_hour": 9},
        },
    },
}

Rationale:

  • Security: Prevents tuple from overriding schema-level security parameters

  • Simplicity: RequiredCaveat is just a string name, not a full ContextualizedCaveat

  • Predictable: All parameters come from runtime context (caller-provided)

  • No conflicts: Eliminates precedence ambiguity

Attack scenario prevented:

// If pre-binding were allowed:
// Schema: RequiredCaveat with env.current_hour=9 (always passes)
// Attacker tuple: TupleCaveat with env.current_hour=23
// If tuple wins → attacker can deny legitimate access
// If schema wins → tuple can't override, but then why allow pre-binding?

Design decision: RequiredCaveat is a string (caveat name), not a ContextualizedCaveat. All context values must come from the runtime Check call.

Edge Case 3: Multiple Subject Types Match

Problem: What if a subject could match multiple AllowedSubjectType entries?

Solution: This is a schema validation error each subject type should appear at most once.

// ❌ INVALID SCHEMA: doctor appears twice
SubjectConstraints: &SubjectConstraints{
    AllowedSubjectTypes: []AllowedSubjectType{
        &AllowedDirectSubject{
            Namespace:      "doctor",
            RequiredCaveat: "business_hours",
        },
        &AllowedDirectSubject{
            Namespace:      "doctor",  // ← Duplicate!
            RequiredCaveat: "mfa_verified",
        },
    },
}

Schema validation:

func ValidateSubjectConstraints(constraints *SubjectConstraints) error {
    seen := make(map[string]bool)
    for _, allowedType := range constraints.AllowedSubjectTypes {
        key := allowedType.GetKey()  // e.g., "direct:doctor" or "set:role#member"
        if seen[key] {
            return fmt.Errorf("duplicate subject type: %s", key)
        }
        seen[key] = true
    }
    return nil
}

Rationale:

  • Unambiguous: Each subject type has exactly one policy

  • Fail-fast: Catch errors at schema compilation time

  • Deterministic: No tie-breaking needed

Implementation Notes

Schema Validation and Deployment Safety

Critical: Schema changes with RequiredCaveat can cause production outages if not validated and deployed carefully.

1. Schema Compilation and Validation

Before deploying a schema, validate that all RequiredCaveat references exist:

func ValidateSchema(schema *NamespaceDefinition, caveatRegistry CaveatRegistry) error {
    for _, relation := range schema.Relations {
        if relation.SubjectConstraints == nil {
            continue
        }

        for _, allowedType := range relation.SubjectConstraints.AllowedSubjectTypes {
            requiredCaveat := allowedType.GetRequiredCaveat()
            if requiredCaveat == "" {
                continue  // No required caveat
            }

            // Verify caveat exists
            caveatDef := caveatRegistry.Get(requiredCaveat)
            if caveatDef == nil {
                return fmt.Errorf(
                    "unknown RequiredCaveat '%s' in %s#%s for subject type %s",
                    requiredCaveat,
                    schema.Name,
                    relation.Name,
                    allowedType.GetKey(),
                )
            }
        }
    }
    return nil
}

Rationale:

  • Fail-fast: Catch typos at schema compile time, not runtime

  • Prevents outages: Invalid schema rejected before deployment

  • Clear errors: Pinpoint exact location of invalid reference

2. Staged Rollout: Observe → Enforce

Deploy new RequiredCaveat in two stages to avoid breaking production:

ALGORITHM: Staged RequiredCaveat Deployment

Stage 1: OBSERVE mode (1-2 weeks)
  - Schema deployed with RequiredCaveat
  - Evaluation logs violations but returns REQUIRES_CONTEXT (not FALSE)
  - Metrics track "would-deny" rate per namespace/relation/caveat
  - Teams notified to add missing context parameters

Stage 2: ENFORCE mode (after "would-deny" rate < 1%)
  - Flip enforcement flag
  - Actually deny access on caveat failure
  - Monitor error rates for spikes
  - Auto-rollback if error rate > threshold

Implementation:

type RequiredCaveatMode int32
const (
    MODE_OBSERVE RequiredCaveatMode = iota  // Log but don't enforce
    MODE_ENFORCE                             // Actually deny access
)

func EvaluateWithMode(effectiveCaveat, context, mode) CaveatEvalResult {
    result := EvaluateCaveat(effectiveCaveat, context)

    if mode == MODE_OBSERVE && result == CAV_FALSE {
        // Log violation but don't deny
        logger.Warn("RequiredCaveat would deny access",
            "caveat", effectiveCaveat.CaveatName,
            "context", context.String(),
        )
        metrics.Increment("required_caveat.would_deny")

        // Return REQUIRES_CONTEXT to signal missing params
        return CAV_REQUIRES_CONTEXT
    }

    return result
}

3. Canary Deployment with Auto-Rollback

Deploy schema changes to 1% of traffic first:

// Route 1% of traffic to canary schema
func GetSchema(namespace string) *NamespaceDefinition {
    if canary.IsEnabled(namespace) && rand.Float64() < 0.01 {
        return schemaRegistry.GetCanary(namespace)
    }
    return schemaRegistry.GetStable(namespace)
}

// Monitor canary metrics
if canaryMetrics.ErrorRate > 0.01 || canaryMetrics.LatencyP99 > 100*time.Millisecond {
    schemaRegistry.RollbackCanary(namespace)
    alert.Send("Schema canary failed - auto-rollback triggered")
}

Context Parameter Discovery

Problem: When you add RequiredCaveat: "business_hours", callers must provide env.current_hour. How do they discover this?

Solution 1: Schema Introspection API (Describe)

Provide an API that returns required context parameters:

GET /v1/schema/{namespace}/{relation}/describe

Response:
{
  "namespace": "patient_record",
  "relation": "viewer",
  "subjectTypes": [
    {
      "subjectType": "doctor",
      "requiredCaveat": {
        "name": "business_hours",
        "parameters": [
          {"name": "env.current_hour", "type": "int", "scope": "env"}
        ]
      }
    }
  ]
}

Solution 2: REQUIRES_CONTEXT with Minimal Missing Set

If caller omits required parameters, Check returns missing parameter names (see Post 9):

// Caller submits Check without full context
result := Check(patient_record:123#viewer, doctor:alice, context={})

// Response: REQUIRES_CONTEXT with missing params
{
  "decision": "REQUIRES_CONTEXT",
  "missing": ["env.current_hour"],
  "caveat": "business_hours"
}

// Caller fetches and retries
context.Set("env.current_hour", time.Now().Hour())
result = Check(patient_record:123#viewer, doctor:alice, context)

Solution 3: SDK/Codegen for Type Safety

Generate typed client from schema:

// Generated from schema
type PatientRecordViewerContext struct {
    CurrentHour int  `json:"env.current_hour"`  // Required
}

// Type-safe API
client.CheckPatientRecordViewer(
    resourceID: "123",
    subject: doctor:alice,
    context: &PatientRecordViewerContext{
        CurrentHour: time.Now().Hour(),
    },
)

Deployment timeline for new RequiredCaveat:

Week 0: Publish schema via Describe API; update SDK
Week 1: Deploy in OBSERVE mode; monitor REQUIRES_CONTEXT rate
Week 2: Teams deploy code to provide required context
Week 3: Flip to ENFORCE mode once REQUIRES_CONTEXT rate < 1%

Performance Considerations

Benchmark target: RequiredCaveat should add <5% latency overhead vs. tuple-only caveats.

1. Schema Caching

Cache compiled schema to avoid repeated lookups:

type CompiledRelation struct {
    Relation           *NamespaceRelationDefinition
    RequiredCaveatMap  map[string]string  // subjectType → caveatName
}

// Build map once at schema load time
func CompileRelation(relation *NamespaceRelationDefinition) *CompiledRelation {
    requiredCaveatMap := make(map[string]string)
    for _, allowedType := range relation.SubjectConstraints.AllowedSubjectTypes {
        key := allowedType.GetKey()  // e.g., "direct:user"
        requiredCaveatMap[key] = allowedType.GetRequiredCaveat()
    }
    return &CompiledRelation{
        Relation:          relation,
        RequiredCaveatMap: requiredCaveatMap,
    }
}

// Fast lookup during evaluation
compiled := schemaCache.GetRelation(resource.Namespace, resource.Relation)
for tuple := range tuples {
    subjectKey := tuple.Subject.GetKey()
    requiredCaveat := compiled.RequiredCaveatMap[subjectKey]
    // ...
}

2. Effective Caveat Caching

Cache effective caveat per (schema_version, tuple_signature):

type EffectiveCaveatCache struct {
    cache map[string]*BooleanExpr  // Key: "v42:user:alice[business_hours]"
}

func (c *EffectiveCaveatCache) Get(schemaVersion, tupleSignature string) *BooleanExpr {
    key := fmt.Sprintf("%s:%s", schemaVersion, tupleSignature)
    return c.cache[key]
}

// Invalidate on schema change
func (c *EffectiveCaveatCache) InvalidateSchema(schemaVersion string) {
    for key := range c.cache {
        if strings.HasPrefix(key, schemaVersion+":") {
            delete(c.cache, key)
        }
    }
}

3. Short-Circuit Evaluation

Evaluate RequiredCaveat first; if FALSE, skip TupleCaveat:

func EvaluateEffectiveCaveat(requiredCaveat, tupleCaveat, context) CaveatEvalResult {
    // Evaluate schema caveat first
    if requiredCaveat != nil {
        schemaResult := EvaluateCaveat(requiredCaveat, context)
        if schemaResult == CAV_FALSE {
            return CAV_FALSE  // Short-circuit
        }

        if tupleCaveat != nil {
            tupleResult := EvaluateCaveat(tupleCaveat, context)
            return CombineAND(schemaResult, tupleResult)
        }
        return schemaResult
    }

    if tupleCaveat != nil {
        return EvaluateCaveat(tupleCaveat, context)
    }

    return CAV_TRUE
}

Audit Trail and Observability

Critical: Audit logs must clearly indicate when RequiredCaveat denies access (compliance requirement).

Structured Audit Log:

type AuditLogEntry struct {
    Timestamp      time.Time
    Decision       string  // "ALLOW", "DENY", "REQUIRES_CONTEXT"
    Reason         string  // "required_caveat_failed", "tuple_caveat_failed"

    CaveatName     string  // "business_hours"
    CaveatSource   string  // "schema", "tuple", "both"
    FailedExpr     string  // "env.current_hour >= 9 AND env.current_hour < 17"

    ProvidedContext map[string]any  // {"env.current_hour": 23}
    MissingParams   []string        // ["user.license_expiry"]

    SchemaVersion  string  // "v42"
    Tuple          string  // "patient_record:123#viewer@doctor:alice"
}

Metrics:

// Counter: RequiredCaveat failures
metrics.Increment("required_caveat.failed",
    "namespace", "patient_record",
    "relation", "viewer",
    "caveat", "business_hours",
)

// Counter: Unknown caveat references
metrics.Increment("required_caveat.unknown",
    "caveat", "typo_caveat",
)

// Histogram: Evaluation latency
metrics.Histogram("required_caveat.eval_latency_ms", latencyMs)

Alerts:

# Unknown caveat (schema validation miss)
- alert: UnknownRequiredCaveat
  expr: rate(required_caveat.unknown[5m]) > 0
  severity: critical

# High REQUIRES_CONTEXT rate (callers missing params)
- alert: HighRequiresContextRate
  expr: rate(required_caveat.requires_context[5m]) > 0.05
  severity: warning

Testing Strategies

1. Schema Validation Tests

Test that invalid schemas are rejected at compile time:

func TestSchemaValidation_UnknownRequiredCaveat(t *testing.T) {
    schema := &NamespaceDefinition{
        Name: "document",
        Relations: map[string]*NamespaceRelationDefinition{
            "viewer": {
                SubjectConstraints: &SubjectConstraints{
                    AllowedSubjectTypes: []AllowedSubjectType{
                        &AllowedDirectSubject{
                            Namespace:      "user",
                            RequiredCaveat: "typo_caveat",  // ← Doesn't exist
                        },
                    },
                },
            },
        },
    }

    err := ValidateSchema(schema, caveatRegistry)
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "unknown RequiredCaveat 'typo_caveat'")
}

func TestSchemaValidation_DuplicateSubjectType(t *testing.T) {
    schema := &NamespaceDefinition{
        Relations: map[string]*NamespaceRelationDefinition{
            "viewer": {
                SubjectConstraints: &SubjectConstraints{
                    AllowedSubjectTypes: []AllowedSubjectType{
                        &AllowedDirectSubject{Namespace: "user", RequiredCaveat: "business_hours"},
                        &AllowedDirectSubject{Namespace: "user", RequiredCaveat: "mfa_verified"},  // ← Duplicate!
                    },
                },
            },
        },
    }

    err := ValidateSchema(schema, caveatRegistry)
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "duplicate subject type")
}

2. Schema Evolution Tests

Test that adding RequiredCaveat doesn't break existing tuples:

func TestSchemaEvolution_AddRequiredCaveat(t *testing.T) {
    // Step 1: Create tuple without caveat
    tuple := &RelationTuple{
        Resource: document:1#viewer,
        Subject:  &DirectSubject{Object: user:alice, Caveat: nil},
    }
    store.Write(tuple)

    // Step 2: Check passes (no required caveat yet)
    result := Check(document:1#viewer, user:alice, ctx)
    assert.True(t, result)

    // Step 3: Deploy schema with RequiredCaveat
    schema := &NamespaceDefinition{
        Relations: map[string]*NamespaceRelationDefinition{
            "viewer": {
                SubjectConstraints: &SubjectConstraints{
                    AllowedSubjectTypes: []AllowedSubjectType{
                        &AllowedDirectSubject{
                            Namespace:      "user",
                            RequiredCaveat: "business_hours",
                        },
                    },
                },
            },
        },
    }
    schemaService.Deploy(schema)

    // Step 4: Check now requires business_hours (no data migration!)
    ctx.SetValue("env.current_hour", 14)
    result = Check(document:1#viewer, user:alice, ctx)
    assert.True(t, result)  // Passes during business hours

    ctx.SetValue("env.current_hour", 23)
    result = Check(document:1#viewer, user:alice, ctx)
    assert.False(t, result)  // Fails outside business hours
}

3. AND Semantics Tests

Test that schema and tuple caveats are both enforced:

func TestRequiredCaveat_ANDSemantics(t *testing.T) {
    // Schema requires business_hours
    // Tuple requires ip_restriction

    // Test 1: Both pass
    ctx := NewCaveatContext()
    ctx.SetValue("env.current_hour", 14)
    ctx.SetValue("request.ip", "10.0.0.1")
    result := Check(document:1#viewer, user:alice, ctx)
    assert.True(t, result)

    // Test 2: Schema caveat fails
    ctx.SetValue("env.current_hour", 23)  // Outside business hours
    ctx.SetValue("request.ip", "10.0.0.1")
    result = Check(document:1#viewer, user:alice, ctx)
    assert.False(t, result)

    // Test 3: Tuple caveat fails
    ctx.SetValue("env.current_hour", 14)
    ctx.SetValue("request.ip", "192.168.1.1")  // Disallowed IP
    result = Check(document:1#viewer, user:alice, ctx)
    assert.False(t, result)

    // Test 4: Both fail
    ctx.SetValue("env.current_hour", 23)
    ctx.SetValue("request.ip", "192.168.1.1")
    result = Check(document:1#viewer, user:alice, ctx)
    assert.False(t, result)
}

4. Subject Type Specificity Tests

Test that different subject types have different requirements:

func TestRequiredCaveat_PerSubjectType(t *testing.T) {
    // Schema: doctors require business_hours, admins require mfa_verified

    // Test 1: Doctor during business hours (passes)
    ctx := NewCaveatContext()
    ctx.SetValue("env.current_hour", 14)
    result := Check(patient_record:1#viewer, doctor:smith, ctx)
    assert.True(t, result)

    // Test 2: Doctor outside business hours (fails)
    ctx.SetValue("env.current_hour", 23)
    result = Check(patient_record:1#viewer, doctor:smith, ctx)
    assert.False(t, result)

    // Test 3: Admin without MFA (fails)
    ctx := NewCaveatContext()
    ctx.SetValue("user.mfa_verified", false)
    result = Check(patient_record:1#viewer, admin:jones, ctx)
    assert.False(t, result)

    // Test 4: Admin with MFA (passes, no business hours requirement!)
    ctx.SetValue("user.mfa_verified", true)
    ctx.SetValue("env.current_hour", 23)  // Outside business hours, but OK for admins
    result = Check(patient_record:1#viewer, admin:jones, ctx)
    assert.True(t, result)
}

5. Property-Based Tests

Test invariants that must hold for all inputs:

// Property: Adding RequiredCaveat never increases access (monotonic security)
func TestProperty_MonotonicSecurity(t *testing.T) {
    // For all tuples T, all contexts C:
    //   Check(T, C, schema_without_required) >= Check(T, C, schema_with_required)
    // (TRUE >= REQUIRES_CONTEXT >= FALSE)

    quick.Check(func(tuple *RelationTuple, context *CaveatContext) bool {
        // Check without RequiredCaveat
        resultWithout := CheckWithSchema(tuple, context, schemaWithoutRequired)

        // Check with RequiredCaveat
        resultWith := CheckWithSchema(tuple, context, schemaWithRequired)

        // RequiredCaveat can only make access stricter
        return resultWithout.Priority() >= resultWith.Priority()
    }, nil)
}

// Property: RequiredCaveat is always enforced
func TestProperty_RequiredCaveatAlwaysEnforced(t *testing.T) {
    // For all tuples T with RequiredCaveat R:
    //   If Check(T, C) == TRUE, then EvaluateCaveat(R, C) == TRUE

    quick.Check(func(tuple *RelationTuple, context *CaveatContext) bool {
        result := Check(tuple, context)
        if result != TRUE {
            return true  // Property only applies when access granted
        }

        // If access granted, RequiredCaveat must have passed
        requiredCaveat := schema.GetRequiredCaveat(tuple)
        if requiredCaveat == "" {
            return true  // No required caveat
        }

        caveatResult := EvaluateCaveat(requiredCaveat, context)
        return caveatResult == CAV_TRUE
    }, nil)
}

// Property: Schema evolution is backward compatible
func TestProperty_SchemaEvolutionBackwardCompatible(t *testing.T) {
    // For all tuples T created before schema change:
    //   If Check(T, C, old_schema) == TRUE, then Check(T, C, new_schema) != ERROR
    // (May become FALSE or REQUIRES_CONTEXT, but never ERROR)

    quick.Check(func(tuple *RelationTuple, context *CaveatContext) bool {
        resultOld := CheckWithSchema(tuple, context, oldSchema)
        if resultOld != TRUE {
            return true
        }

        resultNew := CheckWithSchema(tuple, context, newSchemaWithRequired)
        return resultNew != ERROR  // Never breaks existing tuples
    }, nil)
}

Takeaways

  1. Schema-level enforcement prevents policy violationsRequiredCaveat ensures all tuples satisfy mandatory policies, regardless of when they were created. Evaluation-time enforcement (not write-time) enables zero-downtime schema evolution without data migration.

  2. Production deployment requires operational rigor — Schema validation catches typos before deployment; staged rollout (observe → enforce) prevents breaking production; canary deployment with auto-rollback ensures safety. The gap between "correct design" and "safe to deploy" is significant.

  3. Caller contract is explicit and discoverable — Schema introspection (Describe API), runtime discovery (REQUIRES_CONTEXT), and SDK/codegen make required context parameters discoverable at build-time and runtime. Minimal coupling: callers own their data fetching.

Invariants touched: Fail-safe defaults (unknown caveat FALSE), monotonic security (RequiredCaveat never increases access), determinism (AND semantics, no precedence ambiguity).

Why it matters: Schema-level safety turns policy from a suggestion into a contract the system itself refuses to violate compliance.

We now have powerful schema-level safety nets, but a critical question remains: Do two evaluators always agree?

In distributed systems, determinism is essential. When multiple authorization services evaluate the same Check request, they must produce identical results. But what happens when:

  • Multiple tuples could grant access with different caveats?

  • Some caveats return REQUIRES_CONTEXT while others return TRUE?

  • Schema evolution changes the order of subject types?

Post 9 tackles the determinism problem: canonical signatures, ordering rules, and tie-breaking semantics that guarantee two evaluators always converge on the same answer.

Preview:

// Two paths to access:
// Path A: Direct tuple (REQUIRES_CONTEXT, missing 2 params)
// Path B: Via role (REQUIRES_CONTEXT, missing 1 param)
// Path C: Via group (TRUE, no missing params)

// Question: Which result do we return?
// Answer: Deterministic tie-break rules ensure consistent choice

Keep Reading