Policy Model
Policy in PBAC is code (Rego rules) evaluated against configuration (JSON policy data). Both are stored in the database, served to OPA as a bundle, and evaluated at every access decision. You change authorization behavior by changing policy — not application code.
Two building blocks
Policy rules (Rego)
Policy rules are written in Rego, OPA's declarative policy language. Each rule is stored in the database with a path (where it lives in OPA's namespace) and content (the Rego source). Rules can be enabled or disabled individually.
PBAC ships with built-in rules that handle the core OAuth evaluation model: deny-by-default, denylist checking, entitlement resolution, grant type validation, and scope intersection. You extend this model by adding your own rules — called extensions — that hook into the evaluation pipeline.
Policy data (JSON)
Policy data is the configuration that Rego rules evaluate against. Each entry has a data_key (its path in OPA's data namespace) and a payload (JSON). Policy data controls behavior without writing Rego:
| Data key | Controls |
|---|---|
oauth | Client entitlements, grant type permissions, RAR types, resource type scopes |
oauth.software_id | Per-software-family entitlement overrides |
oauth.client_id | Per-client entitlement overrides |
denylists | Blocked clients and subjects |
user.idp_selection | Which IdP to route authorization requests to |
trust.trusted_issuers | Which JWT issuers are accepted for software statements |
public_client_policy | Rules for plain DCR (no software statement) |
Most day-to-day policy changes — adding a new resource type, adjusting a client's entitlements, blocking a compromised client — are policy data changes. You only write Rego when you need custom evaluation logic.
The bundle
OPA doesn't read the database directly. The AS packages policy rules and data into a bundle — a gzipped tarball containing .rego files and a data.json — and serves it at GET /bundles/pbac/bundle.tar.gz. OPA polls this endpoint to stay current.
When you create or update a policy rule or data entry via the Admin API, the next time OPA polls the bundle endpoint, it picks up the change. There's no restart, no deployment — policy changes propagate automatically.
The bundle is versioned. Each build produces a digest that OPA uses for conditional fetching. OPA only downloads a new bundle when the content has actually changed.
Evaluation model
Every OPA evaluation follows the same pipeline, whether it's a token request, authorization, registration, or introspect:
- Deny by default — nothing is allowed unless a rule explicitly permits it
- Check denylists — blocked clients/subjects are rejected immediately
- Resolve entitlements — the client's software statement claims are the baseline;
software_idandclient_idoverrides in policy data can modify them - Validate grant type — the requested grant must be allowed for this resource type
- Intersect scopes — the granted scopes are the intersection of what was requested and what the client is entitled to
- Run extensions — custom Rego rules can add further constraints, obligations, TTL overrides, or JIT single-use flags
Service-level vs. resource-level
Policy operates at two levels:
Service-level policy controls what clients and software families can do across the board. These are typically data-driven — entitlement overrides, public access rules, RAR type permissions. You configure them by updating policy data entries.
Resource-level policy controls what happens for a specific resource, subject, or context. These are typically Rego extensions — custom rules that evaluate conditions like "only the resource owner can write" or "deny access outside business hours."
Both levels run in the same evaluation pipeline. Service-level rules establish the baseline; resource-level rules refine it.
Writing custom policy
Extensions are Rego rules that contribute to the token_extensions or introspect_extensions rule sets. They receive the full input context and can:
- Deny access — add to
denyset with a reason - Constrain scopes — remove scopes from the granted set
- Add obligations — return obligations the RS must enforce (audit, masking, rate limiting)
- Override TTL — shorten or extend the token lifetime
- Set JIT single-use — make the token valid for one introspection only
package oauth.token.extensions
import rego.v1
# Deny access outside business hours
deny["outside_business_hours"] if {
hour := time.clock(time.now_ns())[0]
not (hour >= 9; hour < 17)
input.context.client.software_statement.restricted_hours == true
}
Extensions are stored as policy rules in the database, served in the bundle, and evaluated alongside the built-in rules. You can enable/disable them individually, version them, and audit when they were deployed.
Policy lifecycle
For production workflows, policy changes can be managed through git-based deployment: author rules in a repository, review via PR, and deploy through the Admin API.
Next steps
- Token Lifecycle — How policy decisions flow into the tokens the gateway enforces
- Architecture — Where the policy engine fits in the overall system