Delegation chains
Use this pattern when one agent hands work to another through token exchange. The second agent presents the first agent's token as the subject_token. The rule allows the exchange only when the client making it is on the approved delegates list. A token the first agent asks for directly is not affected.
It assumes the example set-up in Policy patterns.
Rule
package pbac.controls.reports_delegates
import rego.v1
token_exchange := "urn:ietf:params:oauth:grant-type:token-exchange"
default delegates := []
delegates := data["custom.reports_delegates"].client_ids
deny contains reason if {
input.resource.type == "urn:example:reports"
input.context.grant_type == token_exchange
not input.context.client.client_id in delegates
reason := {
"code": "reports_delegate_not_approved",
"area": "pbac.controls.reports_delegates",
"resource": input.resource.type,
"action": input.action,
"details": {"client_id": input.context.client.client_id},
}
}
Data row
Register a second client the same way as the first and keep its credentials in DELEGATE_ID and DELEGATE_SECRET. Then list it as the only approved delegate.
curl -fsS -X PUT "$POLICYARC_URL/admin/api/policy-data/by-key?dataKey=custom.reports_delegates" \
-H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" \
-H 'Content-Type: application/json' \
-d "{\"dataKey\": \"custom.reports_delegates\", \"payload\": {\"client_ids\": [\"$DELEGATE_ID\"]}}"
Both clients need the token exchange grant. Entitle each one.
for id in "$CLIENT_ID" "$DELEGATE_ID"; do
curl -fsS -X PATCH "$POLICYARC_URL/admin/api/policy-data/by-key?dataKey=pbac.operator.controls.client_entitlements&path=client_id.$id" \
-H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" \
-H 'Content-Type: application/json' \
-d '{"grant_types": ["client_credentials", "urn:ietf:params:oauth:grant-type:token-exchange"], "granted_resources": [{"type": "urn:example:reports", "scopes": ["read", "write"]}]}'
done
Deploy
Save the rule as reports_delegates.rego, then deploy it.
jq -n --rawfile rego reports_delegates.rego \
'{mode: "merge", rules: [{path: "pbac/controls/reports_delegates.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 @-
Check it
Get a token as the first client. It becomes the subject of the exchange.
subject_token=$(curl -fsS -u "$CLIENT_ID:$CLIENT_SECRET" "$POLICYARC_URL/token" \
-d grant_type=client_credentials -d scope=read -d resource_types=urn:example:reports | jq -r .access_token)
Exchange it as the approved delegate, then as the first client itself.
for creds in "$DELEGATE_ID:$DELEGATE_SECRET" "$CLIENT_ID:$CLIENT_SECRET"; do
curl -sS -u "$creds" "$POLICYARC_URL/token" \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d subject_token="$subject_token" \
-d subject_token_type=urn:ietf:params:oauth:token-type:access_token \
-d scope=read -d resource_types=urn:example:reports
echo
done
The delegate gets a token with an issued_token_type. The first client is not on the list, so its exchange returns HTTP 400 access_denied. Its audit row carries the code reports_delegate_not_approved.
The rule looks only at the client making the exchange. The exchange can also carry an actor_token, which policy sees as input.context.actor with the actor's client_id. We have not run an exchange with an actor token yet.
Test it locally
package pbac.controls_test.reports_delegates
import data.pbac.controls.reports_delegates
import rego.v1
exchange(client_id) := {
"resource": {"type": "urn:example:reports"},
"action": "read",
"context": {
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"client": {"client_id": client_id},
},
}
row := {"client_ids": ["approved-delegate"]}
test_approved_delegate_passes if {
count(reports_delegates.deny) == 0 with input as exchange("approved-delegate")
with data["custom.reports_delegates"] as row
}
test_unapproved_delegate_is_denied if {
some r in reports_delegates.deny with input as exchange("unknown-agent")
with data["custom.reports_delegates"] as row
r.code == "reports_delegate_not_approved"
}
test_direct_issuance_is_untouched if {
count(reports_delegates.deny) == 0 with input as {
"resource": {"type": "urn:example:reports"},
"action": "read",
"context": {"grant_type": "client_credentials", "client": {"client_id": "unknown-agent"}},
}
with data["custom.reports_delegates"] as row
}
Send feedback about this page