Recap: What We Built in Post 7A

In Post 7A, we activated boolean algebra in caveats:

  • Boolean operators: AND, OR, NOT for combining predicates

  • Function calls: Dynamic computations with pure, deterministic functions

  • Evaluation algorithms: Clear semantics for complex expressions

Now in Post 7B, we'll explore how to use these primitives to build production-ready composite caveats with proper observability, debugging, and safety measures.

Real-World Example: Security Clearance System

Let's build a complete example for a government security clearance system.

Requirements

Access Policy:

"Grant access to classified documents if:

  1. User is (employee OR contractor) AND NOT suspended

  2. User's clearance level >= document's classification level

  3. Access is during business hours (9 AM - 5 PM) in user's timezone

  4. User's department matches document's owning department OR user has cross_department_access flag"

Schema Definition

// Caveat 1: Employment and suspension check
employmentCheck := &CaveatDefinition{
    Name: "valid_employment",
    Parameters: []*CaveatParameter{
        &ScalarParameter{Name: "user.employment_type", Type: CAV_T_STRING},
        &ScalarParameter{Name: "user.is_suspended", Type: CAV_T_BOOL},
    },
    Expression: &BooleanExpr{
        Op: BOOL_AND,
        Children: []*BooleanExpr{
            // (employee OR contractor)
            {
                Op: BOOL_OR,
                Children: []*BooleanExpr{
                    {
                        Op: BOOL_PREDICATE,
                        Pred: &Predicate{
                            Left:  &IdentifierExpr{Name: "user.employment_type"},
                            Op:    CMP_EQ,
                            Right: &LiteralExpr{Value: "employee", Type: CAV_T_STRING},
                        },
                    },
                    {
                        Op: BOOL_PREDICATE,
                        Pred: &Predicate{
                            Left:  &IdentifierExpr{Name: "user.employment_type"},
                            Op:    CMP_EQ,
                            Right: &LiteralExpr{Value: "contractor", Type: CAV_T_STRING},
                        },
                    },
                },
            },
            // NOT suspended
            {
                Op: BOOL_NOT,
                Children: []*BooleanExpr{
                    {
                        Op: BOOL_PREDICATE,
                        Pred: &Predicate{
                            Left:  &IdentifierExpr{Name: "user.is_suspended"},
                            Op:    CMP_EQ,
                            Right: &LiteralExpr{Value: true, Type: CAV_T_BOOL},
                        },
                    },
                },
            },
        },
    },
}

// Caveat 2: Clearance level check
clearanceCheck := &CaveatDefinition{
    Name: "sufficient_clearance",
    Parameters: []*CaveatParameter{
        &ScalarParameter{Name: "user.clearance_level", Type: CAV_T_INT},
        &ScalarParameter{Name: "document.classification_level", Type: CAV_T_INT},
    },
    Expression: &BooleanExpr{
        Op: BOOL_PREDICATE,
        Pred: &Predicate{
            Left:  &IdentifierExpr{Name: "user.clearance_level"},
            Op:    CMP_GE,
            Right: &IdentifierExpr{Name: "document.classification_level"},
        },
    },
}

// Caveat 3: Business hours check
businessHours := &CaveatDefinition{
    Name: "business_hours",
    Parameters: []*CaveatParameter{
        &ScalarParameter{Name: "env.now_utc", Type: CAV_T_TIMESTAMP},
        &ScalarParameter{Name: "user.timezone", Type: CAV_T_STRING},
    },
    Expression: &BooleanExpr{
        Op: BOOL_AND,
        Children: []*BooleanExpr{
            {
                Op: BOOL_PREDICATE,
                Pred: &Predicate{
                    Left: &CallExpr{
                        FnName: "local_hour",
                        Args: []CaveatExpr{
                            &IdentifierExpr{Name: "env.now_utc"},
                            &IdentifierExpr{Name: "user.timezone"},
                        },
                    },
                    Op:    CMP_GE,
                    Right: &LiteralExpr{Value: 9, Type: CAV_T_INT},
                },
            },
            {
                Op: BOOL_PREDICATE,
                Pred: &Predicate{
                    Left: &CallExpr{
                        FnName: "local_hour",
                        Args: []CaveatExpr{
                            &IdentifierExpr{Name: "env.now_utc"},
                            &IdentifierExpr{Name: "user.timezone"},
                        },
                    },
                    Op:    CMP_LT,
                    Right: &LiteralExpr{Value: 17, Type: CAV_T_INT},
                },
            },
        },
    },
}

