Keep serve on the live lifecycle path until the job stream ends.
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:
parent
06dfe12c18
commit
f211525f00
4 changed files with 299 additions and 151 deletions
|
|
@ -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():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue