Write a policy
A custom policy is a custom control. It is one Rego file in its own package under pbac.controls, and it adds reasons to a set named deny. When the set is not empty for an action, that action is refused. You wire nothing up. The product finds every package under pbac.controls by itself.
Before you write Rego, check whether a shipped control already does the job. A shipped control changes through its data row, with no Rego at all. See Control data.
The rule
This rule refuses write on the resource type urn:example:reports and allows everything else.
package pbac.controls.reports_read_only
import rego.v1
deny contains reason if {
input.resource.type == "urn:example:reports"
input.action == "write"
reason := {
"code": "reports_write_blocked",
"area": "pbac.controls.reports_read_only",
"resource": input.resource.type,
"action": input.action,
}
}
Save it as reports_read_only.rego and check it.
opa check reports_read_only.rego
opa check prints nothing when the file is valid.
Deploy it and see the denial
Set up the example resource type and a client as in Policy patterns. Then deploy the rule.
jq -n --rawfile rego reports_read_only.rego \
'{mode: "merge", rules: [{path: "pbac/controls/reports_read_only.rego", content: $rego}]}' |
curl -fsS -X POST "$POLICYARC_URL/admin/api/policy-bundle/deploy" \
-H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" \
-H 'Content-Type: application/json' -d @-
Ask for three tokens.
for scope in read write 'read write'; do
curl -sS -u "$CLIENT_ID:$CLIENT_SECRET" "$POLICYARC_URL/token" \
-d grant_type=client_credentials -d "scope=$scope" -d resource_types=urn:example:reports
echo
done
What we saw:
| Requested scope | Response |
|---|---|
read | A token with "scope":"read" |
write | HTTP 400 {"error":"access_denied","error_description":"Access denied"} |
read write | A token with "scope":"read". The denied action is dropped and the rest is granted. |
The caller sees only access_denied. Your reason goes to the audit log. Test a policy and explain a denial shows where to find it. The error reference lists what callers see at each endpoint, and each page of the control reference lists the reason codes that control writes.
What the rule sees
The server calls your rule once for each resource and each action in the request. Each time, input holds that one pair.
| Field | What it holds |
|---|---|
input.resource.type | The resource type, such as urn:example:reports |
input.resource.id | The resource indicator, or null when the request named only a type |
input.resource.properties | Properties of the resource. Empty in our runs. |
input.action | One requested scope, such as write |
input.subject | The signed-in user, or null for a client credentials token |
input.context.client.client_id | The client making the request |
input.context.client.registration_type | How the client registered, such as DCR_PLAIN |
input.context.client.software_statement | Every claim of the client's software statement, or {} |
input.context.grant_type | The grant, such as client_credentials |
input.context.environment.ip | The caller's address as the server saw it |
Rego input and output lists every field.
Write the reason
Each member of deny is an object. Give it these fields.
| Field | Use |
|---|---|
code | A stable, snake_case name for this rule. The audit row keeps it in deny_reasons. The audit API has no filter by code, so find the row by client, subject, outcome or flow and read the code there. |
area | Your package name, so a reader knows which control denied. |
resource and action | What was refused. |
details | Optional facts that explain this denial. Leave out personal data. |
The reason must be an object. We deployed a control that added a plain string to deny. The deploy succeeded, and every token request it touched then failed with HTTP 500.
Shorten a token's lifetime
A control can also set expires_in_override, in seconds. When several controls set one, the shortest wins.
package pbac.controls.reports_short_tokens
import rego.v1
expires_in_override := 900 if {
some r in input.resources
r.type == "urn:example:reports"
}
This rule reads input.resources, the list of everything the request asked for. It does not see input.resource, because the server reads a lifetime once for the whole request, not once per resource. Our tokens for the resource type came back with "expires_in":900.
Avoid the common mistakes
Your file is compiled together with every shipped policy. A mistake in it can stop the whole bundle from loading, and the deploy is then refused with HTTP 422. Other mistakes load and quietly do nothing. These rules avoid both.
- Write
deny contains reason if, never a function such asdeny(resource, action, subject, context) if. In a package of its own, the function form deploys and then never denies anything. We deployed one that matched every request, and tokens were still issued. In a package that also has the set form,opa checkreportsrego_type_error: conflicting rules. - Use a package name of your own under
pbac.controls. Every file that declares the same package merges into one. - Read a data row by its literal key with a default, as in
data["custom.reports_hours"]. Reading it withobject.get(data, ...)refers to the whole data document, andopa checkrejects the package as recursive. - You do not need
default rar_allow. The shipped aggregator in packageoauth.evaluations_extdeclares the default and ORs therar_allowof every package underpbac.controls. A second default insideoauth.evaluations_extfails the whole bundle. In our run adefault rar_allow := falsein a package of our own underpbac.controlsdeployed with HTTP 200. - Do not put test packages under
pbac.controls. Name thempbac.controls_test.<name>.
Keep secrets out of policy
Rules and data rows are served to the policy engine in the bundle. Do not put API keys, passwords or tokens in either. Store them in the Admin Portal's secrets store, /admin/api/secrets.
Next steps
- Rego for PolicyArc covers the Rego you need for these rules.
- Policy patterns has five rules you can deploy as they are.
- Deploy and roll back policy covers a refused deploy and how to undo one.
Send feedback about this page