Agent trust tiers
Use this pattern to sort agent clients into tiers that you control. The tier lives in a data row keyed by client id, so moving an agent between tiers is one write and no Rego change.
| Tier | Effect on urn:example:reports |
|---|---|
trusted | Read and write |
limited | Read only |
untrusted, or not listed | Refused |
Every token for the resource type lasts 300 seconds.
It assumes the example set-up in Policy patterns.
Rule
package pbac.controls.agent_tiers
import rego.v1
default tiers := {}
tiers := data["custom.agent_tiers"].tiers
tier_of(client_id) := object.get(tiers, client_id, "untrusted")
tier := tier_of(input.context.client.client_id)
deny contains reason if {
input.resource.type == "urn:example:reports"
tier == "untrusted"
reason := tier_reason("agent_tier_untrusted")
}
deny contains reason if {
input.resource.type == "urn:example:reports"
tier == "limited"
input.action == "write"
reason := tier_reason("agent_tier_limited_write")
}
tier_reason(code) := {
"code": code,
"area": "pbac.controls.agent_tiers",
"resource": input.resource.type,
"action": input.action,
"details": {"tier": tier},
}
expires_in_override := 300 if {
some r in input.resources
r.type == "urn:example:reports"
}
The row is read as data["custom.agent_tiers"], with a default. Do not read it with object.get(data, ...). That form refers to the whole data document, and opa check rejects the package as recursive.
Data row
Register a second client the same way as the first and keep its id in LIMITED_ID. Then give each client a tier.
curl -fsS -X PUT "$POLICYARC_URL/admin/api/policy-data/by-key?dataKey=custom.agent_tiers" \
-H "X-Admin-API-Key: $POLICYARC_ADMIN_KEY" \
-H 'Content-Type: application/json' \
-d "{\"dataKey\": \"custom.agent_tiers\", \"payload\": {\"tiers\": {\"$CLIENT_ID\": \"trusted\", \"$LIMITED_ID\": \"limited\"}}}"
Deploy
Save the rule as agent_tiers.rego, then deploy it.
jq -n --rawfile rego agent_tiers.rego \
'{mode: "merge", rules: [{path: "pbac/controls/agent_tiers.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
What we saw for each tier:
| Client | Request | Result | Code in the audit row |
|---|---|---|---|
| Trusted | scope=write | Token, "expires_in":300 | None |
| Limited | scope=read | Token, "expires_in":300 | None |
| Limited | scope=write | HTTP 400 access_denied | agent_tier_limited_write |
| Not listed | scope=read | HTTP 400 access_denied | agent_tier_untrusted |
Test it locally
package pbac.controls_test.agent_tiers
import data.pbac.controls.agent_tiers
import rego.v1
row := {"tiers": {"trusted-agent": "trusted", "limited-agent": "limited"}}
request(client_id, action) := {
"resource": {"type": "urn:example:reports"},
"action": action,
"context": {"client": {"client_id": client_id}},
}
test_trusted_agent_may_write if {
count(agent_tiers.deny) == 0 with input as request("trusted-agent", "write")
with data["custom.agent_tiers"] as row
}
test_limited_agent_may_read if {
count(agent_tiers.deny) == 0 with input as request("limited-agent", "read")
with data["custom.agent_tiers"] as row
}
test_limited_agent_may_not_write if {
some r in agent_tiers.deny with input as request("limited-agent", "write")
with data["custom.agent_tiers"] as row
r.code == "agent_tier_limited_write"
}
test_unlisted_agent_is_untrusted if {
some r in agent_tiers.deny with input as request("new-agent", "read")
with data["custom.agent_tiers"] as row
r.code == "agent_tier_untrusted"
r.details.tier == "untrusted"
}
test_reports_tokens_last_five_minutes if {
agent_tiers.expires_in_override == 300 with input as {"resources": [{"type": "urn:example:reports"}]}
}
Related
- Agent identity covers how an agent gets the client ID this pattern keys on.
- Give an agent single-use tokens and Hold an agent's write for a person's approval go further than a tier.
Send feedback about this page