v0.8.0: dep prune, memtest introspection fix, concurrent uploads, x-api-key

Dependency cleanup (ponytail audit):
- Drop 14 unused dependency declarations across 7 crates; quick-xml and
  x509-parser leave the tree entirely (SAML cert/XML work is handled by
  bergshamra + roxmltree).

Fixes:
- introspect: drop the over-broad "microsoft" UTF-16 bulk-scan marker that
  mislabeled Secure-Boot-signed non-Windows bootables (memtest86, signed
  BSDs, firmware tools) as Windows — the string lives in their MS-signed
  EFI loader's FAT long-filename entries. INTROSPECT_REV 3 -> 4 re-probes
  existing local ISOs on startup so the bogus label clears on upgrade.
- upload: begin_upload now reclaims an abandoned <id>.partial instead of
  rejecting the re-upload with "already uploading". Robust against browser
  refresh, tab close, and dropped connections (the chunked protocol can't
  resume a dead session anyway).

Features:
- Storage upload: multi-file + concurrent. Each dropped/selected .iso gets
  its own progress row and uploads independently; a single page-leave guard
  plus a pagehide keepalive-abort replace the old shared singletons.
- Operator API key (x-api-key): a persisted key authenticates /api/* exactly
  like an operator session, for Postman/scripts. New core ApiKeyStore
  (generated on first run, regenerable), accepted in require_auth alongside
  the session cookie, surfaced in Settings -> Advanced with copy + regenerate
  and a usage reference. GET /api/api-key + POST /api/api-key/regenerate.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Miles Ward
2026-06-21 23:35:42 -04:00
co-authored by Claude Opus 4.8
parent 27703c437a
commit 1c262a6d61
19 changed files with 495 additions and 221 deletions
+56
View File
@@ -99,6 +99,7 @@ async fn build_state() -> (AppState, tempfile::TempDir) {
let admin = openpxe_core::AdminStore::load_or_default(dir.path());
let sso = openpxe_core::SsoStore::load_or_default(dir.path());
let notify = openpxe_core::NotifyStore::load_or_default(dir.path());
let api_key = openpxe_core::ApiKeyStore::load_or_init(dir.path());
let sessions = openpxe_http_api::auth::SessionStore::default();
let metrics = Metrics::new();
let state = AppState {
@@ -114,6 +115,7 @@ async fn build_state() -> (AppState, tempfile::TempDir) {
pxe_bg_cache: openpxe_http_api::state::PxeBgCache::default(),
admin,
sessions,
api_key,
sso,
saml: openpxe_http_api::saml_routes::SamlRuntime::default(),
notify,
@@ -135,6 +137,60 @@ async fn build_state() -> (AppState, tempfile::TempDir) {
(state, dir)
}
#[tokio::test]
async fn api_key_authenticates_gated_endpoints() {
// v0.8.0: the x-api-key header authenticates /api/* like an operator
// session. The middleware only enforces once an admin is configured
// (before that everything is open), so bootstrap one first.
let (state, _dir) = build_state().await;
state
.admin
.bootstrap("admin", "correct-horse-battery-staple")
.unwrap();
let key = state.api_key.current();
let app = build_router(state);
// No credentials → 401.
let res = app
.clone()
.oneshot(
Request::builder()
.uri("/api/isos")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "no auth must 401");
// Wrong key → 401.
let res = app
.clone()
.oneshot(
Request::builder()
.uri("/api/isos")
.header("x-api-key", "not-the-key")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "wrong key must 401");
// Correct key → 200 (operator-equivalent access).
let res = app
.oneshot(
Request::builder()
.uri("/api/isos")
.header("x-api-key", key)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK, "valid key must authenticate");
}
#[tokio::test]
async fn health_and_ready_endpoints() {
let (state, _dir) = build_state().await;