// Caveat 4: Department access check
departmentAccess := &CaveatDefinition{
    Name: "department_access",
    Parameters: []*CaveatParameter{
        &ScalarParameter{Name: "user.department", Type: CAV_T_STRING},
        &ScalarParameter{Name: "document.department", Type: CAV_T_STRING},
        &ScalarParameter{Name: "user.has_cross_department_access", Type: CAV_T_BOOL},
    },
    Expression: &BooleanExpr{
        Op: BOOL_OR,
        Children: []*BooleanExpr{
            // Department match
            {
                Op: BOOL_PREDICATE,
                Pred: &Predicate{
                    Left:  &IdentifierExpr{Name: "user.department"},
                    Op:    CMP_EQ,
                    Right: &IdentifierExpr{Name: "document.department"},
                },
            },
            // Cross-department access flag
            {
                Op: BOOL_PREDICATE,
                Pred: &Predicate{
                    Left:  &IdentifierExpr{Name: "user.has_cross_department_access"},
                    Op:    CMP_EQ,
                    Right: &LiteralExpr{Value: true, Type: CAV_T_BOOL},
                },
            },
        },
    },
}

Approach 1: Multiple Tuples (Not Recommended)

You could create separate tuples for each caveat, but this is inefficient:

// ❌ NOT RECOMMENDED: Multiple tuples with different caveats
// This creates 4 separate authorization paths that must be evaluated

// Tuple 1: Employment check
&RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "classified-report-001", Relation: "viewer"},
    Subject: &WildcardSubject{
        SubjectNamespace: "user",
        Caveat: &ContextualizedCaveat{CaveatName: "valid_employment"},
    },
}

// Tuple 2: Clearance check
&RelationTuple{
    Resource: &ObjectAndRelation{Namespace: "document", ObjectID: "classified-report-001", Relation: "viewer"},
    Subject: &WildcardSubject{
        SubjectNamespace: "user",
        Caveat: &ContextualizedCaveat{CaveatName: "sufficient_clearance"},
    },
}

// ... and so on for business_hours and department_access

Why this is bad:

  • OR semantics: Multiple tuples are combined with OR logic, not AND

  • Wrong behavior: User only needs to satisfy ONE caveat, not ALL

  • Inefficient: 4 separate evaluations instead of 1

  • Hard to maintain: Changes require updating multiple tuples

Approach 2: Composite Caveat (Recommended)

The correct approach: Combine all four caveats into a single composite caveat using boolean AND:

// Composite caveat combining all 4 conditions
compositeCaveat := &CaveatDefinition{
    Name: "classified_document_access",
    Parameters: []*CaveatParameter{
        // Employment parameters
        &ScalarParameter{Name: "user.employment_type", Type: CAV_T_STRING},
        &ScalarParameter{Name: "user.is_suspended", Type: CAV_T_BOOL},
        // Clearance parameters
        &ScalarParameter{Name: "user.clearance_level", Type: CAV_T_INT},
        &ScalarParameter{Name: "document.classification_level", Type: CAV_T_INT},
        // Business hours parameters
        &ScalarParameter{Name: "env.now_utc", Type: CAV_T_TIMESTAMP},
        &ScalarParameter{Name: "user.timezone", Type: CAV_T_STRING},
        // Department parameters
        &ScalarParameter{Name: "user.department", Type: CAV_T_STRING},
        &ScalarParameter{Name: "document.department", Type: CAV_T_STRING},
        &ScalarParameter{Name: "user.has_cross_department_access", Type: CAV_T_BOOL},
    },
    Expression: &BooleanExpr{
        Op: BOOL_AND,
        Children: []*BooleanExpr{
            // Condition 1: (employee OR contractor) AND NOT suspended
            {
                Op: BOOL_AND,
                Children: []*BooleanExpr{
                    {
                        Op: BOOL_OR,
                        Children: []*BooleanExpr{
                            {
                                Op: BOOL_PREDICATE,
                                Pred: &Predicate{
                                    Left:  &IdentifierExpr{Name: "user.employment_type"},
                                    Op:    CMP_EQ,
                                    Right: &LiteralExpr{Value: "employee", Type: CAV_T_STRING},
                                },
                            },
                            {
                                Op: BOOL_PREDICATE,
                                Pred: &Predicate{
                                    Left:  &IdentifierExpr{Name: "user.employment_type"},
                                    Op:    CMP_EQ,
                                    Right: &LiteralExpr{Value: "contractor", Type: CAV_T_STRING},
                                },
                            },
                        },
                    },
                    {
                        Op: BOOL_NOT,
                        Children: []*BooleanExpr{
                            {
                                Op: BOOL_PREDICATE,
                                Pred: &Predicate{
                                    Left:  &IdentifierExpr{Name: "user.is_suspended"},
                                    Op:    CMP_EQ,
                                    Right: &LiteralExpr{Value: true, Type: CAV_T_BOOL},
                                },
                            },
                        },
                    },
                },
            },
            // Condition 2: clearance >= classification
            {
                Op: BOOL_PREDICATE,
                Pred: &Predicate{
                    Left:  &IdentifierExpr{Name: "user.clearance_level"},
                    Op:    CMP_GE,
                    Right: &IdentifierExpr{Name: "document.classification_level"},
                },
            },
            // Condition 3: business hours (9 AM - 5 PM)
            {
                Op: BOOL_AND,
                Children: []*BooleanExpr{
                    {
                        Op: BOOL_PREDICATE,
                        Pred: &Predicate{
                            Left: &CallExpr{
                                FnName: "local_hour",
                                Args: []CaveatExpr{
                                    &IdentifierExpr{Name: "env.now_utc"},
                                    &IdentifierExpr{Name: "user.timezone"},
                                },
                            },
                            Op:    CMP_GE,
                            Right: &LiteralExpr{Value: 9, Type: CAV_T_INT},
                        },
                    },
                    {
                        Op: BOOL_PREDICATE,
                        Pred: &Predicate{
                            Left: &CallExpr{
                                FnName: "local_hour",
                                Args: []CaveatExpr{
                                    &IdentifierExpr{Name: "env.now_utc"},
                                    &IdentifierExpr{Name: "user.timezone"},
                                },
                            },
                            Op:    CMP_LT,
                            Right: &LiteralExpr{Value: 17, Type: CAV_T_INT},
                        },
                    },
                },
            },
            // Condition 4: department match OR cross-department access
            {
                Op: BOOL_OR,
                Children: []*BooleanExpr{
                    {
                        Op: BOOL_PREDICATE,
                        Pred: &Predicate{
                            Left:  &IdentifierExpr{Name: "user.department"},
                            Op:    CMP_EQ,
                            Right: &IdentifierExpr{Name: "document.department"},
                        },
                    },
                    {
                        Op: BOOL_PREDICATE,
                        Pred: &Predicate{
                            Left:  &IdentifierExpr{Name: "user.has_cross_department_access"},
                            Op:    CMP_EQ,
                            Right: &LiteralExpr{Value: true, Type: CAV_T_BOOL},
                        },
                    },
                },
            },
        },
    },
}

// Now use the composite caveat in the tuple
tuple := &RelationTuple{
    Resource: &ObjectAndRelation{
        Namespace: "document",
        ObjectID:  "classified-report-001",
        Relation:  "viewer",
    },
    Subject: &WildcardSubject{
        SubjectNamespace: "user",
        Caveat: &ContextualizedCaveat{
            CaveatName: "classified_document_access",
            Context: &CaveatContext{
                // Pre-bind document-specific values
                Values: map[string]any{
                    "document.classification_level": 3,
                    "document.department":            "Intelligence",
                },
            },
        },
    },
}

Benefits of composite caveat:

  • Single evaluation: All conditions checked in one pass

  • Atomic: Either all conditions pass or access is denied

  • Efficient: Short-circuit evaluation stops at first failure

  • Maintainable: One caveat definition to update

  • Auditable: Complete policy visible in one place

Approach 3: Schema-Level RequiredCaveat (Alternative)

Another approach is to use schema-level caveats that apply to ALL tuples of a relation:

// Define schema with required caveat
documentNamespace := &NamespaceDefinition{
    Name: "document",
    Relations: map[string]*NamespaceRelationDefinition{
        "viewer": {
            Name: "viewer",
            SubjectsComputationExpression: &DirectExpression{},
            SubjectConstraints: &SubjectConstraints{
                AllowedSubjectTypes: []AllowedSubjectType{
                    &AllowedWildcardSubject{
                        Namespace:      "user",
                        RequiredCaveat: "classified_document_access",  // ← Schema-level requirement
                    },
                },
            },
        },
    },
}

// Now ALL tuples for document#viewer automatically require this caveat
tuple := &RelationTuple{
    Resource: &ObjectAndRelation{
        Namespace: "document",
        ObjectID:  "classified-report-001",
        Relation:  "viewer",
    },
    Subject: &WildcardSubject{
        SubjectNamespace: "user",
        // No caveat needed here - schema enforces it
    },
}

When to use schema-level RequiredCaveat:

  • Policy enforcement: All tuples MUST satisfy the caveat (no exceptions)

  • Compliance: Regulatory requirements (e.g., HIPAA, SOC2)

  • Security: Critical resources that always need extra checks

  • Consistency: Prevent accidentally creating tuples without the caveat

Combination rule:

EffectiveCaveat = RequiredCaveat (from schema) AND TupleCaveat (from subject)

Example with both:

// Schema requires business_hours for all viewers
&AllowedWildcardSubject{
    Namespace:      "user",
    RequiredCaveat: "business_hours",
}

// Tuple adds additional ip_restriction
Subject: &WildcardSubject{
    SubjectNamespace: "user",
    Caveat: &ContextualizedCaveat{CaveatName: "ip_restriction"},
}

// Effective caveat = business_hours AND ip_restriction

Recommendation: Use Approach 2 (Composite Caveat) for most cases. Use Approach 3 (Schema-Level RequiredCaveat) only when you need to enforce organization-wide policies that cannot be bypassed.

Expression tree visualization:

