Final cleanup before hardware testing. No behaviour changes; 248 tests green,
clippy clean.
#1 AppError newtype (http-api/src/error.rs) with one IntoResponse mapping
(NotFound→404, Invalid→400, _→500) + From<core::Error>/From<io::Error>.
Converted the clearly-safe handlers (sso_put, unattended_upload,
branding_clear) to `?`; intentionally left handlers with bespoke
status semantics (Invalid→404 on category, 409 on duplicate share /
open upload) explicit so no asserted status changes.
#2 figment-based Config::load (defaults → TOML → env). Keeps the historical
flat OPENPXE_* names (Unraid/entrypoint compatible) AND adds the nested
OPENPXE_SECTION__FIELD form; now covers every field (apply_env had
silently skipped unattended_dir + bind addrs). 6 Jail tests prove
backward-compat. Removed the hand-rolled apply_env.
#3 thiserror 1→2; dropped unused mime/mime_guess/once_cell deps.
#4 Re-evaluated: Duration::from_hours/from_mins are stable on the pinned
1.95 toolchain and clippy prefers them — kept the readable form
(the "unstable" premise didn't hold; MSRV is intentionally 1.95).
#5 insta snapshot of the rendered iPXE menu (version-filtered) + wiremock
coverage of the SAML metadata-URL fetch (200 + non-2xx).
#6 api_status → typed StatusResponse struct (was a 25-key json! blob) with
a full_flow guard test asserting every UI key + the started_at string
shape. Deferred the /api/docs typed conversion (lowest value, highest
churn, zero functional benefit).
#7 pct_encode/xml_escape de-duplicated into openpxe_core::encoding (were
copied across app.rs + the SAML modules). No new crates.
#8 UploadSessions registry → parking_lot::RwLock (sync, never held across
.await); per-session lock stays tokio::Mutex.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
SAML SSO (the config was storage-only since v0.4.5; now it logs you in):
- New openpxe-core::saml — pure-Rust SP built on bergshamra (XML-DSig +
exclusive c14n via RustCrypto, no OpenSSL/xmlsec/libxml2). The static
musl binary stays C-free; samael was rejected for hard-requiring OpenSSL.
* metadata.rs — parse IdP EntityDescriptor (SSO URLs + signing certs),
build our SP metadata.
* authn_request.rs — build + HTTP-Redirect-encode AuthnRequests.
* response.rs — verify the signature against the pinned IdP cert
(trusted_keys_only + strict_verification for XSW),
then enforce Status/Destination/Audience/time-bounds/
signature-scope. Stateless; returns the IDs the HTTP
layer needs.
- http-api saml_routes: GET /api/sso/login (302 to IdP), POST /api/sso/acs
(verify -> InResponseTo correlation / IdP-initiated gating / assertion
replay guard -> mint operator session -> 302), GET /api/sso/metadata.
Added to the pre-auth allowlist; /api/sso config stays gated.
- SsoConfig gains entity_id (SP Entity ID, defaults to public base URL)
and allow_idp_initiated (default off), mirroring FleetDM.
- Access model: any IdP-authenticated, cryptographically-verified user gets
an operator session (single-tier; local admin remains the fallback owner).
- Login page: the "Sign in with <IdP>" button now drives the real flow and
surfaces sso_error redirects.
UI consolidation:
- Removed the Advanced sidebar tab; folded its webhook-notifications +
API-reference cards into a collapsible "Advanced" disclosure at the
bottom of Settings.
- Merged the Storage tab's separate SMB and NFS cards into one "Remote
shares" card with a protocol dropdown and a unified, protocol-badged
table. No backend changes — same /api/smb-shares + /api/nfs-shares.
Tests: 17 SAML core tests (accept + reject tampered/unsigned/wrong-key/
wrong-audience/expired/future/wrong-issuer/non-success) and 6 ACS
integration tests (happy path, IdP-initiated gating, SP correlation,
replay, garbage). Full workspace: 206 tests green, clippy clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>