Rego Patterns for Authorization
Production authorization policies in EnforceAuth customers' repos usually combine a few Rego patterns. This page shows curated examples — for language semantics and builtins, use the canonical OPA Policy Language reference.
Default deny (fail closed)
Every package should assume deny until a rule proves otherwise:
package authz.api
default allow := false
allow if {
input.action == "read"
input.resource.type == "report"
"analyst" in input.user.roles
}
Returning a structured decision (optional) keeps PEPs consistent:
default decision := {"allow": false, "reason": "no_matching_rule"}
decision := {"allow": true} if {
allow
}
Role-based access control (RBAC)
Map users to roles, roles to permissions:
package authz.rbac
import data.role_permissions
default allow := false
allow if {
some role in input.user.roles
permitted(role, input.action, input.resource.type)
}
permitted(role, action, resource_type) if {
role_permissions[role][_].action == action
role_permissions[role][_].resource == resource_type
}
Keep data.role_permissions in versioned JSON or a separate Rego data package — easier to audit than hard-coded role checks scattered across rules.
Tip: Name rules after intent (allow_read_billing) not ordinal IDs (rule7).
Attribute-based access control (ABAC)
Decide using user, resource, and environment attributes:
package authz.abac
default allow := false
allow if {
input.user.clearance >= input.resource.classification
input.user.department == input.resource.owner_department
input.context.network_zone == "corp"
}
ABAC rules get complex quickly. Split by domain:
authz.api— HTTP actionsauthz.data— row/column levelauthz.admin— break-glass and platform operations
Explicit deny overrides
Some policies need deny wins over allow (separation of duties, compliance blocks):
package authz.override
default allow := false
default deny := false
# Aggregate allow rules from imported packages
allow if {
data.authz.api.allow
data.authz.data.allow
}
# Hard deny — evaluated after allow
deny if {
input.user.suspended == true
}
deny if {
input.resource.tags["regulated"] == "pii"
not input.user.training.pii_complete
}
# Final decision
decision := "deny" if {
deny
} else := "allow" if {
allow
} else := "deny"
Your PEP should check decision == "allow" (or the boolean you export) and log denials with the reason when you attach metadata.
Resource ownership and hierarchy
Common in multi-tenant apps:
allow if {
input.action == "update"
input.resource.owner_id == input.user.id
}
allow if {
input.action in {"read", "list"}
ancestor_owns(input.user.tenant_id, input.resource.tenant_id)
}
ancestor_owns(user_tenant, resource_tenant) if {
tenant_tree[user_tenant].descendants[resource_tenant]
}
Sync hierarchy data via bundle data.json or a PIP — avoid embedding tenant trees inline in every rule file.
Input shape documentation
Document expected input in a README next to policies or in package comments:
input.user.id — stable subject identifier
input.user.roles — array of strings
input.action — verb: read | write | delete | admin
input.resource.type — entity type being accessed
input.resource.id — optional instance id
EnforceAuth decision logs store the input OPA evaluated — consistent shapes make investigation faster. See Decision logs.
Testing these patterns
Pair every non-trivial rule with opa test cases. See Testing policies.
test_allow_analyst_read if {
allow with input as {
"user": {"roles": ["analyst"]},
"action": "read",
"resource": {"type": "report"},
}
}
test_deny_without_role if {
not allow with input as {
"user": {"roles": ["guest"]},
"action": "read",
"resource": {"type": "report"},
}
}
Official references
OPA reorganized its docs in 2025–2026 — RBAC and ABAC examples now live on a single comparisons page.