From d9080e1d19e79edf21de525a16d902eab936d586 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sun, 19 Jul 2026 18:26:16 +0800 Subject: [PATCH 1/2] Remove legacy intake authentication --- .../skills/submit-benchmark-result/SKILL.md | 25 +++++++------ .github/workflows/score-from-r2.yml | 2 +- CONTRIBUTING.md | 11 +++--- benchmark/submit.py | 28 ++++++--------- benchmark/tests/test_docs.py | 18 ++++++++-- benchmark/tests/test_submit.py | 26 +++++--------- intake/cloudflare-worker/README.md | 36 ++++++------------- intake/cloudflare-worker/src/index.js | 11 +----- intake/cloudflare-worker/test/index.test.js | 12 +++---- intake/cloudflare-worker/wrangler.toml | 6 ++-- site/index.html | 6 ++-- submissions/README.md | 5 +-- 12 files changed, 80 insertions(+), 106 deletions(-) diff --git a/.agents/skills/submit-benchmark-result/SKILL.md b/.agents/skills/submit-benchmark-result/SKILL.md index 86a9ccf..290c564 100644 --- a/.agents/skills/submit-benchmark-result/SKILL.md +++ b/.agents/skills/submit-benchmark-result/SKILL.md @@ -54,25 +54,24 @@ offer local-only or test upload instead. Require `PRB_SUBMIT_URL` from the repository documentation or maintainer. Never ask the user to paste any token into chat, print an environment variable, or commit credentials. -Prefer GitHub-backed Cloudflare Access when the deployed client documents -`PRB_ACCESS_TOKEN`: +Use GitHub-backed Cloudflare Access through `PRB_ACCESS_TOKEN`: 1. Require `cloudflared` locally. -2. Obtain the application-scoped token only inside the confirmed upload command, for example - `PRB_ACCESS_TOKEN="$(cloudflared access token -app=)" `. - `cloudflared` opens the configured GitHub login in a browser when needed. -3. Never run `cloudflared access login` or `cloudflared access token` standalone because - some versions print the JWT. Pass it through the client-supported environment variable - for that one process only; do not display, persist, or put it in a command-line flag. +2. For a new session, obtain the application-scoped token only inside the confirmed upload + command: `PRB_ACCESS_TOKEN="$(cloudflared access login --no-verbose --auto-close )" `. + `cloudflared` opens the configured GitHub login. +3. A later upload may use `cloudflared access token -app=` inside the same + command substitution while the local Access session remains valid. +4. Never run either token-producing command standalone. Pass its stdout directly through + the client-supported environment variable; do not display it, save it yourself, or put it + in a command-line flag. Do not substitute `gh auth token`, a GitHub personal access token, or `GITHUB_TOKEN`; the intake must never receive the user's general GitHub credential. -If the deployed client does not yet support Access, use the legacy `PRB_API_KEY` path only -when a credential is already configured locally and the maintainer confirms that endpoint -mode is enabled. If it is absent, stop and report that self-service authentication is not -deployed. Ask the maintainer to enable GitHub Access or issue an out-of-band per-user or -one-time intake credential; never request a shared long-lived key in chat. +If Cloudflare Access is not deployed or the user is not allowed by its policy, stop and ask +the maintainer to fix the Access application. There is no shared-key fallback. Never request +an intake key in chat. ## 4. Confirm and upload once diff --git a/.github/workflows/score-from-r2.yml b/.github/workflows/score-from-r2.yml index 156cf93..e7cbcd1 100644 --- a/.github/workflows/score-from-r2.yml +++ b/.github/workflows/score-from-r2.yml @@ -79,7 +79,7 @@ jobs: # may be archived later; an object arriving while scoring remains in incoming/. aws s3api list-objects-v2 --bucket "$BUCKET" --prefix incoming/ \ --endpoint-url "$R2_ENDPOINT" --query 'Contents[].Key' --output json \ - | jq -r '.[] | select(endswith(".json"))' > queue-manifest.txt + | jq -r '.[]? | select(endswith(".json"))' > queue-manifest.txt while IFS= read -r key; do [ -n "$key" ] || continue rel="${key#incoming/}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1d3e91a..ef1b488 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -195,16 +195,17 @@ GitHub-backed Cloudflare Access application: ```bash export PRB_SUBMIT_URL= # from the maintainer PRB_ACCESS_APP="${PRB_SUBMIT_URL%/submit}" -PRB_ACCESS_TOKEN="$(cloudflared access token -app="$PRB_ACCESS_APP")" \ +PRB_ACCESS_TOKEN="$(cloudflared access login --no-verbose --auto-close "$PRB_ACCESS_APP")" \ python -m benchmark.submit --predictions out/submission.json # --dry-run validate locally, don't send # --test scored + stored privately, but excluded from the public leaderboard ``` -The token is short-lived and scoped to this Access application. Keep token acquisition -inside command substitution because a standalone `cloudflared access login` or -`cloudflared access token` may print it. Never substitute `gh auth token`, a GitHub PAT, or -`GITHUB_TOKEN`. +The token is short-lived and scoped to this Access application. Keep the login inside +command substitution so its JWT goes directly into the one upload process instead of the +terminal. A later upload may replace `access login --no-verbose --auto-close` with +`access token -app` while the local Access session is still valid. Never substitute +`gh auth token`, a GitHub PAT, or `GITHUB_TOKEN`. The CLI validates the file against `submission.schema.json`, then uploads it to a private store (Cloudflare R2). The maintainer's scorer re-verifies it with `pred` (see §3) and opens diff --git a/benchmark/submit.py b/benchmark/submit.py index 03fa309..5d3872a 100644 --- a/benchmark/submit.py +++ b/benchmark/submit.py @@ -4,10 +4,10 @@ Uploads a run's ``submission.json`` (produced by ``make run``) to the benchmark's serverless intake endpoint, which deposits it into the PRIVATE submission store. The submission body carries the answer key (certificates + submit ledger), so it never goes to a -public repo — it travels over HTTPS to the endpoint, which holds the write token. +public repo — it travels over HTTPS through GitHub-backed Cloudflare Access. export PRB_SUBMIT_URL=https:///submit - export PRB_ACCESS_TOKEN="$(cloudflared access token -app=https://)" + export PRB_ACCESS_TOKEN="$(cloudflared access login --no-verbose --auto-close https://)" python -m benchmark.submit --predictions out/submission.json # → prints the submission id returned by the endpoint @@ -19,7 +19,6 @@ HTTP contract (endpoint side): POST Cf-Access-Token: Content-Type: application/json - legacy: Authorization: Bearer body = the submission.json object 200/201 → {"submission_id": "...", "status": "accepted", ...} 4xx/5xx → {"error": "..."} (or a non-JSON body, surfaced as text) @@ -121,13 +120,9 @@ def validate_submission(sub: dict) -> list[str]: return problems -def _auth_headers(api_key: str | None, access_token: str | None) -> dict[str, str]: - """Build one authentication header, preferring the scoped Access credential.""" - if access_token: - return {"Cf-Access-Token": access_token} - if api_key: - return {"Authorization": f"Bearer {api_key}"} - return {} +def _auth_headers(access_token: str | None) -> dict[str, str]: + """Build the Cloudflare Access authentication header.""" + return {"Cf-Access-Token": access_token} if access_token else {} def _post(url: str, payload: bytes, auth_headers: dict[str, str], @@ -158,8 +153,8 @@ def _maybe_json(text: str): return text -def submit(path: Path, url: str | None, api_key: str | None = None, *, - access_token: str | None = None, dry_run: bool = False, +def submit(path: Path, url: str | None, *, access_token: str | None = None, + dry_run: bool = False, timeout: float = 60.0, mark_test: bool = False) -> dict: """Validate and (unless dry_run) upload a submission. Returns a result dict. @@ -181,10 +176,9 @@ def submit(path: Path, url: str | None, api_key: str | None = None, *, if not url: raise ValueError("no endpoint URL — set PRB_SUBMIT_URL or pass --url") - auth_headers = _auth_headers(api_key, access_token) + auth_headers = _auth_headers(access_token) if not auth_headers: - raise ValueError("no intake credential — set PRB_ACCESS_TOKEN (preferred) or " - "PRB_API_KEY (legacy)") + raise ValueError("no intake credential — set PRB_ACCESS_TOKEN") payload = json.dumps(sub).encode("utf-8") status, body = _post(url, payload, auth_headers, timeout=timeout) @@ -206,8 +200,6 @@ def main() -> None: help="Path to submission.json (from `make run`)") parser.add_argument("--url", default=os.environ.get("PRB_SUBMIT_URL"), help="Intake endpoint URL (env PRB_SUBMIT_URL)") - parser.add_argument("--api-key", default=os.environ.get("PRB_API_KEY"), - help="Legacy bearer token (env PRB_API_KEY); prefer PRB_ACCESS_TOKEN") parser.add_argument("--dry-run", action="store_true", help="Validate the file locally and report what would be sent; no upload") parser.add_argument("--test", action="store_true", @@ -221,7 +213,7 @@ def main() -> None: parser.error(f"no such file: {path}") try: - result = submit(path, args.url, args.api_key, + result = submit(path, args.url, access_token=os.environ.get("PRB_ACCESS_TOKEN"), dry_run=args.dry_run, timeout=args.timeout, mark_test=args.test) except (ValueError, json.JSONDecodeError, OSError) as e: diff --git a/benchmark/tests/test_docs.py b/benchmark/tests/test_docs.py index 9594def..6e2d3a5 100644 --- a/benchmark/tests/test_docs.py +++ b/benchmark/tests/test_docs.py @@ -17,6 +17,10 @@ API_SKILL = REPO_ROOT / ".agents/skills/run-api-benchmark/SKILL.md" CLI_SKILL = REPO_ROOT / ".agents/skills/run-cli-benchmark/SKILL.md" SUBMIT_SKILL = REPO_ROOT / ".agents/skills/submit-benchmark-result/SKILL.md" +SCORER_WORKFLOW = REPO_ROOT / ".github/workflows/score-from-r2.yml" +SUBMISSIONS_README = REPO_ROOT / "submissions/README.md" +SITE_INDEX = REPO_ROOT / "site/index.html" +INTAKE_README = REPO_ROOT / "intake/cloudflare-worker/README.md" def _text(path: Path) -> str: @@ -106,8 +110,18 @@ def test_submit_skill_validates_before_upload(self): def test_submit_skill_keeps_github_credentials_out_of_intake(self): t = _text(SUBMIT_SKILL) - prose = " ".join(t.split()) assert "github-backed cloudflare access" in t assert "gh auth token" in t assert "personal access token" in t - assert "self-service authentication is not deployed" in prose + assert "cloudflare access is not deployed" in t + + @pytest.mark.parametrize( + "path", [GUIDE, SUBMIT_SKILL, SUBMISSIONS_README, SITE_INDEX, INTAKE_README]) + def test_public_submission_docs_have_no_shared_intake_key(self, path): + assert "PRB_API_KEY" not in path.read_text(encoding="utf-8") + + +class TestScorerWorkflow: + def test_empty_r2_queue_is_valid(self): + text = SCORER_WORKFLOW.read_text(encoding="utf-8") + assert "jq -r '.[]? | select(endswith(\".json\"))'" in text diff --git a/benchmark/tests/test_submit.py b/benchmark/tests/test_submit.py index e7609bc..b436663 100644 --- a/benchmark/tests/test_submit.py +++ b/benchmark/tests/test_submit.py @@ -67,51 +67,43 @@ def test_valid_submit_ledger_passes(self, tmp_path): class TestSubmit: def test_dry_run_does_not_send(self, tmp_path, monkeypatch): monkeypatch.setattr(sub, "_post", lambda *a, **k: pytest.fail("must not POST on dry-run")) - out = sub.submit(_valid(tmp_path, results=[_bug_row()]), "https://x/submit", "k", dry_run=True) + out = sub.submit(_valid(tmp_path, results=[_bug_row()]), + "https://x/submit", dry_run=True) assert out["claimed_bugs"] == 1 and "dry-run" in out["status"] def test_invalid_submission_raises_before_network(self, tmp_path, monkeypatch): monkeypatch.setattr(sub, "_post", lambda *a, **k: pytest.fail("must not POST invalid")) p = _valid(tmp_path, results=[_bug_row(with_cert=False, with_traj=False)]) with pytest.raises(ValueError, match="certificate"): - sub.submit(p, "https://x/submit", "k") + sub.submit(p, "https://x/submit") def test_success_returns_body(self, tmp_path, monkeypatch): monkeypatch.setattr(sub, "_post", lambda url, payload, headers, timeout=60.0: ( 201, {"submission_id": "abc", "status": "accepted"})) - out = sub.submit(_valid(tmp_path), "https://x/submit", "k") + out = sub.submit(_valid(tmp_path), "https://x/submit", access_token="access-jwt") assert out["submission_id"] == "abc" - def test_access_token_is_preferred_over_legacy_key(self, tmp_path, monkeypatch): + def test_access_token_uses_access_header(self, tmp_path, monkeypatch): def post(url, payload, headers, timeout=60.0): assert headers == {"Cf-Access-Token": "access-jwt"} return 201, {"submission_id": "abc", "status": "accepted"} monkeypatch.setattr(sub, "_post", post) - out = sub.submit(_valid(tmp_path), "https://x/submit", "legacy-key", - access_token="access-jwt") + out = sub.submit(_valid(tmp_path), "https://x/submit", access_token="access-jwt") assert out["submission_id"] == "abc" - def test_legacy_key_uses_authorization_header(self, tmp_path, monkeypatch): - def post(url, payload, headers, timeout=60.0): - assert headers == {"Authorization": "Bearer legacy-key"} - return 201, {"submission_id": "abc", "status": "accepted"} - - monkeypatch.setattr(sub, "_post", post) - sub.submit(_valid(tmp_path), "https://x/submit", "legacy-key") - def test_non_2xx_raises(self, tmp_path, monkeypatch): monkeypatch.setattr(sub, "_post", lambda *a, **k: (429, {"error": "quota exceeded"})) with pytest.raises(ValueError, match="429.*quota"): - sub.submit(_valid(tmp_path), "https://x/submit", "k") + sub.submit(_valid(tmp_path), "https://x/submit", access_token="access-jwt") def test_missing_url_or_key_raises(self, tmp_path): with pytest.raises(ValueError, match="URL"): - sub.submit(_valid(tmp_path), "", "k") + sub.submit(_valid(tmp_path), "", access_token="access-jwt") with pytest.raises(ValueError, match="intake credential"): - sub.submit(_valid(tmp_path), "https://x/submit", "") + sub.submit(_valid(tmp_path), "https://x/submit") def test_non_json_success_is_not_reported_as_accepted(self, tmp_path, monkeypatch): monkeypatch.setattr(sub, "_post", lambda *a, **k: (200, "Access login page")) diff --git a/intake/cloudflare-worker/README.md b/intake/cloudflare-worker/README.md index 9733b22..86a6153 100644 --- a/intake/cloudflare-worker/README.md +++ b/intake/cloudflare-worker/README.md @@ -21,10 +21,7 @@ npx wrangler login # 1. private bucket for raw submissions npx wrangler r2 bucket create prb-submissions -# 2. migration-only bearer token; remove it after the Access test succeeds -npx wrangler secret put PRB_API_KEY - -# 3. deploy the legacy-compatible Worker once +# 2. deploy the Access-only Worker npm run deploy # → registers your workers.dev subdomain (e.g. prb-bench) and prints the endpoint, # e.g. https://intake.prb-bench.workers.dev @@ -33,10 +30,9 @@ npm run deploy ### GitHub-backed Cloudflare Access Create a GitHub identity provider under **Zero Trust → Integrations → Identity providers**. -Then create a self-hosted Access application for the `intake` Worker. During migration, -protect the preview hostname first. Its Allow policy should select the intended GitHub -organization/team, or exact GitHub-verified emails together with `Login Methods: GitHub`; -never use `Everyone`. +Then create a self-hosted Access application for the `intake` Worker. Its Allow policy +should select the intended GitHub organization/team, or exact GitHub-verified emails +together with `Login Methods: GitHub`; never use `Everyone`. The checked-in `TEAM_DOMAIN` and `POLICY_AUD` in `wrangler.toml` identify the configured Access organization and application. They are public identifiers, not credentials. The @@ -51,8 +47,8 @@ npm run preview The Worker validates the `Cf-Access-Jwt-Assertion` signature, issuer, audience, and expiry against Cloudflare's rotating JWKS. It records the verified Access subject/email separately -from the submitter-claimed `submitted_by` field. If an assertion is present but invalid, the -request is rejected and cannot fall back to `PRB_API_KEY`. +from the submitter-claimed `submitted_by` field. A missing or invalid assertion is rejected; +there is no shared intake credential. The Worker only writes to R2 — no GitHub token needed. Scoring is picked up by `.github/workflows/score-from-r2.yml` on its **daily cron** (or trigger it manually via @@ -69,25 +65,16 @@ Submitters authenticate in their own browser and obtain a short-lived, applicati token. No maintainer-issued secret or GitHub personal access token is involved: ```bash -export PRB_SUBMIT_URL=https://access-auth-intake.prb-bench.workers.dev/submit +export PRB_SUBMIT_URL=https://intake.prb-bench.workers.dev/submit PRB_ACCESS_APP="${PRB_SUBMIT_URL%/submit}" -PRB_ACCESS_TOKEN="$(cloudflared access token -app="$PRB_ACCESS_APP")" \ +PRB_ACCESS_TOKEN="$(cloudflared access login --no-verbose --auto-close "$PRB_ACCESS_APP")" \ python3 -m benchmark.submit --predictions out/submission.json --test ``` Remove `--test` only when the run is ready to become an official leaderboard submission. -Keep the `cloudflared access token` call inside command substitution: running -`cloudflared access login` or `cloudflared access token` by itself may print the JWT. Do not -print, persist, or pass `PRB_ACCESS_TOKEN` as a command-line argument. The client prefers it -over an ambient legacy `PRB_API_KEY` and never sends both. - -After an end-to-end preview test succeeds, change the Access destination from Preview Worker -to the production `intake` Worker, deploy the tested version with `npm run deploy`, repeat the -`--test` upload against the production URL, and remove the migration secret: - -```bash -npx wrangler secret delete PRB_API_KEY -``` +Keep token acquisition inside command substitution so the JWT goes directly into the upload +process instead of the terminal. While the local Access session is valid, a later upload may +use `cloudflared access token -app="$PRB_ACCESS_APP"` in the same position. ## R2 credentials for the scoring worker @@ -98,7 +85,6 @@ R2 → *Manage API Tokens*, create an **Object Read & Write** token, then add th See `.github/workflows/score-from-r2.yml`. ## Notes -- `PRB_API_KEY` is only a migration fallback for requests with no Access assertion. - `gh auth token`, GitHub PATs, and `GITHUB_TOKEN` are never intake credentials. - Max body 25 MB. For larger submissions, hand out an R2 presigned PUT URL instead of POSTing the body — not needed at current scale. diff --git a/intake/cloudflare-worker/src/index.js b/intake/cloudflare-worker/src/index.js index 827d19f..d6e8d1e 100644 --- a/intake/cloudflare-worker/src/index.js +++ b/intake/cloudflare-worker/src/index.js @@ -6,8 +6,7 @@ // submitter never reads the bucket. The public repo / leaderboard only ever see the // aggregate the scoring worker derives later (see .github/workflows/score-from-r2.yml). // -// Bindings (wrangler.toml): R2 bucket `SUBMISSIONS`. -// Migration-only secret: `PRB_API_KEY` (set with `wrangler secret put PRB_API_KEY`). +// Bindings (wrangler.toml): R2 bucket `SUBMISSIONS` plus the Access issuer/audience. import { createRemoteJWKSet, jwtVerify } from "jose"; @@ -74,17 +73,9 @@ export async function authenticate(request, env) { }, }; } catch { - // An assertion must never fall back to the legacy key after failed verification. return { response: json({ error: "invalid Access identity" }, 403) }; } } - - const token = (request.headers.get("authorization") || "").replace(/^Bearer\s+/i, ""); - if (env.PRB_API_KEY && token === env.PRB_API_KEY) { - return { - identity: { method: "legacy-api-key", subject: "", email: "" }, - }; - } return { response: json({ error: "unauthorized" }, 401) }; } diff --git a/intake/cloudflare-worker/test/index.test.js b/intake/cloudflare-worker/test/index.test.js index 53e31dc..bb76c27 100644 --- a/intake/cloudflare-worker/test/index.test.js +++ b/intake/cloudflare-worker/test/index.test.js @@ -84,12 +84,10 @@ test("rejects an assertion issued for a different Access application", async () const { env, writes } = environment({ TEAM_DOMAIN: auth.teamDomain, POLICY_AUD: auth.expectedAudience, - PRB_API_KEY: "legacy-secret", }); const response = await worker.fetch(submissionRequest({ "cf-access-jwt-assertion": auth.token, - authorization: "Bearer legacy-secret", }), env); assert.equal(response.status, 403); @@ -110,14 +108,14 @@ test("rejects a valid application token with no user identity", async () => { assert.equal(writes.length, 0); }); -test("supports the legacy API key only when no Access assertion is present", async () => { - const { env, writes } = environment({ PRB_API_KEY: "legacy-secret" }); +test("rejects bearer authorization without an Access assertion", async () => { + const { env, writes } = environment(); const response = await worker.fetch( - submissionRequest({ authorization: "Bearer legacy-secret" }), env); + submissionRequest({ authorization: "Bearer obsolete-secret" }), env); - assert.equal(response.status, 201); - assert.equal(writes[0].options.customMetadata.auth_method, "legacy-api-key"); + assert.equal(response.status, 401); + assert.equal(writes.length, 0); }); test("rejects unauthenticated requests", async () => { diff --git a/intake/cloudflare-worker/wrangler.toml b/intake/cloudflare-worker/wrangler.toml index 31f2ad8..dbef1ae 100644 --- a/intake/cloudflare-worker/wrangler.toml +++ b/intake/cloudflare-worker/wrangler.toml @@ -15,7 +15,5 @@ POLICY_AUD = "9dbeab04b28ab3bfeeebf0014e13a221ccb1a8ccd2858055c867579b7393d880" binding = "SUBMISSIONS" bucket_name = "prb-submissions" -# Migration-only secret (NOT stored here) — remove after the Access cutover: -# wrangler secret put PRB_API_KEY -# (Scoring is triggered by score-from-r2.yml's daily cron, so the Worker needs no GitHub -# token — it only writes to R2.) +# Scoring is triggered by score-from-r2.yml's daily cron, so the Worker needs no GitHub +# token or intake secret — it only verifies Access identity and writes to R2. diff --git a/site/index.html b/site/index.html index 2e6ee57..cc06c16 100644 --- a/site/index.html +++ b/site/index.html @@ -344,8 +344,10 @@

