Keep serve on the live lifecycle path until the job stream ends.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

The process reads one stage credential file at startup, applies each job through the CLI lifecycle functions, and waits for further jobs. A second stage credential file refuses startup. A local allow flag does not authorize the call.

Assistant: grok
Assistant-Session: 01a1609f-72b8-7222-9dff-fae300115e42
This commit is contained in:
tegwick 2026-10-22 18:15:23 +02:00
parent 06dfe12c18
commit f211525f00
4 changed files with 299 additions and 151 deletions

View file

@ -6,6 +6,9 @@ refusal of a local allow flag. Fixture credentials stay out of captured output.
from __future__ import annotations
import copy
import io
import json
import sys
from pathlib import Path
import pytest
@ -14,11 +17,10 @@ from secrets_engine import cli, lifecycle
from secrets_engine.apply import apply_plan
from secrets_engine.catalog import validate_entry
from secrets_engine.config import Config, repo_root
from secrets_engine.errors import DecisionError
from secrets_engine.openbao import OpenBaoClient
from secrets_engine.plan import build_plan
from secrets_engine.service import invoke_protected, run_service
from secrets_engine.stage_binding import STAGE_TOKEN_FILES, StageBinding, StartupError
from secrets_engine.service import run_service
from secrets_engine.stage_binding import STAGE_TOKEN_FILES, StartupError
from tests.test_catalog import VALID
SENTINEL = "fixture-stage-credential-wp0016"
@ -62,10 +64,10 @@ def _clear_stage_env(monkeypatch):
monkeypatch.delenv("SECRETS_ENGINE_LOCAL_ALLOW", raising=False)
def _isolate_authority(monkeypatch, tmp_path: Path):
"""No serving PDP, no published stance file, no stage credential."""
_clear_stage_env(monkeypatch)
monkeypatch.setenv("SECRETS_ENGINE_PEP_STANCE", str(tmp_path / "missing-stance.yaml"))
def _quiet_runtime(monkeypatch, tmp_path: Path):
"""Keep the test off the hub, the PDP, and the repo evidence directory."""
monkeypatch.setenv("SECRETS_ENGINE_HUB_URL", "")
monkeypatch.setenv("SECRETS_ENGINE_EVIDENCE", str(tmp_path / "evidence"))
for key in (
"SECRETS_ENGINE_APPROVAL_URL",
"SECRETS_ENGINE_APPROVAL_TOKEN_FILE",
@ -80,6 +82,32 @@ def _isolate_authority(monkeypatch, tmp_path: Path):
monkeypatch.delenv(key, raising=False)
def _isolate_authority(monkeypatch, tmp_path: Path):
"""No serving PDP, no published stance file, no stage credential."""
_clear_stage_env(monkeypatch)
_quiet_runtime(monkeypatch, tmp_path)
monkeypatch.setenv("SECRETS_ENGINE_PEP_STANCE", str(tmp_path / "missing-stance.yaml"))
class _Auth:
auth = "env"
bootstrap_token_file = None
class _LifecycleClient:
def __init__(self):
self.calls: list[str] = []
def delete_approle(self, name):
self.calls.append(f"delete_approle:{name}")
def delete_policy(self, name):
raise AssertionError(f"unexpected policy delete {name}")
def kv_delete_metadata(self, mount, path):
raise AssertionError(f"unexpected kv delete {mount}/{path}")
def test_plan_path_constructs_no_openbao_client(monkeypatch):
constructed: list[str] = []
@ -150,43 +178,121 @@ def test_cli_refuses_two_stage_credentials(tmp_path, monkeypatch, capsys):
_assert_sentinel_absent(captured)
def test_service_lifecycle_uses_the_cli_library_function(monkeypatch):
entry = validate_entry(copy.deepcopy(VALID))
binding = StageBinding(stage="test", credential=SENTINEL)
seen: list[tuple[str, str]] = []
real = lifecycle.build_lifecycle_plan
def _bind_stage(monkeypatch, tmp_path: Path, stage: str) -> None:
_clear_stage_env(monkeypatch)
monkeypatch.setenv(STAGE_TOKEN_FILES[stage], str(_write_stage_file(tmp_path, stage)))
def spy(entry_arg, operation):
seen.append((entry_arg.id, operation))
return real(entry_arg, operation)
monkeypatch.setattr(lifecycle, "build_lifecycle_plan", spy)
plans = run_service(binding, [{"entry": entry, "operation": "suspend"}])
direct = real(entry, "suspend")
assert seen == [(entry.id, "suspend")]
assert plans[0].operation == direct.operation
assert plans[0].catalog_id == direct.catalog_id
assert [action.kind for action in plans[0].actions] == [
action.kind for action in direct.actions
def test_service_lifecycle_uses_the_cli_library_and_waits(tmp_path, monkeypatch):
_quiet_runtime(monkeypatch, tmp_path)
_bind_stage(monkeypatch, tmp_path, "build")
client = _LifecycleClient()
monkeypatch.setattr(OpenBaoClient, "resolve", lambda *_a, **_k: client)
order: list[str] = []
real_build = lifecycle.build_lifecycle_plan
real_apply = lifecycle.apply_lifecycle_plan
def spy_build(entry, operation):
order.append(f"build:{operation}")
return real_build(entry, operation)
def spy_apply(bound_client, plan):
order.append(f"apply:{plan.operation}")
return real_apply(bound_client, plan)
monkeypatch.setattr(lifecycle, "build_lifecycle_plan", spy_build)
monkeypatch.setattr(lifecycle, "apply_lifecycle_plan", spy_apply)
def jobs():
order.append("yield-1")
yield {
"catalog_id": "example-build-test-token",
"operation": "suspend",
"confirm_destroy": "",
}
order.append("yield-2")
yield {
"catalog_id": "example-build-test-token",
"operation": "suspend",
"confirm_destroy": "",
}
outcomes = run_service(
Config.load(), jobs(), allow=True, auth_args=_Auth()
)
assert order == [
"yield-1",
"build:suspend",
"apply:suspend",
"yield-2",
"build:suspend",
"apply:suspend",
]
rendered = plans[0].render()
assert len(client.calls) == 2
assert all(call.startswith("delete_approle:") for call in client.calls)
rendered = "\n".join(plan.render() + result.render() for plan, result in outcomes)
assert SENTINEL not in rendered
assert "applied:" in rendered
def test_service_refuses_a_second_stage_credential():
entry = validate_entry(copy.deepcopy(VALID))
binding = StageBinding(stage="build", credential=SENTINEL)
with pytest.raises(StartupError, match="another stage credential"):
run_service(
binding,
[
{
"entry": entry,
"operation": "suspend",
"foreign_stage_credential": "present",
}
],
)
def test_serve_reads_a_second_job_from_stdin(tmp_path, monkeypatch, capsys):
_quiet_runtime(monkeypatch, tmp_path)
_bind_stage(monkeypatch, tmp_path, "build")
client = _LifecycleClient()
monkeypatch.setattr(OpenBaoClient, "resolve", lambda *_a, **_k: client)
extra = json.dumps(
{"catalog_id": "example-build-test-token", "operation": "suspend"}
)
monkeypatch.setattr(sys, "stdin", io.StringIO(extra + "\n"))
code = cli.main(
[
"serve",
"example-build-test-token",
"--operation",
"suspend",
"--auth",
"env",
"--allow",
]
)
captured = capsys.readouterr()
assert code == 0
assert len(client.calls) == 2
assert captured.out.count("applied:") == 2
_assert_sentinel_absent(captured)
def test_service_refuses_a_second_stage_credential_file(tmp_path, monkeypatch, capsys):
_clear_stage_env(monkeypatch)
monkeypatch.setenv(STAGE_TOKEN_FILES["build"], str(_write_stage_file(tmp_path, "build")))
monkeypatch.setenv(
STAGE_TOKEN_FILES["production"],
str(_write_stage_file(tmp_path, "production")),
)
pulled: list[str] = []
def jobs():
pulled.append("pulled")
yield {
"catalog_id": "example-build-test-token",
"operation": "suspend",
"confirm_destroy": "",
}
with pytest.raises(StartupError, match="more than one stage credential is present") as raised:
run_service(Config.load(), jobs(), allow=True, auth_args=_Auth())
assert pulled == []
assert SENTINEL not in str(raised.value)
capsys.readouterr()
code = cli.main(
["serve", "example-build-test-token", "--operation", "suspend", "--auth", "env"]
)
captured = capsys.readouterr()
assert code != 0
assert "more than one stage credential is present" in captured.err
_assert_sentinel_absent(captured)
def test_no_deployment_manifest_was_added():
@ -224,17 +330,31 @@ def test_local_allow_does_not_authorize_protected_apply(tmp_path, monkeypatch, c
_assert_sentinel_absent(captured)
def test_local_allow_does_not_authorize_protected_service_call(tmp_path, monkeypatch):
def test_serve_allow_flag_does_not_authorize(tmp_path, monkeypatch, capsys):
_isolate_authority(monkeypatch, tmp_path)
_bind_stage(monkeypatch, tmp_path, "production")
monkeypatch.setenv("SECRETS_ENGINE_LOCAL_ALLOW", "1")
entry = cli.get_entry(repo_root() / "catalog", "whynot-design-npm-publish")
with pytest.raises(DecisionError):
invoke_protected(Config.load(), entry, "apply", allow=True, auth_args=_Auth())
class _Auth:
auth = "env"
bootstrap_token_file = None
monkeypatch.setattr(
OpenBaoClient,
"resolve",
lambda *_a, **_k: pytest.fail("serve opened a backend"),
)
code = cli.main(
[
"serve",
"whynot-design-npm-publish",
"--operation",
"suspend",
"--auth",
"env",
"--allow",
]
)
captured = capsys.readouterr()
assert code != 0
assert "applied:" not in captured.out
assert "unable to load PEP stance" in captured.err
_assert_sentinel_absent(captured)
def test_use_claim_names_access_engine_and_stores_no_credential():