Skip to main content
Version: Latest

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 keyControls
oauthClient entitlements, grant type permissions, RAR types, resource type scopes
oauth.software_idPer-software-family entitlement overrides
oauth.client_idPer-client entitlement overrides
denylistsBlocked clients and subjects
user.idp_selectionWhich IdP to route authorization requests to
trust.trusted_issuersWhich JWT issuers are accepted for software statements
public_client_policyRules 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:

  1. Deny by default — nothing is allowed unless a rule explicitly permits it
  2. Check denylists — blocked clients/subjects are rejected immediately
  3. Resolve entitlements — the client's software statement claims are the baseline; software_id and client_id overrides in policy data can modify them
  4. Validate grant type — the requested grant must be allowed for this resource type
  5. Intersect scopes — the granted scopes are the intersection of what was requested and what the client is entitled to
  6. 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 deny set 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