This guide explains how to interpret the human-readable AccessRecord output from the PolicyEngine, helping you understand exactly what happened during a policy evaluation.
Every authorization decision generates an AccessRecord that captures the complete evaluation context. While the AccessRecord Schema Reference documents the structure, this guide walks through practical examples to help you understand what you're seeing.
You'll want to read AccessRecords when:
By default, AccessRecords are emitted as compact single-line JSON. For human readability, use the --pretty-log flag:
mpe serve -b my-domain.yml --pretty-log
Compact (default):
{"metadata":{"timestamp":"2024-01-15T10:30:00Z","id":"550e8400-e29b-41d4-a716-446655440000"},"principal":{"subject":"alice@example.com","realm":"employees"},"operation":"api:documents:read","resource":"mrn:app:document:12345","decision":"GRANT","references":[...],"porc":"{...}"}
Pretty (--pretty-log):
{
"metadata": {
"timestamp": "2024-01-15T10:30:00Z",
"id": "550e8400-e29b-41d4-a716-446655440000"
},
"principal": {
"subject": "alice@example.com",
"realm": "employees"
},
"operation": "api:documents:read",
"resource": "mrn:app:document:12345",
"decision": "GRANT",
"references": [...],
"porc": {
"principal": {...},
"operation": "api:documents:read",
...
}
}
Note that --pretty-log also expands the porc field from a JSON string into a proper JSON object, making it much easier to inspect.
| Field | Description |
|---|---|
metadata | Timestamp, unique ID, and environment context |
principal | The authenticated subject making the request |
operation | The operation being attempted |
resource | The resource MRN being accessed |
decision | Final outcome: GRANT or DENY |
references | Array of bundle evaluations (the evaluation story) |
porc | The complete PORC expression that was evaluated |
system_override | Whether a system bypass occurred |
grant_reason / deny_reason | Bypass reason when system_override is true |
The references array is the heart of debugging—it shows every policy bundle that was evaluated and how each contributed to the final decision.
Each entry in the array is a BundleReference with these fields:
| Field | Description |
|---|---|
id | Operation name (e.g., api:documents:update) or MRN (e.g., mrn:iam:role:editor) |
phase | Which conjunction phase: SYSTEM, IDENTITY, RESOURCE, or SCOPE |
decision | This bundle's outcome: GRANT or DENY |
reason_code | POLICY_OUTCOME for normal evaluation, or an error code |
reason | Human-readable explanation (especially useful for errors or denials) |
policies | Array of exact policy versions evaluated (MRN + fingerprint) |
:::note Phase Naming
In AccessRecords, the Operation phase appears as SYSTEM in the phase field. This reflects the internal protobuf naming. When you see "phase": "SYSTEM", think "Operation phase."
:::
The porc field contains the complete input that was evaluated. With --pretty-log, it's expanded from a JSON string into a readable object:
"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"
},
"context": {}
}
This is invaluable for correlating inputs with outputs—you can see exactly what principal attributes, roles, and scopes were present when the decision was made.
Let's walk through a complete example based on a document update scenario.
user123 with two roles: editor and viewerapi:documents:updateuser123 in the owner-exclusive resource groupdocuments and read-only{
"metadata": {
"timestamp": "2024-01-15T10:30:00.123Z",
"id": "550e8400-e29b-41d4-a716-446655440000",
"env": {
"service": "document-service",
"environment": "production"
}
},
"principal": {
"subject": "user123",
"realm": "employees"
},
"operation": "api:documents:update",
"resource": "mrn:data:document:doc456",
"decision": "GRANT",
"references": [
{
"id": "api:documents:update",
"policies": [
{
"mrn": "mrn:iam:policy:require-authenticated",
"fingerprint": "YTNmMmI4YzE..."
}
],
"decision": "GRANT",
"phase": "SYSTEM",
"reason_code": "POLICY_OUTCOME"
},
{
"id": "mrn:iam:role:editor",
"policies": [
{
"mrn": "mrn:iam:policy:editor-permissions",
"fingerprint": "ZDRlNWY2YTc..."
}
],
"decision": "GRANT",
"phase": "IDENTITY",
"reason_code": "POLICY_OUTCOME"
},
{
"id": "mrn:iam:role:viewer",
"policies": [
{
"mrn": "mrn:iam:policy:viewer-permissions",
"fingerprint": "YjJjM2Q0ZTU..."
}
],
"decision": "DENY",
"phase": "IDENTITY",
"reason_code": "POLICY_OUTCOME",
"reason": "viewer role does not permit update operations"
},
{
"id": "mrn:iam:resource-group:owner-exclusive",
"policies": [
{
"mrn": "mrn:iam:policy:owner-only",
"fingerprint": "M2E0YjVjNmQ..."
}
],
"decision": "GRANT",
"phase": "RESOURCE",
"reason_code": "POLICY_OUTCOME"
},
{
"id": "mrn:iam:scope:documents",
"policies": [
{
"mrn": "mrn:iam:policy:documents-scope",
"fingerprint": "N2I4YzlkMGU..."
}
],
"decision": "GRANT",
"phase": "SCOPE",
"reason_code": "POLICY_OUTCOME"
},
{
"id": "mrn:iam:scope:read-only",
"policies": [
{
"mrn": "mrn:iam:policy:read-only-scope",
"fingerprint": "OGM5ZDFlMmY..."
}
],
"decision": "DENY",
"phase": "SCOPE",
"reason_code": "POLICY_OUTCOME",
"reason": "read-only scope does not permit update operations"
}
],
"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"
},
"context": {}
},
"system_override": false
}
1. Operation Phase (SYSTEM):
The require-authenticated policy verified the request is authenticated. Phase result: GRANT.
2. Identity Phase:
Two roles were evaluated:
editor → (can update documents)viewer → (read-only role)Phase result: GRANT (only one GRANT needed within a phase)
3. Resource Phase:
The owner-exclusive resource group policy verified the user owns the document. Phase result: GRANT.
4. Scope Phase:
Two scopes were evaluated:
documents → (document operations allowed)read-only → (no write operations)Phase result: GRANT (only one GRANT needed within a phase)
5. Final Decision:
All phases have at least one GRANT, so access is granted.
Notice that both the viewer role and the read-only scope voted DENY, but the request was still granted. This is because:
The editor role provided the necessary GRANT for the Identity phase, and the documents scope provided the necessary GRANT for the Scope phase. For more details, see Policy Conjunction.
This is normal and expected. A user might have multiple roles, but only one needs to permit the operation:
"references": [
{ "id": "mrn:iam:role:guest", "decision": "DENY", "phase": "IDENTITY" },
{ "id": "mrn:iam:role:member", "decision": "DENY", "phase": "IDENTITY" },
{ "id": "mrn:iam:role:admin", "decision": "GRANT", "phase": "IDENTITY" }
]
Phase result: GRANT (the admin role was sufficient)
When a policy fails to load or compile, you'll see an error code:
{
"id": "mrn:iam:role:custom",
"decision": "DENY",
"phase": "IDENTITY",
"reason_code": "COMPILATION_ERROR",
"reason": "rego_type_error: undefined ref: data.policy.custom_rule"
}
This bundle is treated as DENY for safety. Check:
When system_override is true, the normal policy evaluation was bypassed:
{
"decision": "GRANT",
"system_override": true,
"grant_reason": "PUBLIC"
}
This means the resource or operation is marked as public, so no policy evaluation was needed. Other reasons include VISITOR (visitor access permitted) and ANTI_LOCKOUT (anti-lockout protection triggered).
For denials, you might see JWT_REQUIRED or OPERATOR_REQUIRED.
Find the AccessRecord for the denied request (filter by principal and timestamp)
Check for system_override:
system_override: true, check deny_reason for the cause (e.g., JWT_REQUIRED)Scan the references array:
decision: "GRANT"Within that phase, examine each bundle:
reason_code for errors (COMPILATION_ERROR, NOTFOUND_ERROR)reason for human-readable explanationsCorrelate with policy code:
mrn and fingerprint to identify the exact policy version:::tip Deep Debugging
If the AccessRecord shows which policy denied but you need to understand why, enable trace output with mpe --trace. See Debugging Policies for interpreting the trace.
:::
Scan through the references and group by phase. A request is denied when a mandatory phase has no GRANT votes:
| Phase | Mandatory | Has GRANT? | Result |
|---|---|---|---|
| SYSTEM (Operation) | Yes | No | Request denied |
| IDENTITY | Yes | No | Request denied |
| RESOURCE | Yes | No | Request denied |
| SCOPE | Only if scopes present | No | Request denied |
Each policy reference includes a fingerprint—a cryptographic hash of the policy content. This lets you identify the exact version that was evaluated, even if the policy has since been updated:
{
"mrn": "mrn:iam:policy:editor-permissions",
"fingerprint": "ZDRlNWY2YTc..."
}
If you're investigating a historical decision, you can compare this fingerprint against your policy version history.
| AccessRecord Value | Conjunction Phase | Description |
|---|---|---|
SYSTEM | Operation | Coarse-grained request control |
IDENTITY | Identity | Role-based policies |
RESOURCE | Resource | Resource group policies |
SCOPE | Scope | Access-method constraints |
| Code | Meaning |
|---|---|
POLICY_OUTCOME | Normal policy evaluation completed |
COMPILATION_ERROR | Policy failed to compile (Rego syntax error) |
NOTFOUND_ERROR | Referenced policy could not be found |
NETWORK_ERROR | Network issue prevented policy resolution |
EVALUATION_ERROR | OPA evaluation error during execution |
INVALPARAM_ERROR | Invalid parameter or identifier |
UNKNOWN_ERROR | Unspecified error |
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 |