Skip to content

Let CI reach the admin vhost once Access is in front of it - #371

Merged
Babissimo merged 3 commits into
mainfrom
feat/admin-access-ci
Sep 14, 2026
Merged

Let CI reach the admin vhost once Access is in front of it#371
Babissimo merged 3 commits into
mainfrom
feat/admin-access-ci

Conversation

@Babissimo

@Babissimo Babissimo commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Prepares CI for the Cloudflare Access applications that ClickUp
123zgec1gtf puts in front of the admin
hostnames. Nothing here depends on those applications existing, and nothing
changes behaviour until they do: this is the half that has to land first, so
they can be created without the pipeline going red in the window before the
backend change follows.

Independent of the backend PR #372 and touches no file it does. Either order.

Why now rather than with the rest

An Access application gates its hostname the moment it is saved, and there is no
disabled state. staging-admin.retina.fm is probed by deploy/staging-smoke-test.sh
and driven by frontend/playwright.config.ts, and e2e-staging is a needs: of
deploy-production, so creating that application before CI can present a
credential stops every production deploy. With this merged, the applications can
be created at leisure.

Three changes

The /api/admin/storage probe is deleted. It polled that route on
api.retina.fm for two minutes and failed the job if it never answered 200 or
202, and a failed production smoke test runs deploy/rollback.sh on
retina-prod. Once require_admin enforces identity it answers 401 to CI's
unauthenticated curl and would revert every deploy. A service token cannot
rescue it: api.retina.fm is the fleet's ingest hostname and must never carry an
Access application, so there is nothing to authenticate against and the 401 comes
from our own backend.

Deleted rather than moved, because what it measures does not belong in that gate:
it reports whether a background du scan has finished, which is a capacity signal
on a timer rather than evidence the build now running is healthy. Everything else
in that block probes public routes on the request path a user actually takes.

The smoke suites can present a service token. The headers live in
deploy/tower-contract.sh, which both suites already source, so the two cannot
drift on what they send. Empty unless both variables are set, so an ungated
hostname and a developer running the script by hand see exactly today's request.
Half a credential contributes nothing either, so a secret configured on its own
cannot produce a request refused for a reason nobody can see.

Playwright can too, via use.extraHTTPHeaders, plus an exported
accessHeaders for specs that build their own request contexts (those do not
inherit use).

