Pattern
Policy Engines
A policy engine evaluates rules to determine authorization and data visibility. It separates access control logic from business logic, making permissions explicit, testable, and maintainable.
Part 1: Policy Engines
Overview
A policy engine is a system that evaluates rules to make authorization decisions. Instead of embedding permission checks throughout your application code, you centralize them in a policy engine that can evaluate rules based on the current context: the user, the resource, the action, and any relevant attributes.
Policy engines provide a declarative way to express access control rules. They separate the "what" (what actions are allowed) from the "who" (who can perform them), making authorization logic easier to understand, test, and modify.
In collaborative systems, policy engines are essential for managing complex permission models where different users have different capabilities based on their roles, relationships to resources, or other contextual factors.
When to use this pattern
Use a policy engine when you have:
- Complex authorization rules: Permissions that depend on multiple factors: user roles, resource ownership, state, relationships, or contextual attributes
- Dynamic permissions: Rules that change based on state, time, or other runtime conditions
- Multiple authorization concerns: You need to control both who can perform actions and what data they can see
- Need for maintainability: Authorization rules change frequently, and you want to update them without modifying application code
- Audit requirements: You need to track and explain why certain actions were allowed or denied
- Role-based or attribute-based access control: Permissions based on user attributes, roles, or relationships to resources
Policy engines are particularly valuable in multi-tenant systems, collaborative applications, and any system where authorization logic is complex or frequently changing.
Core concepts
Rules Engine
The core component that processes the rules and applies them to data. The rules engine takes rules from the repository and evaluates them against incoming requests or data.
Rules Repository
Stores the policies or rules in a separate, configurable location like a database or file. This separation allows rules to be managed independently from the application code, making them easier to update and maintain.
Policy Evaluator
The part of the engine that evaluates requests against the rules. The evaluator takes a request (containing user, action, resource, and context) and applies the relevant rules to determine the outcome (allow, deny, or filtered data).
Example: Document review authorization
Consider a document review system where authorization depends on multiple factors:
- User: alice (role: Reviewer)
- Resource: doc-123 (status: Submitted, owner: bob)
- Action: ApproveDocument
- Check if user has "Reviewer" role → ✓
- Check if document is in "Submitted" state → ✓
- Check if user is not the document owner → ✓ (alice ≠ bob)
- Check if user has not already reviewed this document → ✓
The policy engine evaluates all relevant rules and returns a decision. If any rule fails, the action is denied. This makes authorization logic explicit and auditable.
Benefits
- Separation of concerns: Keeps complex rules out of the main application code, making the codebase cleaner and easier to understand
- Maintainability: Allows rules to be updated or changed without modifying the application's core logic
- Flexibility: Makes it easier to add or remove rules as requirements change
- Testability: Individual rules can be tested in isolation, making it easier to verify correctness and catch bugs
Part 2: Policy Engines in the State-Based Collaboration Framework
How policy engines fit in the framework
In the State-Based Collaboration Framework, policy engines are used for two primary purposes:
Transition authorization
The policy engine determines whether a specific user is allowed to trigger a specific action/transition. This is separate from the state machine's role: the finite state machine (FSM) determines which transitions are allowed for an object (e.g., a document can transition from "Draft" to "Submitted"), while the policy engine determines whether a given user is allowed to trigger that transition (e.g., only the document owner can submit it).
Data filters
The policy engine also controls data visibility through filters. When users query projections or collections, the policy engine determines what data they can see based on their permissions, relationships, and other contextual factors.
FSM vs. Policy Engine: A key distinction
It's important to understand the separation of concerns between the state machine and the policy engine:
- Defines which transitions are structurally allowed for an object
- Answers: "Can this object transition from state A to state B?"
- Example: A document can transition from "Draft" to "Submitted" (structural rule)
- Defines who is allowed to trigger a transition
- Answers: "Is this user allowed to trigger this transition?"
- Example: Only the document owner can submit it (authorization rule)
This separation allows you to model business rules (what transitions are possible) separately from authorization rules (who can trigger them). Both must pass for an action to succeed: the FSM must allow the transition, and the policy engine must authorize the user.
Action-control policies
In the framework, action-control policies are evaluated before a transition is executed. The policy engine receives:
- The user attempting the action
- The action/transition being triggered
- The current state of the object
- Any relevant attributes (ownership, relationships, etc.)
The policy engine evaluates all relevant rules and returns either allow or deny. If denied, the action fails before any state transition occurs.
Data filters
The framework also uses policy engines to filter data in projections. When a user queries a projection or collection, the policy engine determines what data they can see:
- Collection filtering: Which items appear in a list (e.g., users only see documents they own or have access to)
- Field filtering: Which fields are visible in an object (e.g., sensitive fields hidden from certain roles)
- Relationship-based access: Data visibility based on user relationships to resources
This ensures that users only see data they're authorized to access, maintaining data privacy and security at the projection level.
Related patterns
This pattern works well with:
- State Machine Design Pattern - Policy engines determine who can trigger FSM transitions
- State Reducer Pattern - Reducers implement transitions; policies authorize them
- Framework overview - Learn about action-control policies and filters
Explore more patterns
This pattern is one of many for building collaborative systems. Check out other patterns or dive deeper into the framework components.