Produce submission.json

2

Upload it

The backend picks it up and re-verifies every certificate.

-
export PRB_SUBMIT_URL=… PRB_API_KEY=…
-python -m benchmark.submit --predictions out/submission.json
+
export PRB_SUBMIT_URL=<intake endpoint>
+PRB_ACCESS_APP="${PRB_SUBMIT_URL%/submit}"
+PRB_ACCESS_TOKEN="$(cloudflared access login --no-verbose --auto-close "$PRB_ACCESS_APP")" \
+  python -m benchmark.submit --predictions out/submission.json

Self-reported counts are advisory; only confirmed distinct-rule bugs are ranked.

diff --git a/submissions/README.md b/submissions/README.md index a9492da..d21cc4d 100644 --- a/submissions/README.md +++ b/submissions/README.md @@ -10,9 +10,10 @@ Use the CLI intake — it uploads to a private store; only the aggregate becomes ```bash export PRB_SUBMIT_URL= # from the maintainer -export PRB_API_KEY= # from the maintainer make run # → out/submission.json -python -m benchmark.submit --predictions out/submission.json +PRB_ACCESS_APP="${PRB_SUBMIT_URL%/submit}" +PRB_ACCESS_TOKEN="$(cloudflared access login --no-verbose --auto-close "$PRB_ACCESS_APP")" \ + python -m benchmark.submit --predictions out/submission.json ``` Add `--test` to run an end-to-end check that is scored + stored privately but excluded from From 080affa8f14cc3ed30b1b9043edd8a0a13b3538d Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sun, 19 Jul 2026 18:46:39 +0800 Subject: [PATCH 2/2] Simplify Access-only intake --- benchmark/submit.py | 15 +++++---------- intake/cloudflare-worker/src/index.js | 11 +++++------ intake/cloudflare-worker/test/index.test.js | 13 ++++++++++++- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/benchmark/submit.py b/benchmark/submit.py index 5d3872a..84d233b 100644 --- a/benchmark/submit.py +++ b/benchmark/submit.py @@ -7,8 +7,8 @@ public repo — it travels over HTTPS through GitHub-backed Cloudflare Access. export PRB_SUBMIT_URL=https:///submit - export PRB_ACCESS_TOKEN="$(cloudflared access login --no-verbose --auto-close https://)" - python -m benchmark.submit --predictions out/submission.json + PRB_ACCESS_TOKEN="$(cloudflared access login --no-verbose --auto-close https://)" \ + python -m benchmark.submit --predictions out/submission.json # → prints the submission id returned by the endpoint The client validates the file locally FIRST (valid JSON, required envelope fields, and — @@ -120,11 +120,6 @@ def validate_submission(sub: dict) -> list[str]: return problems -def _auth_headers(access_token: str | None) -> dict[str, str]: - """Build the Cloudflare Access authentication header.""" - return {"Cf-Access-Token": access_token} if access_token else {} - - def _post(url: str, payload: bytes, auth_headers: dict[str, str], timeout: float = 60.0) -> tuple[int, dict | str]: """POST raw JSON bytes. Returns (status_code, parsed-or-text body). @@ -176,12 +171,12 @@ def submit(path: Path, url: str | None, *, access_token: str | None = None, if not url: raise ValueError("no endpoint URL — set PRB_SUBMIT_URL or pass --url") - auth_headers = _auth_headers(access_token) - if not auth_headers: + if not access_token: raise ValueError("no intake credential — set PRB_ACCESS_TOKEN") payload = json.dumps(sub).encode("utf-8") - status, body = _post(url, payload, auth_headers, timeout=timeout) + status, body = _post( + url, payload, {"Cf-Access-Token": access_token}, timeout=timeout) if not (200 <= status < 300): reason = body.get("error") if isinstance(body, dict) else body raise ValueError(f"endpoint returned HTTP {status}: {reason}") diff --git a/intake/cloudflare-worker/src/index.js b/intake/cloudflare-worker/src/index.js index d6e8d1e..3bc0069 100644 --- a/intake/cloudflare-worker/src/index.js +++ b/intake/cloudflare-worker/src/index.js @@ -18,14 +18,15 @@ export default { if (request.method !== "POST") return json({ error: "POST only" }, 405); if (new URL(request.url).pathname !== "/submit") return json({ error: "not found" }, 404); - const authentication = await authenticate(request, env); - if (authentication.response) return authentication.response; - const identity = authentication.identity; - const declaredBytes = Number(request.headers.get("content-length")); if (Number.isFinite(declaredBytes) && declaredBytes > MAX_BYTES) { return json({ error: "submission too large" }, 413); } + + const authentication = await authenticate(request, env); + if (authentication.response) return authentication.response; + const identity = authentication.identity; + const bodyBytes = await request.arrayBuffer(); if (bodyBytes.byteLength > MAX_BYTES) return json({ error: "submission too large" }, 413); const body = new TextDecoder().decode(bodyBytes); @@ -45,7 +46,6 @@ export default { customMetadata: { model: String(sub.model).slice(0, 128), submitted_by: String(sub.submitted_by || "").slice(0, 128), - auth_method: identity.method, authenticated_subject: identity.subject, authenticated_email: identity.email, }, @@ -67,7 +67,6 @@ export async function authenticate(request, env) { const payload = await verifyAccessAssertion(assertion, env); return { identity: { - method: "cloudflare-access", subject: String(payload.sub || "").slice(0, 128), email: String(payload.email || "").slice(0, 128), }, diff --git a/intake/cloudflare-worker/test/index.test.js b/intake/cloudflare-worker/test/index.test.js index bb76c27..1e21944 100644 --- a/intake/cloudflare-worker/test/index.test.js +++ b/intake/cloudflare-worker/test/index.test.js @@ -73,7 +73,6 @@ test("accepts a valid Access assertion and records the authenticated identity", assert.deepEqual(writes[0].options.customMetadata, { model: "test/model", submitted_by: "claimed", - auth_method: "cloudflare-access", authenticated_subject: "access-user-id", authenticated_email: "submitter@example.com", }); @@ -118,6 +117,18 @@ test("rejects bearer authorization without an Access assertion", async () => { assert.equal(writes.length, 0); }); +test("rejects a declared oversized body before Access verification", async () => { + const { env, writes } = environment(); + + const response = await worker.fetch(submissionRequest({ + "cf-access-jwt-assertion": "not-a-jwt", + "content-length": String(25 * 1024 * 1024 + 1), + }), env); + + assert.equal(response.status, 413); + assert.equal(writes.length, 0); +}); + test("rejects unauthenticated requests", async () => { const { env, writes } = environment();