classified_document_access (BOOL_AND)
├── Employment Check (BOOL_AND)
│   ├── Employment Type (BOOL_OR)
│   │   ├── is employee?
│   │   └── is contractor?
│   └── NOT suspended (BOOL_NOT)
│       └── is_suspended == true?
├── Clearance Check (BOOL_PREDICATE)
│   └── clearance_level >= classification_level?
├── Business Hours (BOOL_AND)
│   ├── local_hour(now, tz) >= 9?
│   └── local_hour(now, tz) < 17?
└── Department Access (BOOL_OR)
    ├── department match?
    └── has cross_department_access?

Evaluation Flow Diagram:

Testing Scenarios

Let's test various scenarios:

Scenario 1: Valid Employee During Business Hours

ctx := NewCaveatContext()
ctx.SetValue("user.employment_type", "employee")
ctx.SetValue("user.is_suspended", false)
ctx.SetValue("user.clearance_level", 4)
ctx.SetValue("document.classification_level", 3)
ctx.SetValue("env.now_utc", 1640000000) // 2021-12-20 14:13:20 UTC
ctx.SetValue("user.timezone", "America/New_York")
ctx.SetValue("user.department", "Intelligence")
ctx.SetValue("document.department", "Intelligence")
ctx.SetValue("user.has_cross_department_access", false)

result := authzService.Check(
    &ObjectAndRelation{Namespace: "document", ObjectID: "classified-report-001", Relation: "viewer"},
    &DirectSubject{Namespace: "user", ObjectID: "alice"},
    ctx,
)

// Expected: GRANTED
// - Employment: employee (TRUE) OR contractor (FALSE) = TRUE
// - NOT suspended: NOT FALSE = TRUE
// - Employment check: TRUE AND TRUE = TRUE
// - Clearance: 4 >= 3 = TRUE
// - Business hours: local_hour(1640000000, "America/New_York") = 9
//   9 >= 9 AND 9 < 17 = TRUE
// - Department: "Intelligence" == "Intelligence" = TRUE
// - Final: TRUE AND TRUE AND TRUE AND TRUE = TRUE

Scenario 2: Suspended Employee (Denied)

ctx := NewCaveatContext()
ctx.SetValue("user.employment_type", "employee")
ctx.SetValue("user.is_suspended", true)  // ← Suspended!
ctx.SetValue("user.clearance_level", 4)
ctx.SetValue("document.classification_level", 3)
ctx.SetValue("env.now_utc", 1640000000)
ctx.SetValue("user.timezone", "America/New_York")
ctx.SetValue("user.department", "Intelligence")
ctx.SetValue("document.department", "Intelligence")
ctx.SetValue("user.has_cross_department_access", false)

result := authzService.Check(...)

// Expected: DENIED
// - Employment: employee (TRUE) OR contractor (FALSE) = TRUE
// - NOT suspended: NOT TRUE = FALSE ← Fails here!
// - Employment check: TRUE AND FALSE = FALSE
// - Final: FALSE (short-circuit, other checks not evaluated)

Scenario 3: Insufficient Clearance (Denied)

ctx := NewCaveatContext()
ctx.SetValue("user.employment_type", "contractor")
ctx.SetValue("user.is_suspended", false)
ctx.SetValue("user.clearance_level", 2)  // ← Too low!
ctx.SetValue("document.classification_level", 3)
ctx.SetValue("env.now_utc", 1640000000)
ctx.SetValue("user.timezone", "America/New_York")
ctx.SetValue("user.department", "Intelligence")
ctx.SetValue("document.department", "Intelligence")
ctx.SetValue("user.has_cross_department_access", false)

result := authzService.Check(...)

// Expected: DENIED
// - Employment: employee (FALSE) OR contractor (TRUE) = TRUE
// - NOT suspended: NOT FALSE = TRUE
// - Employment check: TRUE AND TRUE = TRUE
// - Clearance: 2 >= 3 = FALSE ← Fails here!
// - Final: FALSE

Scenario 4: After Hours Access (Denied)


ctx := NewCaveatContext()
ctx.SetValue("user.employment_type", "employee")
ctx.SetValue("user.is_suspended", false)
ctx.SetValue("user.clearance_level", 4)
ctx.SetValue("document.classification_level", 3)
ctx.SetValue("env.now_utc", 1640050000) // 2021-12-21 04:06:40 UTC (late night)
ctx.SetValue("user.timezone", "America/New_York")
ctx.SetValue("user.department", "Intelligence")
ctx.SetValue("document.department", "Intelligence")
ctx.SetValue("user.has_cross_department_access", false)

result := authzService.Check(...)

// Expected: DENIED
// - Employment check: TRUE
// - Clearance: TRUE
// - Business hours: local_hour(1640050000, "America/New_York") = 23
//   23 >= 9 = TRUE, but 23 < 17 = FALSE ← Fails here!
// - Final: FALSE

Scenario 5: Cross-Department Access (Granted)

