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 file | Package | Called during | Deny pattern |
|---|---|---|---|
oauth/evaluations_ext.rego | oauth.evaluations_ext | /token and /introspect | deny(resource, action, subject, context) — function |
oauth/register_ext.rego | oauth.register.ext | /register | deny — 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(notdata.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
- Concepts: Policy — Full hook contract, input schema, and extension architecture
- Policy Data Guide — Data-driven policy changes (no Rego needed)
- OPA Rego Reference — Complete Rego language documentation