diff --git a/docs/backup-credential-recovery.md b/docs/backup-credential-recovery.md index dc7045b..0d7d5fa 100644 --- a/docs/backup-credential-recovery.md +++ b/docs/backup-credential-recovery.md @@ -139,13 +139,14 @@ python3 scripts/openbao-attended-exec.py -- /absolute/path/to/reviewed-silent-co ``` The wrapper keeps Warden's `openbao-platform-admin-login --exec` envelope and -puts the repository launcher ahead of the default `xdg-open`. The launcher -allows only the Keycloak HTTPS host and exact local OIDC callback. It opens -that page in a new empty profile of the headed Chromium pinned by -coulomb-social's Playwright install. It does not open Firefox or another -Windows default browser, and it does not record the address. The remote -headless rapp-playwright service is a different facility and cannot complete -this localhost callback. +puts `scripts/operator-browser` ahead of the default command path. On this +WSL host, OpenBao opens the page with `cmd.exe /c start`, which is the +Windows default browser. The directory supplies a `cmd.exe` for that one +shape. It allows only the Keycloak HTTPS host and exact local OIDC callback, +then opens that page in a new empty profile of the headed Chromium pinned by +coulomb-social's Playwright install. It does not record the address. The +remote headless rapp-playwright service is a different facility and cannot +complete this localhost callback. No token helper or captured Warden output may be inspected. A receipt written by the owner command proves handoff; session cleanup is assessed separately from the final Warden exit status. diff --git a/scripts/operator-browser/cmd.exe b/scripts/operator-browser/cmd.exe new file mode 100755 index 0000000..97642b2 --- /dev/null +++ b/scripts/operator-browser/cmd.exe @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +"""Intercept OpenBao's WSL browser launch. + +hashicorp/cap opens a WSL login with `cmd.exe /c start `. That reaches +the Windows default browser. This command sits ahead of the real cmd.exe on +the attended-login PATH and opens the allowlisted URL in Playwright Chromium. +""" +import os +import runpy +import sys +from pathlib import Path + + +REAL_CMD = "/mnt/c/Windows/System32/cmd.exe" +launcher = runpy.run_path(str(Path(__file__).resolve().with_name("xdg-open"))) + + +def main(): + kind, url = launcher["classify_cmd"](sys.argv[1:]) + if kind == "open": + launcher["stamp"]("called") + return launcher["open_url"](url) + if kind == "reject": + launcher["stamp"]("called") + launcher["stamp"]("rejected") + return 1 + os.execv(REAL_CMD, [REAL_CMD, *sys.argv[1:]]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/operator-browser/xdg-open b/scripts/operator-browser/xdg-open index e8a0706..0428ad9 100755 --- a/scripts/operator-browser/xdg-open +++ b/scripts/operator-browser/xdg-open @@ -78,6 +78,20 @@ def launch_argv(browser, profile_dir, url): ] +def classify_cmd(argv): + """Classify OpenBao's WSL launch, `cmd.exe /c start `. + + hashicorp/cap replaces each & with ^& before calling the Windows + default browser. Other cmd.exe uses are left for the real binary. + """ + if list(argv[:2]) != ["/c", "start"] or len(argv) != 3: + return "delegate", "" + url = str(argv[2]).replace("^&", "&") + if allowed(url): + return "open", url + return "reject", "" + + def stamp(result): path = Path("/home/worsch/.local/state/railiance/receipts/operator-browser-last.txt") try: @@ -88,11 +102,7 @@ def stamp(result): return -def main(): - stamp("called") - if len(sys.argv) != 2 or not allowed(sys.argv[1]): - stamp("rejected") - return 1 +def open_url(url): if not os.environ.get("DISPLAY") and not os.environ.get("WAYLAND_DISPLAY"): stamp("no-display") return 1 @@ -102,7 +112,7 @@ def main(): return 1 profile = Path(tempfile.mkdtemp(prefix="operator-browser-")) os.chmod(profile, 0o700) - command = launch_argv(browser, str(profile), sys.argv[1]) + command = launch_argv(browser, str(profile), url) try: subprocess.Popen( command, @@ -118,5 +128,13 @@ def main(): return 0 +def main(): + stamp("called") + if len(sys.argv) != 2 or not allowed(sys.argv[1]): + stamp("rejected") + return 1 + return open_url(sys.argv[1]) + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/tests/test_operator_browser.py b/tests/test_operator_browser.py index c577bda..10463cf 100644 --- a/tests/test_operator_browser.py +++ b/tests/test_operator_browser.py @@ -40,3 +40,9 @@ class BrowserBoundary(unittest.TestCase): binary.chmod(0o755) self.assertEqual(launcher['chromium_executable'](spec, root), str(binary)) self.assertEqual(launcher['chromium_executable'](root/'missing.json', root), '') + def test_wsl_cmd_start_is_the_login_opener(self): + url='https://kc.coulomb.social/authorize?'+urlencode({'redirect_uri':'http://127.0.0.1:8250/oidc/callback','state':'test'}) + escaped=url.replace('&','^&') + self.assertEqual(launcher['classify_cmd'](['/c','start',escaped]), ('open', url)) + self.assertEqual(launcher['classify_cmd'](['/c','start','https://evil.example/']), ('reject', '')) + self.assertEqual(launcher['classify_cmd'](['/c','dir']), ('delegate', ''))