ctx := NewCaveatContext()
ctx.SetValue("user.employment_type", "employee")
ctx.SetValue("user.is_suspended", false)
ctx.SetValue("user.clearance_level", 4)
ctx.SetValue("document.classification_level", 3)
ctx.SetValue("env.now_utc", 1640000000)
ctx.SetValue("user.timezone", "America/New_York")
ctx.SetValue("user.department", "Operations")  // ← Different department
ctx.SetValue("document.department", "Intelligence")
ctx.SetValue("user.has_cross_department_access", true)  // ← But has special access!

result := authzService.Check(...)

// Expected: GRANTED
// - Employment check: TRUE
// - Clearance: TRUE
// - Business hours: TRUE
// - Department: "Operations" == "Intelligence" = FALSE
//   BUT has_cross_department_access = TRUE
//   FALSE OR TRUE = TRUE ← Passes!
// - Final: TRUE

Scenario 6: Missing Parameter (Requires Context)

ctx := NewCaveatContext()
ctx.SetValue("user.employment_type", "employee")
// Missing: user.is_suspended
ctx.SetValue("user.clearance_level", 4)
ctx.SetValue("document.classification_level", 3)
ctx.SetValue("env.now_utc", 1640000000)
ctx.SetValue("user.timezone", "America/New_York")
ctx.SetValue("user.department", "Intelligence")
ctx.SetValue("document.department", "Intelligence")
ctx.SetValue("user.has_cross_department_access", false)

result := authzService.Check(...)

// Expected: REQUIRES_CONTEXT
// - Employment: employee (TRUE) OR contractor (FALSE) = TRUE
// - NOT suspended: is_suspended is missing → REQUIRES_CONTEXT
//   NOT REQUIRES_CONTEXT = REQUIRES_CONTEXT
// - Employment check: TRUE AND REQUIRES_CONTEXT = REQUIRES_CONTEXT
// - Final: REQUIRES_CONTEXT
//
// Missing parameters: ["user.is_suspended"]

Observability: Debugging Complex Caveats

Problem: How do you debug a complex caveat evaluation when access is unexpectedly denied or granted?

Solution: Evaluation traces detailed step-by-step logs of caveat evaluation.

Evaluation Trace Structure

type EvaluationTrace struct {
    CaveatName   string
    Result       CaveatEvalResult
    Duration     time.Duration
    Steps        []EvaluationStep
    MissingParams []string
}

type EvaluationStep struct {
    Type       string           // "BOOL_AND", "BOOL_OR", "BOOL_NOT", "PREDICATE", "FUNCTION"
    Expression string           // Human-readable expression
    Result     CaveatEvalResult // TRUE, FALSE, REQUIRES_CONTEXT
    Duration   time.Duration
    Children   []EvaluationStep
    Details    map[string]any   // Additional context
}

// Enable tracing for debugging
result := authzService.CheckWithTrace(resource, subject, ctx)
fmt.Println(result.Trace.Pretty())

Example: Trace Output

Implementation

func EvaluateBooleanExprWithTrace(expr *BooleanExpr, ctx *CaveatContext, params []*CaveatParameter) (CaveatEvalResult, *EvaluationStep) {
    start := time.Now()
    step := &EvaluationStep{
        Type:       expr.Op.String(),
        Expression: expr.String(),
        Children:   []EvaluationStep{},
        Details:    make(map[string]any),
    }

    switch expr.Op {
    case BOOL_AND:
        hasRequiresContext := false
        for _, child := range expr.Children {
            childResult, childStep := EvaluateBooleanExprWithTrace(child, ctx, params)
            step.Children = append(step.Children, *childStep)

            if childResult == FALSE {
                step.Result = FALSE
                step.Duration = time.Since(start)
                step.Details["short_circuit"] = true
                return FALSE, step
            }
            if childResult == REQUIRES_CONTEXT {
                hasRequiresContext = true
            }
        }

        if hasRequiresContext {
            step.Result = REQUIRES_CONTEXT
        } else {
            step.Result = TRUE
        }

    case BOOL_OR:
        // Similar implementation...

    case BOOL_PREDICATE:
        leftVal, leftResult := EvaluateCaveatExpr(expr.Pred.Left, ctx)
        rightVal, rightResult := EvaluateCaveatExpr(expr.Pred.Right, ctx)

        step.Details["left_value"] = leftVal
        step.Details["right_value"] = rightVal
        step.Details["operator"] = expr.Pred.Op.String()

        if leftResult == REQUIRES_CONTEXT || rightResult == REQUIRES_CONTEXT {
            step.Result = REQUIRES_CONTEXT
        } else {
            result := ApplyCompareOp(expr.Pred.Op, leftVal, rightVal)
            step.Result = result
        }
    }

    step.Duration = time.Since(start)
    return step.Result, step
}

Metrics & Monitoring

Recommended metrics to track:

// Caveat evaluation metrics
caveat_evaluation_duration_ms{caveat_name, result}
caveat_evaluation_total{caveat_name, result}
caveat_function_calls_total{function_name, version}
caveat_missing_parameters_total{parameter_name}
caveat_type_errors_total{caveat_name, error_type}
caveat_evaluation_depth{caveat_name}

