secrets-engine/tests/test_integration_bao.py
tegwick c4504c6de9
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Add value-safe verification and audit reporting
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0217e-8c4c-7383-be6b-f50a6e485306
2026-08-23 12:33:38 +02:00

157 lines
5.5 KiB
Python

"""Live integration test against a throwaway OpenBao dev server.
Skipped automatically if the `bao` CLI is not on PATH. Boots an in-memory dev
server on a private port, then drives apply -> provision -> verify(+/-) ->
exec-delivery and asserts the value is reachable by the child but not the parent.
"""
import json
import os
import shutil
import socket
import subprocess
import time
from pathlib import Path
import pytest
from secrets_engine.apply import apply_plan
from secrets_engine.catalog import get_entry
from secrets_engine.config import repo_root
from secrets_engine.errors import BackendError
from secrets_engine.exec_delivery import exec_with_secret
from secrets_engine.openbao import OpenBaoClient
from secrets_engine.plan import build_plan
from secrets_engine.provision import provision_from_file
from secrets_engine.verify import verify_negative, verify_positive
pytestmark = pytest.mark.skipif(
shutil.which("bao") is None and shutil.which("vault") is None,
reason="no OpenBao/Vault CLI on PATH",
)
def _free_port() -> int:
s = socket.socket()
s.bind(("127.0.0.1", 0))
port = s.getsockname()[1]
s.close()
return port
@pytest.fixture()
def bao_dev():
bao = shutil.which("bao") or shutil.which("vault")
port = _free_port()
addr = f"http://127.0.0.1:{port}"
token = "se-test-root"
proc = subprocess.Popen(
[bao, "server", "-dev", "-dev-no-store-token", f"-dev-root-token-id={token}",
f"-dev-listen-address=127.0.0.1:{port}"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
client = OpenBaoClient(addr=addr, token=token, bao_bin=bao)
for _ in range(50):
if client.is_reachable():
break
time.sleep(0.2)
else:
proc.kill()
pytest.fail("dev OpenBao did not become reachable")
try:
yield client
finally:
proc.kill()
def test_full_chain(bao_dev, tmp_path):
client = bao_dev
entry = get_entry(repo_root() / "catalog", "whynot-design-npm-publish")
plan = build_plan(entry, "prod", decision_id="test")
apply_plan(client, entry, plan)
# provision from a mode-0600 file outside the repo (tmp_path is outside)
tokenfile = tmp_path / "tok"
tokenfile.write_text("npm_integrationTESTvalue1234567890")
os.chmod(tokenfile, 0o600)
provision_from_file(client, entry, "npm_token", tokenfile)
# Server-side patch preserves an unmentioned sibling. Provisioning the
# declared field again must not replace that sibling.
client.kv_patch_fields(
entry.mount, entry.path, {"integration_sibling": "still-present"}
)
tokenfile.write_text("npm_integrationTESTvalue0987654321")
os.chmod(tokenfile, 0o600)
provision_from_file(client, entry, "npm_token", tokenfile)
assert client.kv_field_present(
entry.mount, entry.path, "integration_sibling", token=client.token
)
pos = verify_positive(client, entry, "npm_token")
assert pos.passed, pos.detail
client.write_policy(
"se-unrelated-denied",
f'path "{entry.mount}/data/{entry.path}" {{ capabilities = ["deny"] }}\n',
)
unrelated = json.loads(
client._run_ok(
["token", "create", "-format=json", "-policy=se-unrelated-denied"]
)
)["auth"]["client_token"]
neg = verify_negative(client, entry, unrelated_token=unrelated)
assert neg.passed, neg.detail
# A real unrelated identity with accidental policy overlap must make the
# negative check fail; an invalid/garbage token could not detect this.
client.write_policy(
"se-unrelated-overlap",
f'path "{entry.mount}/data/{entry.path}" {{ capabilities = ["read"] }}\n',
)
overlapping = json.loads(
client._run_ok(
["token", "create", "-format=json", "-policy=se-unrelated-overlap"]
)
)["auth"]["client_token"]
leaked = verify_negative(client, entry, unrelated_token=overlapping)
assert not leaked.passed, leaked.detail
# exec delivery: child can resolve token via npmrc; assert via a probe script
probe = tmp_path / "probe.sh"
probe.write_text(
"#!/usr/bin/env bash\n"
'grep -q _authToken "$NPM_CONFIG_USERCONFIG" && echo CHILD_HAS_TOKEN\n'
)
os.chmod(probe, 0o755)
rc = exec_with_secret(client, entry, "npm_token", [str(probe)], mode="npm-config")
assert rc == 0
# the parent process never received the value as an env var
assert "SE_NPM_TOKEN" not in os.environ
def test_merge_safe_patch_rejects_stale_cas(bao_dev):
client = bao_dev
mount = "cas-test"
path = "build/example"
client.ensure_kv_mount(mount)
client.kv_patch_fields(mount, path, {"first": "one", "second": "two"})
stale = client.kv_current_version(mount, path)
client.kv_patch_fields(mount, path, {"first": "new"}, expected_version=stale)
with pytest.raises(BackendError):
client.kv_patch_fields(
mount, path, {"second": "stale-write"}, expected_version=stale
)
assert client.kv_field_present(mount, path, "first", token=client.token)
assert client.kv_field_present(mount, path, "second", token=client.token)
def test_idempotent_apply(bao_dev):
client = bao_dev
entry = get_entry(repo_root() / "catalog", "whynot-design-npm-publish")
plan = build_plan(entry, "prod", decision_id="test")
first = apply_plan(client, entry, plan)
second = apply_plan(client, entry, plan)
# policy should be reported unchanged on the second apply
assert any("unchanged" in s for s in second.skipped)