Commit Graph
12 Commits
Author SHA1 Message Date
Miles WardandClaude Opus 4.8 cbcd63bb14 feat(saml): wire SAML 2.0 SSO end-to-end (pure-Rust) + Settings/Storage UI consolidation (v0.5.1)
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]>
2026-05-31 00:50:28 -04:00
Miles WardandClaude Opus 4.8 fc99973ac3 v0.5.0: Wake-on-LAN, webhook notifications, Advanced tab, login logo, update check
Closes the v0.4.x chapter — NFS works end to end. Five additions:

## Wake-on-LAN (Hosts → Bound hosts)
- New core::wol module: parse any MAC form, build the 102-byte magic
  packet, broadcast it. No special capability needed (ephemeral source
  port; SO_BROADCAST). Sends to the limited broadcast (255.255.255.255)
  AND the server's own subnet broadcast (computed from advertised IP +
  detected mask) so it reaches the right VLAN.
- POST /api/hosts/:mac/wol — only fires for *bound* MACs (404 otherwise)
  so it's not an open packet sprayer.
- Bound-hosts table grows a "Wake" button with inline Waking…/Sent ✓
  state.

## Webhook notifications (Advanced tab)
- core::notify: NotifyConfig + NotifyStore (notify.json), one provider
  at a time — Slack / Discord / Teams (incoming-webhook JSON) or SMTP.
  SMTP password is persisted but redacted on GET behind a __keep__
  sentinel the UI round-trips so the secret never leaves the box.
- http-api::notify: delivery — reqwest POST for chat (provider-shaped
  bodies), lettre for SMTP (rustls, STARTTLS/implicit TLS, no plaintext).
  10s timeout; every send is best-effort.
- GET/PUT /api/notify, POST /api/notify/test.
- Fired fire-and-forget on the canonical "machine is imaging" boot event
  and on WoL — never blocks the boot path.

## UI: Advanced tab
- New nav item. Holds the webhook config card and the API reference
  block (relocated from the bottom of Settings).

## UI: login/setup logo (FleetDM treatment)
- /api/me now returns has_custom_logo + logo_rev (public bootstrap).
  The login, setup, and connection-error cards render the uploaded logo
  full-width with the "OpenPXE" wordmark dropped — matching the sidebar.

## About: update check + licenses
- "Check for updates" button → GET /api/updates/check queries the Gitea
  releases API (derived from CARGO_PKG_REPOSITORY) and compares to the
  running version. Strictly on-demand — no background polling, keeps the
  air-gapped promise.
- License card documents the MIT OR Apache-2.0 dual license with links,
  plus a note on bundled components (iPXE GPLv2/UBDL, samba, wimtools).

Deps: lettre (SMTP, rustls) + reqwest gains the json feature. Both
rustls so the static musl binary stays OpenSSL-free.

Tests: 179 passing (+notify round-trip/redaction, webhook validation,
WoL-unbound-404, WoL packet loopback, version-compare). clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-05-29 14:34:20 -04:00
Miles WardandClaude Opus 4.7 3f9d8568f0 v0.4.67: NFSv3 alongside SMB (in-process via nfs3_client crate)
NFS is back — done right this time. v0.4.67 ships a pure-Rust NFSv3
client (`nfs3_client` 0.9 from the xetdata/Vaiz crate family) running
in-process inside the openpxe binary. No `mount.nfs`, no kernel
modules, no `CAP_SYS_ADMIN`, no subprocess. Works in every container
that the v0.4.65 SMB path works in (Unraid included).

The v0.4.65 SMB path stays as-is. Operators get both protocols
side-by-side and pick whichever their NAS prefers — or use both
together. NFSv3 has one architectural advantage over the SMB
userspace path: HTTP Range requests work for NFS-sourced ISOs
because NFSv3 READ3 takes an explicit offset. SMB-sourced ISOs still
return 416 for ranges (smbclient CLI can't seek mid-stream).

## What's new

- `crates/iso-store/src/nfs_share.rs` — `NfsShareManager` mirroring
  `SmbShareManager` structurally. Lists ISOs via READDIR3+LOOKUP3+
  GETATTR3, streams files via READ3 in 64 KiB chunks piped to axum
  body streams. Uses `connect_from_privileged_port(false)` because
  the openpxe binary runs as uid 10001 — most modern NFS servers
  allow that; a server that demands privileged ports needs
  `insecure` in /etc/exports, and the hint translation calls that
  out specifically.
- `IsoSource::Nfs { share_id, relative_path }` variant alongside the
  existing `Smb`. `IsoStore::iso_path_for` returns None for both;
  the HTTP handler dispatches to the right share manager.
- `/api/nfs-shares` CRUD + scan endpoints, parallel to
  `/api/smb-shares`. `POST` body: `{ server, export, port? }`.
- `nfs` terminal command back (this time as in-process, not kernel
  mount): `list | add <srv>:<export> [port] | remove | scan`. The
  v0.4.64 `nfs` command name pointing at kernel mount is moot
  history — same name, completely different mechanism.
- Storage tab: a new NFS shares card sits directly below the SMB
  shares card. The form is simpler (no auth fields) since NFSv3
  uses AUTH_SYS and access is gated server-side by client IP.
- Dashboard "Images available" tile sums SMB + NFS reachable shares
  into a generic "N remote shares" line.

## What's the same

- The structured `{error, stderr, hint}` JSON shape on failures
  matches the SMB API exactly, so the UI's error banner renders
  identically.
- Hint translation: NFS3ERR_ACCES → "exports list", NFS3ERR_NOENT →
  "export path doesn't exist", `mount denied` → "/etc/exports may
  need `insecure`", timeouts → "check IP/port/firewall".
- Persistence: `<work_dir>/nfs_shares.json`. No conflict with the
  long-dead v0.4.64 `nfs.json`.

## Why nfs3_client

User picked it: pure-Rust matches the architecture, NFSv3 covers the
real-world cases, AUTH_SYS keeps the UI simple. The crate is at
0.9.0, MIT/Unlicense, rust-version 1.88 (we're on 1.95). Tokio
feature flag enabled. Image size unchanged at compile time — single
musl static binary, no extra OS packages.

## Tests

160 passing (was 150 in v0.4.66, +10):
- nfs_share parser: stable share ids, server normalization (smb://,
  cifs://, \\, // all stripped).
- hint_for(): NFS3ERR_ACCES, NFS3ERR_NOENT, mount denied, unknown.
- status_label() covers the common nfsstat3 codes.
- HTTP integration: nfs-shares list starts empty, missing server
  rejected, export without leading slash rejected.

`cargo clippy --workspace --all-targets -- -D warnings` clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-28 12:56:46 -04:00
Miles WardandClaude Opus 4.7 900b65b3ec v0.4.65: swap kernel-mount NFS for userspace SMB (smbclient)
v0.4.64's NFS path didn't work on Unraid even with --privileged
because Unraid's base kernel ships without the nfs/nfsv4 client
modules — and no container-side configuration can load a host kernel
module. SMB has the same kernel-mount problem (`mount -t cifs` needs
the cifs module) but it also has a usable *userspace* client: Samba's
`smbclient` CLI, which speaks the SMB protocol over a plain TCP socket
with no kernel involvement. This is the same approach Bootimus uses,
and works in every container regardless of host kernel modules or
container capabilities.

What's gone:

* `crates/iso-store/src/nfs.rs` (in entirety)
* `NfsManager`, `NfsMount`, `NfsAddRequest`, `NfsVersion` types
* `IsoSource::Nfs` variant
* `IsoStore::nfs_root` / `IsoStore::set_nfs_root`
* `/api/nfs`, `/api/nfs/:id`, `/api/nfs/:id/scan` routes
* `nfs` terminal command
* Storage tab's NFS shares card and the v0.4.64 fstab-options
  diagnostics work (the whole error path is moot now)

What's new:

* `crates/iso-store/src/smb_share.rs` — `SmbShareManager` that drives
  `smbclient` as a subprocess. Indexes shares via `smbclient -c "ls
  *.iso"` and streams files via `smbclient -c "get file -"` piped
  straight into HTTP response bodies. No local cache, no double disk
  usage.
* `IsoSource::Smb { share_id, relative_path }` variant.
* `IsoStore::iso_path_for` returns None for SMB sources — the HTTP
  ISO download handler dispatches on the source kind and streams via
  the SmbShareManager when it's SMB.
* `/api/smb-shares` + `/api/smb-shares/:id` + `/api/smb-shares/:id/scan`
  routes.
* `share` terminal command (`list | add //srv/share [auth] | remove |
  scan`). Auth spec is `guest` or `user:password`.
* Storage tab: SMB shares card replaces the NFS one. Two-column form
  for server + share name, three-column form for guest checkbox /
  username / password. Username and password fields auto-disable when
  Guest is checked.
* Credentials live under <work_dir>/smb_creds/<id>.cred at 0600
  permissions so they don't leak through `ps`. Persisted state at
  <work_dir>/smb_shares.json (sans password — re-entered on add /
  re-scan).

Why subprocess and not a Rust crate:

* The Debian runtime image already ships the `samba` package
  (Dockerfile line 84) — `smbclient` is right there.
* Library options (pavao, etc.) wrap libsmbclient so they still pull
  in the same C library at runtime.
* Subprocess gives operators a verifiable mental model — anything
  OpenPXE can do over SMB, they can reproduce by running `smbclient`
  manually at a shell.

Range-request limitation, called out in the smb_share.rs module docs
and the UI explainer: `smbclient -c 'get file -'` is a sequential
whole-file stream. HTTP range requests on SMB-sourced ISOs return
416. PXE workloads (iPXE chain, casper sanboot, wimboot) do
whole-file sequential reads, so this works in practice. A follow-up
release can add libsmbclient-based seek if a real workload needs it.

Stderr-to-hint translation patterns mirror v0.4.64's NFS work:
NT_STATUS_LOGON_FAILURE → "check credentials", BAD_NETWORK_NAME →
"check share name", connection refused / timeout → "verify
reachability + firewall", etc. UI renders the raw smbclient error
plus the hint as two lines.

Tests (149 total, was 142 in v0.4.64):
* smb_share parser tests covering ISO + skipped directory, filenames
  with spaces, non-ISO filtering.
* hint_for() translation tests for the dominant NT_STATUS codes.
* Server normalization (smb://, cifs://, \\, // prefixes all stripped).
* HTTP integration: shares list starts empty, invalid server / missing
  username / path in share name all rejected with actionable hints.

`cargo clippy --workspace --all-targets -- -D warnings` clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-28 11:19:47 -04:00
Miles Ward 07e7c18698 Revert "v0.4.65: Local directory ISO source (bind-mount workaround for Unraid)"
This reverts commit 72a2089c98.
2026-05-28 10:47:17 -04:00
Miles WardandClaude Opus 4.7 761489761c v0.4.65: Local directory ISO source (bind-mount workaround for Unraid)
Field report: even with CAP_SYS_ADMIN and full --privileged, NFS mounts
inside the OpenPXE container fail on Unraid with the same
"failed to apply fstab options" error v0.4.64 added diagnostics for.
The root cause is the host kernel: Unraid's base kernel ships without
the nfs/nfsv4 client modules loaded. Capabilities are necessary but
not sufficient; the modules have to be present on the host kernel for
in-container mount(2) to do anything. No container-side change can
fix that.

This is exactly the case every other PXE/imaging tool sidesteps
(Bootimus uses SMB; iVentoy, FOG, MAAS, Cobbler all rely on the host
to mount network storage and bind-mount the path into the imaging
service). v0.4.65 brings OpenPXE in line with that pattern.

What's new:

* `IsoSource::LocalDir { dir_id, relative_path }` — third source kind
  alongside `Local` (uploaded) and `Nfs` (in-container mount).
* `LocalDirManager` (crates/iso-store/src/local_dir.rs) — registers
  bind-mounted directories, validates them (absolute path, exists, is
  a directory, readable), scans for *.iso files, registers them with
  IsoStore. Persisted to <work_dir>/local_dirs.json so the relationship
  survives restarts.
* `NfsHostCaps::detect()` — pure read of /proc/filesystems on startup.
  Surfaced via GET /api/nfs/capabilities and used by the Storage tab to
  show a prominent red banner above the NFS form when in-container
  mounts cannot possibly work, pointing the operator at the Local
  Directories card as the recommended path.
* Four new API routes:
    GET    /api/nfs/capabilities
    GET    /api/local-dirs
    POST   /api/local-dirs           { path, label? }
    DELETE /api/local-dirs/:id
    POST   /api/local-dirs/:id/scan

UI changes (crates/webui/src/app.js):
* Storage tab: new "Local directories" card under the NFS card with
  the bind-mount form, an explainer paragraph (with the Docker
  `-v /mnt/user/isos:/mnt/external-isos` command), and the list of
  registered directories with rescan + remove actions.
* When NFS host caps are unavailable, the NFS card sprouts a red
  banner explaining what's wrong and pointing at the local-dir
  workaround. The card sub-header also flips to "N registered ·
  recommended on this host".
* ISO table: new "dir:<id>" source badge; on-disk ISOs show "on disk"
  in the actions column instead of a delete button (same pattern as
  NFS — OpenPXE doesn't own those bytes).
* API reference table picks up the four new endpoints + a hint about
  the new `port` field on NFS add.

Tests (+12, total 162):
* iso-store: 7 local_dir unit tests covering relative-path rejection,
  missing path, non-directory file, empty-directory success, default
  label, idempotent re-add, remove + iso-path-resolution clear.
* iso-store: 1 nfs unit test confirming NfsHostCaps::detect() never
  panics and the boolean accessors are consistent.
* http-api: 4 integration tests covering /api/nfs/capabilities,
  /api/local-dirs list/add/remove + relative-path 400.

`cargo clippy --workspace --all-targets -- -D warnings` clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-28 03:09:43 -04:00
Miles WardandClaude Opus 4.7 a1518110ed 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]>
2026-05-25 22:37:20 -04:00
Miles WardandClaude Opus 4.7 7b972dc049 v0.4.4: Settings tab, API reference, ISO category, branding, disk space
Settings:
- New top-level Settings tab. Carries a placeholder for the planned
  LDAP / OIDC / user-management work, the new branding controls, and
  the API reference at the bottom.
- Custom logo upload (PNG/SVG/JPEG/WebP/GIF up to 2 MB) replaces the
  bundled brand mark via /assets/logo.svg; bytes live at
  <work_dir>/branding/ and survive restart. The original "OpenPXE
  v<x.y.z>" pins to the sidebar footer for support.
- API reference rendered from a new GET /api/docs into a per-method
  coloured pill list grouped by area.

ISO category (Storage):
- New IsoCategory { Os, Tools } on IsoMeta with PUT
  /api/isos/:id/category. Storage table's Type cell becomes a
  dropdown; selecting Tools moves the ISO into the Tools submenu next
  to memtest / shell / NIC info and removes it from the OS Installers
  family submenu. Family detection still drives BIOS/UEFI / kernel
  args; only the menu placement changes.

Storage telemetry:
- New IsoStore::disk_usage (libc::statvfs, lives in iso-store so the
  http-api crate stays #![forbid(unsafe_code)]) and GET
  /api/storage/disk. The Storage tab now shows free/used/total for
  the volume hosting the ISO directory with an 80%/95% colour ramp.

UI polish:
- Brand block in the sidebar now matches the topbar height exactly,
  so the divider runs straight across the top of the app rather than
  stepping; version label moved out of the brand and pinned to the
  sidebar footer ("OpenPXE v0.4.4").
- Light-mode terminal: --terminal-bg + per-level text colours track
  the active theme rather than being hard-coded dark.
- About: lead paragraph spans the full content width; new Docs row
  links to https://openpxe.com/.

106 tests passing (was 89 in v0.4.1, +17 across branding unit tests
and new integration coverage for category / disk / docs / branding).
cargo clippy --workspace --all-targets clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-25 17:46:52 -04:00
Miles Ward 2c1c80a7ca v0.4.1: harden ISO uploads and beta UI polish
Add browser-safe chunked ISO uploads with progress, partial-file visibility, offset validation, and abort cleanup while keeping the legacy multipart endpoint for API clients.

Record host-log validation coverage, keep the queue/status UI copy clean, move release docs to 0.4.1, and tighten the dark theme to a near-black Netbox-style palette.
2026-05-24 13:45:35 -04:00
Miles WardandClaude Opus 4.7 ec171ede47 v0.4.0: upload telemetry, host log, jet-black UI
- Upload reliability + diagnostics:
  - api_upload_iso now distinguishes clean EOF from mid-stream errors;
    a truncated multipart body (proxy buffer cap, network drop) returns
    400 with the cause and a "try the LAN IP" hint instead of silently
    finalising a partial file.
  - Per-stage tracing (begin/MB-watermark/finish/abort) so a stuck
    upload is debuggable from the Terminal tab.
  - Web upload UI surfaces bytes/total, percent, throughput, ETA, and
    maps 413/502/504/network-drop to actionable hints.
- New BootLog feature under Hosts:
  - openpxe-core::BootLog — bounded in-memory ring (500) + append-only
    JSONL on disk, recording (timestamp, mac, ip, target_id,
    target_title) every time a boot entry script is served.
  - iPXE per-entry chain URLs grow ?mac=${mac}; password prompt
    submission carries it through; host-binding short-circuit uses the
    bound MAC. ConnectInfo<SocketAddr> wired for peer IP capture (with
    optional fallback so tower::oneshot in tests still works).
  - GET /api/boot-log endpoint + Host log table under the Hosts tab.
- UI changes:
  - Queue card header "Forge" → "Status".
  - Removed Tinkerbell attribution sentence from Hosts tab.
  - Topbar readiness chip moved into the sidebar footer as
    "Service status: Ready / Advertised to clients / <url>", grouping
    advertised PXE URL with operator-relevant status.
  - Jet-black dark palette (#000 / #0a0a0a / #141414 / #1c1c1c)
    replacing the blue-tinted ramp; terminal toolbar/input recoloured
    to match.
- 89 tests passing (was 85 in v0.3.2); cargo clippy --workspace
  --all-targets clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-24 13:10:40 -04:00
Miles Ward 115ba779da Name update 2026-05-21 02:13:08 -04:00
Miles Ward 20c585e3ed Name update 2026-05-06 14:13:38 -04:00