// Example Prometheus queries
// P99 evaluation latency by caveat
histogram_quantile(0.99, caveat_evaluation_duration_ms{caveat_name="classified_document_access"})

// Most common missing parameters
topk(10, sum by (parameter_name) (caveat_missing_parameters_total))

// Caveat success rate
sum(caveat_evaluation_total{result="TRUE"}) / sum(caveat_evaluation_total)

Security note: When exporting metrics, hash parameter names if they might contain PII (e.g., user.email, user.ssn). Use a stable hash function to preserve aggregation while protecting sensitive data:

// Hash parameter names to avoid PII leaks in metrics
func hashParameterName(name string) string {
    h := sha256.Sum256([]byte(name))
    return hex.EncodeToString(h[:8]) // Use first 8 bytes for readability
}

// Safe metric export
caveat_missing_parameters_total{parameter_hash=hashParameterName("user.email")}

Alerting rules:

# Alert if caveat evaluation is too slow
- alert: SlowCaveatEvaluation
  expr: caveat_evaluation_duration_ms{quantile="0.99"} > 100
  annotations:
    summary: "Caveat {{ $labels.caveat_name }} is slow (P99: {{ $value }}ms)"

# Alert if many evaluations require context
- alert: HighMissingParameterRate
  expr: sum(caveat_evaluation_total{result="REQUIRES_CONTEXT"}) / sum(caveat_evaluation_total) > 0.1
  annotations:
    summary: "{{ $value }}% of evaluations missing parameters"

Edge Cases and Safety Measures

Edge Case 1: Deeply Nested Boolean Expressions

Problem: What if someone creates a caveat with 100 levels of nested AND/OR?

Solution: Enforce maximum expression depth during schema validation using configurable limits.

// Evaluation limits configuration
type EvaluationLimits struct {
    MaxExprDepth int  // Maximum boolean expression depth (default: 10)
    MaxFuncDepth int  // Maximum function nesting depth (default: 3)
}

func DefaultEvaluationLimits() *EvaluationLimits {
    return &EvaluationLimits{
        MaxExprDepth: 10,
        MaxFuncDepth: 3,
    }
}

func ValidateBooleanExpr(expr *BooleanExpr, depth int, limits *EvaluationLimits) error {
    if depth > limits.MaxExprDepth {
        return fmt.Errorf("expression depth exceeds maximum of %d", limits.MaxExprDepth)
    }

    for _, child := range expr.Children {
        if err := ValidateBooleanExpr(child, depth+1, limits); err != nil {
            return err
        }
    }

    return nil
}

Why this matters: Prevents stack overflow and ensures reasonable evaluation time. Limits are configurable for different deployment environments.

Edge Case 2: Function Call Nesting Depth

Problem: What if functions are nested too deeply, causing stack overflow or excessive computation?

Solution: Allow nested CallExpr with maximum depth limit functions can call other functions, but with bounded nesting.

func ValidateCallExpr(expr *CallExpr, depth int, limits *EvaluationLimits) error {
    if depth > limits.MaxFuncDepth {
        return fmt.Errorf("function nesting depth exceeds maximum of %d", limits.MaxFuncDepth)
    }

    for _, arg := range expr.Args {
        if callArg, ok := arg.(*CallExpr); ok {
            if err := ValidateCallExpr(callArg, depth+1, limits); err != nil {
                return err
            }
        }
    }

    return nil
}

Examples:

// ✅ Valid: Simple function call (depth 1)
&CallExpr{
    FnName: "local_hour",
    Args: []CaveatExpr{
        &IdentifierExpr{Name: "env.now_utc"},
        &IdentifierExpr{Name: "user.timezone"},
    },
}

// ✅ Valid: Nested function call (depth 2)
&CallExpr{
    FnName: "format_date",
    Args: []CaveatExpr{
        &CallExpr{
            FnName: "local_hour",
            Args: []CaveatExpr{
                &IdentifierExpr{Name: "env.now_utc"},
                &IdentifierExpr{Name: "user.timezone"},
            },
        },
    },
}

// ❌ Invalid: Exceeds depth limit (depth 4)
&CallExpr{
    FnName: "fn1",
    Args: []CaveatExpr{
        &CallExpr{
            FnName: "fn2",
            Args: []CaveatExpr{
                &CallExpr{
                    FnName: "fn3",
                    Args: []CaveatExpr{
                        &CallExpr{FnName: "fn4", Args: []CaveatExpr{}},
                    },
                },
            },
        },
    },
}

Rationale:

  • Composability: Allows useful patterns like to_lower(trim(email))

  • Safety: Depth limit prevents stack overflow and excessive computation

  • Determinism: All functions are pure and bounded, so cycles are impossible

  • Performance: Shallow nesting (≤3) has negligible overhead

