MASON-WP-0012: named-role login without session reuse, diagnosis receipt, capabilities-self proof (NK-WP-0051-T06)
verify-reuse-surface-hub-writer.py logs in with skip_browser=true and shows the address (or opens a fresh temporary browser profile), so the default Authelia session is never reused. The receipt records uid, openbao_error and assurance per net-kingdom/docs/sign-in-diagnosis.md, with tokens, codes and emails redacted. Capabilities come from sys/capabilities-self on the lane token; no operator or root token is read. Needs railiance-platform CCR-2026-0045 applied to run live. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 1041694@bnt-lap001 Assistant-Session: 7af0bb68-ac4a-4ce3-8639-24ddd84c0b52
This commit is contained in:
parent
97d1fed949
commit
fca5591bc6
8 changed files with 616 additions and 136 deletions
181
tests/test_attended_login.py
Normal file
181
tests/test_attended_login.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
"""NK-WP-0051-T06: attended named-role login and its diagnosis receipt."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from ops_mason.attended_login import (
|
||||
classify_login_line,
|
||||
fresh_profile_argv,
|
||||
login_args,
|
||||
openbao_error_text,
|
||||
redact,
|
||||
resolve_assurance,
|
||||
resolve_uid,
|
||||
uid_from_display_name,
|
||||
)
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = REPO / "scripts/verify-reuse-surface-hub-writer.py"
|
||||
FAKE_TOKEN = "s.FAKElanetoken0123456789"
|
||||
REFUSAL = (
|
||||
"Error authenticating: Error making API request. Code: 400. Errors: "
|
||||
'* claim "groups" does not match any associated bound claim values'
|
||||
)
|
||||
AUTH_URL = (
|
||||
"https://kc.coulomb.social/authorize?client_id=openbao-admin&nonce=n1"
|
||||
"&redirect_uri=http%3A%2F%2Flocalhost%3A8250%2Foidc%2Fcallback&state=st1"
|
||||
)
|
||||
|
||||
|
||||
def test_login_prints_address_and_waits_instead_of_launching_browser():
|
||||
args = login_args("netkingdom", "reuse-surface-hub-writer-workload-kv-read")
|
||||
assert "skip_browser=true" in args
|
||||
assert "-token-only" in args
|
||||
assert "role=reuse-surface-hub-writer-workload-kv-read" in args
|
||||
|
||||
|
||||
def test_fresh_profile_argv_never_uses_default_profile(tmp_path):
|
||||
chromium = fresh_profile_argv("/usr/bin/chromium", tmp_path, AUTH_URL)
|
||||
assert f"--user-data-dir={tmp_path}" in chromium and chromium[-1] == AUTH_URL
|
||||
firefox = fresh_profile_argv("/usr/bin/firefox", tmp_path, AUTH_URL)
|
||||
assert firefox[1:4] == ["-no-remote", "-profile", str(tmp_path)]
|
||||
|
||||
|
||||
def test_stderr_classification_keeps_only_error_text():
|
||||
assert classify_login_line(" " + AUTH_URL) == "url"
|
||||
assert classify_login_line("Complete the login via your OIDC provider.") == "instruction"
|
||||
assert classify_login_line("Waiting for OIDC authentication to complete...") == "instruction"
|
||||
assert classify_login_line(REFUSAL) == "error"
|
||||
assert openbao_error_text(["", REFUSAL + "\n"]) == REFUSAL
|
||||
|
||||
|
||||
def test_redact_removes_never_logged_values():
|
||||
text = f"token {FAKE_TOKEN} code=abc123&state=x mail bernd@example.org"
|
||||
out = redact(text)
|
||||
assert FAKE_TOKEN not in out and "abc123" not in out and "bernd@example.org" not in out
|
||||
assert "state=x" in out
|
||||
|
||||
|
||||
def test_uid_resolution():
|
||||
assert uid_from_display_name("netkingdom-bernd.worsch-3", "netkingdom") == "bernd.worsch-3"
|
||||
assert uid_from_display_name("netkingdom-someone@example.org", "netkingdom") == ""
|
||||
assert resolve_uid("signin-probe-negative", "", "netkingdom") == ("signin-probe-negative", "operator")
|
||||
assert resolve_uid("", "netkingdom-bernd.worsch-3", "netkingdom") == (
|
||||
"bernd.worsch-3", "token-display-name")
|
||||
assert resolve_uid("", "", "netkingdom") == ("", "unknown")
|
||||
with pytest.raises(ValueError):
|
||||
resolve_uid("someone@example.org", "", "netkingdom")
|
||||
|
||||
|
||||
def test_assurance_resolution():
|
||||
assert resolve_assurance("", True, True) == ("aal2", "role-bound-claim")
|
||||
assert resolve_assurance("", False, True) == ("", "unknown")
|
||||
assert resolve_assurance("aal2", False, True) == ("aal2", "operator")
|
||||
with pytest.raises(ValueError):
|
||||
resolve_assurance("high", False, True)
|
||||
|
||||
|
||||
FAKE_BAO = r"""#!/usr/bin/env python3
|
||||
import json, os, sys
|
||||
args = sys.argv[1:]
|
||||
mode = os.environ["FAKE_BAO_MODE"]
|
||||
log = os.environ["FAKE_BAO_LOG"]
|
||||
with open(log, "a") as fh:
|
||||
fh.write(json.dumps({"args": args, "has_token": bool(os.environ.get("BAO_TOKEN"))}) + "\n")
|
||||
if args[0] == "login":
|
||||
sys.stderr.write("Complete the login via your OIDC provider. Open the following link in your browser:\n\n")
|
||||
sys.stderr.write(" " + os.environ["FAKE_AUTH_URL"] + "\n\n")
|
||||
sys.stderr.write("Waiting for OIDC authentication to complete...\n")
|
||||
if mode == "member":
|
||||
print(os.environ["FAKE_TOKEN"])
|
||||
sys.exit(0)
|
||||
sys.stderr.write(os.environ["FAKE_REFUSAL"] + "\n")
|
||||
sys.exit(2)
|
||||
if args[:2] == ["token", "lookup"]:
|
||||
print(json.dumps({"data": {"policies": ["workload-kv-read-reuse-surface-runtime"],
|
||||
"identity_policies": [], "entity_id": "11111111-1111-1111-1111-111111111111",
|
||||
"display_name": "netkingdom-bernd.worsch-3"}}))
|
||||
sys.exit(0)
|
||||
if args[:3] == ["write", "-format=json", "sys/capabilities-self"]:
|
||||
paths = args[3].split("=", 1)[1].split(",")
|
||||
data = {p: (["read"] if p.endswith("runtime-secrets") else ["deny"]) for p in paths}
|
||||
print(json.dumps({"data": data}))
|
||||
sys.exit(0)
|
||||
if args[:3] == ["token", "revoke", "-self"]:
|
||||
sys.exit(0)
|
||||
sys.stderr.write("unexpected bao call\n")
|
||||
sys.exit(9)
|
||||
"""
|
||||
|
||||
|
||||
def run_script(tmp_path, monkeypatch, mode, *extra):
|
||||
bindir = tmp_path / "bin"
|
||||
bindir.mkdir()
|
||||
fake = bindir / "bao"
|
||||
fake.write_text(FAKE_BAO)
|
||||
fake.chmod(fake.stat().st_mode | stat.S_IEXEC)
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
log = tmp_path / "bao.log"
|
||||
monkeypatch.setenv("PATH", f"{bindir}{os.pathsep}{os.environ['PATH']}")
|
||||
monkeypatch.setenv("HOME", str(home))
|
||||
monkeypatch.setenv("BAO_TOKEN", "s.OPERATORtoken0123456789")
|
||||
monkeypatch.setenv("FAKE_BAO_MODE", mode)
|
||||
monkeypatch.setenv("FAKE_BAO_LOG", str(log))
|
||||
monkeypatch.setenv("FAKE_TOKEN", FAKE_TOKEN)
|
||||
monkeypatch.setenv("FAKE_REFUSAL", REFUSAL)
|
||||
monkeypatch.setenv("FAKE_AUTH_URL", AUTH_URL)
|
||||
monkeypatch.delenv("OPS_MASON_LOGIN_BROWSER", raising=False)
|
||||
spec = importlib.util.spec_from_file_location("verify_hub_writer", SCRIPT)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
code = module.main([str(SCRIPT), mode, *extra])
|
||||
receipt = home / f".local/state/railiance/receipts/reuse-surface-hub-writer-{mode}.txt"
|
||||
calls = [json.loads(line) for line in log.read_text().splitlines()]
|
||||
return code, receipt, calls
|
||||
|
||||
|
||||
def test_member_receipt_uses_capabilities_self_and_records_diagnosis(tmp_path, monkeypatch):
|
||||
code, receipt, calls = run_script(tmp_path, monkeypatch, "member")
|
||||
text = receipt.read_text()
|
||||
assert code == 0, text
|
||||
assert stat.S_IMODE(receipt.stat().st_mode) == 0o600
|
||||
assert "uid=bernd.worsch-3\n" in text
|
||||
assert "assurance=aal2\n" in text
|
||||
assert "openbao_error=\n" in text
|
||||
assert "result=pass" in text
|
||||
assert FAKE_TOKEN not in text and "OPERATORtoken" not in text
|
||||
assert "state=st1" not in text
|
||||
assert not any(c["args"][:2] == ["write", "-format=json"] and "sys/capabilities" in c["args"]
|
||||
for c in calls)
|
||||
login = calls[0]
|
||||
assert login["args"][0] == "login" and "skip_browser=true" in login["args"]
|
||||
assert login["has_token"] is False
|
||||
|
||||
|
||||
def test_non_member_receipt_records_verbatim_refusal(tmp_path, monkeypatch):
|
||||
code, receipt, calls = run_script(
|
||||
tmp_path, monkeypatch, "non-member", "--uid", "signin-probe-negative", "--assurance", "aal2")
|
||||
text = receipt.read_text()
|
||||
assert code == 0, text
|
||||
assert "uid=signin-probe-negative\n" in text
|
||||
assert "uid_source=operator\n" in text
|
||||
assert "assurance=aal2\n" in text
|
||||
assert f"openbao_error={REFUSAL}\n" in text
|
||||
assert "login=refused" in text
|
||||
assert [c["args"][0] for c in calls] == ["login"]
|
||||
|
||||
|
||||
def test_email_uid_is_refused(tmp_path, monkeypatch):
|
||||
spec = importlib.util.spec_from_file_location("verify_hub_writer_uid", SCRIPT)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
assert module.main([str(SCRIPT), "non-member", "--uid", "x@example.org"]) == 2
|
||||
|
|
@ -63,7 +63,11 @@ def test_missing_revoke_fails():
|
|||
|
||||
|
||||
def test_non_member_refusal_passes_and_unexpected_login_fails():
|
||||
assert assess({"mode": "non-member", "login": "refused"}) == []
|
||||
refusal = 'claim "groups" does not match any associated bound claim values'
|
||||
assert assess({"mode": "non-member", "login": "refused", "openbao_error": refusal}) == []
|
||||
assert assess({"mode": "non-member", "login": "refused"}) == [
|
||||
"non-member refusal recorded no openbao_error text"
|
||||
]
|
||||
assert assess({"mode": "non-member", "login": "ok"})
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue