PolicyFlow

Rules and Decisions

Understanding rule structure, priorities, and decision logic

Rules are the building blocks of authorization logic in PolicyFlow. Each rule evaluates a condition and produces a decision (ALLOW or DENY) when that condition is met.

Rule Structure

A rule consists of several components:

rule RuleName {
    when <condition>
    then <decision>
    priority: <number>            // Optional (0-10000, default 5000)
    reason: <explanation>         // Optional but recommended
}

Basic Rule Example

rule OwnerCanRead {
    when user.id == resource.ownerId AND action == "read"
    then ALLOW
    reason: "Resource owners can read their own resources"
}

Rule Conditions

Simple Conditions

The when clause can contain simple boolean expressions:

rule AdminAccess {
    when "admin" in user.roles
    then ALLOW
}

rule ActiveUserAccess {
    when user.isActive AND user.emailVerified
    then ALLOW
}

Complex Conditions with Blocks

For complex logic, use a block with multiple statements:

rule ComplexAccessCheck {
    when {
        // Early return for super admins
        if ("super-admin" in user.roles) {
            return true
        }

        // Check department match
        const sameDepartment = user.department == resource.department

        // Check clearance level
        const hasClearance = user.clearanceLevel >= resource.requiredClearance

        // Check time-based access
        const currentHour = DateTime.Now().ToUnit(TimeUnit.Hours)
        const duringBusinessHours = currentHour >= 9 AND currentHour < 17

        // Combine all conditions
        return sameDepartment AND hasClearance AND duringBusinessHours
    }
    then ALLOW
    reason: "Access granted based on department, clearance, and time"
}

Using Helper Functions

Rules can call policy-level functions:

policy DocumentAccess {
    function isManager(user: User, targetUserId: UUID): Boolean {
        return user.directReports.Contains(targetUserId) ||
               user.id == targetUserId;
    }

    rules {
        rule ManagerCanViewReports {
            when isManager(user, resource.ownerId)
                AND action == "view-performance"
            then ALLOW
            reason: "Managers can view their reports' performance"
        }
    }
}

Priorities

Priorities determine the evaluation order of rules. Rules with lower priority numbers are evaluated first, but this does NOT affect the final decision outcome.

Priority System

  • Range: 0 to 10000
  • Default: 5000 (if not specified)
  • Lower numbers = Evaluated first
  • Important: Priority does NOT override decisions. If ANY rule returns DENY, the request is denied regardless of priorities.

Priority Guidelines

Priority RangeUsageExample
0-999Critical checks evaluated firstSecurity lockdowns, emergency stops
1000-2999Compliance checksGDPR, HIPAA, SOX requirements
3000-4999Important business rulesAdmin checks, special permissions
5000Default (no priority specified)Standard business logic
5001-7999Standard business rulesDepartment access, role checks
8000-10000Evaluated lastExpensive operations, fallbacks

Priority Examples

rules {
    // Priority 0: Evaluated first
    rule SecurityLockdown {
        when context.securityAlert == true
        then DENY
        priority: 0
        reason: "System is in security lockdown mode"
    }

    // Priority 1500: Evaluated second
    rule GDPRCompliance {
        when resource.containsPII AND user.region != "EU" AND resource.region == "EU"
        then DENY
        priority: 1500
        reason: "GDPR: Cannot access EU PII from outside EU"
    }

    // Priority 3500: Evaluated third
    rule AdminOverride {
        when "admin" in user.roles
        then ALLOW
        priority: 3500
        reason: "Administrator access"
    }

    // Priority 5000: Default (evaluated fourth)
    rule DepartmentAccess {
        when user.department == resource.department
        then ALLOW
        reason: "Same department access"
    }

    // Priority 8000: Evaluated last
    rule ExpensiveCheck {
        when {
            // Some expensive computation
            const result = performExpensiveCheck(user, resource);
            return result;
        }
        then ALLOW
        priority: 8000
        reason: "Passed expensive validation"
    }
}

// IMPORTANT: If ANY of these rules returns DENY, the final decision is DENY
// regardless of the priority values. Priority only controls evaluation order.

Decision Flow

How Decisions Are Made

  1. Rule Evaluation: Rules are evaluated in priority order (lower numbers first)
  2. Short-Circuit on DENY: If any rule returns DENY, evaluation stops and the request is denied
  3. Continue on ALLOW: If a rule returns ALLOW, continue evaluating remaining rules
  4. Final Decision:
  • If ANY rule returned DENY → Final decision is DENY
  • If NO rules returned DENY but at least one returned ALLOW → Final decision is ALLOW
  • If NO rules matched → Request is denied (secure by default)

Deny-Overrides Strategy

PolicyFlow uses a strict Deny-Overrides strategy:

  • One DENY anywhere = DENY: If any rule in any policy returns DENY, the request is denied
  • Priority doesn't change outcomes: Priority only controls evaluation order for performance
  • No exceptions: Even 1 DENY with priority 10000 overrides 100 ALLOWs with priority 0

Example Decision Flow

policy ExamplePolicy {
    rules {
        // Evaluated first (priority 100)
        rule ExpensiveSecurityCheck {
            when performExpensiveSecurityCheck(user, resource)
            then DENY
            priority: 100
        }

        // Evaluated second (priority 2000)
        rule AdminAccess {
            when "admin" in user.roles
            then ALLOW
            priority: 2000
        }

        // Evaluated third (priority 5000 - default)
        rule OwnerAccess {
            when user.id == resource.ownerId
            then ALLOW
        }
    }
}

// Scenario 1: Admin accessing their own resource
// - ExpensiveSecurityCheck: DENY
// Result: DENY (stops here, admin check never runs)

// Scenario 2: Admin with security clearance
// - ExpensiveSecurityCheck: No match (passes check)
// - AdminAccess: ALLOW
// - OwnerAccess: ALLOW
// Result: ALLOW (no denies found)

// Scenario 3: Regular user, not owner
// - ExpensiveSecurityCheck: No match
// - AdminAccess: No match
// - OwnerAccess: No match
// Result: DENY (no rules matched, so the policy denies by default)

Why Use Priorities?

Since DENY always wins, priorities serve these purposes:

  1. Performance: Evaluate cheap denials first to fail fast
  2. Debugging: Control evaluation order for logging/debugging
  3. Readability: Group related rules by priority ranges
  4. Short-circuiting: Stop expensive checks if cheap checks already deny
rules {
    // Check simple denials first (fast)
    rule QuickDenyCheck {
        when user.isBanned
        then DENY
        priority: 0  // Check first, fail fast
    }

    // Expensive checks last (slow)
    rule ExpensiveValidation {
        when {
            // Complex computation
            const valid = performDetailedValidation(user, resource)
            return valid
        }
        then ALLOW
        priority: 9000  // Check last, only if needed
    }
}

Context Variable

The context variable is automatically available in all rules:

rule SecureConnection {
    when context.protocol == "https"
    then ALLOW
    reason: "Secure connection required"
}

rule InternalNetwork {
    when context.ipAddress.Matches("10.0.0.0/8")
    then ALLOW
    priority: 3000
    reason: "Internal network access"
}

Best Practices

1. Use Clear Reason Strings

Always provide clear, actionable reason strings:

// Good
reason: "User lacks required clearance level 3 for classified documents"

// Bad
reason: "Access denied"

2. Choose Appropriate Priorities

// Security and compliance rules: 0-2999
rule EmergencyShutdown {
    when env["EMERGENCY_MODE"] == "true"
    then DENY
    priority: 100
}

// Business logic: 3000-7999
rule ManagerAccess {
    when user.role == "manager"
    then ALLOW
    priority: 5000  // Default
}

// Fallback rules: 8000-10000
rule DefaultPublicRead {
    when action == "read" AND resource.visibility == "public"
    then ALLOW
    priority: 9000
}

3. Order Rules Logically

Even though priority determines the final decision, ordering rules logically improves readability:

rules {
    // Denial rules first
    rule DenyExpiredAccounts {
        when user.accountExpired
        then DENY
        priority: 1000
    }

    // Then allow rules
    rule AllowActiveUsers {
        when user.isActive
        then ALLOW
    }

    // Then general/fallback rules
    rule PublicReadAccess {
        when resource.isPublic AND action == "read"
        then ALLOW
        priority: 8000
    }
}

4. Keep Rules Focused

Each rule should check one logical concept:

// Good: Focused rules
rule CheckOwnership {
    when user.id == resource.ownerId
    then ALLOW
}

rule CheckDepartment {
    when user.department == resource.department
    then ALLOW
}

// Bad: Mixed concerns
rule CheckEverything {
    when (user.id == resource.ownerId OR user.department == resource.department)
        AND user.isActive AND !resource.isArchived
    then ALLOW
}

Next Steps