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:
- Security First: Secure by default, least privilege, defense in depth
- Clear Design: Use appropriate access control patterns (RBAC, ABAC, ReBAC)
- Maintainable Code: Modular structure, consistent naming, comprehensive tests
- Performance: Optimize conditions, order rules efficiently, design for caching
- 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.