import json from pathlib import Path from types import SimpleNamespace import pytest from secrets_engine.errors import BackendError from secrets_engine.openbao import OpenBaoClient def test_kv_patch_keeps_secret_out_of_argv_and_cleans_input(monkeypatch): secret = "fake-SUPER-SECRET-value" client = OpenBaoClient(addr="http://example.invalid", token="", bao_bin="bao") monkeypatch.setattr(client, "kv_current_version", lambda _mount, _path: 7) captured = {} def fake_run_ok(args, *, stdin=None): captured["args"] = list(args) captured["stdin"] = stdin input_path = Path(args[-1][1:]) captured["input_path"] = input_path captured["mode"] = input_path.stat().st_mode & 0o777 captured["payload"] = json.loads(input_path.read_text(encoding="utf-8")) return "" monkeypatch.setattr(client, "_run_ok", fake_run_ok) base = client.kv_patch_fields("secret", "prod/example", {"TOKEN": secret}) assert base == 7 assert secret not in " ".join(captured["args"]) assert captured["stdin"] is None assert captured["mode"] == 0o600 assert captured["payload"] == {"TOKEN": secret} assert not captured["input_path"].exists() def test_new_path_uses_cas_zero_put_without_secret_in_argv(monkeypatch): secret = "fake-new-path-secret" client = OpenBaoClient(addr="http://example.invalid", token="", bao_bin="bao") monkeypatch.setattr(client, "kv_current_version", lambda _mount, _path: 0) captured = {} def fake_run_ok(args, *, stdin=None): captured["args"] = list(args) return "" monkeypatch.setattr(client, "_run_ok", fake_run_ok) client.kv_patch_fields("secret", "build/example", {"TOKEN": secret}) assert captured["args"][:5] == [ "kv", "put", "-mount=secret", "-cas=0", "build/example", ] assert secret not in " ".join(captured["args"]) assert not Path(captured["args"][-1][1:]).exists() def test_kv_current_version_fails_closed_on_permission_error(monkeypatch): client = OpenBaoClient(addr="http://example.invalid", token="", bao_bin="bao") monkeypatch.setattr( client, "_run", lambda _args: SimpleNamespace( returncode=2, stdout="", stderr="permission denied" ), ) with pytest.raises(BackendError, match="metadata get failed"): client.kv_current_version("secret", "prod/example") def test_kv_current_version_returns_zero_only_for_absent_path(monkeypatch): client = OpenBaoClient(addr="http://example.invalid", token="", bao_bin="bao") monkeypatch.setattr( client, "_run", lambda _args: SimpleNamespace( returncode=2, stdout="No value found at secret/metadata/example", stderr="" ), ) assert client.kv_current_version("secret", "example") == 0 def test_approle_session_keeps_login_material_out_of_argv_and_revokes_self(monkeypatch): client = OpenBaoClient(addr="http://example.invalid", token="parent", bao_bin="bao") monkeypatch.setattr(client, "read_approle_role_id", lambda _role: "role-id-value") monkeypatch.setattr( client, "create_approle_secret_id", lambda _role: "secret-id-value" ) captured = {} def fake_json_call(args, payload): captured["args"] = list(args) captured["payload"] = dict(payload) return json.dumps( {"auth": {"client_token": "scoped-token", "accessor": "accessor-value"}} ) monkeypatch.setattr(client, "_run_ok_with_json_file", fake_json_call) session = client.create_approle_session("example-role") revoke_calls = [] monkeypatch.setattr( session.client, "_run_ok", lambda args, **_kwargs: revoke_calls.append(list(args)) or "", ) assert "role-id-value" not in " ".join(captured["args"]) assert "secret-id-value" not in " ".join(captured["args"]) assert captured["payload"] == { "role_id": "role-id-value", "secret_id": "secret-id-value", } assert session.client.token == "scoped-token" assert session.accessor_fingerprint session.close() session.close() assert revoke_calls == [["token", "revoke", "-self"]] assert session.client.token == "" assert session.closed is True assert session.evidence() == { "session_handle": session.accessor_fingerprint, "established": True, "revocation_attempted": True, "revocation_succeeded": True, } assert "accessor-value" not in json.dumps(session.evidence()) assert "scoped-token" not in json.dumps(session.evidence()) def test_failed_session_revocation_is_visible_and_credential_is_dropped(monkeypatch): client = OpenBaoClient(addr="http://example.invalid", token="issued", bao_bin="bao") from secrets_engine.openbao import ScopedTokenSession session = ScopedTokenSession(client=client, accessor_fingerprint="safe-handle") monkeypatch.setattr( client, "_run_ok", lambda *_args, **_kwargs: (_ for _ in ()).throw(BackendError("revoke failed")), ) with pytest.raises(BackendError, match="revoke failed"): session.close() assert session.client.token == "" assert session.closed is True assert session.evidence() == { "session_handle": "safe-handle", "established": True, "revocation_attempted": True, "revocation_succeeded": False, } def test_approle_session_context_revokes_on_failure(monkeypatch): client = OpenBaoClient(addr="http://example.invalid", token="parent", bao_bin="bao") session = SimpleNamespace(closed=False) def close(): session.closed = True session.close = close monkeypatch.setattr(client, "create_approle_session", lambda _role: session) with pytest.raises(RuntimeError, match="child failed"): with client.approle_session("example-role"): raise RuntimeError("child failed") assert session.closed is True