fix(control-plane): close tenant-isolation, webhook-secret, and overlap bugs before first deploy - #9170
Merged
Conversation
…ap bugs before first deploy (#9143) control-plane/ has never been deployed (no deploy workflow, placeholder wrangler.jsonc values) so every defect here is latent, but a first deploy would ship all eight intact. Deploy blockers: - neon-database-driver.ts: branchNameFor now always appends a hash suffix derived from the RAW pre-sanitization tenant identity (not the already case-folded/punctuation-collapsed string #8026's suffix hashed), so tenant names that sanitize identically ("Acme"/"acme", "acme.corp"/"acme-corp") can no longer share one Neon branch/database/role/password. - tenant-registry.ts: upsert's installation-ID secondary-index cleanup is now a compare-and-delete (only clears the index if it still points at the upserting tenant's own primary key), so a stale tenant's later re-creation/teardown can no longer delete a different, currently-active tenant's live webhook-routing pointer. Added PATCH /v1/tenants/:name/orb-installation as the manual recovery path for a pointer already lost to the pre-fix bug. - secret-driver.ts + src/orb/hosted-webhook-secret.ts (new): a hosted ORB tenant's container now gets its own webhook secret bundled into the same broker exchange as its database credential, and handleOrbWebhook resolves it via the brokered path when no direct env value is set -- wiring fetchBrokeredStoredSecret's previously-unused call site for real, so correctly-signed deliveries no longer 401 forever. Also addressed: - http-app.ts: POST /v1/tenants/rollout now returns an explicit 501 instead of silently no-opping on all four of its promised effects; DELETE refuses a tenant still in the "provisioning" state (409). - container-driver.ts + worker.ts: envVars are now persisted in the container DO's own durable storage and reloaded on construction, so a pinned version / bootstrap secret / central PostHog key survive an implicit restart instead of resetting to an empty environment. - orb-webhook-router.ts: the unauthenticated webhook route now enforces the same content-length precheck + streaming body-size limit its declared twin (src/orb/webhook.ts, #8888) already has. - ams-wake.ts: nextDueAt is claimed before a tenant's poll starts (not after it resolves), the per-tenant poll timeout is capped well under the 5-minute cron interval, and one tick now bounds how many due tenants it wakes -- closing the guaranteed overlapping-tick race. Deferred, documented in the PR: the full KV-eventual-consistency fix (a Durable Object/D1-backed serialized claim) is out of scope for this PR; the deploy workflow and real wrangler.jsonc placeholder values remain outstanding for whichever change first makes this workspace deployable.
Contributor
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9170 +/- ##
==========================================
- Coverage 93.90% 92.69% -1.21%
==========================================
Files 817 818 +1
Lines 81039 81254 +215
Branches 24611 24636 +25
==========================================
- Hits 76096 75322 -774
- Misses 3568 4848 +1280
+ Partials 1375 1084 -291
Flags with carried forward coverage won't be shown. Click here to find out more.
|
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.
Summary
Closes #9143
control-plane/(the hosted-ORB tenant-provisioning substrate) is not currently deployed and cannot be — there is no deploy workflow, andwrangler.jsoncstill has placeholderREPLACE_WITH_REAL_*values. Every defect below is latent today; a first deploy would ship all eight intact. This PR fixes the three deploy blockers fully, and fixes or explicitly scopes down the remaining five.Fixed fully
Cross-tenant Neon-branch collision (deploy blocker).
branchNameFor(neon-database-driver.ts) now appends a hash suffix unconditionally (not only past the 63-char truncation threshold, Neon branchNameFor's 63-char truncation has no collision guard — two long, prefix-similar tenant names could resolve to the same branch #8026), and hashes the raw, pre-sanitization${product}:${tenant.name}identity instead of the already-lowercased/punctuation-collapsed string. Two tenant names that sanitize identically ("Acme"/"acme", "acme.corp"/"acme-corp"/"acme corp") used to derive the identical branch name and share one Neon branch/database/role/live password; they now always diverge. Regression test:neon-database-driver.test.ts's"sanitize-colliding tenant names ... never share a branch (#9143 isolation)".Cross-tenant webhook-routing-index deletion (deploy blocker).
tenant-registry.ts'supsertno longer unconditionally deletes the oldinstallation:<id>index entry when a tenant'sorbInstallationIdchanges/clears — it now re-reads the index and only deletes it if it still points at the upserting tenant's own primary key (a compare-and-delete). Before this fix, a stale tenant (still carrying an old installationId from creation) being re-created or torn down long after a different tenant legitimately reclaimed that installation ID would silently delete the current claimant's live routing pointer, leaving itactivebut permanently unreachable by webhook with no way back (create 409s;orbInstallationIdis settable only at create). AddedPATCH /v1/tenants/:name/orb-installationas the manual recovery path for a pointer already lost to the pre-fix bug. Regression test:tenant-registry.test.ts's"an unrelated tenant's installation-index entry SURVIVES a stale record's later re-creation/teardown upsert (#9143)".Hosted tenant containers could never verify a routed webhook (deploy blocker).
createTenantContainernever injectedORB_GITHUB_WEBHOOK_SECRET, so every correctly-signed GitHub delivery to a hosted ORB tenant would 401 forever, andfetchBrokeredStoredSecret(Deliver secrets/env vars into a running hosted tenant container (OrbTenantContainer/AmsTenantContainer) #8202) had full test coverage but zero production call sites. Fixed by bundling a freshly generated per-ORB-tenant webhook secret into the same broker enrollment/bootstrap exchangesecret-driver.tsalready mints for the tenant's database credential (one bootstrap token, one exchange — not a second plumbing path), and adding the missing call site:src/orb/hosted-webhook-secret.ts(new) resolves it viafetchBrokeredStoredSecretwhenLOOPOVER_TENANT_SECRET_TOKENis set and no directORB_GITHUB_WEBHOOK_SECRETis configured, wired intohandleOrbWebhook. Memoized per-container (successful resolution cached forever; a failed exchange is not cached, so a transient broker outage self-heals on the next delivery). Zero behavior change for normal self-host/cloud (direct env value short-circuits with no network call).Fixed, scoped down where noted
Env vars lost on container restart.
envVarsare now persisted in the container DO's own durable storage (worker.ts'sProvisionedContainer) and reloaded intothis.envVarsin the constructor viactx.blockConcurrencyWhile— the vendored@cloudflare/containersSDK never itself persists astart({envVars})call's values, so an implicit restart (e.g.containerFetch's auto-wake-on-request path) used to silently boot with an empty environment.POST /v1/tenants/rolloutwas a silent no-op. All four of its promised effects were broken (only wrote the registry field;createTenantContainerearly-returns once provisioned so it never reached the env-var injection site;provisionTenantconstructs a freshTenantthat drops the pin anyway on next provision; nothing ever readsLOOPOVER_PINNED_VERSIONback out). Per the issue's own framing ("Rollback is the control you least want to discover is fake"), this now returns an explicit501 not_implementedrather than accepting a request that changes nothing observable.Tenant.pinnedVersion/PINNED_VERSION_ENV_VARare left in place for whenever a real rollout mechanism is built.Unauthenticated webhook route had no body-size limit.
orb-webhook-router.tsnow has the same content-length precheck + streaming-limit enforcement as its declared twinsrc/orb/webhook.ts(fixed there by fix(github): handleOrbRelay skips the pre-read Content-Length rejection that handleGitHubWebhook has #8888) — it used to buffer an unbounded body twice (clone +.text()) before ever checking the signature.Guaranteed overlapping AMS-wake ticks.
ams-wake.tsnow claims a tenant'snextDueAtbefore starting its poll (not after it resolves), caps the per-tenant poll timeout well under the 5-minute cron interval (10min → 2min), and bounds how many due tenants one tick wakes (DEFAULT_MAX_TENANTS_PER_TICK = 20, deferring the rest to the next tick). Regression test proves an overlapping tick no longer re-wakes a tenant whose prior-tick poll is still in flight.KV eventual consistency makes every claim check advisory. Implemented the issue's stated minimum:
DELETE /v1/tenants/:namenow refuses a tenant still in the"provisioning"state (409) instead of racing the in-flightprovisionTenantcall. The full fix — moving the tenant index into a Durable Object or D1 so check-and-claim is one serialized transaction — is a materially larger migration and is deferred, left as an explicit follow-up.Explicitly out of scope for this PR
wrangler.jsoncvalues (REPLACE_WITH_REAL_NEON_PROJECT_ID,REPLACE_WITH_REAL_KV_NAMESPACE_ID) — this PR does not make the workspace deployable, so per the issue these remain outstanding for whichever change does.Scope
type(scope): short summaryConventional Commit format, for examplefix(api): restore profile access checks.control-plane/plus thesrc/orb/webhook-secret call site that issue control-plane: eight pre-deploy defects, headlined by two tenants sharing one Neon database (and one customer's teardown deleting another's) #9143 explicitly requires.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Validation
git diff --checknpm run typecheck(root) — cleancontrol-plane's own gate:npm run build,npm run cf:typecheck, andnode --test --experimental-strip-types "test/**/*.test.ts"— 227/227 passingcontrol-plane's own coverage (node scripts/control-plane-coverage.mjs): every file touched in this PR is at 100% line+branch coverage (verified per-file with a scoped c8 run); the one remaining project-level gap (settlement-backend-driver.ts) is pre-existing and untouched by this PRnpm run test:changed(vitest, diff-scoped againstorigin/main): 121 files / 1551 tests passing, including the newtest/unit/orb-hosted-webhook-secret.test.ts(100% coverage) and updatedtest/integration/orb-webhook.test.tsnpm run docs:drift-check,npm run manifest:drift-check,npm run coverage-boltons:check— all passnpm run test:ci/npm audit --audit-level=moderate— not run locally (this PR's scope is large enough that the full monorepo gate is left to CI, per direction from the repo owner); every check above that overlaps withtest:ci(typecheck, control-plane's own build/test/coverage, drift checks) was run and is greennpm run ui:openapi:check/ui:lint/ui:typecheck/ui:build/test:workers/build:mcp/test:mcp-pack— not applicable, noapps/loopover-ui, worker-test, or MCP-package changes in this PRIf any required check was skipped, explain why:
npm run test:ciandnpm auditwere not run locally for this PR at the repo owner's direction (large multi-defect PR; CI will run the full gate). Every check materially relevant to the actual diff (control-plane's own build/typecheck/tests/coverage, root typecheck, diff-scoped vitest, and the drift checks) was run locally and is green.Safety
PATCH /v1/tenants/:name/orb-installationroute,POST /v1/tenants/rolloutbehavior change — both fully covered by new tests). No OpenAPI regen needed: control-plane is a separate Hono service with no OpenAPI generation pipeline.UI Evidence— not applicable, no visible UI change.CHANGELOG.mdis untouched, as required.UI Evidence
Not applicable — no visible UI/frontend change in this PR.
Notes
wrangler.jsoncplaceholders — the workspace remains non-deployable after this PR, and the issue's own framing only asks for that "as part of whichever change first makes this deployable."DELETEon"provisioning") is implemented here.