feat(mvp): working secrets-engine CLI for the whynot-design npm publish lane
Implements SECRETS-WP-0002 end to end as a uv-managed Python package: - catalog: non-secret lane registry + strict validator (build/test/prod) - stage roles + OpenBao ACL policies; guards refuse wildcards, sys/, identity/, admin names, and cross-stage paths before any backend call - plan/apply: dry-run-first, idempotent policy + approle apply, decision-gated - decisions: State Hub lookup with local-fixture fallback; non-secret evidence to JSONL + hub progress, scrubbed of any value - provision/verify: mode-0600 file import + generated test values; positive/ negative checks that never print the value - exec delivery: `exec --catalog ... -- npm publish` injects the token via a temp .npmrc for the child only, cleaned up on exit/failure/interrupt - ops-warden routing contract + hardening backlog docs - 34 tests incl. live OpenBao integration; scripts/demo-e2e.sh runs the full chain against a throwaway bao dev server Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
58c24cff53
commit
a852d3f1ff
47 changed files with 3743 additions and 122 deletions
103
tests/test_integration_bao.py
Normal file
103
tests/test_integration_bao.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""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.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", 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)
|
||||
|
||||
pos = verify_positive(client, entry, "npm_token")
|
||||
assert pos.passed, pos.detail
|
||||
neg = verify_negative(client, entry)
|
||||
assert neg.passed, neg.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_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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue