PolicyFlow

Best Practices and Security

Design patterns and security guidelines for PolicyFlow

This guide covers best practices for writing secure, maintainable, and performant PolicyFlow policies. Following these guidelines will help you build a robust authorization system.

Security Best Practices

1. Secure by Default

PolicyFlow uses a secure-by-default approach where requests are denied unless explicitly allowed:

policy SecureByDefault {
    rules {
        // Explicit allow rules
        rule AllowOwner {
            when user.id == resource.ownerId
            then ALLOW
        }
    }

    // No explicit default needed - PolicyFlow denies by default
}

2. Principle of Least Privilege

Grant only the minimum permissions required:

// Good: Specific permissions
rule EditOwnDrafts {
    when user.id == resource.ownerId
        AND resource.status == "draft"
        AND action == "edit"
    then ALLOW
}

// Avoid: Overly broad permissions
rule OwnerAllAccess {
    when user.id == resource.ownerId
    then ALLOW  // Too permissive
}

3. Defense in Depth

Layer multiple policies for comprehensive protection:

// Layer 1: Authentication
policy AuthenticationRequired {
    rules {
        rule RequireAuth {
            when user == null OR !user.isAuthenticated
            then DENY
            priority: 0
            reason: "Authentication required"
        }
    }
}

// Layer 2: Authorization
policy ResourceAuthorization {
    schemas {
        User from Auth.AuthenticatedUser
    }
    rules {
        rule CheckPermissions {
            when !user.permissions.Contains(action)
            then DENY
            priority: 1000
        }
    }
}

// Layer 3: Compliance
policy DataCompliance {
    rules {
        rule GDPRRestriction {
            when resource.containsPII
                AND user.region != resource.dataRegion
            then DENY
            priority: 1000
            reason: "GDPR data locality requirement"
        }
    }
}

4. Secure Sensitive Operations

Add extra security for high-risk actions:

policy SensitiveOperations {
    rules {
        rule RequireMFAForDeletion {
            when action == "delete"
                AND resource.isPermanent
                AND context.authMethod != "mfa"
            then DENY
            priority: 1000
        }

        rule RequireApprovalForBulkExport {
            when action == "bulk_export"
                AND resource.recordCount > 1000
                AND !resource.hasApproval
            then DENY
            priority: 1000
            reason: "Bulk exports require approval"
        }
    }
}

Design Patterns

1. Role-Based Access Control (RBAC)

Implement clean RBAC patterns:

// Define role hierarchies in schema
schema RoleSchema {
    User type Employee {
        id: UUID
        roles: String[]
        department: String
    }
}

// Implement role-based rules
policy RoleBasedAccess {
    // Helper function for role hierarchy
    function hasRole(user: Employee, requiredRole: String): Boolean {
        if ("ADMIN" in user.roles) return true;
        if (requiredRole == "MANAGER" AND "MANAGER" in user.roles) return true;
        if (requiredRole == "EMPLOYEE" AND "EMPLOYEE" in user.roles) return true;
        return requiredRole in user.roles;
    }

    rules {
        rule AdminFullAccess {
            when hasRole(user, "ADMIN")
            then ALLOW
            priority: 1000
        }

        rule ManagerDepartmentAccess {
            when hasRole(user, "MANAGER")
                AND user.department == resource.department
            then ALLOW
        }
    }
}

2. Attribute-Based Access Control (ABAC)

Use attributes for fine-grained control:

policy AttributeBasedAccess {
    rules {
        rule ClearanceLevelAccess {
            when user.clearanceLevel >= resource.requiredClearance
                AND user.department in resource.allowedDepartments
                AND context.location in resource.allowedLocations
            then ALLOW
        }

        rule TimeWindowAccess {
            when {
                const now = DateTime.Now();
                const accessStart = resource.accessWindow.start;
                const accessEnd = resource.accessWindow.end;

                return now >= accessStart AND now <= accessEnd
                    AND user.timezone == resource.timezone;
            }
            then ALLOW
        }
    }
}

3. Relationship-Based Access Control (ReBAC)

Leverage relationships for authorization:

policy RelationshipBasedAccess {
    rules {
        rule DirectRelationship {
            when Relationships.Has(user, "owns", resource)
                OR Relationships.Has(user, "manages", resource)
            then ALLOW
        }

        rule IndirectRelationship {
            when {
                // Can access if user manages someone who owns the resource
                return Relationships.PathExists(
                    user,
                    "manages.owns",
                    resource
                );
            }
            then ALLOW
        }

        rule TeamAccess {
            when {
                // Complex relationship: user -> member_of -> team -> owns -> project -> contains -> resource
                return Relationships.PathExists(
                    user,
                    "member_of.owns.contains",
                    resource
                );
            }
            then ALLOW
        }
    }
}

Code Organization

1. Modular Policy Structure

policies/
├── core/
│   ├── authentication.pf      # Universal auth checks
│   ├── rate-limiting.pf       # API rate limits
│   └── maintenance.pf         # System maintenance mode
├── compliance/
│   ├── gdpr.pf               # GDPR compliance
│   ├── hipaa.pf              # HIPAA compliance
│   └── sox.pf                # SOX compliance
├── features/
│   ├── documents.pf          # Document access
│   ├── reporting.pf          # Report generation
│   └── admin-tools.pf        # Admin functionality
└── shared/
    └── common-rules.pf       # Shared rule definitions

2. Schema Organization

Keep schemas focused and reusable:

// schemas/base.pfs - Common base types
schema BaseSchema {
    type Entity {
        id: UUID
        createdAt: DateTime
        updatedAt: DateTime
    }

    type Auditable : Entity {
        createdBy: UUID
        lastModifiedBy: UUID
        version: Number
    }
}

// schemas/auth.pfs - Authentication types
import * as Base from "@/schemas/base.pfs";

schema AuthSchema {
    User type AuthenticatedUser : Base.Entity {
        email: Email
        isActive: Boolean
        lastLogin: DateTime
    }
}

// schemas/resources.pfs - Resource types
import * as Base from "@/schemas/base.pfs";

schema ResourceSchema {
    Resource type Document : Base.Auditable {
        title: String
        content: String
        classification: String
    }
}

3. Naming Conventions

Use clear, consistent naming:

// Policies: PascalCase, descriptive
policy DocumentAccessControl { }
policy GDPRCompliance { }

// Rules: PascalCase, action-focused
rule AllowOwnerRead { }
rule DenyExpiredAccounts { }

// Functions: camelCase, verb-based
function hasPermission() { }
function isBusinessHours() { }

// Variables: camelCase, descriptive
const userDepartment = user.department;
let effectivePermissions = [];

Performance Optimization

1. Optimize Schema Conditions

// Good: Simple conditions on indexed fields
policy OptimizedPolicy {
    schemas {
        User from Auth.User where user.isActive == true
        Resource from Data.Resource where resource.type == "document"
    }
}

// Avoid: Complex computations in where clause
policy SlowPolicy {
    schemas {
        User from Auth.User where {
            user.transactions.Filter(t => t.amount > 1000).Count() > 5
        }
    }
}

2. Efficient Rule Ordering

policy EfficientPolicy {
    rules {
        // 1. Most common cases first (fail fast)
        rule DenyInactive {
            when !user.isActive
            then DENY
        }

        // 2. Simple checks before complex ones
        rule PublicAccess {
            when resource.isPublic
            then ALLOW
        }

        // 3. Expensive operations last
        rule ComplexRelationshipCheck {
            when {
                const paths = Relationships.GetRelated(user, "manages");
                return paths.Any(p => p.owns.Contains(resource));
            }
            then ALLOW
        }
    }
}

3. Cache-Friendly Policies

Design policies that can be cached:

// Cacheable: Based on stable attributes
policy CacheablePolicy {
    rules {
        rule RoleBasedAccess {
            when user.role == "editor" AND resource.type == "article"
            then ALLOW
        }
    }
}

// Not cacheable: Time-dependent
policy TimeBasedPolicy {
    rules {
        rule BusinessHours {
            when {
                const hour = DateTime.Now().ToUnit(TimeUnit.Hours);
                return hour >= 9 AND hour < 17;
            }
            then ALLOW
        }
    }
}

// Solution: Separate time checks
policy StablePolicy {
    rules {
        rule EditorAccess {
            when user.role == "editor"
            then ALLOW
        }
    }
}

// Check time at enforcement point, not in policy

