Policy Conjunction is the mechanism by which the Manetu PolicyEngine combines multiple policy phases together dynamically on a request-by-request basis to reach a final access decision.
Rather than relying on a single policy to make all access decisions, the PolicyEngine evaluates policies across four distinct phases:
Each phase represents a different aspect of the access decision, and all phases must agree for access to be granted. This separation provides the following benefits:
The PolicyEngine processes all phases in parallel for maximum performance. However, the final decision requires at least one GRANT vote from each phase for the top-level decision to be GRANT.
| Phase | Mandatory | Default if Missing |
|---|---|---|
| Operation | Yes | |
| Identity | Yes | |
| Resource | Yes | |
| Scope | No |
:::warning Important If a PORC expression is missing references to any mandatory phase (operation, identity, or resource), that phase votes DENY implicitly. The scope phase is the exception: if no scopes are present in the PORC, it defaults to GRANT. However, once at least one scope is present in the PORC, the scope phase behaves like the others and requires at least one policy to vote GRANT. :::
Some phases, particularly identity and scope, can have multiple policies associated with a single request. Within a phase:
For example, if a user has three roles, and each role has an associated identity policy:
Identity Phase:
├── Policy for Role A → DENY
├── Policy for Role B → GRANT ← One GRANT is sufficient
└── Policy for Role C → DENY
Identity Phase Result: GRANT
The operation phase has a unique capability: it can issue a GRANT Override that immediately grants access, bypassing the identity, resource, and scope phases entirely.
This solves an important problem for public endpoints. Consider a health-check endpoint that requires no authentication—without a JWT, there are no roles, so the identity phase would vote DENY. GRANT Override allows the operation phase to short-circuit the evaluation and grant access without requiring the other phases to participate.
For implementation details on how to express GRANT Override in operation phase policies, see Tri-Level Policies.
Each policy gets a vote for GRANT or DENY. However, if a policy cannot be evaluated due to:
The policy is treated as if it evaluated to DENY. This fail-closed behavior ensures that system failures don't inadvertently grant access.
While failures are treated as DENY for decision purposes, the audit record captures the distinction:
| Vote Type | Decision Impact | Audit Reason Code |
|---|---|---|
| GRANT | Contributes GRANT | Policy evaluated to GRANT |
| DENY | Contributes DENY | Policy evaluated to DENY |
| Not Found | Treated as DENY | Policy not found |
| Error | Treated as DENY | Evaluation error (with details) |
| Timeout | Treated as DENY | Evaluation timeout |
This separation allows auditors to distinguish between:
Consider a request with the following PORC:
principal:
sub: "user123"
mroles:
- "mrn:iam:role:editor"
- "mrn:iam:role:viewer"
scopes:
- "mrn:iam:scope:documents"
- "mrn:iam:scope:read-only"
operation: "api:documents:update"
resource:
id: "mrn:data:document:doc456"
owner: "user123"
group: "mrn:iam:resource-group:owner-exclusive"
The PolicyEngine evaluates:
Operation Phase (1 policy)
Identity Phase (2 policies, one per role)
Resource Phase (1 policy)
Scope Phase (2 scopes present, 2 policies)
Final Decision: (all phases agreed)
Now consider if the resource policy fails to load:
Final Decision: (resource phase did not GRANT)
The audit record shows that the resource phase's outcome was DENY, not because of policy logic, but because the policy could not be found—enabling operators to identify and fix the issue.
Policy conjunction provides several benefits:
Different teams can own different phases:
Conjunction enables each policy to remain small and focused on a single concern. Without conjunction, policies must account for every possible combination of principal, operation, resource, and context—leading to complex, unwieldy code that is difficult to debug and maintain.
With conjunction, the PolicyEngine dynamically assembles only the relevant policy fragments for each request. This provides several advantages:
Multiple layers must all agree, reducing the impact of a misconfigured policy in any single phase.
Missing or broken policies result in DENY, preventing errors from creating security holes.
The phase-based structure provides clear visibility into why access was granted or denied, making compliance and debugging easier.
Ensure all mandatory phases are defined: Missing operation, identity, or resource policies will result in DENY.
Handle scope intentionally: Decide whether your application uses scopes (for PATs, federation, etc.). If not, omit them from the PORC for implicit GRANT.
Monitor for evaluation failures: Use audit logs to detect policies that fail to evaluate, as these may indicate configuration or infrastructure issues.
Test each phase independently: Verify that each phase grants access appropriately before combining them.
Document phase ownership: Clearly assign responsibility for each phase to avoid gaps in policy coverage.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |