From 8d06dd975d4345236d0c1a71e289d300e3494815 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Thu, 13 Aug 2026 19:36:26 +0530 Subject: [PATCH] docs(changelog): correct the no-`type agent` behaviour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2.4.0 entry still claimed a model without `type agent` keeps pre-2.4.0 behaviour byte-for-byte. e70c0b77 changed that: the check now denies, with --fga-allow-unconstrained-agents as the migration opt-out. The flag shipped with no changelog entry at all. Both states still meter as outcome="not_enforced", so a reader hitting that metric was being told their agents run unconstrained when in fact their delegated calls are being denied — opposite diagnosis, opposite remedy. Refs #764 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e5ef626..0569eda2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,7 +88,7 @@ Targets the 2.4.0 release. Significant additions include enterprise SSO (SAML Id - **Type-safe error handling in gRPC admin service**: admin service methods now return properly-typed errors (400 for validation, 409 for conflicts, etc.) instead of generic Internal errors (500), and public-method bypass is tightly scoped to only the public service and `AdminLogin` ([#700](https://github.com/authorizerdev/authorizer/pull/700)). - **Atomic storage operations with transaction guards**: `UpdateUsers` empty-ids filter is now enforced across all 13 database providers (preventing silent full-table updates on Mongo/Arango/Cassandra/Couchbase/DynamoDB); cascade deletes (`DeleteOrganization`, `DeleteClient`, `DeleteWebhook`, `DeleteUser`) are now wrapped in transactions, rolling back on partial failure ([#699](https://github.com/authorizerdev/authorizer/pull/699)). - **Delegated tokens are gated per operation by their `scope` claim**: an RFC 8693 delegated token may only reach operations cleared for delegated callers, and only while its `scope` carries the scope that operation requires. Refusals are `insufficient_scope` (RFC 6750 §3.1). Until this existed the attenuation the token endpoint computes — `subject_token.scope ∩ agent.allowed_scopes` — was returned to the caller and then never consulted, so a delegated token reached **every** first-party operation: an agent an operator granted `openid` for a downstream MCP server could read the delegating user's profile, mutate the account, and deactivate it. Read-only identity and permission queries (`check_permissions`, `list_permissions`, `profile`, `meta` — exactly the built-in MCP tool set) require only `openid` and are unaffected. Mutating operations require a scope no client requests by default: `authorizer:profile:write` for `update_profile`, `authorizer:account:delete` for `deactivate_account`. Because a delegated scope is the intersection of the user's and the agent's, a sensitive operation needs **both** halves — the user's own token must carry the scope and an admin must have granted the agent a ceiling including it — so neither party can widen an agent alone. **Fails closed**: any operation not explicitly cleared is denied to delegated callers whatever scope they hold, so new operations are unreachable by agents until deliberately added. **First-party tokens are deliberately not gated** — `login` accepts a caller-supplied `scope` with no allow-list, making it a hint rather than a boundary, so enforcing it there would break existing clients while granting no security. Enforced identically on GraphQL and on gRPC (which also covers the REST gateway and the MCP server) ([#742](https://github.com/authorizerdev/authorizer/pull/742)). -- **Agent authority is the intersection of agent and user permissions**: a delegated (RFC 8693) caller's effective authority on `check_permissions` and `list_permissions` is now `perms(agent) ∩ perms(user)`, evaluated per action at request time, rather than the delegating user's full authority. This is the Confused Deputy fix: an agent can no longer act on anything its user happens to be able to reach, and equally cannot exceed what its user could have done itself. Enumeration intersects too — an agent that cannot act on an object must not see it listed, or the user's resource names leak. Only the **immediate** actor participates; prior hops in the `act` chain are audit-only. The subject can never be widened by a request parameter: an explicit `user` is honoured only as the caller's own subject and never sheds the agent half, and a delegated token naming any other subject is refused outright — including when an admin credential rides along on the same request. **Opt-in is declaring `type agent` in the authorization model**, with no flag: checking `agent:` against a model lacking the type errors rather than returning false, so a flag switched on against an unprepared model would deny every delegated request. Deployments without the type keep today's behaviour byte-for-byte and are counted as `authorizer_fga_delegated_checks_total{outcome="not_enforced"}` so the unenforced state is visible. Fails closed throughout: a model-read failure, a malformed agent id, or an inactive subject denies. See [Agent Identity & Permissions](https://docs.authorizer.dev/enterprise/agent-identity). +- **Agent authority is the intersection of agent and user permissions**: a delegated (RFC 8693) caller's effective authority on `check_permissions` and `list_permissions` is now `perms(agent) ∩ perms(user)`, evaluated per action at request time, rather than the delegating user's full authority. This is the Confused Deputy fix: an agent can no longer act on anything its user happens to be able to reach, and equally cannot exceed what its user could have done itself. Enumeration intersects too — an agent that cannot act on an object must not see it listed, or the user's resource names leak. Only the **immediate** actor participates; prior hops in the `act` chain are audit-only. The subject can never be widened by a request parameter: an explicit `user` is honoured only as the caller's own subject and never sheds the agent half, and a delegated token naming any other subject is refused outright — including when an admin credential rides along on the same request. **Opt-in is declaring `type agent` in the authorization model**, with no flag: checking `agent:` against a model lacking the type errors rather than returning false, so a flag switched on against an unprepared model would deny every delegated request. A model without the type therefore **denies** delegated checks rather than authorizing as the delegating user alone: authorizing as the user alone would hand the agent the user's full authority — the exact Confused Deputy this fix exists to prevent — invisibly, at the moment the control is least able to defend itself. Deployments migrating from 2.3.x can set `--fga-allow-unconstrained-agents` to restore the old user-alone behaviour while they add the type. Either state is counted as `authorizer_fga_delegated_checks_total{outcome="not_enforced"}` and logged at warn level, so both the unconstrained exposure and the missing-type denial are visible in dashboards rather than discovered during an incident. Fails closed throughout: a model-read failure, a malformed agent id, or an inactive subject denies. See [Agent Identity & Permissions](https://docs.authorizer.dev/enterprise/agent-identity). - **Delegated tokens are revocable at Authorizer's own API**: a delegated token now carries an opaque `sid` naming the session it was derived from, so logout, password reset, email change and admin session wipes stop it on the next call. Previously nothing a user or admin could do stopped one — it stayed valid for its full TTL, and the only working lever was revoking the user outright. A downstream resource server validates offline against the JWKS and still cannot see this, so the short TTL remains the only bound **there**; do not build a resource server that assumes otherwise. Fails closed: a delegation whose origin cannot be verified does not authenticate here. - **Agent actions are attributed to the agent in the audit log**: an action taken by an agent on a user's behalf is recorded with the agent as `actor_id`, `actor_type: agent`, no actor email, and the delegating user preserved in metadata (`delegated_user_id`, `delegated_user_email`). Previously the delegating user was recorded as the actor on the GraphQL surface — the actor was read from a request principal that only the gRPC interceptor constructs — making an agent's actions indistinguishable from the human's, which cannot be reconstructed after the fact. RFC 8693 §1.1 draws exactly this line between delegation and impersonation.