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:
User is (employee OR contractor) AND NOT suspended
User's clearance level >= document's classification level
Access is during business hours (9 AM - 5 PM) in user's timezone
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_accessWhy 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_restrictionRecommendation: 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 = TRUEScenario 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: FALSEScenario 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: TRUEScenario 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?
Static validation catches errors early (at schema creation), preventing invalid policies from being deployed
Runtime validation catches caller errors (wrong type provided in context), providing clear debugging information
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 >= BOne 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 ∧ DUnlimited 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:
Composite caveats — Combine multiple conditions with boolean algebra (AND, OR, NOT)
Function calls — Dynamic computations with whitelist-based registry and versioning
Tri-state logic — TRUE, FALSE, REQUIRES_CONTEXT for handling missing parameters
Short-circuit evaluation — Efficient evaluation with early termination
Evaluation traces — Step-by-step debugging for complex policies
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 caveatNaive 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.
