v0.4.5: VMware UEFI fix, static musl binary, Forms auth + SSO config
VMware UEFI / Casper boot fix:
- Linux cmdline for Debian/Ubuntu/Mint/Pop!_OS/elementary now uses the
canonical Casper `iso-url=` option and `ds=nocloud`, matching the
fix Bootimus shipped in v0.1.67. The previous
`boot=casper netboot=url url=… ip=dhcp ---` form booted fine on
bare-metal UEFI but hung at "cloud-init running" on VMware guests
because subiquity / cloud-init can't reach a metadata datasource
through PXE.
Static binary (matches Bootimus v0.1.70):
- Dockerfile build stage now compiles against
x86_64-unknown-linux-musl. The resulting /openpxe has no glibc
dependency at all; the runtime stage still ships Debian slim for the
samba/wimtools/nfs-common shellouts, but a future scratch/distroless
variant is now a one-line swap. Cuts a class of "GLIBC_2.39 not
found" surprises on older RHEL/Rocky hosts.
Forms auth (Sonarr/Radarr-style):
- New AdminStore in openpxe-core: single admin record persisted to
<work_dir>/auth.json, bcrypt-hashed credentials, rotation requires
current password.
- New SessionStore in openpxe-http-api: in-memory UUID-keyed sessions
with 24h sliding TTL, openpxe_session HttpOnly cookie.
- Endpoints: POST /api/setup (first-run), POST /api/login, POST
/api/logout, GET /api/me, PUT /api/me/credentials (rotates and
revokes every other session).
- Auth middleware gates /api/* once the admin is configured;
passes through entirely until then (tests + fresh installs ride this
path). Allowlists PXE-essential paths (/boot.ipxe, /iso/*, /ipxe/*,
/api/queue/join, /api/queue/poll/*) so iPXE clients still work
without a cookie they can't send.
- WebUI: first-run setup card, login card, logout chip in the sidebar
footer, Account card in Settings for rotating creds. Auth screen is
fully styled (centered narrow card, matches Sonarr layout).
SSO config (FleetDM-shaped, storage-only):
- New SsoStore in openpxe-core: { enabled, idp_name, metadata,
metadata_url } persisted to <work_dir>/sso.json with size caps and
URL-scheme validation.
- Endpoints: GET /api/sso, PUT /api/sso. Validation: enabling SSO
without either metadata or metadata_url returns 400.
- WebUI: SSO card in Settings with a URL-vs-XML mode switch and an
inert "Sign in with X" button on the login screen while runtime
flow is pending. Per the brief: no Entity ID field (defaults to the
advertised public_base_url internally when SAML wiring lands).
Quality:
- 132 tests passing (was 106 in v0.4.4): +5 auth unit tests, +5 SSO
unit tests, +7 auth integration tests, +1 SSO integration test, +1
regression guard pinning the new Casper cmdline.
- cargo clippy --workspace --all-targets clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
55d74662c2
commit
4a354a8664
@@ -13,6 +13,7 @@
|
||||
//! | `/iso/<id>/*` | Files inside the ISO (for wimboot & kernel/initrd) |
|
||||
//! | `/api/*` | JSON/HTML API for the web UI |
|
||||
|
||||
use crate::auth as auth_api;
|
||||
use crate::ipxe_script::{
|
||||
render_entry, render_family_menu, render_local_hdd, render_menu, render_nic_info,
|
||||
render_queue_entry, render_shell, render_tools_menu, render_util,
|
||||
@@ -30,7 +31,8 @@ use axum::{
|
||||
Json, Router,
|
||||
};
|
||||
use openpxe_core::{
|
||||
ext_for_mime, BootEvent, ClientEvent, Error, Settings, ALLOWED_LOGO_MIMES, MAX_LOGO_BYTES,
|
||||
ext_for_mime, BootEvent, ClientEvent, Error, Settings, SsoConfig, ALLOWED_LOGO_MIMES,
|
||||
MAX_LOGO_BYTES,
|
||||
};
|
||||
use openpxe_ipxe_assets::asset_bytes;
|
||||
use openpxe_iso_store::{IsoCategory, IsoMeta, NfsAddRequest};
|
||||
@@ -97,6 +99,24 @@ pub fn build_router(state: AppState) -> Router {
|
||||
// under the Settings tab — operators chasing an integration get
|
||||
// it in-product instead of having to fetch the OpenAPI YAML.
|
||||
.route("/api/docs", get(api_docs))
|
||||
// v0.4.5: Sonarr/Radarr-style admin Forms auth. First-run
|
||||
// /setup creates the single admin account; /login validates;
|
||||
// /logout revokes the session; /me powers the front-end's
|
||||
// "should I show the setup page, the login page, or the
|
||||
// dashboard?" decision. /me/credentials rotates the admin's
|
||||
// username/password.
|
||||
.route("/api/setup", post(auth_api::api_setup))
|
||||
.route("/api/login", post(auth_api::api_login))
|
||||
.route("/api/logout", post(auth_api::api_logout))
|
||||
.route("/api/me", get(auth_api::api_me))
|
||||
.route(
|
||||
"/api/me/credentials",
|
||||
put(auth_api::api_update_credentials),
|
||||
)
|
||||
// v0.4.5: SAML SSO configuration (FleetDM-shaped, storage-only).
|
||||
// The actual sign-in flow lands in a later release; this just
|
||||
// gives operators a place to paste their IdP metadata today.
|
||||
.route("/api/sso", get(api_sso_get).put(api_sso_put))
|
||||
.route("/api/clients", get(api_list_clients))
|
||||
.route("/api/status", get(api_status))
|
||||
.route("/api/settings", get(api_get_settings).put(api_put_settings))
|
||||
@@ -128,12 +148,41 @@ pub fn build_router(state: AppState) -> Router {
|
||||
// format. No auth — the metrics surface is intentionally
|
||||
// boring (counts, no payloads).
|
||||
.route("/metrics", get(api_metrics))
|
||||
// v0.4.5: Forms-auth middleware. Layered *after* `.route(...)`
|
||||
// calls so it applies uniformly; passes everything through when
|
||||
// no admin is configured (tests + fresh installs ride this path).
|
||||
// The allowlist inside `auth_api::require_auth` keeps PXE-essential
|
||||
// endpoints reachable for iPXE clients that can't authenticate.
|
||||
.layer(axum::middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
auth_api::require_auth,
|
||||
))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
// 16 GiB upload cap — ISOs are big; chunks stream so this isn't memory use.
|
||||
.layer(DefaultBodyLimit::max(16 * 1024 * 1024 * 1024))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
// ─── SSO config endpoints ─────────────────────────────────────────────────
|
||||
|
||||
async fn api_sso_get(State(state): State<AppState>) -> Json<SsoConfig> {
|
||||
// We deliberately do not redact the metadata — the operator who's
|
||||
// signed in needs to be able to round-trip it. /api/sso requires
|
||||
// the auth middleware anyway, so unauthenticated callers can't see
|
||||
// it once admin is configured.
|
||||
Json(state.sso.snapshot())
|
||||
}
|
||||
|
||||
async fn api_sso_put(State(state): State<AppState>, Json(body): Json<SsoConfig>) -> Response {
|
||||
match state.sso.replace(body) {
|
||||
Ok(cfg) => (StatusCode::OK, Json(cfg)).into_response(),
|
||||
Err(Error::Invalid(msg)) => {
|
||||
(StatusCode::BAD_REQUEST, msg).into_response()
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
// ─── UI ────────────────────────────────────────────────────────────────────
|
||||
|
||||
async fn index(State(state): State<AppState>) -> Response {
|
||||
@@ -896,10 +945,29 @@ async fn api_docs() -> Json<serde_json::Value> {
|
||||
"summary": "Upload a custom WebUI logo (multipart 'file', PNG/SVG/JPEG/WebP/GIF up to 2 MB)."},
|
||||
{"method": "DELETE", "path": "/api/branding/logo",
|
||||
"summary": "Remove the custom logo and revert to the bundled mark."},
|
||||
{"method": "GET", "path": "/api/sso",
|
||||
"summary": "Current SAML SSO configuration."},
|
||||
{"method": "PUT", "path": "/api/sso",
|
||||
"summary": "Replace SAML SSO configuration. Body: { enabled, idp_name, metadata, metadata_url }."},
|
||||
{"method": "GET", "path": "/api/docs",
|
||||
"summary": "This API reference."},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Auth (Forms)",
|
||||
"endpoints": [
|
||||
{"method": "POST", "path": "/api/setup",
|
||||
"summary": "First-run admin bootstrap. Body: { username, password }. Refuses after the admin exists."},
|
||||
{"method": "POST", "path": "/api/login",
|
||||
"summary": "Sign in. Body: { username, password }. Sets the openpxe_session cookie."},
|
||||
{"method": "POST", "path": "/api/logout",
|
||||
"summary": "Revoke the current session and clear the cookie."},
|
||||
{"method": "GET", "path": "/api/me",
|
||||
"summary": "Auth status — { setup_required, authenticated, user }. Always 200."},
|
||||
{"method": "PUT", "path": "/api/me/credentials",
|
||||
"summary": "Rotate the admin's credentials. Body: { current_password, new_username?, new_password? }. Revokes all other sessions on success."},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Storage telemetry",
|
||||
"endpoints": [
|
||||
|
||||
Reference in New Issue
Block a user