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]>
128 lines
5.6 KiB
HTML
128 lines
5.6 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<meta name="color-scheme" content="dark light" />
|
|
<title>OpenPXE</title>
|
|
<!-- v0.4.61: the `?v=…` query string is replaced by the server at
|
|
request time with the running OpenPXE version. That guarantees a
|
|
fresh URL on every upgrade so browsers (and intermediary proxies)
|
|
can't keep serving stale JS / CSS / branding from before the
|
|
deploy. Combined with `Cache-Control: no-cache, must-revalidate`
|
|
on the asset handlers, the practical caching window is one
|
|
version. -->
|
|
<link rel="stylesheet" href="/assets/app.css?v={{ASSET_VERSION}}" />
|
|
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg?v={{ASSET_VERSION}}&r={{LOGO_REV}}" />
|
|
<!-- Theme is read from localStorage *before* paint to avoid the
|
|
dark→light flash on every navigation. Falls back to the OS
|
|
preference and finally to dark. -->
|
|
<script>
|
|
(function() {
|
|
try {
|
|
var stored = localStorage.getItem('openpxe-theme');
|
|
var theme = stored || (matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
} catch (e) {
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
}
|
|
})();
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div class="shell">
|
|
<aside class="sidebar">
|
|
<div class="{{BRAND_CLASS}}">
|
|
<img src="/assets/logo.svg?v={{ASSET_VERSION}}&r={{LOGO_REV}}" alt="OpenPXE" />
|
|
<strong>OpenPXE</strong>
|
|
</div>
|
|
<nav>
|
|
<a data-view="dashboard" class="active">Dashboard</a>
|
|
<a data-view="network">Network</a>
|
|
<a data-view="queue">
|
|
Queue
|
|
<span class="count" data-bind="queue_count">0</span>
|
|
</a>
|
|
<a data-view="storage">
|
|
Storage
|
|
<span class="count" data-bind="iso_count">0</span>
|
|
</a>
|
|
<a data-view="hosts">
|
|
Hosts
|
|
<span class="count" data-bind="host_count">0</span>
|
|
</a>
|
|
<a data-view="terminal">Terminal</a>
|
|
<a data-view="settings">Settings</a>
|
|
<a data-view="about">About</a>
|
|
</nav>
|
|
<div class="footer">
|
|
<div class="status-row">
|
|
<span class="dot" data-bind="ready_dot" title="Server readiness"></span>
|
|
<span class="status-label">Service status:</span>
|
|
<span class="status-value" data-bind="ready_label">checking…</span>
|
|
</div>
|
|
<div class="footer-sub">Advertised to clients</div>
|
|
<code>{{BASE_URL}}</code>
|
|
<!-- The brand badge at the top can be overridden by operator-uploaded
|
|
logos; keep "OpenPXE v…" pinned in the footer so the backend
|
|
identity is always visible regardless of branding. -->
|
|
<div class="footer-version">OpenPXE v<span data-bind="version">0.4.63</span></div>
|
|
</div>
|
|
</aside>
|
|
|
|
<header class="topbar">
|
|
<h1 data-bind="view_title">Dashboard</h1>
|
|
<div class="spacer"></div>
|
|
<span class="chip"><strong data-bind="iso_count2">0</strong> images</span>
|
|
<span class="chip"><strong data-bind="client_count2">0</strong> clients</span>
|
|
<span class="chip"><strong data-bind="queue_count2">0</strong> in queue</span>
|
|
<button id="theme-toggle" class="theme-toggle" type="button"
|
|
aria-label="Toggle light/dark theme" title="Toggle theme (T)">
|
|
<!-- Two glyphs; CSS shows whichever matches the active theme. -->
|
|
<svg class="t-sun" viewBox="0 0 24 24" width="18" height="18" fill="none"
|
|
stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
<circle cx="12" cy="12" r="4.2"/>
|
|
<line x1="12" y1="2.5" x2="12" y2="5.5"/>
|
|
<line x1="12" y1="18.5" x2="12" y2="21.5"/>
|
|
<line x1="2.5" y1="12" x2="5.5" y2="12"/>
|
|
<line x1="18.5" y1="12" x2="21.5" y2="12"/>
|
|
<line x1="5.2" y1="5.2" x2="7.3" y2="7.3"/>
|
|
<line x1="16.7" y1="16.7" x2="18.8" y2="18.8"/>
|
|
<line x1="5.2" y1="18.8" x2="7.3" y2="16.7"/>
|
|
<line x1="16.7" y1="7.3" x2="18.8" y2="5.2"/>
|
|
</svg>
|
|
<svg class="t-moon" viewBox="0 0 24 24" width="18" height="18" fill="none"
|
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M20.5 14A8 8 0 0 1 10 3.5 a8 8 0 1 0 10.5 10.5z"/>
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- v0.4.6: signed-in operator menu. Sits next to the theme toggle
|
|
in the top-right corner so the sidebar footer stays clean for
|
|
the "Service status / Advertised URL / Backend version" trio.
|
|
The whole block is hidden until /api/me confirms a session. -->
|
|
<div class="user-menu" data-bind="user_menu_wrap" style="display:none">
|
|
<button id="user-menu-btn" class="user-btn" type="button"
|
|
aria-label="Account menu" aria-haspopup="true" aria-expanded="false"
|
|
title="Account">
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none"
|
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="8" r="3.6"/>
|
|
<path d="M4.5 20a7.5 7.5 0 0 1 15 0"/>
|
|
</svg>
|
|
</button>
|
|
<div id="user-menu-pop" class="user-pop" data-bind="user_menu_pop" hidden>
|
|
<div class="user-pop-name" data-bind="user_pop_name">—</div>
|
|
<button type="button" class="user-pop-item" data-bind="user_pop_edit">Edit account</button>
|
|
<button type="button" class="user-pop-item user-pop-danger" data-bind="user_pop_logout">Sign out</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="main" id="view-root"></main>
|
|
</div>
|
|
|
|
<script src="/assets/app.js?v={{ASSET_VERSION}}"></script>
|
|
</body>
|
|
</html>
|