Skip to main content

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 actions
  • authz.data — row/column level
  • authz.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.

Next