Remove /runs/{run_id}/… API routes
Server and quire-ci are deployed together, so the token-only /run/…
routes are sufficient. Drops verify_bearer, the path-param get_bootstrap
variant, and all tests that targeted the old routes.
https://claude.ai/code/session_01XsLrdwUarxN9QHTfagwxJT
diff --git a/quire-server/src/quire/web/api.rs b/quire-server/src/quire/web/api.rs
index a75fe5d..1d5f69e 100644
--- a/quire-server/src/quire/web/api.rs
+++ b/quire-server/src/quire/web/api.rs
@@ -2,13 +2,9 @@
//!
//! These routes use per-run bearer-token auth (not the Remote-User
//! header auth used by the web UI). Each token is minted when the run
-//! is created and stored in `runs.run_token`.
-//!
-//! Two route families are provided:
-//! - `/runs/{run_id}/…` — legacy; path carries the run UUID, bearer token is verified against it.
-//! - `/run/…` — token-only; no run ID in the path, the bearer token itself identifies the run.
+//! is created and stored in `runs.run_token`. The bearer token itself
+//! identifies the run — no run ID appears in the path.
-use std::collections::HashMap;
use std::path::PathBuf;
use serde::Deserialize;
@@ -27,30 +23,19 @@ use crate::Quire;
/// Build the API router. Intended to be mounted at `/api` via `Router::nest`.
///
-/// - `/runs/{run_id}/…` — [`verify_bearer`] checks the bearer token against the run's stored
-/// token before any handler runs.
-/// - `/run/…` — [`verify_run_token`] looks the run up by the bearer token itself (no run ID
-/// in the path) and injects the resolved run ID as a request extension.
+/// All routes are under `/run/…`. [`verify_run_token`] looks the run up by the bearer
+/// token and injects the resolved run ID as a request extension before any handler runs.
pub fn router(quire: Quire) -> axum::Router {
let run_routes = axum::Router::new()
.route("/bootstrap", axum::routing::get(get_bootstrap))
.route("/secrets/{name}", axum::routing::get(get_secret))
- .layer(axum::middleware::from_fn_with_state(
- quire.clone(),
- verify_bearer,
- ));
-
- let token_routes = axum::Router::new()
- .route("/bootstrap", axum::routing::get(get_bootstrap_by_token))
- .route("/secrets/{name}", axum::routing::get(get_secret))
.layer(axum::middleware::from_fn_with_state(
quire.clone(),
verify_run_token,
));
axum::Router::new()
- .nest("/runs/{run_id}", run_routes)
- .nest("/run", token_routes)
+ .nest("/run", run_routes)
.with_state(quire)
}
@@ -103,61 +88,7 @@ impl IntoResponse for ApiError {
}
}
-/// Middleware for `/runs/{run_id}/…`: verifies the `Authorization: Bearer <token>` header
-/// against `runs.run_token` in the DB. Returns 401 if the header is absent or the token
-/// doesn't match, 404 if the run doesn't exist.
-async fn verify_bearer(
- State(quire): State<Quire>,
- req: axum::extract::Request,
- next: Next,
-) -> Result<Response, ApiError> {
- let (mut parts, body) = req.into_parts();
-
- let Some(TypedHeader(Authorization(bearer))) =
- <TypedHeader<Authorization<Bearer>> as FromRequestParts<()>>::from_request_parts(
- &mut parts,
- &(),
- )
- .await
- .ok()
- else {
- return Err(ApiError::Unauthorized);
- };
- let token = bearer.token().to_string();
-
- let run_id = <AxumPath<HashMap<String, String>> as FromRequestParts<()>>::from_request_parts(
- &mut parts,
- &(),
- )
- .await
- .ok()
- .and_then(|mut p| p.0.remove("run_id"));
-
- let req = axum::extract::Request::from_parts(parts, body);
-
- let Some(run_id) = run_id else {
- return Err(ApiError::NotFound);
- };
-
- tokio::task::spawn_blocking(move || -> Result<(), ApiError> {
- let db = quire.db_pool().lock().expect("db mutex poisoned");
- let stored: Option<String> = db.query_row(
- "SELECT run_token FROM runs WHERE id = ?1",
- rusqlite::params![run_id],
- |row| row.get(0),
- )?;
- match stored {
- Some(ref t) if t == &token => Ok(()),
- _ => Err(ApiError::Unauthorized),
- }
- })
- .await
- .expect("blocking task panicked")?;
-
- Ok(next.run(req).await)
-}
-
-/// Middleware for `/run/…`: looks up the run by the `Authorization: Bearer <token>` header
+/// Middleware that looks up the run by the `Authorization: Bearer <token>` header
/// value against `runs.run_token`. Returns 401 if the header is absent or no run matches.
/// On success, injects the resolved run ID as a request extension so handlers can use it.
async fn verify_run_token(
@@ -200,9 +131,19 @@ async fn verify_run_token(
Ok(next.run(req).await)
}
-/// Fetch and activate bootstrap data for `run_id`. One-shot: transitions the run from
-/// `pending` → `active` and returns 410 on any subsequent call.
-async fn fetch_bootstrap(quire: Quire, run_id: String) -> Result<axum::Json<Bootstrap>, ApiError> {
+/// `GET /api/run/bootstrap`
+///
+/// Returns the bootstrap payload for a run. One-shot: the server marks
+/// bootstrap as fetched on the first successful read and returns 410 on
+/// any subsequent call. Auth is handled by [`verify_run_token`] middleware.
+///
+/// Returns 404 if the run does not have API bootstrap data (e.g. the run
+/// was created with filesystem transport and `store_bootstrap_data` was
+/// never called).
+async fn get_bootstrap(
+ State(quire): State<Quire>,
+ axum::Extension(run_id): axum::Extension<String>,
+) -> Result<axum::Json<Bootstrap>, ApiError> {
let bootstrap =
tokio::task::spawn_blocking(move || -> std::result::Result<Bootstrap, ApiError> {
let db = crate::db::open(&quire.db_path())?;
@@ -257,39 +198,10 @@ async fn fetch_bootstrap(quire: Quire, run_id: String) -> Result<axum::Json<Boot
Ok(axum::Json(bootstrap))
}
-/// `GET /api/runs/:run_id/bootstrap`
-///
-/// Returns the bootstrap payload for a run. One-shot: the server marks
-/// bootstrap as fetched on the first successful read and returns 410 on
-/// any subsequent call. Auth is handled by [`verify_bearer`] middleware.
-///
-/// Returns 404 if the run does not have API bootstrap data (e.g. the run
-/// was created with filesystem transport and `store_bootstrap_data` was
-/// never called).
-async fn get_bootstrap(
- State(quire): State<Quire>,
- AxumPath(params): AxumPath<HashMap<String, String>>,
-) -> Result<axum::Json<Bootstrap>, ApiError> {
- let run_id = params.get("run_id").cloned().ok_or(ApiError::NotFound)?;
- fetch_bootstrap(quire, run_id).await
-}
-
-/// `GET /api/run/bootstrap`
-///
-/// Token-only variant of [`get_bootstrap`]: no run ID in the path; the bearer token
-/// itself identifies the run. Auth and run-ID injection are handled by
-/// [`verify_run_token`] middleware.
-async fn get_bootstrap_by_token(
- State(quire): State<Quire>,
- axum::Extension(run_id): axum::Extension<String>,
-) -> Result<axum::Json<Bootstrap>, ApiError> {
- fetch_bootstrap(quire, run_id).await
-}
-
-/// `GET /api/runs/:run_id/secrets/:name`
+/// `GET /api/run/secrets/:name`
///
/// Returns the plain-text value of a named secret from the global config.
-/// Auth is handled by [`verify_bearer`] middleware before this handler runs.
+/// Auth is handled by [`verify_run_token`] middleware before this handler runs.
/// Returns 404 if the secret is not declared in config.
#[derive(serde::Deserialize)]
struct SecretPath {
@@ -365,57 +277,6 @@ mod tests {
app.oneshot(req).await.unwrap()
}
- #[tokio::test]
- async fn secret_returns_401_without_auth_header() {
- let env = TestEnv::new();
- let transport = new_transport(TransportMode::Api, 3000);
- env.runs()
- .create(&TestEnv::meta(), Some(&transport))
- .expect("create");
- let url = format!("/runs/{}/secrets/my_secret", transport.session.run_id);
-
- let resp = get(env.app(), &url, None).await;
- assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
- }
-
- #[tokio::test]
- async fn secret_returns_401_for_wrong_token() {
- let env = TestEnv::new();
- let transport = new_transport(TransportMode::Api, 3000);
- env.runs()
- .create(&TestEnv::meta(), Some(&transport))
- .expect("create");
- let url = format!("/runs/{}/secrets/my_secret", transport.session.run_id);
-
- let resp = get(env.app(), &url, Some("wrongtoken")).await;
- assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
- }
-
- #[tokio::test]
- async fn secret_returns_404_for_unknown_run() {
- let env = TestEnv::new();
- let resp = get(
- env.app(),
- "/runs/00000000-0000-0000-0000-000000000001/secrets/my_secret",
- Some("token"),
- )
- .await;
- assert_eq!(resp.status(), StatusCode::NOT_FOUND);
- }
-
- #[tokio::test]
- async fn secret_returns_404_for_unknown_secret_name() {
- let env = TestEnv::new();
- let transport = new_transport(TransportMode::Api, 3000);
- env.runs()
- .create(&TestEnv::meta(), Some(&transport))
- .expect("create");
- let url = format!("/runs/{}/secrets/no_such_secret", transport.session.run_id);
-
- let resp = get(env.app(), &url, Some(&transport.session.run_token)).await;
- assert_eq!(resp.status(), StatusCode::NOT_FOUND);
- }
-
async fn create_run_with_bootstrap(
env: &TestEnv,
transport: &crate::ci::Transport,
@@ -439,148 +300,6 @@ mod tests {
#[tokio::test]
async fn bootstrap_returns_401_without_auth() {
- let env = TestEnv::new();
- let transport = new_transport(TransportMode::Api, 3000);
- let run_id = create_run_with_bootstrap(&env, &transport, "/repos/test.git", None).await;
- let url = format!("/runs/{run_id}/bootstrap");
-
- let resp = get(env.app(), &url, None).await;
- assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
- }
-
- #[tokio::test]
- async fn bootstrap_returns_404_for_unknown_run() {
- let env = TestEnv::new();
- let resp = get(
- env.app(),
- "/runs/00000000-0000-0000-0000-000000000001/bootstrap",
- Some("token"),
- )
- .await;
- assert_eq!(resp.status(), StatusCode::NOT_FOUND);
- }
-
- #[tokio::test]
- async fn bootstrap_returns_404_when_git_dir_not_stored() {
- let env = TestEnv::new();
- let transport = new_transport(TransportMode::Api, 3000);
- env.runs()
- .create(&TestEnv::meta(), Some(&transport))
- .expect("create run");
- let url = format!("/runs/{}/bootstrap", transport.session.run_id);
-
- let resp = get(env.app(), &url, Some(&transport.session.run_token)).await;
- assert_eq!(resp.status(), StatusCode::NOT_FOUND);
- }
-
- #[tokio::test]
- async fn bootstrap_returns_payload_on_first_fetch() {
- let env = TestEnv::new();
- let transport = new_transport(TransportMode::Api, 3000);
- let run_id = create_run_with_bootstrap(&env, &transport, "/repos/test.git", None).await;
- let url = format!("/runs/{run_id}/bootstrap");
-
- let resp = get(env.app(), &url, Some(&transport.session.run_token)).await;
- assert_eq!(resp.status(), StatusCode::OK);
-
- use http_body_util::BodyExt;
- let body = resp.into_body().collect().await.unwrap().to_bytes();
- let parsed: serde_json::Value = serde_json::from_slice(&body).expect("json body");
- assert_eq!(parsed["git_dir"], "/repos/test.git");
- assert_eq!(parsed["meta"]["sha"], "abc1".repeat(10));
- assert_eq!(parsed["meta"]["ref"], "refs/heads/main");
- assert!(parsed["sentry_trace_id"].is_null());
- }
-
- #[tokio::test]
- async fn bootstrap_returns_sentry_trace_id_when_stored() {
- let env = TestEnv::new();
- let transport = new_transport(TransportMode::Api, 3000);
- let run_id = create_run_with_bootstrap(
- &env,
- &transport,
- "/repos/test.git",
- Some("aaaabbbbccccdddd0000111122223333"),
- )
- .await;
- let url = format!("/runs/{run_id}/bootstrap");
-
- let resp = get(env.app(), &url, Some(&transport.session.run_token)).await;
- assert_eq!(resp.status(), StatusCode::OK);
-
- use http_body_util::BodyExt;
- let body = resp.into_body().collect().await.unwrap().to_bytes();
- let parsed: serde_json::Value = serde_json::from_slice(&body).expect("json body");
- assert_eq!(
- parsed["sentry_trace_id"],
- "aaaabbbbccccdddd0000111122223333"
- );
- }
-
- #[tokio::test]
- async fn bootstrap_returns_410_on_second_fetch() {
- let env = TestEnv::new();
- let transport = new_transport(TransportMode::Api, 3000);
- let run_id = create_run_with_bootstrap(&env, &transport, "/repos/test.git", None).await;
- let url = format!("/runs/{run_id}/bootstrap");
- let token = &transport.session.run_token;
-
- let first = get(env.app(), &url, Some(token)).await;
- assert_eq!(first.status(), StatusCode::OK);
-
- let second = get(env.app(), &url, Some(token)).await;
- assert_eq!(second.status(), StatusCode::GONE);
- }
-
- #[tokio::test]
- async fn bootstrap_transitions_run_to_active() {
- let env = TestEnv::new();
- let transport = new_transport(TransportMode::Api, 3000);
- let run_id = create_run_with_bootstrap(&env, &transport, "/repos/test.git", None).await;
- let url = format!("/runs/{run_id}/bootstrap");
-
- get(env.app(), &url, Some(&transport.session.run_token)).await;
-
- let db = crate::db::open(&env.quire.db_path()).expect("db open");
- let (state, started_at_ms): (String, Option<i64>) = db
- .query_row(
- "SELECT state, started_at_ms FROM runs WHERE id = ?1",
- rusqlite::params![run_id],
- |row| Ok((row.get(0)?, row.get(1)?)),
- )
- .expect("query");
- assert_eq!(state, "active");
- assert!(started_at_ms.is_some());
- }
-
- #[tokio::test]
- async fn secret_returns_plaintext_value() {
- let env = TestEnv::new();
- // Write config with a secret.
- fs_err::write(
- env.quire.config_path(),
- r#"{:secrets {:my_token "hunter2"}}"#,
- )
- .expect("write config");
- let transport = new_transport(TransportMode::Api, 3000);
- env.runs()
- .create(&TestEnv::meta(), Some(&transport))
- .expect("create");
- let url = format!("/runs/{}/secrets/my_token", transport.session.run_id);
-
- let resp = get(env.app(), &url, Some(&transport.session.run_token)).await;
- assert_eq!(resp.status(), StatusCode::OK);
-
- use http_body_util::BodyExt;
- let body = resp.into_body().collect().await.unwrap().to_bytes();
- let parsed: serde_json::Value = serde_json::from_slice(&body).expect("json body");
- assert_eq!(parsed["value"], "hunter2");
- }
-
- // Token-only routes (/run/…)
-
- #[tokio::test]
- async fn token_route_bootstrap_returns_401_without_auth() {
let env = TestEnv::new();
let transport = new_transport(TransportMode::Api, 3000);
create_run_with_bootstrap(&env, &transport, "/repos/test.git", None).await;
@@ -590,7 +309,7 @@ mod tests {
}
#[tokio::test]
- async fn token_route_bootstrap_returns_401_for_unknown_token() {
+ async fn bootstrap_returns_401_for_unknown_token() {
let env = TestEnv::new();
let resp = get(env.app(), "/run/bootstrap", Some("nosuchtoken")).await;
@@ -598,7 +317,7 @@ mod tests {
}
#[tokio::test]
- async fn token_route_bootstrap_returns_payload_on_first_fetch() {
+ async fn bootstrap_returns_payload_on_first_fetch() {
let env = TestEnv::new();
let transport = new_transport(TransportMode::Api, 3000);
create_run_with_bootstrap(&env, &transport, "/repos/test.git", None).await;
@@ -618,7 +337,7 @@ mod tests {
}
#[tokio::test]
- async fn token_route_bootstrap_returns_410_on_second_fetch() {
+ async fn bootstrap_returns_410_on_second_fetch() {
let env = TestEnv::new();
let transport = new_transport(TransportMode::Api, 3000);
create_run_with_bootstrap(&env, &transport, "/repos/test.git", None).await;
@@ -632,7 +351,7 @@ mod tests {
}
#[tokio::test]
- async fn token_route_secret_returns_401_without_auth() {
+ async fn secret_returns_401_without_auth() {
let env = TestEnv::new();
let transport = new_transport(TransportMode::Api, 3000);
env.runs()
@@ -644,7 +363,7 @@ mod tests {
}
#[tokio::test]
- async fn token_route_secret_returns_plaintext_value() {
+ async fn secret_returns_plaintext_value() {
let env = TestEnv::new();
fs_err::write(
env.quire.config_path(),