Repository navigation
feat(aster-srv): person tokens read every dashboard dataset; aster srv; route-table auth gate - #189
Merged
Merged
Conversation
Contributor
Author
|
ArchDev is watching this pull request. State: settled (cycle 2 of 2 for head 7cb70b4).
Paused by the |
calvin-archastro
marked this pull request as draft
October 23, 2026 21:09
…ble auth gate - Person tokens (scope read, cache:read still accepted) read every dataset the /ui pages show over /v1 JSON, for any served repository named with ?repo=, and are refused every write. New GET /v1/test-results/clusters reuses the dashboard's query. - aster srv get|cache|flaky|broken|quarantine|clusters|test prints that JSON with the stored sign-in (CI credentials win when present). - routes::ROUTES is the single route table (method, pattern, auth class, handler). The router authenticates from headers before reading any body, deny by default for /v1; lookup/upload bodies are capped at 1 MiB. - Writers: cache writes need event_name push; repository_owner_id pins; writer workflows required when writers are configured; pull_request_target refused for results uploads. Exact IAP audience; nbf; generic token errors; stale JWKS keys evicted in both verifiers; JWKS fetch shared in iap.rs; person lookups do not count; blob stand-in behind ASTER_SRV_DEV_BLOB_ROUTE. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
calvin-archastro
force-pushed
the
calvin/aster-person-reads
branch
from
October 23, 2026 21:24
7cb70b4 to
4d7bd3d
Compare
calvin-archastro
marked this pull request as ready for review
October 23, 2026 21:34
Merged
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.
Review on ArchCode
Merge order: rebased onto #188 (aster-srv hardening) and #183/#184/#187; ships in 0.24.0 with #182 (person-token login), already merged. #185 rebases onto this after it merges.
Problem and author intent
Calvin and the overseer need to read dashboard data (flaky, broken, clusters, quarantine, cache) with the token
aster cache loginalready gives them./uiis IAP-gated HTML only, so a script or agent cannot read it./v1/cache/summary|entries|lookupand403s on every/v1/test-results/*route.Intent: every dashboard dataset is readable over
/v1JSON by a person token, no write is, and a CLI prints it. Review of #182 also found auth gaps, fixed here (checklist below):What changed
/uipage/uiGET /v1/test-results/tests?state=flakyaster srv flaky/uiGET /v1/test-results/tests?state=brokenaster srv broken/ui/clustersGET /v1/test-results/clusters(sameui::data::clustersquery)aster srv clusters/ui/quarantineGET /v1/test-results/quarantineaster srv quarantine/ui/tests/<id>GET /v1/test-results/tests/<id>aster srv test <id>GET /v1/test-results/runs/<id>/failuresaster srv getGET /v1/test-results/durationsaster srv gettarget=)/ui/cacheGET /v1/cache/summary,/entriesaster srv cachePOST /v1/cache/lookupPOST /v1/test-results/runs,/v1/cache/upload,/commit?repo=; it must be onASTER_SRV_REPOSITORIESand the email domain rule applies. The handlers' own-repo rule then runs on an identity for that repository, so there is no second copy of the SQL.read.cache:readis still accepted as the same scope (person.rs,LEGACY_SCOPE): feat(aster-srv): harden the non-IAP /v1 channel #188 rejects feat: laptop cache login through aster-srv (aster cache login, person tokens) #182-shape tokens, but tokens it issued carrycache:readfor up to 12 hours, and refusing them would force a re-login on deploy. DropLEGACY_SCOPEafter 12 hours in production. feat(aster-srv): harden the non-IAP /v1 channel #188's strict claims (sub,nbf,iat,jti,email, HS256, 30 s leeway, key entropy) are unchanged.aster srv get|cache|flaky|broken|quarantine|clusters|test(src/test_results/read.rs) uses CI credentials when the env has them, else the stored sign-in. Repo defaults like the cache client (GITHUB_REPOSITORY, elseorigin). Non-zero exit with the reason on not-signed-in, 401, 403, 404.routes::ROUTES(method, pattern, auth classPublic|Iap|SignedUrl|Ci|CiOrPerson|CiWriter, handler).routes::gateis the only auth step: the main listener and feat(aster-srv): harden the non-IAP /v1 channel #188's/v1-only listener (ASTER_SRV_API_ADDR) both run the sameroute(), so both mount it.Rebase onto #188
is_v1_authed,preauthandaudit::route_labelwith the table.routes::gatenow does whatpreauthdid (audit and count every 401/403, charge the subject's rate limit,429) and whatauthenticatedid (auditperson_token_not_allowed).routes::labelderives the metric/audit route label fromROUTES; label strings are unchanged.http.rs::body_limit), used both for the declared-length check and the read: forms 8 KiB, lookup/upload 1 MiB, commit and results uploadmax_body_bytes, everything else none. feat(aster-srv): harden the non-IAP /v1 channel #188's check on the declared length still runs before auth, so a declared 100 MB body is413(no body read) rather than401; neither reads the body. A known path with the wrong method is405before the size check./v1-only listener: still404for non-/v1paths and for any route whose class isIap;/v1/healthzis answered there without auth as before./v1/path (including/v1/healthzon the main port, and on the API port) is401without a token and404only with one; it was404. Tests inhardening.rsupdated for this.ASTER_SRV_ALLOW_HTTP_PUBLIC_URL; key entropy). Test fixtures that used the bare backend-service prefix now use/projects/123456/global/backendServices/4242, since feat(aster-srv): person tokens read every dashboard dataset; aster srv; route-table auth gate #189 refuses the bare prefix.Scope, risk, user impact
/v1route and tightens write rules; no schema change. Read access is widened to people, and the deny-by-default gate narrows everything else.event_name == push; tokens without it are403.ASTER_SRV_CACHE_WRITERSwithoutASTER_SRV_CACHE_WRITER_WORKFLOWSfails startup (opt-outASTER_SRV_ALLOW_ANY_WRITER_WORKFLOW=1).ASTER_SRV_IAP_AUDIENCEmust be the exact backend-service audience (a trailing-/prefix fails startup).POSTto a read route is now401without a token (was405) and405with one.aster srv ...after oneaster cache login. Nothing changes for CI uploads that already come frompushonmainthroughci.yml.Security checklist
Paths under
crates/aster-srv/. Tests are intests/routes.rsunless noted.src/routes.rs(ROUTES:71,lookup:197,label:217,gate:251),src/http.rs:404-430(route)every_v1_route_answers_401_to_every_kind_of_bad_credentialwalksROUTES(+ an unlisted path + an unsupported method) with: none, garbage, empty, wrong scheme, expired, wrong key, nbf in the future, alg=none, RS/HS confusion (public and private PEM as HMAC key), wrong aud, wrong iss, person token with wrong aud/iss, valid IAP assertion with no token, forgedx-goog-*headerssrc/routes.rs(Public)the_code_exchange_is_the_only_v1_route_without_a_token_and_never_says_200_without_a_codeasserts400 invalid_grant, never 200;routes::tests::every_v1_route_but_the_code_exchange_needs_a_tokensrc/routes.rs(CiOrPerson)a_person_reaches_only_the_read_allowlist_and_every_other_route_is_403pins the allowlist and walksROUTES: reads allowed for a served repo, 403 for an unserved repo, 403 on everyCi/CiWriterroute;tests/cli_auth.rs::a_person_reads_every_dashboard_dataset_and_writes_nothingsrc/auth.rs(OIDC verify),src/cache/policy.rs:82(pushed),ci_readercache_writes_need_a_push_to_main_from_the_ci_workflow_in_an_allowed_repository: PR, PR claiming main,pull_request_target, feature branch, merge queue,workflow_dispatch,schedule,workflow_run, no event, other workflow, no workflow, other/norepository_owner_id, repo off the allowlist: all 403 on upload and commit; push to main from ci.yml is 200src/cache/policy.rs:92test_results_uploads_need_the_ci_workflow_and_refuse_pull_request_targetsrc/auth.rs(7dcd475),src/iap.rs:180-200concurrent_requests_for_an_unseen_key_share_one_slow_fetch,concurrent_assertions_for_an_unseen_key_share_one_slow_fetchsrc/auth.rs:119(into_replysends a generic message, logs the reason)src/http.rs:404-430(gate),:34(SMALL_BODY_BYTES),:311(body_limit: 1 MiB lookup/upload,max_body_bytesonly for commit)a_request_is_authenticated_before_its_body_is_read_and_capped_after: declared 100 MB body never sent gets 401 or 413 within 5 s on every POST route; 2 MiB lookup/upload 413src/iap.rs:234,src/config.rsiap::tests::the_audience_must_be_the_one_configured_backend_service,refuses_other_audiences...(another backend of the project),config::tests::reads_the_dashboard_settings(prefix refused at startup)src/cache/policy.rs,src/config.rs(OwnerIds, writers need workflows)config::tests::writers_need_workflows_and_owner_ids_pin_repositoriessrc/auth.rs,src/iap.rssrc/cache/routes.rs:556a_persons_lookup_does_not_count_or_keep_an_entry_alive_but_ci_doessrc/auth.rs:284,src/iap.rs:162src/http.rs:536,src/cache/routes.rs:63,ASTER_SRV_DEV_BLOB_ROUTEthe_signed_blob_stand_in_exists_only_when_the_dev_flag_is_setChoices to confirm:
pushonly (notscheduleorworkflow_dispatch).pull_request(and other non-pull_request_targetevents). A PR run's retry-then-pass is the flake signal, so refusing PR uploads would stop flake detection. Tell me if you want uploads restricted like cache writes.workflow_ref/job_workflow_refare not matched whenASTER_SRV_CACHE_WRITER_WORKFLOWSis unset for results (unchanged);repository_owner_idpinning is opt-in viaASTER_SRV_REPOSITORY_OWNER_IDS(firstlanding must set it).Also fixed
iap.rsJWKS refetch race: same fix as7dcd475(lock held across the fetch, re-check after acquiring) plus the regression test above.Testing
Canonical end-to-end proof:
tests/cache_login_e2e.rs::a_signed_in_laptop_reads_every_dashboard_dataset_with_aster_srv.asterbinary, real aster-srv, real Postgres.aster results upload(OIDC token from a stand-in endpoint); the classifier pass runs.aster srv flakyexits non-zero withnot signed in; run aster cache login.aster srvreads flaky, broken, quarantine, clusters, a test, a run's failures and durations, each as JSON, with the repo derived from the checkout.--repo evil/otheris 403;POST /v1/test-results/runs,/v1/cache/upload,/v1/cache/commitare 403.a_laptop_signs_in_through_aster_srv_and_restores_what_ci_wrotestill passes.Server tests:
crates/aster-srv/tests/{routes,cli_auth,cache,query}.rs,src/**unit tests. CLI unit tests:src/test_results/read.rs.Automatic coverage: CI job
aster-srvrunscargo test --locked --test cache_remote_e2e --test cache_login_e2e -- --test-threads=1andcargo test --locked -p aster-srvagainst a Postgres service, so all of the above run on PRs (confirm each prints... ok, not skipped).Integration matrix
/v1path, wrong methodCovered: same instance, one caller. Not covered: concurrent callers across replicas for reads, dependency down mid-flight, old/new client versions beyond the legacy
cache:readtoken.Fidelity: real Postgres, real HTTP server, real
asterbinary, real JWT verification. Faked: IAP (a signed assertion injected, signed with a test P-256 key), the browser (scripted HTTP client), GCS (in-memory store behind the dev flag), GitHub OIDC (test RSA key and a stand-in token endpoint). The fakes share the verifier code with production.Not proven here: real IAP and a real browser; the url-map
/v1split (firstlanding PR B); the dedicated listener, audit, rate limits and HSTS are #188's and now share this gate (hardening.rsandroutes.rsboth pass).Follow-ups
/ui/quarantine"Recent changes") has no JSON route yet.iap.rsfix, CLI and route table could be split if you prefer.🤖 Generated with Claude Code