Repository navigation
fix(iam): name the missing grant in a caller's own denial - #3699
Merged
Merged
Conversation
The API answered every caller 403 with the same sentence, though the error carries the checked action and resource. Name the grant and who grants it, an ID only when the request carried it; pass the six caller-state reasons through; console and CLI show the new text and keep the old one working.
…tests Map the new caller-denial test to checks-baseline-1, compare a real and an unknown ID's denial with each sent ID hidden, keep the console's own grant text where a view names one, and document the denial text and the breaking change for clients that matched the old sentence.
…delete denial in storybook Plugin discovery checks the Agent's own service principal with a plain denial; its grant must not read as the caller's own, so a decision whose evidence names another principal keeps the generic sentence.
The authorization reference went over the 2,500-word limit; it now links the cheat sheet's denial messages section and drops a repeated bullet.
Contributor
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review blockedAutomated review did not run, so no review verdict was produced. Reason: The input-safety check rejected material in this revision. No detected value, path, or scanner output is reproduced here. Automatic review is on hold, including after source changes. Request a fresh re-review after resolving the failure; maintainers can also release the hold through an intentional scanner-policy update. Next step: If this is a genuine credential, remove and rotate it. If it is an intentional test fixture, a maintainer must review and qualify it. |
A caller's named deploy denial names Secret operate but no Secret the request did not send, so the Agent page still reads the Secret operate answer again and names the bound Secrets. Browser tests now expect the named Preset read denial, the Request ID after a named Create Agent denial, and the named Secret operate deploy denial.
# Conflicts: # tests/browser/console-agents.test.ts
…ess retry #3690's retry test sends the real API denial, which now names the Agent read grant the retry's reread lacks.
- A create denial no longer asks for a grant: no Role can hold create, so it says only an Installation administrator can create it (finding 2581). - An ID counts as sent only from a path parameter or a reference field (`id`, `...Id`, `...Ids`) in the query or body, never free text such as `q` (finding 2585). - AuthorizationDeniedError records the principal whose grant was checked; HTTP names the grant only when that is the caller, so a denial for an unprovisioned Agent principal stays generic even though Native IAM reports no identity in its evidence (finding 2586). - The permissions cheat sheet and the breaking-change notice list the six caller-state sentences (finding 2587). - A provisioning retry by another actor says "retry provisioning"; the unreachable retry check is folded into the shared lookup (finding 2588). - The native admin proxy keeps the generic text for a signed-in person with no platform identity, decided before the host (finding 2589).
freeqaz
added a commit
that referenced
this pull request
Oct 26, 2026
…t list.mts #3699 added a case that imported accessDenied from list.mts; this branch folded it into access-reasons.mts (notIncluded), so the file failed to load in Checks and Conformance 1. The case now compares with namespaceListDenied, the written-out expectation.
This was referenced Oct 26, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The API answered every 403 for the caller's own permissions with "The exact platform operation was not authorized.", although
AuthorizationDeniedErroralready carries the checked action and resource. Callers could not tell which grant was missing or who could grant it. The console fell back to "Access denied. You do not have permission for this operation." from about 15 call sites, and the CLI kept a per-command hint table to work around it.docs/reference/agents/deployment.mdsaid "Denials of your own permissions stay generic" without a reason; the owner approved reversing it for clearer UX.Change
API (
apps/controller/src/http/caller-denial.ts, used byhttp/errors.ts,requireInstallationAdminand the native admin proxy):Not authorized: this needs Agent deploy on Agent <id>. Ask an Installation administrator to grant it.Not authorized: this needs Secret create in Namespace <id>. Only an Installation administrator can create Secrets: ask one to create it.;installation:administerasks an Installation administrator to do it or grant it;read_logssaysAgent read_logs (or Agent administer when no Restriction applies).CallerStateDeniedErrorand pass through as written. A provisioning retry by another actor now says "retry provisioning" (it said "read provisioning status").codevalues are unchanged. Who may do what is unchanged.Disclosure rules (tested):
id,...Id,...Ids) of the query or body; free text such as a searchqnever counts. An ID read from stored state reads as "a Secret this request uses". IDs outside a plain[A-Za-z0-9_.:-]shape are never echoed.AuthorizationDeniedErrornow records the principal whose grant was checked, and HTTP names the grant only when that is the caller. A decision for another principal (an Agent's own service principal checked withoutAgentPrincipalAuthorizationError, as in saved plugin discovery) keeps the generic sentence, even when the IAM Driver reports no identity in its evidence (Native IAM does so for an unprovisioned principal).Clients:
namedDenial()inapi-client.mts.message()shows the API's named denial where the view names no grant or refusal of its own (a view'sAccessNeedstill wins; an Installation administrator's own denial keeps fix(console): show the API's typed refusal reasons through one shared describer #3690's plain text, since it asks an administrator); Delete shows it; the deploy refusal already showed any non-generic 403. After a named deploy denial the Agent page still reads the Secret operate answer again, so it names the bound Secrets the API's text leaves out. An older API's generic sentence keeps the console's own guidance.Docs: permissions cheat sheet "Denial messages" (with the six caller-state sentences), console reference, authorization reference, Agent deployment reference, CLI reference, native admin flow, and a breaking-change notice for clients that matched the old sentence. The Storybook "Permission denied" deletion scenario now carries the named denial.
Verification
tests/integration/caller-denial-api.test.ts(production Fastify app, Native IAM Driver, live policy; mapped to checks-baseline-1): Agent read/update/delete/deploy/read_logs for an unreadable and an unknown Agent; Secret and Namespace create; Installation administer from the route and from the controller; a Restriction and no grant give the same answer; Configuration read names the ID when PATCH sends it and none when deploy reads it from storage. Fails on main, passes on this branch.tests/conformance/http-errors.test.mjs: table over every resource kind and supported action (prefix, wire cap, Restriction vs no grant identical, ID only when sent, free text never counts, a create asks for no grant, another principal or no recorded principal stays generic, odd IDs not echoed), which fields count as sent IDs, a real Native IAM decision for an unprovisioned other principal, the six caller-state reasons, and the consolemessage()precedence.tests/integration/native-admin-access.test.mjs: the JSON denial for a denied caller, an unknown host and a Restriction is the same named text; a person with no platform identity gets the generic text. Fails on main.tests/integration/postgres-agent-provisioning.test.mjs: another administrator's status read and retry each get their own sentence.tests/integration/plugin-discovery-api.test.mjs: the caller's denials name the grant; the Agent principal's denial stays generic.go test ./internal/occcli(named denial gets no hint).tsc --build, ESLint and Prettier on touched files; docs word counts checked (authorization 2463, deployment 2492).Risks
codeFORBIDDENkeeps working.deletion.mtsand oneconsole.mdsentence; whichever lands second merges them (small, textual).error.authorizationenvelope field so clients stop parsing text (contract change).