Testing Strategies

1. Comprehensive Test Coverage

test PolicyTests for MyPolicy {
    // Test happy paths
    case "Authorized user can access" { }

    // Test edge cases
    case "Null values handled gracefully" { }
    case "Empty arrays don't cause errors" { }

    // Test security boundaries
    case "Unauthorized access is denied" { }
    case "Expired tokens are rejected" { }

    // Test all rule branches
    case "Each rule can be triggered" { }
}

2. Property-Based Security Tests

test SecurityProperties for AccessPolicy {
    // Security invariants
    property "No access without authentication" {
        for User where user == null OR !user.isAuthenticated
        for Resource
        for Action
        expect: DENY
    }

    property "Deleted resources are never accessible" {
        for User
        for Resource where resource.isDeleted == true
        for Action
        expect: DENY
    }

    property "Suspended users cannot perform any action" {
        for User where (user?.status ?? "") == "suspended"
        for Resource
        for Action
        expect: DENY
    }
}

3. Regression Tests

test RegressionTests for UpdatedPolicy {
    // Test for specific bug fixes
    case "Issue #123: Manager can view team reports" {
        given {
            user: {
                id: "manager-1",
                role: "manager",
                teamId: "team-a"
            }
            resource: {
                type: "team_report",
                teamId: "team-a"
            }
            action: "view"
        }
        expect: ALLOW
        reason: "Managers can view their team's reports"
    }
}

CI/CD Integration

1. Automated Testing Pipeline

# .github/workflows/policy-tests.yml
name: Policy Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Validate Schemas
        run: policyflow validate schemas/

      - name: Validate Policies
        run: policyflow validate policies/

      - name: Run Tests
        run: policyflow test --coverage

      - name: Check Coverage
        run: |
          coverage=$(policyflow coverage --json | jq .total)
          if [ $coverage -lt 80 ]; then
            echo "Coverage too low: $coverage%"
            exit 1
          fi

2. Policy Versioning

policy VersionedPolicy {
    version: "2.1.0"  // Semantic versioning tag (informational only)
}

Note: The version field is purely metadata for tracking changes. It does not affect policy evaluation, routing, or backward compatibility.

Monitoring and Auditing

1. Decision Auditing

policy AuditablePolicy {
    rules {
        rule SensitiveAccess {
            when resource.classification == "Confidential"
            then ALLOW
        }
    }
}

2. Performance Monitoring

Track key metrics:

  • Policy evaluation time
  • Cache hit rates
  • Number of rules evaluated
  • Frequency of each decision type

Common Pitfalls to Avoid

1. Overly Complex Conditions

// Bad: Too complex to understand
when (user.role == "admin" OR (user.role == "manager" AND user.dept == resource.dept))
    AND (resource.status != "archived" OR user.canViewArchived)
    AND {
        const currentHour = DateTime.Now().ToUnit(TimeUnit.Hours);
        return currentHour >= 9 AND currentHour < 17 OR user.isOnCall;
    }
then ALLOW

// Better: Break into multiple rules
rule AdminAccess {
    when user.role == "admin"
    then ALLOW
}

rule ManagerDepartmentAccess {
    when user.role == "manager"
        AND user.dept == resource.dept
        AND resource.status != "archived"
    then ALLOW
}

2. Missing Null Checks

// Bad: Will fail if manager is null
when user.manager.department == "Engineering"

// Good: Null-safe
when user.manager?.department == "Engineering"

3. Hardcoded Values

// Bad: Hardcoded
when user.clearanceLevel >= 5

// Good: Configurable (note: MIN_CLEARANCE_LEVEL is already a Number)
when user.clearanceLevel >= env["MIN_CLEARANCE_LEVEL"]

Summary

Key takeaways for PolicyFlow best practices:

  1. Security First: Secure by default, least privilege, defense in depth
  2. Clear Design: Use appropriate access control patterns (RBAC, ABAC, ReBAC)
  3. Maintainable Code: Modular structure, consistent naming, comprehensive tests
  4. Performance: Optimize conditions, order rules efficiently, design for caching
  5. Operations: Version policies for tracking (not evaluation), automate testing, monitor performance

Following these practices will help you build a robust, secure, and maintainable authorization system with PolicyFlow.

Next Steps