Edge Case 3: Type Mismatches

Problem: What if caller provides wrong type for a parameter, or if predicates compare incompatible types?

Solution: Two-layer type validation static validation at schema creation + runtime validation at evaluation.

Layer 1: Static Type Validation (Schema Creation Time)

Validate type compatibility when the caveat definition is created:

func ValidateCaveatDefinition(def *CaveatDefinition) error {
    // Build parameter type map
    paramTypes := make(map[string]CaveatParameterType)
    for _, param := range def.Parameters {
        paramTypes[param.Name] = param.Type
    }

    // Validate expression tree
    return ValidateExpressionTypes(def.Expression, paramTypes)
}

func ValidateExpressionTypes(expr *BooleanExpr, paramTypes map[string]CaveatParameterType) error {
    switch expr.Op {
    case BOOL_PREDICATE:
        return ValidatePredicateTypes(expr.Pred, paramTypes)
    case BOOL_AND, BOOL_OR:
        for _, child := range expr.Children {
            if err := ValidateExpressionTypes(child, paramTypes); err != nil {
                return err
            }
        }
    case BOOL_NOT:
        return ValidateExpressionTypes(expr.Children[0], paramTypes)
    }
    return nil
}

func ValidatePredicateTypes(pred *Predicate, paramTypes map[string]CaveatParameterType) error {
    // Get types of left and right sides
    leftType := GetExprType(pred.Left, paramTypes)
    rightType := GetExprType(pred.Right, paramTypes)

    // Check if types are compatible with the operator
    if !TypesCompatibleForOp(leftType, rightType, pred.Op) {
        return fmt.Errorf(
            "type mismatch in predicate: cannot compare %s with %s using %s",
            leftType, rightType, pred.Op,
        )
    }

    return nil
}

func GetExprType(expr CaveatExpr, paramTypes map[string]CaveatParameterType) CaveatParameterType {
    switch e := expr.(type) {
    case *IdentifierExpr:
        return paramTypes[e.Name]
    case *LiteralExpr:
        return e.Type
    case *CallExpr:
        // Look up function return type
        // (Implementation detail: function registry would provide this)
        return GetFunctionReturnType(e.FnName)
    }
    return CAV_T_UNKNOWN
}

func TypesCompatibleForOp(left, right CaveatParameterType, op CompareOp) bool {
    switch op {
    case CMP_EQ, CMP_NE:
        // Equality works for same types
        return left == right
    case CMP_LT, CMP_LE, CMP_GT, CMP_GE:
        // Comparison works for numeric types
        return isNumeric(left) && isNumeric(right)
    case CMP_IN:
        // IN requires right to be a list AND left's type must match the list's element type
        // This ensures type safety: checking if INT is in LIST<STRING> is invalid
        return right == CAV_T_LIST && left == listElementType(right)
    case CMP_STARTS_WITH, CMP_ENDS_WITH, CMP_CONTAINS:
        // String operations require both to be strings
        return left == CAV_T_STRING && right == CAV_T_STRING
    }
    return false
}

Example: Invalid schema rejected at creation time

// ❌ This will fail validation
invalidCaveat := &CaveatDefinition{
    Name: "invalid_type_comparison",
    Parameters: []*CaveatParameter{
        &ScalarParameter{Name: "user.age", Type: CAV_T_INT},
        &ScalarParameter{Name: "user.department", Type: CAV_T_STRING},
    },
    Expression: &BooleanExpr{
        Op: BOOL_PREDICATE,
        Pred: &Predicate{
            Left:  &IdentifierExpr{Name: "user.age"},        // INT
            Op:    CMP_EQ,
            Right: &IdentifierExpr{Name: "user.department"}, // STRING
        },
    },
}

err := ValidateCaveatDefinition(invalidCaveat)
// Error: "type mismatch in predicate: cannot compare int with string using =="

Layer 2: Runtime Type Validation (Evaluation Time)

Validate actual values match declared types:

func EvaluatePredicate(pred *Predicate, ctx *CaveatContext, params []*CaveatParameter) (CaveatEvalResult, error) {
    leftVal, leftResult := EvaluateCaveatExpr(pred.Left, ctx)
    if leftResult == REQUIRES_CONTEXT {
        return CaveatResultRequiresContext(), nil
    }

    rightVal, rightResult := EvaluateCaveatExpr(pred.Right, ctx)
    if rightResult == REQUIRES_CONTEXT {
        return CaveatResultRequiresContext(), nil
    }

    // Runtime type check: ensure actual values match expected types
    if !ValueMatchesType(leftVal, GetExpectedType(pred.Left, params)) {
        return CaveatResultFalse(), fmt.Errorf(
            "runtime type mismatch: expected %s, got %T",
            GetExpectedType(pred.Left, params), leftVal,
        )
    }

    if !ValueMatchesType(rightVal, GetExpectedType(pred.Right, params)) {
        return CaveatResultFalse(), fmt.Errorf(
            "runtime type mismatch: expected %s, got %T",
            GetExpectedType(pred.Right, params), rightVal,
        )
    }

    // Apply comparison
    result := ApplyCompareOp(pred.Op, leftVal, rightVal)
    return result, nil
}

