Access reviews for delegations
A delegation grant is a standing permission, given by a human, for an agent to act on their behalf. That is an access — and accesses get re-examined, or they quietly accumulate.
From laravel-iam-agents v1.6 the module registers itself as a reviewable source in the IAM server’s access-review engine (IGA), so a certification campaign can look delegations in the face alongside ordinary RBAC/ABAC grants.
A role granted to a person eventually gets revoked because the person moves team or leaves — the organisation has a process that notices. An agent has no such lifecycle event. A delegation that stopped being necessary six months ago is still there, still valid, still exchangeable for a token, and nothing in the ordinary course of business will point at it. That is exactly what a review campaign is for.
Including delegations in a campaign
Deliberately opt-in. A campaign’s scope_json.reviewable_types names the sources it certifies; when the key is absent the campaign behaves as it always has — grants only.
POST /api/iam/v1/access-reviews/campaigns
{
"name": "Q3 — delegations",
"on_unconfirmed": "revoke",
"scope_json": {
"reviewable_types": ["delegation_grant"]
}
}
Installing this module must not make delegations appear inside campaigns somebody already planned and scheduled. So they never appear unless asked for.
Both kinds in one campaign:
"scope_json": { "reviewable_types": ["grant", "delegation_grant"] }
Narrow to specific agents:
"scope_json": {
"reviewable_types": ["delegation_grant"],
"agent_ids": ["agt_01J8...", "agt_01J9..."]
}
Only active, unexpired grants are certified: a revoked or expired delegation is not an access any more, and asking somebody to certify it would waste the one thing a review campaign spends — reviewer attention.
Who reviews a delegation
By default, the delegating user. They gave the consent, and they are the only person who actually knows whether the agent is still needed. The item lands in their queue as user:<their id>.
A campaign with reviewer_strategy: "named" and a reviewer in its scope overrides that — a compliance function running a centralised audit must be able to take the decision itself.
The signals
Each item carries an immutable snapshot, frozen when the campaign opens, meant to answer “should this still exist?” before the reviewer has to think:
| Signal | What it tells the reviewer |
|---|---|
never_used |
The delegation was granted and never once acted on. |
dormant |
Never used, or unused for longer than the threshold. |
last_used_days |
Days since the last signed receipt — real actions, not token issuance. |
expires_in_days |
How long it has left anyway (negative would mean expired). |
agent_status / agent_suspended |
The agent’s lifecycle state right now. |
scopes_count |
How wide the delegation is. |
consent_aal |
The assurance level at which consent was actually given. |
has_budget |
Whether a spend ceiling bounds it. |
Two of these deserve a note:
last_used_days comes from the signed action receipts, not from a last_used_at column updated on every touch. Receipts record what the delegation did; a token exchange only records that somebody asked. “Used” should mean the former.
agent_suspended is the case to close first. A suspended agent still holds live delegations, and lifting the suspension brings them all back without anyone re-confirming anything. Revoking the delegations of a suspended agent is how you make sure the return is deliberate.
The dormancy threshold defaults to 30 days, not the 90 used for RBAC grants — a delegation has a shorter natural life, and a month of silence is already a good reason to ask:
// config/iam-agents.php
'reviews' => [
'unused_days' => (int) env('IAM_AGENTS_REVIEW_UNUSED_DAYS', 30),
],
What a decision does
Revoke goes through the delegation grant store, not a bare UPDATE: the grant is revoked, the delegation audit stream records it, and DelegationGrantRevoked fires — so the consumers listening for it (anomaly detection, FinOps, compliance) see it exactly as they would see any other revocation. The campaign id and the review item id ride along in the audit metadata, so the revocation is traceable back to the campaign that caused it.
Approve touches nothing. It certifies that the delegation is still wanted.
On close, on_unconfirmed applies to whatever is still pending: revoke closes them, keep certifies them, and suspend — which delegations have no state for — is treated as revoke, fail-closed.
Revocation is idempotent: a grant already revoked or already gone is a no-op, not an error.
If the module is uninstalled
The server keeps the items — they are audit evidence, and evidence does not disappear with the thing it describes — but it cannot revoke what it can no longer reach. Such an item stays pending, is audited as iam.access_review.item_unrevocable, and does not block the campaign from closing for everything else.
Marking it revoked anyway would write into the audit trail a revocation that never happened, which is the one thing an IGA system cannot afford.
In the admin API
Items expose the shared fields (subject_type, subject_id, privilege_type, privilege_key, application_key, effect) so one table renders both kinds, plus the ones only a delegation has: agent_id, agent_name, purpose, expires_at, grant_status.
GET /api/iam/v1/access-reviews/campaigns/{id}/items
The privilege_key of a delegation is its scope list; privilege_type is delegation.
See also
- Signed action receipts — where
last_used_dayscomes from - The asymmetric kill switch — stopping everything now, versus certifying over time
- Consent — how the grant came to exist in the first place