Skip to content

fix(teams): gate team create, update and delete server-side - #92

Open
atbagan wants to merge 2 commits into
mainfrom
fix/team-write-authorization
Open

atbagan wants to merge 2 commits into
mainfrom
fix/team-write-authorization

Conversation

@atbagan

@atbagan atbagan commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Problem

POST /api/teams, PUT /api/teams/{name} and DELETE /api/teams/{name} sit in the plain authenticated group (internal/api/router.go:495-498) and the handlers performed no authorization. The Team admission webhook (ADR-009) only guards spec.resourceLimits and spec.environments[].limits. Result: any authenticated user could create teams, rename or re-default any team, and delete any team. Delete ran as the server service account, so even the webhook never saw a caller.

Found during the 2026-08 portal/console parity audit (server route table, H1).

Change

  • Create: platform admin only.
  • Update: platform admin, or team admin of that team; resourceLimits in the body stays platform-admin only (same split as the webhook).
  • Delete: platform admin only, and now impersonates the caller (AsUser) like create and update so the apiserver audit trail carries the real identity.
  • Decisions are pure functions authorizeTeamCreate / authorizeTeamUpdate / authorizeTeamDelete in handlers/teams.go.

The /api/admin/teams duplicates are unchanged; they keep AdminMiddleware and now also pass through the same handler checks.

Verification

  • CGO_ENABLED=0 go build ./... clean
  • CGO_ENABLED=0 go vet ./internal/api/handlers/ clean
  • CGO_ENABLED=0 go test ./... passes; new teams_authz_test.go covers nil session (401), platform admin, platform viewer, team admin own/other team, operator, viewer, and the resourceLimits carve-out.
  • Console callers: AdminTeamsPage (POST /admin/teams, platform admin), AdminTeamDetailPage (PUT limits/defaults, platform admin), TeamSettingsPage (PUT display fields, team admin), DELETE /teams/{t} from the admin team page. None of these paths change for their intended roles.

Out of scope

Replacing the any-team-admin AdminMiddleware on the /admin write group (separate PR), member and group sync handlers (per-team checks tracked separately), audit middleware attribution.

POST /api/teams, PUT /api/teams/{name} and DELETE /api/teams/{name}
are registered in the plain authenticated group with no middleware,
and the handlers had no authorization of their own. The Team admission
webhook only guards spec.resourceLimits and spec.environments[].limits,
so any authenticated user could create teams, rename or re-default any
team, and delete any team (the delete ran as the server service
account, bypassing the webhook entirely).

Create and delete now require a platform admin. Update allows a
platform admin, or a team admin of that team for everything except
resourceLimits, which stays platform-admin only to match the webhook.
Delete impersonates the caller like create and update so the apiserver
audit trail carries the real identity.

Decisions live in authorizeTeamCreate/Update/Delete with table tests.
Review follow-up. An RBAC Forbidden from the impersonated Delete call
was mapped to 500; it now returns 403 with a clear message and a
warning log, matching how the other impersonated writes surface
admission denials.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant