Skip to main content
Version: 1.0

Rego for PBAC

Most PBAC policy changes are data-driven — you update JSON policy data and the built-in rules handle the rest. But when you need custom logic (time-based access, external lookups, complex conditions), you write a Rego extension rule.

This primer covers the minimum Rego you need to write PBAC extensions. For comprehensive Rego documentation, see the OPA Rego reference.


How extensions work

PBAC uses an extension hook pattern. The baseline policy rules (which you don't modify) call your extension at decision time:

Extension filePackageCalled duringDeny pattern
oauth/evaluations_ext.regooauth.evaluations_ext/token and /introspectdeny(resource, action, subject, context) — function
oauth/register_ext.regooauth.register.ext/registerdeny — boolean

Your extension can deny a request or leave it to the baseline policy.

See Concepts: Policy for the full hook contract and input schema.


Rego basics

Package declaration

Every Rego file starts with a package that matches its role:

package oauth.evaluations_ext    # for token/introspect extensions
package oauth.register.ext # for registration extensions

Imports

Use future.keywords for readable if and in syntax:

import future.keywords.if
import future.keywords.in

The deny patterns

For token/introspect (evaluations_ext): deny is a function that receives the resource, action, subject, and context:

deny(resource, action, subject, context) if {
# conditions that trigger denial
some_condition_is_true
}

For registration (register.ext): deny is a boolean:

deny if {
# conditions that trigger denial
}
reason := "custom:my_reason" if deny

Accessing input and data

  • input — the current request context (varies by extension point)
  • data.oauth / data.oauth_config — your policy data
# In evaluations_ext, the function arguments give you direct access:
deny(resource, action, subject, context) if {
context.client.client_id == "blocked-client"
}

# Access your policy data
data.oauth_config.custom.maintenance_mode

Practical examples

Example 1: Deny a resource evaluation based on custom data

Add a maintenance mode flag to your policy data, then check it in the extension:

{
"oauth": {
"custom": {
"maintenance_mode": true
}
}
}
package oauth.evaluations_ext

import future.keywords.if

deny(resource, action, subject, context) if {
data.oauth_config.custom.maintenance_mode == true
}

Example 2: Deny registration from specific IP ranges

package oauth.register.ext

import future.keywords.if

input_req := object.get(input, "input", input)

deny if {
input_req.environment.ip
startswith(input_req.environment.ip, "10.0.0.")
}

reason := "custom:ip_blocked" if deny

Example 3: Add custom obligations to a resource evaluation

package oauth.evaluations_ext

import future.keywords.if
import future.keywords.in

# Add an audit obligation for sensitive resource types
rs_obligations(resource, actions, subject, context) contains {"audit": {"level": "detailed", "reason": "sensitive_data"}} if {
resource.type == "urn:example:patient"
"write" in actions
}

Loading your extension

Upload your extension via the Admin API:

curl -s -X PUT "$PBAC_URL/admin/api/policy-rules/by-path?path=oauth/evaluations_ext.rego" \
-H "X-Admin-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"path": "oauth/evaluations_ext.rego",
"version": "current",
"kind": "rego",
"enabled": true,
"content": "package oauth.evaluations_ext\n\nimport future.keywords.if\n\ndeny(resource, action, subject, context) if {\n data.oauth_config.custom.maintenance_mode == true\n}"
}' | jq .path

OPA picks up the new rule on its next bundle poll (typically a few seconds).


Custom software statement claims

Any claim in the software statement is available to OPA policy. At token/introspect time the path is input.client.software_statement.<claim>; at register time it is input.request.software_statement.<claim>. This is how you map application metadata to policy decisions.

Example: trust_level claim controlling scope access

Software statement:

{
"sub": "software-id-limited",
"trust_level": "low",
"granted_resources": [
{ "type": "urn:example:Patient", "scopes": ["read", "write"] }
]
}

Extension policy (token_ext.rego) that caps low-trust clients to read only:

package oauth.token.ext

import future.keywords.if
import future.keywords.in

default deny = false

deny if {
ss := input.client.software_statement
ss.trust_level == "low"
"write" in input.request.scopes
}

reason := "custom:low_trust_client_cannot_write" if deny

Example: Custom role claim controlling resource type access

Software statement:

{
"sub": "software-id-admin-app",
"app_role": "admin",
"granted_resources": [
{ "type": "urn:example:Patient", "scopes": ["read", "write", "delete"] }
]
}

Token extension to deny delete unless app_role == "admin":

deny if {
ss := input.client.software_statement
ss.app_role != "admin"
"delete" in input.request.scopes
}

Tips

  • Prefer policy data for static rules (denylists, client restrictions). You don't need Rego for those — the built-in rules handle them.
  • Use extensions for dynamic logic: time checks, external data lookups, conditional scope restrictions, custom obligations.
  • Use data.oauth_config (not data.oauth) when reading policy data in extensions — this avoids OPA recursion issues.
  • Test before deploying by checking the audit log after your first request — the full policy input and decision are logged.

Next steps