Architecture and Advanced Topics
Understanding PolicyFlow's architecture and evaluation model
This section covers the architectural concepts behind PolicyFlow, including policy composition, evaluation flow, conflict resolution, and performance considerations.
Policy Composition
PolicyFlow's architecture is built on the principle of policy composition - multiple independent policies work together to make authorization decisions.
The Composition Model
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Universal │ │ Conditional │ │ Conditional │
│ Policy A │ │ Policy B │ │ Policy C │
│ (All requests) │ │ (EU users) │ │ (Financial) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└───────────────────────┴───────────────────────┘
│
┌───────▼────────┐
│ Policy Engine │
│ (Evaluation & │
│ Resolution) │
└───────┬────────┘
│
┌───────▼────────┐
│ Final Decision │
│ ALLOW / DENY │
└────────────────┘
Benefits of Composition
- Separation of Concerns: Each policy handles a specific aspect
- Reusability: Policies can be shared across applications
- Maintainability: Smaller, focused policies are easier to understand
- Scalability: New policies can be added without modifying existing ones
Evaluation Flow
The PolicyFlow engine follows a systematic evaluation process:
1. Request Receipt
The engine receives an authorization request containing:
- User: The principal performing the action
- Resource: The object being accessed
- Action: The operation being attempted
- Context: Additional request information
2. Policy Selection
The engine filters all loaded policies:
// Step 1: Type matching
for each policy in all_policies:
if policy.schemas.User matches request.user.type
AND policy.schemas.Resource matches request.resource.type
AND policy.schemas.Context matches request.context.type:
candidate_policies.add(policy)
// Step 2: Where clause evaluation
for each policy in candidate_policies:
if policy.schemas.where_conditions.evaluate(request):
selected_policies.add(policy)
// Step 3: Action filtering (if specified)
for each policy in selected_policies:
if policy.actions is empty OR request.action matches policy.actions:
policies_to_evaluate.add(policy)
3. Rule Evaluation
For each selected policy, the engine determines a decision. This pseudocode illustrates the logic for a single policy evaluation.
function evaluate_policy(policy, request):
let allow_found = false
// Evaluate rules in priority order (lower number first)
rules_sorted = policy.rules.sortBy(r => r.priority ?? 5000)
for each rule in rules_sorted:
if rule.when.evaluate(request):
decision = rule.then // ALLOW or DENY
// Deny-Overrides: If any rule denies, the policy's decision is DENY.
if decision == DENY:
return DENY
if decision == ALLOW:
allow_found = true
// Continue evaluating to check for a subsequent DENY
// If we finished the loop without finding a DENY:
if allow_found:
return ALLOW
// If no rules matched, return DENY (secure by default)
return DENY
4. Final Resolution
After evaluating all applicable policies, the engine combines the results using a strict Deny-Overrides strategy.
function resolve_all_decisions(policy_decisions):
// If ANY policy returned DENY, the final decision is DENY.
if policy_decisions.any(d => d == DENY):
return DENY
// If at least one policy returned ALLOW and none returned DENY.
if policy_decisions.any(d => d == ALLOW):
return ALLOW
// If no policies were selected to be evaluated, deny by default.
return DENY
Conflict Resolution Strategy
PolicyFlow uses a strict Deny-Overrides strategy: if ANY rule or policy returns DENY, the request is denied.
Priority System
Priorities control evaluation order only, not decision outcomes:
Priority | Description | Purpose |
---|---|---|
0-999 | Evaluated first | Quick security checks, fail fast |
1000-2999 | Early evaluation | Compliance checks |
3000-4999 | Mid evaluation | Business logic |
5000 | Default priority | Standard rules |
5001-7999 | Later evaluation | Additional checks |
8000-10000 | Evaluated last | Expensive operations |
Key Principles
- Deny Always Wins: One DENY anywhere = request denied
- Priority is for Performance: Lower priority rules are evaluated first to fail fast
- No Override Mechanism: Priority cannot make an ALLOW override a DENY
Resolution Examples
Example 1: Priority Affects Evaluation Order
// Policy A
rule QuickSecurityCheck {
when user.isSuspended
then DENY
priority: 100 // Evaluated first - fail fast
}
// Policy B
rule ExpensiveOwnerCheck {
when {
// Complex ownership validation
const isOwner = complexOwnershipCheck(user, resource);
return isOwner;
}
then ALLOW
priority: 9000 // Evaluated last - expensive
}
// If user is suspended, QuickSecurityCheck denies immediately
// ExpensiveOwnerCheck is never evaluated (performance optimization)
Example 2: Deny Always Wins
// Policy A
rule AdminFullAccess {
when "admin" in user.roles
then ALLOW
priority: 0 // Highest priority (evaluated first)
}
// Policy B
rule ComplianceRestriction {
when resource.underAudit == true
then DENY
priority: 10000 // Lowest priority (evaluated last)
}
// Result: DENY
// Even though AdminFullAccess has priority 0 and ComplianceRestriction has
// priority 10000, the DENY wins. Priority only controlled evaluation order.
Performance Optimization
1. Policy Selection Optimization
The most critical optimization is reducing the number of policies evaluated:
// Indexed attributes for fast filtering
policy OptimizedPolicy {
schemas {
// These attributes should be indexed:
User from Auth.User where user.isActive == true
Resource from Data.Resource where resource.type == "document"
Context from Web.Context where context.environment == "production"
}
}
2. Rule Ordering
Use priority to optimize evaluation order:
rules {
// Quick denials first (priority 1000) - fail fast
rule DenyInactive {
when !user.isActive
then DENY
priority: 1000
}
// Common allows next (priority 5000 - default)
rule PublicReadAccess {
when resource.isPublic AND action == "read"
then ALLOW
}
// Expensive checks last (priority 8000)
rule ComplexOwnershipCheck {
when {
// Complex computation
const ownership = calculateOwnershipChain(user, resource);
return ownership.isValid;
}
then ALLOW
priority: 8000
}
}
// Performance benefit: If user is inactive, we deny immediately
// without running the expensive ownership calculation
3. Caching Strategy
// Cache-friendly policy design
policy CacheablePolicy {
// Use stable attributes for caching
schemas {
User from Auth.User where user.accountType in ["basic", "premium"]
Resource from Data.Resource where resource.cacheKey != null
}
rules {
// Avoid time-based conditions in cached policies
rule StableRule {
when user.subscription == "active" AND resource.isPublished
then ALLOW
}
}
}
// Non-cacheable policy (separate)
policy TimeBasedPolicy {
rules {
rule TimeCheck {
when {
const hour = DateTime.Now().ToUnit(TimeUnit.Hours)
return hour >= 9 AND hour < 17
}
then ALLOW
}
}
}
4. Relationship Query Optimization
// Efficient: Check existence
when Relationships.Has(user, "member_of", resource.team)
then ALLOW
// Less efficient: Retrieve all relations
when {
const teams = Relationships.GetRelated(user, "member_of")
return resource.team in teams
}
then ALLOW
// Optimize path queries
when Relationships.PathExists(user, "manages", resource.owner)
then ALLOW
Deployment Architecture
Typical Deployment
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Application │────►│ Policy │────►│ Policy │
│ (PEP) │ │ Decision │ │ Information │
│ │◄────│ Point (PDP) │◄────│ Point (PIP) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ PolicyFlow │ │ - User Service │
│ Engine │ │ - Resource DB │
│ │ │ - Context API │
│ - Policy Cache │ └─────────────────┘
│ - Rule Engine │
│ - Decision Log │
└─────────────────┘
Scaling Considerations
- Horizontal Scaling: PolicyFlow engines are stateless and can be scaled horizontally
- Policy Distribution: Use a policy repository with versioning
- Caching Layer: Implement decision caching for frequently repeated requests
- Monitoring: Track policy evaluation times and cache hit rates
Advanced Patterns
1. Dynamic Policy Loading
// Policy with feature flags
policy FeatureAccess {
rules {
rule BetaFeature {
when env["BETA_ENABLED"] == true
AND "beta_tester" in user.roles
then ALLOW
}
}
}
2. Multi-Tenant Policies
policy TenantIsolation {
schemas {
User from Auth.User
Resource from Data.Resource where resource.tenantId != null
}
rules {
rule SameTenantOnly {
when user.tenantId != resource.tenantId
then DENY
priority: 0 // Check first for performance
reason: "Cross-tenant access prohibited"
}
}
}