| index 2f35a07dc..a74696222 100644 |
| --- a/internal/console/authcli.go |
| +++ b/internal/console/authcli.go |
| @@ -473,7 +473,7 @@ func (s *Server) registerCLIRoutes() { |
| // The CLI's read-only API (apicli.go) — deliberately a different prefix |
| // (/api/cli/) from the identity routes above (/auth/cli/), which must not |
| // move now that CLIs depend on them. |
| - s.cliRoute("GET /api/cli/apps", svcrole.Allow(svcrole.Publish, svcrole.Instance, svcrole.Operate), s.handleAPIApps) |
| + s.cliRoute("GET /api/cli/apps", svcrole.Allow(svcrole.Publish, svcrole.Deploy, svcrole.Instance, svcrole.Operate), s.handleAPIApps) |
| // The fleet-wide live tenancy read (discussion #11). An EMPTY role set: |
| // no service credential of any role reaches it, because the answer names |
| // every account's instances on every box, and that is an operator's view |
| @@ -485,10 +485,10 @@ func (s *Server) registerCLIRoutes() { |
| // bare POST .../{app} route exists to collide with, and the mux prefers |
| // the literal if one ever does. |
| s.cliRoute("POST /api/cli/apps/{account}/new", svcrole.Allow(svcrole.Publish), s.handleAPICreateApp) |
| - s.cliRoute("GET /api/cli/apps/{account}/{app}/channels", svcrole.Allow(svcrole.Publish), s.handleAPIChannels) |
| - s.cliRoute("POST /api/cli/apps/{account}/{app}/promote", svcrole.Allow(svcrole.Publish), s.handleAPIPromote) |
| - s.cliRoute("POST /api/cli/apps/{account}/{app}/rollback", svcrole.Allow(svcrole.Publish), s.handleAPIRollback) |
| - s.cliRoute("POST /api/cli/apps/{account}/{app}/releases", svcrole.Allow(svcrole.Publish), s.handleAPIShip) |
| + s.cliRoute("GET /api/cli/apps/{account}/{app}/channels", svcrole.Allow(svcrole.Publish, svcrole.Deploy), s.handleAPIChannels) |
| + s.cliRoute("POST /api/cli/apps/{account}/{app}/promote", svcrole.Allow(svcrole.Publish, svcrole.Deploy), s.handleAPIPromote) |
| + s.cliRoute("POST /api/cli/apps/{account}/{app}/rollback", svcrole.Allow(svcrole.Publish, svcrole.Deploy), s.handleAPIRollback) |
| + s.cliRoute("POST /api/cli/apps/{account}/{app}/releases", svcrole.Allow(svcrole.Publish, svcrole.Deploy), s.handleAPIShip) |
| // `carlos restart` (issue #113): a member's "cycle this app's processes" |
| // with no version and no config to push — the same write-door class as |
| // promote and rollback, and the only one whose subject is a running |
| @@ -522,11 +522,11 @@ func (s *Server) registerCLIRoutes() { |
| // release versioning spec 2026-08-08). GET .../releases coexists with |
| // the POST above on the SAME path — a distinct method route, not a |
| // collision — for `carlos releases` to read what `carlos ship` writes. |
| - s.cliRoute("GET /api/cli/apps/{account}/{app}/version-target", svcrole.Allow(svcrole.Publish), s.handleAPIVersionTargetGet) |
| + s.cliRoute("GET /api/cli/apps/{account}/{app}/version-target", svcrole.Allow(svcrole.Publish, svcrole.Deploy), s.handleAPIVersionTargetGet) |
| s.cliRoute("PUT /api/cli/apps/{account}/{app}/version-target", svcrole.Allow(svcrole.Publish), s.handleAPIVersionTargetSet) |
| - s.cliRoute("GET /api/cli/apps/{account}/{app}/tags", svcrole.Allow(svcrole.Publish), s.handleAPITagsList) |
| - s.cliRoute("GET /api/cli/apps/{account}/{app}/tags/{version}", svcrole.Allow(svcrole.Publish), s.handleAPITagGet) |
| - s.cliRoute("GET /api/cli/apps/{account}/{app}/releases", svcrole.Allow(svcrole.Publish), s.handleAPIReleasesList) |
| + s.cliRoute("GET /api/cli/apps/{account}/{app}/tags", svcrole.Allow(svcrole.Publish, svcrole.Deploy), s.handleAPITagsList) |
| + s.cliRoute("GET /api/cli/apps/{account}/{app}/tags/{version}", svcrole.Allow(svcrole.Publish, svcrole.Deploy), s.handleAPITagGet) |
| + s.cliRoute("GET /api/cli/apps/{account}/{app}/releases", svcrole.Allow(svcrole.Publish, svcrole.Deploy), s.handleAPIReleasesList) |
| // The pipeline API (apipipeline.go, release pipelines v2). GET is |
| // release metadata like channels/releases above — Publish reaches it. |
| // PUT rewrites the policy governing every promotion into every channel |
| @@ -539,14 +539,14 @@ func (s *Server) registerCLIRoutes() { |
| // below. Naming Admin explicitly is what actually opens the door to an |
| // admin-role service and closes it to every other role, exactly like |
| // the ledgers and store families. |
| - s.cliRoute("GET /api/cli/apps/{account}/{app}/pipeline", svcrole.Allow(svcrole.Publish), s.handleAPIPipelineGet) |
| + s.cliRoute("GET /api/cli/apps/{account}/{app}/pipeline", svcrole.Allow(svcrole.Publish, svcrole.Deploy), s.handleAPIPipelineGet) |
| s.cliRoute("PUT /api/cli/apps/{account}/{app}/pipeline", svcrole.Allow(svcrole.Admin), s.handleAPIPipelinePut) |
| // What the app is waiting on: the proposals a gated promote, fast-track |
| // or policy edit turned into (pendinghandlers.go). Read-only, and Publish |
| // like channels and releases — a proposal names no secret, and the CLI |
| // needs to see one precisely because it can never confirm one itself |
| // (spec §4: confirming takes a passkey, in the console). |
| - s.cliRoute("GET /api/cli/apps/{account}/{app}/pending", svcrole.Allow(svcrole.Publish), s.handleAPIPending) |
| + s.cliRoute("GET /api/cli/apps/{account}/{app}/pending", svcrole.Allow(svcrole.Publish, svcrole.Deploy), s.handleAPIPending) |
| // Trash and restore (apicli.go). Delete resolves through the LIVE-apps |
| // door and restore through the trashed-apps one, so each verb answers |
| // only for an app in the state it can act on — and both give a stranger |
| @@ -585,10 +585,10 @@ func (s *Server) registerCLIRoutes() { |
| // while the handler refuses is a registration that lies. As at the mint |
| // door, the ANSWER a credential gets here moves from 401 to 403. |
| s.cliRoute("PUT /api/cli/apps/{account}/{app}/instances-manifest", svcrole.Set{}, s.handleAPIInstanceEnable) |
| - s.cliRoute("GET /api/cli/apps/{account}/{app}/instances", svcrole.Allow(svcrole.Instance), s.handleAPIInstanceList) |
| + s.cliRoute("GET /api/cli/apps/{account}/{app}/instances", svcrole.Allow(svcrole.Instance, svcrole.Deploy), s.handleAPIInstanceList) |
| // The armed-regions answer for consumer pickers (issue #346) — the |
| // same gate and role as the list beside it; see handleAPIRegions. |
| - s.cliRoute("GET /api/cli/apps/{account}/{app}/regions", svcrole.Allow(svcrole.Instance), s.handleAPIRegions) |
| + s.cliRoute("GET /api/cli/apps/{account}/{app}/regions", svcrole.Allow(svcrole.Instance, svcrole.Deploy), s.handleAPIRegions) |
| // The per-caller region guess (nearest-region spec §9.4): POST, so the |
| // hints never ride a URL a redirect could echo. |
| s.cliRoute("POST /api/cli/apps/{account}/{app}/regions/guess", svcrole.Allow(svcrole.Instance), s.handleAPIRegionGuess) |