Why two layers?

  1. Static validation catches errors early (at schema creation), preventing invalid policies from being deployed

  2. Runtime validation catches caller errors (wrong type provided in context), providing clear debugging information

  3. Defense in depth: Both layers together ensure type safety

Why this matters: Prevents runtime panics, catches errors early, and provides clear debugging information.

What We've Gained

Let's compare what we can express now vs. before:

🔴 Post 5 (ABAC v1) — Simple Predicates Only

Boolean Logic:

  • Single predicate only

  • Can't combine conditions with AND/OR/NOT

Function Calls:

  • Reserved (not active)

  • No dynamic computations

Complexity:

  • Simple: A >= B

  • One condition per caveat

Use Cases:

  • Time windows

  • Simple attribute checks

Evaluation:

  • ~5μs per check

  • Fast but limited

Type Safety:

  • Runtime validation only

  • Errors caught during evaluation

Observability:

  • Basic logging

  • Limited debugging info

Versioning:

  • No function versioning

  • Breaking changes on updates

🟢 Post 7 (ABAC v2) — Full Boolean Algebra + Functions

Boolean Logic:

  • Full AND/OR/NOT support

  • Can express complex policies like (A ∨ B) ∧ ¬C ∧ D

Function Calls:

  • Active with whitelist registry

  • Dynamic computations: time conversions, string operations, list checks

Complexity:

  • Complex: (A ∨ B) ∧ ¬C ∧ D

  • Unlimited conditions per caveat

Use Cases:

  • Security clearance systems

  • HIPAA compliance rules

  • Financial transaction approval

  • Matches real-world systems (Google Cloud IAM, AWS IAM, Azure ABAC)

Evaluation:

  • ~50μs per complex caveat evaluation

  • ≈10× more work per check but typically < 100μs (acceptable for authorization)*

*Single-expression evaluation only; graph traversal adds proportional cost (Post 8).

Type Safety:

  • Static validation at schema creation

  • Runtime validation at evaluation

  • Catch errors early, before deployment

Observability:

  • Full evaluation traces

  • Step-by-step debugging

  • Metrics and alerting

Versioning:

  • Explicit function versions (v1, v2, etc.)

  • Safe updates with gradual migration

  • No breaking changes

Takeaways

What we built:

  1. Composite caveats — Combine multiple conditions with boolean algebra (AND, OR, NOT)

  2. Function calls — Dynamic computations with whitelist-based registry and versioning

  3. Tri-state logic — TRUE, FALSE, REQUIRES_CONTEXT for handling missing parameters

  4. Short-circuit evaluation — Efficient evaluation with early termination

  5. Evaluation traces — Step-by-step debugging for complex policies

  6. Safety measures — Depth limits, type validation, determinism guarantees

Design principles:

  • Determinism: Same inputs → same outputs (no side effects, no randomness)

  • Caller-provided context: Time and dynamic values come from caller, not system

  • Pure functions: All functions are pure, total, and bounded

  • Type safety: Two-layer validation (static + runtime)

  • Observability: Full traces for debugging complex policies

Real-world impact:

  • Express complex policies like security clearance systems

  • Match capabilities of Google Cloud IAM, AWS IAM, Azure ABAC

  • Maintain sub-100μs evaluation latency

  • Provide clear debugging information with evaluation traces

  • Ensure type safety with two-layer validation

What's Next?

In Post 8, we'll tackle the performance challenge of evaluating caveats during graph traversal:

The problem:

  • Post 7 evaluates a single caveat in ~50μs

  • But authorization checks traverse entire permission graphs

  • A single Check() might evaluate hundreds of tuples, each with its own caveat

  • Naive approach: 100 tuples × 50μs = 5ms per check (too slow!)

What we'll build:

  • Caveat-aware graph traversal — Integrate caveat evaluation into the graph algorithm

  • Context propagation — Pass caveat context through the graph efficiently

  • Early termination — Stop traversal as soon as we know the answer

  • Batch evaluation — Evaluate multiple caveats in parallel when possible

The goal: Maintain sub-millisecond latency for authorization checks, even with complex caveats on every tuple.

Summary

We extended Enma's ABAC system from simple predicates to full boolean algebra with function calls, enabling real-world policies like security clearance systems. We added composite caveats to combine multiple conditions, evaluation traces for debugging, and safety measures (depth limits, type validation) to prevent abuse.

The result: a production-ready ABAC system that matches the capabilities of Google Cloud IAM, AWS IAM, and Azure ABAC, while maintaining sub-100μs evaluation latency and providing clear debugging information.

Next up: Post 8 Integrating caveat evaluation into graph traversal for sub-millisecond authorization checks.

Keep Reading