Harden secret provisioning and lifecycle controls
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a0217e-8c4c-7383-be6b-f50a6e485306
This commit is contained in:
parent
0617923ff1
commit
3a1bd4f1c8
23 changed files with 1369 additions and 162 deletions
171
tests/test_openbao_safe_write.py
Normal file
171
tests/test_openbao_safe_write.py
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue