Skip to main content

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.

TierEffect on urn:example:reports
trustedRead and write
limitedRead only
untrusted, or not listedRefused

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:

ClientRequestResultCode in the audit row
Trustedscope=writeToken, "expires_in":300None
Limitedscope=readToken, "expires_in":300None
Limitedscope=writeHTTP 400 access_deniedagent_tier_limited_write
Not listedscope=readHTTP 400 access_deniedagent_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"}]}
}