152 lines
4.9 KiB
Python
152 lines
4.9 KiB
Python
|
|
import copy
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from secrets_engine.catalog import get_entry, validate_entry
|
||
|
|
from secrets_engine.config import repo_root
|
||
|
|
from secrets_engine.errors import PolicyGuardError
|
||
|
|
from secrets_engine.lifecycle import (
|
||
|
|
apply_lifecycle_plan,
|
||
|
|
build_lifecycle_plan,
|
||
|
|
build_native_deactivation_plan,
|
||
|
|
require_destroy_confirmation,
|
||
|
|
)
|
||
|
|
from tests.test_catalog import VALID
|
||
|
|
|
||
|
|
|
||
|
|
class RecordingLifecycleClient:
|
||
|
|
def __init__(self):
|
||
|
|
self.calls = []
|
||
|
|
|
||
|
|
def delete_approle(self, role_name):
|
||
|
|
self.calls.append(("delete-approle", role_name))
|
||
|
|
|
||
|
|
def delete_policy(self, policy_name):
|
||
|
|
self.calls.append(("delete-policy", policy_name))
|
||
|
|
|
||
|
|
def kv_delete_metadata(self, mount, path):
|
||
|
|
self.calls.append(("delete-kv-metadata", f"{mount}/{path}"))
|
||
|
|
|
||
|
|
|
||
|
|
def _existing_kv_entry(*, auth_management="engine"):
|
||
|
|
data = copy.deepcopy(VALID)
|
||
|
|
data.update(
|
||
|
|
{
|
||
|
|
"stage": "prod",
|
||
|
|
"mount": "platform",
|
||
|
|
"path": "workloads/example/runtime",
|
||
|
|
"mount_management": "existing",
|
||
|
|
"workload_delivery": [
|
||
|
|
{"mode": "external-secrets", "owner": "rapp-example"}
|
||
|
|
],
|
||
|
|
}
|
||
|
|
)
|
||
|
|
if auth_management == "existing":
|
||
|
|
data["delivery_auth"] = {
|
||
|
|
"method": "approle",
|
||
|
|
"management": "existing",
|
||
|
|
"role_name": "external-example-role",
|
||
|
|
"policy_name": "external-example-policy",
|
||
|
|
}
|
||
|
|
return validate_entry(data)
|
||
|
|
|
||
|
|
|
||
|
|
def test_kv_revoke_plan_deactivates_native_auth_and_preserves_custody():
|
||
|
|
entry = _existing_kv_entry()
|
||
|
|
plan = build_native_deactivation_plan(entry)
|
||
|
|
|
||
|
|
assert [(a.kind, a.target, a.mutation) for a in plan.actions] == [
|
||
|
|
("delete-approle", entry.role_name, True),
|
||
|
|
("delete-policy", entry.policy_name, True),
|
||
|
|
("preserve-kv-custody", "platform/workloads/example/runtime", False),
|
||
|
|
("preserve-workload-delivery", entry.id, False),
|
||
|
|
]
|
||
|
|
|
||
|
|
client = RecordingLifecycleClient()
|
||
|
|
result = apply_lifecycle_plan(client, plan)
|
||
|
|
assert client.calls == [
|
||
|
|
("delete-approle", entry.role_name),
|
||
|
|
("delete-policy", entry.policy_name),
|
||
|
|
]
|
||
|
|
assert "platform/workloads/example/runtime" in result.preserved
|
||
|
|
|
||
|
|
|
||
|
|
def test_revoke_plan_never_mutates_externally_managed_delivery_auth():
|
||
|
|
entry = _existing_kv_entry(auth_management="existing")
|
||
|
|
plan = build_native_deactivation_plan(entry)
|
||
|
|
assert not any(action.mutation for action in plan.actions)
|
||
|
|
|
||
|
|
client = RecordingLifecycleClient()
|
||
|
|
result = apply_lifecycle_plan(client, plan)
|
||
|
|
assert client.calls == []
|
||
|
|
assert entry.role_name in result.preserved
|
||
|
|
assert entry.policy_name in result.preserved
|
||
|
|
|
||
|
|
|
||
|
|
def test_suspend_removes_only_approle_and_preserves_policy_and_kv():
|
||
|
|
entry = _existing_kv_entry()
|
||
|
|
plan = build_lifecycle_plan(entry, "suspend")
|
||
|
|
client = RecordingLifecycleClient()
|
||
|
|
|
||
|
|
result = apply_lifecycle_plan(client, plan)
|
||
|
|
|
||
|
|
assert client.calls == [("delete-approle", entry.role_name)]
|
||
|
|
assert entry.policy_name in result.preserved
|
||
|
|
assert f"{entry.mount}/{entry.path}" in result.preserved
|
||
|
|
|
||
|
|
|
||
|
|
def test_destroy_plan_is_explicit_and_deletes_auth_before_kv_metadata():
|
||
|
|
entry = _existing_kv_entry()
|
||
|
|
plan = build_lifecycle_plan(entry, "destroy")
|
||
|
|
client = RecordingLifecycleClient()
|
||
|
|
|
||
|
|
result = apply_lifecycle_plan(client, plan)
|
||
|
|
|
||
|
|
assert plan.operation == "destroy"
|
||
|
|
assert client.calls == [
|
||
|
|
("delete-approle", entry.role_name),
|
||
|
|
("delete-policy", entry.policy_name),
|
||
|
|
("delete-kv-metadata", f"{entry.mount}/{entry.path}"),
|
||
|
|
]
|
||
|
|
assert list(result.applied) == [
|
||
|
|
entry.role_name,
|
||
|
|
entry.policy_name,
|
||
|
|
f"{entry.mount}/{entry.path}",
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def test_destroy_requires_exact_confirmation_and_kv_lane():
|
||
|
|
entry = _existing_kv_entry()
|
||
|
|
with pytest.raises(PolicyGuardError, match="exact catalog id"):
|
||
|
|
require_destroy_confirmation(entry, "wrong-lane")
|
||
|
|
require_destroy_confirmation(entry, entry.id)
|
||
|
|
|
||
|
|
auth_entry = get_entry(repo_root() / "catalog", "warden-sign")
|
||
|
|
with pytest.raises(PolicyGuardError, match="no KV custody"):
|
||
|
|
build_lifecycle_plan(auth_entry, "destroy")
|
||
|
|
|
||
|
|
|
||
|
|
def test_auth_capability_revoke_deletes_only_role_and_policy():
|
||
|
|
entry = get_entry(repo_root() / "catalog", "warden-sign")
|
||
|
|
plan = build_native_deactivation_plan(entry)
|
||
|
|
client = RecordingLifecycleClient()
|
||
|
|
|
||
|
|
apply_lifecycle_plan(client, plan)
|
||
|
|
|
||
|
|
assert client.calls == [
|
||
|
|
("delete-approle", entry.role_name),
|
||
|
|
("delete-policy", entry.policy_name),
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def test_rendered_and_applied_mutation_targets_are_identical():
|
||
|
|
entry = _existing_kv_entry()
|
||
|
|
plan = build_native_deactivation_plan(entry)
|
||
|
|
rendered = plan.render()
|
||
|
|
expected_targets = [action.target for action in plan.actions if action.mutation]
|
||
|
|
assert all(target in rendered for target in expected_targets)
|
||
|
|
|
||
|
|
client = RecordingLifecycleClient()
|
||
|
|
result = apply_lifecycle_plan(client, plan)
|
||
|
|
assert list(result.applied) == expected_targets
|