v0.4.6: iVentoy-style PXE menu, top-right user menu, Settings touchups
PXE boot menu polish (iVentoy-inspired):
- render_menu now opens with a best-effort `console --picture
<base>/branding/pxe-logo || console` line so iPXE builds with PNG
support paint the operator's uploaded raster logo as the background.
- ASCII OpenPXE wordmark banner sits at the top of the menu in
`item --gap` lines — always visible on every iPXE build, including
the snponly/undionly variants without graphics console.
- New footer line above `choose`: "OpenPXE v0.4.6 - <arch label>",
where <arch label> is mapped from iPXE's ${buildarch}/${platform}
to "x86 BIOS", "x86_64 UEFI", or "arm64 UEFI". No URL, per brief.
- New GET /branding/pxe-logo route serves the operator's PNG / JPEG /
WebP / GIF as-is for iPXE to consume. SVG uploads 404 here (iPXE
can't rasterize SVG) — the always-visible ASCII wordmark stands in.
Route stays public after admin setup so iPXE clients (no cookies)
can fetch it.
UI:
- Removed the bottom-left "signed in as / Sign out" row.
- Added a person-icon button next to the theme toggle in the topbar.
Click opens a small popover with: Name (display only), Edit account
(jumps to Settings), Sign out. Esc + click-outside close it.
- Settings → Account card form chrome made consistent. The previous
`label.field` selector only styled type=text/number, leaving
password inputs with default browser chrome. Switched to a
negation-list selector that covers every typed input we use, plus
-webkit-appearance:none + a 1px focus ring. Light + dark mode both
show the same border/padding/focus state across all four account
fields.
- Settings → SSO card now renders display name, IdP logo URL (new),
and metadata source on one 3-column row. The metadata <select>
inherits the same chrome as the text inputs so it baseline-aligns
with them. SsoConfig grew an idp_logo_url field, persisted to
sso.json, length-capped and validated to http(s) only.
Quality:
- 138 tests passing (was 132 in v0.4.5). +1 IdP-logo-URL validation,
+1 PXE menu polish regression guard, +4 /branding/pxe-logo
integration tests covering missing-config / SVG-fallback / raster-
serve / post-auth public-allowlist cases.
- 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
a1518110ed
commit
55f4765a20
@@ -33,6 +33,11 @@ pub struct SsoConfig {
|
||||
/// with X" button label. Empty/whitespace falls back to "SSO".
|
||||
#[serde(default)]
|
||||
pub idp_name: String,
|
||||
/// Optional HTTPS URL pointing at the IdP's brand logo. Rendered
|
||||
/// next to `idp_name` on the WebUI's login screen (FleetDM-style).
|
||||
/// Length-capped at [`MAX_URL_LEN`]; empty is fine.
|
||||
#[serde(default)]
|
||||
pub idp_logo_url: String,
|
||||
/// Raw SAML metadata XML pasted by the operator. Mutually exclusive
|
||||
/// with `metadata_url`; if both are set, the URL wins at apply time
|
||||
/// (operators typically forget about a stale XML paste).
|
||||
@@ -100,6 +105,7 @@ impl SsoStore {
|
||||
/// but the server enforces a hard ceiling regardless.
|
||||
pub fn replace(&self, mut cfg: SsoConfig) -> Result<SsoConfig> {
|
||||
cfg.idp_name = cfg.idp_name.trim().to_string();
|
||||
cfg.idp_logo_url = cfg.idp_logo_url.trim().to_string();
|
||||
cfg.metadata = cfg.metadata.trim().to_string();
|
||||
cfg.metadata_url = cfg.metadata_url.trim().to_string();
|
||||
if cfg.metadata.len() > MAX_METADATA_BYTES {
|
||||
@@ -112,6 +118,11 @@ impl SsoStore {
|
||||
"metadata_url exceeds {MAX_URL_LEN}-char cap"
|
||||
)));
|
||||
}
|
||||
if cfg.idp_logo_url.len() > MAX_URL_LEN {
|
||||
return Err(Error::Invalid(format!(
|
||||
"idp_logo_url exceeds {MAX_URL_LEN}-char cap"
|
||||
)));
|
||||
}
|
||||
if !cfg.metadata_url.is_empty()
|
||||
&& !cfg.metadata_url.starts_with("http://")
|
||||
&& !cfg.metadata_url.starts_with("https://")
|
||||
@@ -120,6 +131,14 @@ impl SsoStore {
|
||||
"metadata_url must start with http:// or https://".into(),
|
||||
));
|
||||
}
|
||||
if !cfg.idp_logo_url.is_empty()
|
||||
&& !cfg.idp_logo_url.starts_with("http://")
|
||||
&& !cfg.idp_logo_url.starts_with("https://")
|
||||
{
|
||||
return Err(Error::Invalid(
|
||||
"idp_logo_url must start with http:// or https://".into(),
|
||||
));
|
||||
}
|
||||
// If they're trying to *enable* the integration but haven't
|
||||
// supplied either source, reject — saves a "configured but
|
||||
// unusable" surprise later.
|
||||
@@ -197,6 +216,7 @@ mod tests {
|
||||
idp_name: "Okta".into(),
|
||||
metadata: String::new(),
|
||||
metadata_url: "https://idp.example.com/metadata".into(),
|
||||
idp_logo_url: String::new(),
|
||||
})
|
||||
.unwrap();
|
||||
drop(s);
|
||||
@@ -218,6 +238,7 @@ mod tests {
|
||||
idp_name: "Test IdP".into(),
|
||||
metadata: xml.into(),
|
||||
metadata_url: String::new(),
|
||||
idp_logo_url: String::new(),
|
||||
})
|
||||
.unwrap();
|
||||
assert!(s.snapshot().is_usable());
|
||||
@@ -232,6 +253,7 @@ mod tests {
|
||||
idp_name: "Okta".into(),
|
||||
metadata: String::new(),
|
||||
metadata_url: String::new(),
|
||||
idp_logo_url: String::new(),
|
||||
});
|
||||
assert!(matches!(r, Err(Error::Invalid(_))));
|
||||
// …and a disabled blank config is fine.
|
||||
@@ -247,10 +269,38 @@ mod tests {
|
||||
idp_name: String::new(),
|
||||
metadata: String::new(),
|
||||
metadata_url: "ftp://idp.example.com/metadata".into(),
|
||||
idp_logo_url: String::new(),
|
||||
});
|
||||
assert!(matches!(r, Err(Error::Invalid(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idp_logo_url_must_be_http_scheme() {
|
||||
// v0.4.6: SSO settings learned an idp_logo_url so the login
|
||||
// screen can render the FleetDM-style "Sign in with <IdP-logo>"
|
||||
// affordance. Same scheme rule as metadata_url.
|
||||
let dir = tempdir().unwrap();
|
||||
let s = SsoStore::load_or_default(dir.path());
|
||||
let r = s.replace(SsoConfig {
|
||||
enabled: false,
|
||||
idp_name: "Okta".into(),
|
||||
metadata: String::new(),
|
||||
metadata_url: String::new(),
|
||||
idp_logo_url: "data:image/png;base64,...".into(),
|
||||
});
|
||||
assert!(matches!(r, Err(Error::Invalid(_))));
|
||||
// Real HTTPS URL is fine.
|
||||
s.replace(SsoConfig {
|
||||
enabled: false,
|
||||
idp_name: "Okta".into(),
|
||||
metadata: String::new(),
|
||||
metadata_url: String::new(),
|
||||
idp_logo_url: "https://idp.example.com/logo.png".into(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(s.snapshot().idp_logo_url, "https://idp.example.com/logo.png");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_size_cap_enforced() {
|
||||
let dir = tempdir().unwrap();
|
||||
@@ -261,6 +311,7 @@ mod tests {
|
||||
idp_name: String::new(),
|
||||
metadata: oversize,
|
||||
metadata_url: String::new(),
|
||||
idp_logo_url: String::new(),
|
||||
});
|
||||
assert!(matches!(r, Err(Error::Invalid(_))));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user