Verification

  • deploy/tower-contract.sh exercised in all three states: neither variable set
    (no headers, and survives set -euo pipefail), both set (four correctly quoted
    argv words), one set (nothing).
  • Workflow YAML parses; both smoke steps and e2e-staging receive the secrets.
  • typecheck:e2e passes; npm run typecheck also fails in src/ on this
    machine for unrelated reasons (local Node 26 against CI's pinned 20).
  • Backend suite green, pre-commit run --all-files clean.

What this does not do

The token buys passage through the edge and nothing more. A service-token
assertion carries common_name rather than email, so the origin finds no
identity in it and it cannot reach an admin route. /api/towers, which these
probes actually assert, needs none.

🤖 Generated with Claude Code

Babissimo and others added 3 commits September 14, 2026 11:48
The production smoke test polled GET /api/admin/storage on api.retina.fm for two
minutes and failed the job if it never answered 200 or 202. A failed production
smoke test SSHes to retina-prod and runs deploy/rollback.sh, so this probe could
revert a release on its own.

It has to go before require_admin starts enforcing identity, because it would
then answer 401 to CI's unauthenticated curl and revert every deploy. A service
token cannot rescue it either: api.retina.fm is the fleet's ingest hostname and
must never carry an Access application, so there is nothing for a token to
authenticate against, and the 401 comes from this codebase rather than the edge.

Deleting rather than relocating it, because what it measures does not belong in
that gate. It reports whether a background du scan has finished, which is a
capacity signal on a timer, not evidence that the build now running is healthy;
everything else in the block probes public routes and asserts on the request
path a user actually takes. If the storage figure is worth watching it belongs
in alerting, where noticing it late costs nothing.

Nothing pinned this probe. test_towers_vhost_coverage.py constrains the staging
smoke list, and test_smoke_tally_shared.py constrains only the shared counters,
which the block still sources and still uses for the elevation and aircraft
checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tower contract is asserted against admin.retina.fm and staging-admin, and
once those hostnames carry an Access application a browserless request meets a
302 to the login page. curl runs without -L, so assert_tower_contract reports
`got HTTP 302` and the suite reads a missing credential as a routing fault.

Both suites already share this file, so the headers go here rather than at each
call site: the production block in ci.yml and staging-smoke-test.sh pick them up
by sourcing it, and the two cannot drift on what they present.

Inert until both variables are set, which is what makes it safe to land before
the Access applications exist: an ungated hostname sees exactly the request it
sees today, and so does a developer running the script by hand. Half a
credential contributes nothing either, so a secret configured on its own cannot
produce a request that is refused for a reason nobody can see.

The array is expanded with the `+` guard at every call site because expanding an
empty one is an unbound-variable error under `set -u` before bash 4.4, and
staging-smoke-test.sh runs with `set -euo pipefail`.

The token buys passage through the edge and nothing more. A service-token
assertion carries common_name rather than email, so the origin's verifier finds
no identity in it and it cannot reach an admin route; /api/towers, which is what
these probes actually assert, needs none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The admin vhost will sit behind an Access application, and Playwright follows
redirects, so a browser test against it would fetch Cloudflare's login page and
then fail somewhere unhelpful: parsing that HTML as the app, rather than saying
it was never admitted.

The headers go on `use` so page navigation carries them, and are exported as
well because request.newContext() does not inherit `use` and a spec building its
own context against a gated host has to pass them itself.

Empty unless both variables are set, so an ungated hostname and anyone running
the suite locally see exactly what they see today. Both or neither: half a
credential is refused at the edge just like none, and sending one would only
make the refusal harder to read.

Note this buys passage and not identity. A service-token assertion carries
common_name rather than email, so the SPA's own /api/auth/me still comes back
unauthenticated behind it, which is why this alone does not keep the admin
surface-selection test alive once the bypass is closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown

Claude finished @Babissimo's task in 4m 39s —— View job


Review: Let CI reach the admin vhost once Access is in front of it

Traced every place this PR touches — ci.yml's three smoke/E2E jobs, tower-contract.sh's three curl call sites, and playwright.config.ts — against the rest of the E2E suite and the backend's current require_admin/AUTH_BYPASS state. Overall this is careful, well-scoped prep work and I didn't find a correctness bug.

Verified correct:

  • TOWER_CONTRACT_CF_HEADERS=() + ${arr[@]+"${arr[@]}"} at all three curl sites (assert_tower_contract, _assert_json_keys, assert_elevation_contract) is the standard bash idiom for "expand this array only if it's non-empty" under set -u — correct, and matches the set -euo pipefail constraint staging-smoke-test.sh imposes.
  • Both gated hostnames are covered by the credentialed path: staging-admin.retina.fm/api/towers (staging-smoke-test.sh:294) and admin.retina.fm/api/towers (ci.yml:1258) both route through assert_tower_contract, which now carries the headers. No admin-vhost curl in either script bypasses tower-contract.sh.
  • e2e-prod deliberately does not get the new secrets — consistent with playwright.config.ts's admin: null for prod, so there's no admin-gated request in that job to authenticate.
  • The deleted /api/admin/storage block leaves no orphaned references (SCODE, the loop counters) and the tally in smoke-tally.sh has no dependency on check count, so the deletion is safe.
  • Cross-checked the "why now" claim against backend/core/users.py: require_admin currently short-circuits to the anonymous admin whenever AUTH_BYPASS is set (which the E2E docstring says is every current deployment), so today this route still answers 200/202 unauthenticated. The probe's removal is genuinely pre-emptive for when a future backend change flips that off — matches the PR's own framing that this is independent, no-op prep.
  • Each of the three secrets-consuming steps in ci.yml has the env: block scoped to the correct step (not job-wide), matching the existing style of sibling env vars (e.g. RADAR_API_KEY) in the same steps.

Worth noting, not blocking:

  • accessHeaders (playwright.config.ts:85) is currently dead code — every existing spec that builds its own request.newContext() targets hosts.api or hosts.dash (both intended to stay ungated per the PR's own claim about api.retina.fm), so nothing in the current suite actually imports/uses the export yet. That's explicitly disclosed in the PR body as forward-looking prep for admin-surface specs that don't exist yet, so I'd call it intentional rather than an oversight — just flagging so the reviewer knows it's unexercised by this diff's own tests.
  • Once the (separate, backend) PR flips AUTH_BYPASS off, frontend/e2e/dashboard.spec.ts's "admin API backing (no auth required)" tests (GET /api/admin/events, GET /api/admin/storage, lines ~217–241) will start getting 401s the same way the deleted CI probe would have — those hit hosts.api, so no Access application applies, and the failure would come from the backend itself, same as this PR's own reasoning about api.retina.fm. Not this PR's problem to fix (it explicitly touches no file the backend PR does), but likely worth a heads-up to whoever lands that PR so it isn't a surprise red e2e-staging/e2e-prod run.

Verification claims in the PR description (tower-contract.sh exercised in all three secret states, workflow YAML parsing, typecheck:e2e, pre-commit) look consistent with what's in the diff; I wasn't able to independently re-run them in this sandbox (shell execution beyond git/ls is not permitted here), so I relied on static reading of the full call sites instead.

No changes requested.

@Babissimo
Babissimo merged commit 9f355f8 into main Sep 14, 2026
31 checks passed
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