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 Range | Usage | Example |
---|---|---|
0-999 | Critical checks evaluated first | Security lockdowns, emergency stops |
1000-2999 | Compliance checks | GDPR, HIPAA, SOX requirements |
3000-4999 | Important business rules | Admin checks, special permissions |
5000 | Default (no priority specified) | Standard business logic |
5001-7999 | Standard business rules | Department access, role checks |
8000-10000 | Evaluated last | Expensive 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
- Rule Evaluation: Rules are evaluated in priority order (lower numbers first)
- Short-Circuit on DENY: If any rule returns DENY, evaluation stops and the request is denied
- Continue on ALLOW: If a rule returns ALLOW, continue evaluating remaining rules
- 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:
- Performance: Evaluate cheap denials first to fail fast
- Debugging: Control evaluation order for logging/debugging
- Readability: Group related rules by priority ranges
- 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
}