Deny by default
Use this pattern when only named clients may use a resource type. A client that is not on the allowlist is refused, and so is every client when the allowlist row does not exist.
It assumes the example set-up in Policy patterns.
Rule
package pbac.controls.reports_allowlist
import rego.v1
default allowed_clients := []
allowed_clients := data["custom.reports_allowlist"].client_ids
deny contains reason if {
input.resource.type == "urn:example:reports"
not input.context.client.client_id in allowed_clients
reason := {
"code": "reports_client_not_allowlisted",
"area": "pbac.controls.reports_allowlist",
"resource": input.resource.type,
"action": input.action,
"details": {"client_id": input.context.client.client_id},
}
}
The default line is what makes the rule fail closed. When the row is missing, allowed_clients is an empty list and no client is on it.
Data row
curl -fsS -X PUT "$POLICYARC_URL/admin/api/policy-data/by-key?dataKey=custom.reports_allowlist" \
-H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" \
-H 'Content-Type: application/json' \
-d "{\"dataKey\": \"custom.reports_allowlist\", \"payload\": {\"client_ids\": [\"$CLIENT_ID\"]}}"
Deploy
Save the rule as reports_allowlist.rego, then deploy it.
jq -n --rawfile rego reports_allowlist.rego \
'{mode: "merge", rules: [{path: "pbac/controls/reports_allowlist.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 @-
The response carries "activation":"confirmed".
Check it
The client on the allowlist still gets a token.
curl -sS -u "$CLIENT_ID:$CLIENT_SECRET" "$POLICYARC_URL/token" \
-d grant_type=client_credentials -d scope=read -d resource_types=urn:example:reports
Register a second client the same way as the first and entitle it to the resource type. Its token request returns HTTP 400 with {"error":"access_denied","error_description":"Access denied"}. The audit row for that request records this reason.
{
"code": "reports_client_not_allowlisted",
"area": "pbac.controls.reports_allowlist",
"resource": "urn:example:reports",
"action": "read",
"details": {"client_id": "the second client's id"}
}
Test it locally
package pbac.controls_test.reports_allowlist
import data.pbac.controls.reports_allowlist
import rego.v1
request(client_id) := {
"resource": {"type": "urn:example:reports"},
"action": "read",
"context": {"client": {"client_id": client_id}},
}
test_listed_client_passes if {
count(reports_allowlist.deny) == 0 with input as request("reports-agent")
with data["custom.reports_allowlist"] as {"client_ids": ["reports-agent"]}
}
test_unlisted_client_is_denied if {
some r in reports_allowlist.deny with input as request("other-agent")
with data["custom.reports_allowlist"] as {"client_ids": ["reports-agent"]}
r.code == "reports_client_not_allowlisted"
}
test_missing_row_denies_everyone if {
some r in reports_allowlist.deny with input as request("reports-agent")
r.code == "reports_client_not_allowlisted"
}
test_other_resource_types_are_untouched if {
count(reports_allowlist.deny) == 0 with input as {
"resource": {"type": "urn:example:invoices"},
"action": "read",
"context": {"client": {"client_id": "other-agent"}},
}
}
Run opa test . in the folder that holds both files.
Send feedback about this page