Plan 76 milestone A, drift/bug row 2 — scheduled as a fix, not a refactor,
because it is a behaviour difference shipping today.
rbac.get_current_user() is the only lookup that knows about both auth
mechanisms: it returns g.api_key_user for an X-API-Key request and falls back
to the JWT. 73 call sites answered "who is calling" with
`User.query.get(get_jwt_identity())` instead, behind 19 near-identical private
helpers (12 named get_current_user(), shadowing the real door).
The audit that scheduled this row predicted those return None for API-key
callers. Measured, it is worse: auth_required() deliberately skips
verify_jwt_in_request() once the key middleware has authenticated, so there is
no JWT context and get_jwt_identity() *raises*. Nine handlers reached through
an API-key-capable policy decorator therefore returned 500 to every API-key
caller:
bandwidth.get_app_bandwidth @viewer_required
servers.set_server_workspace @developer_required
servers.create_server @developer_required
sso.get_sso_config / update_provider_config / update_general_settings
source_connections.update_{github,gitlab,bitbucket}_admin_config
source_connections.complete_github_app_manifest
databases.execute_{mysql,pg,sqlite,docker}_query (see below)
Three service helpers had the same lookup inside a blanket `except Exception`,
which converted the raise into a silent user_id=None — the write landed and
the audit trail lost the actor (shared_resource_service, cf_ops_change_service,
secrets_webhooks).
The four databases.execute_*_query sites were a second admin check *inside* an
already-@admin_required route: dead code that always passed for JWT callers and
crashed for API-key ones. They now call require_admin_user() rather than
re-deriving the answer.
Also folds in a sixth _require_admin() found in the cloudflare-ops extension
source (29 gates, the same inverted contract as jobs.py), and fixes
serverkit-gui's blueprint at its sibling-repo source rather than in this repo's
live copy.
Ratchet (second-wave rule 1 — a migration without one regrows):
tests/identity_door_census.py counts both bypass shapes by AST, direct and the
two-line `uid = get_jwt_identity(); User.query.get(uid)` variant. The ceiling is
87, all of them on @jwt_required()-only routes where reading the JWT is correct
today; the ratchet's third assertion holds the API-key-capable population at
exactly zero, so migrating one of those routes to a policy decorator without
fixing its lookup now fails. Verified non-vacuous: an injected bypass fails both
assertions and passes again on revert.
The first wave's controller-boundary baseline is regenerated — this removes 74
persistence crossings (593 -> 519), a debt reduction with no new entries.
Gate: tests/test_api_key_identity.py (7, each red before the fix),
test_identity_door_ratchet.py (10), test_api_controller_boundaries.py (4), plus
the 65 test files covering every touched module: 1024 passed, 10 skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Project / Environment hierarchy (C2): Workspace -> Project -> Environment
org model. New Project/Environment models + CRUD APIs, nullable
applications.project_id/environment_id (opt-in, no destructive backfill),
ProjectService.ensure_default, a Projects list + ProjectDetail (env tabs),
a project/environment selector in the create wizard, and sidebar nav.
- Polymorphic shared resources facade (C9): ResourceTag + SharedVariableGroup
/SharedVariable/attachment models keyed by (resource_type, resource_id),
reusing the env-var Fernet encryption; SharedResourceService resolves merged
variables (most-recent attachment wins) with secrets masked. /api/v1/shared
endpoints + reusable TagsPanel/EnvironmentVariablesPanel + a Shared Variables
management page. Existing per-resource tables untouched (facade first).
- PR preview environments (C3): ApplicationPreview + settings models, a
PreviewService with pure domain-templating + reconcile (open PRs vs active
previews) cores and best-effort provisioning (incl. guarded WordPress
file+DB clone), a pull_request webhook endpoint, preview.* jobs, and a
Previews tab on the app detail page.
- Per-server managed proxy stack (C6): opt-in Dockerized Traefik/Caddy as a
Compose stack (host Nginx stays default and superior for PHP/WordPress).
ProxyStack model + ProxyStackService with pure compose/config generators and
versioned config backups, /api/v1/servers/<id>/proxy endpoints, and a Proxy
tab on the server detail page.
Migrations 031-034 add the new tables/columns; the full chain applies cleanly
from an empty DB. Full backend suite: 871 passed, 14 skipped; frontend builds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>