From cb45f29fb2ebec87847a69e7ca40d97c1b9edf42 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 19 Jun 2026 21:04:31 +0200 Subject: [PATCH] Fix OpenBao login falling back to token auth Add synchronous redirect-bootstrap, direct KeyCape OIDC on sign-in, and mount watching so the UI no longer lands on ?with=token when netkingdom is hidden from unauthenticated mount listing. Document listing_visibility tune helper. --- docs/openbao.md | 11 ++ helm/openbao-ui-overlay/nginx.conf | 2 +- helm/openbao-ui-overlay/overlay.css | 3 + helm/openbao-ui-overlay/overlay.js | 135 +++++++++++++++--- helm/openbao-ui-overlay/redirect-bootstrap.js | 23 +++ scripts/openbao-tune-auth-listing.sh | 49 +++++++ scripts/openbao-ui-overlay-apply.sh | 3 +- scripts/openbao-verify-login-overlay.sh | 10 ++ 8 files changed, 218 insertions(+), 18 deletions(-) create mode 100644 helm/openbao-ui-overlay/redirect-bootstrap.js create mode 100755 scripts/openbao-tune-auth-listing.sh diff --git a/docs/openbao.md b/docs/openbao.md index a04d0f1..d9b2c58 100644 --- a/docs/openbao.md +++ b/docs/openbao.md @@ -330,6 +330,17 @@ After an OpenBao image or chart upgrade, follow `patches//manifest.sha256` fingerprints if upstream login markup changed. +OIDC mounts must be visible to the unauthenticated UI listing or Ember falls +back to token auth (`?with=token`). Apply once per cluster: + +```bash +OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token \ + scripts/openbao-tune-auth-listing.sh +``` + +The login overlay also redirects to `?with=netkingdom/` and starts KeyCape OIDC +directly when the operator clicks **Sign in with KeyCape**. + The OpenBao UI redirects the browser to KeyCape at `kc.coulomb.social`, then returns to: diff --git a/helm/openbao-ui-overlay/nginx.conf b/helm/openbao-ui-overlay/nginx.conf index 8facdf3..efcb7ea 100644 --- a/helm/openbao-ui-overlay/nginx.conf +++ b/helm/openbao-ui-overlay/nginx.conf @@ -49,7 +49,7 @@ http { sub_filter_types text/html; sub_filter_once on; - sub_filter '' ''; + sub_filter '' ''; } } } \ No newline at end of file diff --git a/helm/openbao-ui-overlay/overlay.css b/helm/openbao-ui-overlay/overlay.css index 4fa851a..3e12ff2 100644 --- a/helm/openbao-ui-overlay/overlay.css +++ b/helm/openbao-ui-overlay/overlay.css @@ -8,6 +8,9 @@ html.keycape-overlay-active label[for="custom-path"], html.keycape-overlay-active #namespace, html.keycape-overlay-active #role, html.keycape-overlay-active #custom-path, +html.keycape-overlay-active #token, +html.keycape-overlay-active #username, +html.keycape-overlay-active #password, html.keycape-overlay-active select[name="auth-method"], html.keycape-overlay-active .auth-form .box.has-slim-padding.is-shadowless, html.keycape-overlay-active .auth-form .has-bottom-margin-s { diff --git a/helm/openbao-ui-overlay/overlay.js b/helm/openbao-ui-overlay/overlay.js index d6a2fe8..2bdc3b6 100644 --- a/helm/openbao-ui-overlay/overlay.js +++ b/helm/openbao-ui-overlay/overlay.js @@ -4,6 +4,8 @@ const PRESETS_URL = "/ui/platform-overlay/presets.json"; const MAX_APPLY_ATTEMPTS = 40; const APPLY_INTERVAL_MS = 250; + const MOUNT_WATCH_MS = 500; + const MOUNT_WATCH_MAX = 24; const DEFAULT_PRESETS = { namespace: "", method: "oidc", @@ -18,7 +20,9 @@ let presets = { ...DEFAULT_PRESETS }; let applyAttempts = 0; let applyTimer = null; + let mountWatchTimer = null; let overlayApplied = false; + let signInHandlerInstalled = false; function isAuthPage() { const path = window.location.pathname; @@ -28,6 +32,40 @@ ); } + function normalizedMount(value) { + return (value || "").replace(/\/$/, ""); + } + + function desiredMount() { + return normalizedMount(presets.mount || "netkingdom"); + } + + function currentMountFromQuery() { + return normalizedMount( + new URLSearchParams(window.location.search).get("with") || "" + ); + } + + function ensureAuthMountSelected() { + const mount = desiredMount(); + const current = currentMountFromQuery(); + + if (current === mount || current === "keycape") { + return false; + } + + if (!isAuthPage() || window.location.pathname.includes("/oidc/")) { + return false; + } + + const params = new URLSearchParams(window.location.search); + params.set("with", `${mount}/`); + window.location.replace( + `${window.location.pathname}?${params.toString()}` + ); + return true; + } + function hideNode(node) { if (!node || node.dataset.keycapeOverlayHidden === "true") return; const field = @@ -45,30 +83,63 @@ if (!input || input.dataset.keycapeOverlayPreset === value) return; input.value = value; input.dataset.keycapeOverlayPreset = value; - // Fire once so Ember picks up the preset without a mutation feedback loop. input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true })); } - function ensureAuthMountSelected() { + async function redirectToKeyCape() { const mount = presets.mount || "netkingdom"; - const withValue = mount.endsWith("/") ? mount : `${mount}/`; - const params = new URLSearchParams(window.location.search); - const current = params.get("with") || ""; + const role = presets.role || "platform-admin"; + const redirectUri = `${window.location.origin}/ui/vault/auth/${mount}/oidc/callback`; - if (current.replace(/\/$/, "") === withValue.replace(/\/$/, "")) { - return; + const response = await fetch(`/v1/auth/${mount}/oidc/auth_url`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + role, + redirect_uri: redirectUri, + }), + }); + + if (!response.ok) { + throw new Error(`OIDC auth_url request failed (${response.status})`); } - if (!isAuthPage() || window.location.pathname.includes("/oidc/")) { - return; + const payload = await response.json(); + const authUrl = payload?.data?.auth_url; + if (!authUrl) { + throw new Error("OIDC auth_url missing from OpenBao response"); } - params.set("with", withValue); - const next = `${window.location.pathname}?${params.toString()}`; - if (next !== `${window.location.pathname}${window.location.search}`) { - window.location.replace(next); - } + window.location.assign(authUrl); + } + + function installKeyCapeSignInHandler() { + if (signInHandlerInstalled) return; + signInHandlerInstalled = true; + + document.addEventListener( + "click", + (event) => { + if (!isAuthPage()) return; + + const button = event.target.closest( + '#auth-submit, button[data-test="auth-submit"], form#auth-form button[type="submit"]' + ); + if (!button) return; + + event.preventDefault(); + event.stopPropagation(); + + button.disabled = true; + button.classList.add("is-loading"); + redirectToKeyCape().catch(() => { + button.disabled = false; + button.classList.remove("is-loading"); + }); + }, + true + ); } function loginShellReady() { @@ -101,6 +172,12 @@ .querySelectorAll('#role, input[name="role"], label[for="role"]') .forEach(hideNode); + document + .querySelectorAll( + '#token, input[name="token"], label[for="token"], #username, input[name="username"], #password, input[name="password"]' + ) + .forEach(hideNode); + document.querySelectorAll("nav.tabs").forEach((el) => { if (el.dataset.keycapeOverlayHidden === "true") return; el.style.display = "none"; @@ -151,6 +228,7 @@ } document.documentElement.classList.add("keycape-overlay-active"); + installKeyCapeSignInHandler(); if (loginShellReady()) { overlayApplied = true; @@ -167,6 +245,25 @@ } } + function stopMountWatch() { + if (mountWatchTimer !== null) { + window.clearInterval(mountWatchTimer); + mountWatchTimer = null; + } + } + + function watchAuthMount() { + stopMountWatch(); + let checks = 0; + + mountWatchTimer = window.setInterval(() => { + checks += 1; + if (ensureAuthMountSelected() || checks >= MOUNT_WATCH_MAX) { + stopMountWatch(); + } + }, MOUNT_WATCH_MS); + } + function scheduleApply() { stopApplyLoop(); applyAttempts = 0; @@ -195,10 +292,16 @@ } async function init() { - await loadPresets(); if (!isAuthPage()) return; - ensureAuthMountSelected(); + await loadPresets(); + + if (ensureAuthMountSelected()) { + return; + } + + watchAuthMount(); + installKeyCapeSignInHandler(); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", scheduleApply, { diff --git a/helm/openbao-ui-overlay/redirect-bootstrap.js b/helm/openbao-ui-overlay/redirect-bootstrap.js new file mode 100644 index 0000000..bf391de --- /dev/null +++ b/helm/openbao-ui-overlay/redirect-bootstrap.js @@ -0,0 +1,23 @@ +(function () { + "use strict"; + + var path = window.location.pathname; + if (path.indexOf("/oidc/") !== -1) { + return; + } + + if (!/\/ui\/vault\/auth(?:\/|$)/.test(path) && !/\/ui\/?$/.test(path)) { + return; + } + + var params = new URLSearchParams(window.location.search); + var current = (params.get("with") || "").replace(/\/$/, ""); + if (current === "netkingdom" || current === "keycape") { + return; + } + + params.set("with", "netkingdom/"); + window.location.replace( + window.location.pathname + "?" + params.toString() + ); +})(); \ No newline at end of file diff --git a/scripts/openbao-tune-auth-listing.sh b/scripts/openbao-tune-auth-listing.sh new file mode 100755 index 0000000..8093cbe --- /dev/null +++ b/scripts/openbao-tune-auth-listing.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +OPENBAO_NAMESPACE="${OPENBAO_NAMESPACE:-openbao}" +OPENBAO_RELEASE="${OPENBAO_RELEASE:-openbao}" +KUBECTL="${KUBECTL:-kubectl}" +TOKEN_FILE="${OPENBAO_TOKEN_FILE:-}" +MOUNTS="${OPENBAO_AUTH_LISTING_MOUNTS:-netkingdom keycape}" + +usage() { + cat <<'USAGE' +Usage: scripts/openbao-tune-auth-listing.sh + +Sets listing_visibility=unauth on configured OIDC auth mounts so the OpenBao +browser UI can discover netkingdom without falling back to token auth. + +Environment: + OPENBAO_TOKEN_FILE Token file with platform-admin or root token + OPENBAO_AUTH_LISTING_MOUNTS Space-separated mount paths. Default: netkingdom keycape +USAGE +} + +read_token() { + if [ -n "$TOKEN_FILE" ]; then + head -n 1 "$TOKEN_FILE" + return + fi + local token + read -r -s -p "OpenBao token: " token + printf '\n' >&2 + printf '%s\n' "$token" +} + +if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + usage + exit 0 +fi + +pod="${OPENBAO_RELEASE}-0" +token="$(read_token)" + +for mount in $MOUNTS; do + printf '%s\n' "$token" | $KUBECTL exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \ + bao write "sys/auth/${mount}/tune" listing_visibility=unauth + printf '[OK] auth/%s listing_visibility=unauth\n' "$mount" +done + +printf '\nVerify unauthenticated UI mount listing:\n' +curl -fsS "https://bao.coulomb.social/v1/sys/internal/ui/mounts" | python3 -m json.tool \ No newline at end of file diff --git a/scripts/openbao-ui-overlay-apply.sh b/scripts/openbao-ui-overlay-apply.sh index 93e2c25..0d4a9f2 100755 --- a/scripts/openbao-ui-overlay-apply.sh +++ b/scripts/openbao-ui-overlay-apply.sh @@ -27,7 +27,7 @@ if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then exit 0 fi -for required in overlay.css overlay.js presets.json nginx.conf VERSION; do +for required in overlay.css overlay.js redirect-bootstrap.js presets.json nginx.conf VERSION; do if [ ! -f "$OVERLAY_DIR/$required" ]; then echo "missing overlay asset: $OVERLAY_DIR/$required" >&2 exit 1 @@ -47,6 +47,7 @@ $KUBECTL create configmap openbao-ui-overlay \ --namespace "$OPENBAO_NAMESPACE" \ --from-file="$OVERLAY_DIR/overlay.css" \ --from-file="$OVERLAY_DIR/overlay.js" \ + --from-file="$OVERLAY_DIR/redirect-bootstrap.js" \ --from-file="$OVERLAY_DIR/presets.json" \ --from-file="$OVERLAY_DIR/VERSION" \ --dry-run=client -o yaml | $KUBECTL apply -f - diff --git a/scripts/openbao-verify-login-overlay.sh b/scripts/openbao-verify-login-overlay.sh index 074a810..37ce461 100755 --- a/scripts/openbao-verify-login-overlay.sh +++ b/scripts/openbao-verify-login-overlay.sh @@ -58,6 +58,11 @@ overlay_js="$(curl -fsS "$BASE_URL/ui/platform-overlay/overlay.js")" overlay_css="$(curl -fsS "$BASE_URL/ui/platform-overlay/overlay.css")" presets_json="$(curl -fsS "$BASE_URL/ui/platform-overlay/presets.json")" +require_pattern \ + "index.html injects redirect bootstrap" \ + "$index_html" \ + '/ui/platform-overlay/redirect-bootstrap\.js' + require_pattern \ "index.html injects overlay.js" \ "$index_html" \ @@ -73,6 +78,11 @@ require_pattern \ "$overlay_js" \ 'keycape-overlay-active' +require_pattern \ + "overlay.js starts direct KeyCape OIDC redirect" \ + "$overlay_js" \ + 'oidc/auth_url' + require_pattern \ "presets.json targets netkingdom mount" \ "$presets_json" \