Skip to main content
Version: 1.0

Your First Policy

Three ways to learn about policy

You are here: Hands-on tutorial — write your first policy changes step by step. For the conceptual model, see Policy engine. For the full data key reference, see Policy data guide.

Prerequisite: Complete the Quickstart first. This guide builds on the clients and policy data you created there.

This guide walks through customizing policy — from simple data-driven changes to writing your first Rego deny rule.


How policy works

The AS evaluates policy at three key points:

  1. /authorize — OPA decides whether to allow the authorization flow and which IdP to use.
  2. /token — OPA decides whether to issue a token and what scopes to grant.
  3. /introspect — OPA re-evaluates the token with RS-supplied action/resource context.

Policy has two parts: policy data (JSON you edit via the Admin API) and policy rules (Rego code that defines the evaluation logic). Most policy changes are data changes — update a JSON key and OPA picks it up within seconds. Rego rules are for custom logic that data can't express.


Data-driven policy changes

Most policy changes don't require writing Rego — you update a JSON data key via the Admin API.

Example: Deny a specific client

The simplest policy change is adding a client_id to the denylist. The PATCH API sets a single path within the oauth data key without overwriting sibling keys:

curl -s -X PATCH "$PBAC_URL/admin/api/policy-data/by-key?dataKey=oauth&path=denylist.client_ids" \
-H "X-Admin-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '["quickstart-app"]'

The built-in token policy checks data.oauth.denylist.client_ids automatically — you don't write Rego for this. Just update the data, and OPA denies the client on the next request. No code change, no restart.

Example: Restrict a user from a scope

Add a subject restriction to deny a specific user from requesting the admin scope:

curl -s -X PATCH "$PBAC_URL/admin/api/policy-data/by-key?dataKey=oauth&path=user.subject_restrictions.alice" \
-H "X-Admin-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"denied_scopes": ["admin"], "denied_resource_types": []}'

Example: Add a new resource type

Define a new resource type with scopes:

curl -s -X PATCH "$PBAC_URL/admin/api/policy-data/by-key?dataKey=oauth&path=resource.types.urn:example:patient" \
-H "X-Admin-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"scopes": ["read", "write", "delete"]}'
PATCH vs PUT

These examples use the PATCH API (?dataKey=oauth&path=...) which sets a value at a single dotted path without overwriting sibling keys. The quickstart used PUT for initial bulk seeding — but for incremental changes, always use PATCH to avoid accidentally wiping out existing data.


Verifying policy changes

After adding quickstart-app to the denylist above, wait 10-15 seconds for OPA to refresh, then verify the denial:

curl -s -X POST $PBAC_URL/token \
-u quickstart-app:app-secret \
-d 'grant_type=client_credentials&scope=read&resource_types=urn:quickstart:data' \
| jq .

You should see {"error":"access_denied",...}. OPA's denylist rule matched quickstart-app and denied the request. No code change, no restart — just a data update.

Clean up: Remove quickstart-app from the denylist so the next guide works:

curl -s -X PATCH "$PBAC_URL/admin/api/policy-data/by-key?dataKey=oauth&path=denylist.client_ids" \
-H "X-Admin-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '[]'

Wait 10-15 seconds, then confirm the client can get tokens again:

curl -s -X POST $PBAC_URL/token \
-u quickstart-app:app-secret \
-d 'grant_type=client_credentials&scope=read&resource_types=urn:quickstart:data' \
| jq .access_token
# Should return a token string (not an error)

Writing a custom Rego rule

For policy logic beyond data-driven changes — custom conditions, time-based access, context-aware decisions — you write Rego rules and load them via the Admin API.

Example: Deny resource evaluation during maintenance

The evaluations_ext extension is called during token issuance and introspection. Add a custom deny rule that blocks access based on a policy data flag:

Required defaults

The evaluations_ext package must define default for deny, rar_allow, rs_obligations, and subject_obligations. Omitting any of these causes OPA to reject the bundle, breaking all policy evaluation. Always include these four default lines.

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\n# Required defaults — do not remove\ndefault deny(_, _, _, _) := false\ndefault rar_allow(_, _, _, _) := false\ndefault rs_obligations(_, _, _, _) := set()\ndefault subject_obligations(_, _, _, _) := set()\n\n# Custom rule: deny during maintenance\ndeny(resource, action, subject, context) if {\n data.oauth_config.custom.maintenance_mode == true\n}"
}' | jq .path

Here is the complete extension for readability:

package oauth.evaluations_ext

import future.keywords.if

# Required defaults — do not remove
default deny(_, _, _, _) := false
default rar_allow(_, _, _, _) := false
default rs_obligations(_, _, _, _) := set()
default subject_obligations(_, _, _, _) := set()

# Custom rule: deny during maintenance
deny(resource, action, subject, context) if {
data.oauth_config.custom.maintenance_mode == true
}

This rule denies all resource evaluations when maintenance_mode is true in your policy data. The AS merges extension rules with the base policy — no need to modify the built-in rules.

Why data.oauth_config? Extensions must use data.oauth_config instead of data.oauth to avoid an OPA recursion error. The AS serves a copy of your policy data at this path — same data, different key. See Policy: Extension hooks.

To test this rule: Add a custom key to your policy data with "maintenance_mode": true, then try requesting a token — it should be denied. Set it back to false and verify tokens work again.

See the Rego Primer for more examples and the full extension API.


You should now understand:

  • Most policy changes are data changes (denylists, entitlements, restrictions)
  • Rego extensions let you add custom deny rules, obligations, and TTL overrides
  • Extensions use data.oauth_config (not data.oauth) to read policy data

For the full policy data structure and all configurable keys, see the Policy Data guide.


Cleanup

Before proceeding to the next guide, restore the default extension and clear any test data:

Restore the default policy extension (removes the maintenance_mode rule):

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\ndefault deny(_, _, _, _) := false\ndefault rar_allow(_, _, _, _) := false\ndefault rs_obligations(_, _, _, _) := set()\ndefault subject_obligations(_, _, _, _) := set()"
}' | jq .path

Ensure the denylist is clear (should already be empty from the verification step):

curl -s -X PATCH "$PBAC_URL/admin/api/policy-data/by-key?dataKey=oauth&path=denylist.client_ids" \
-H "X-Admin-API-Key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '[]'

Your instance is now ready for First Integration.


Next steps