railiance-platform/tests/test_mas_evidence_publisher_gate.py
codex 28d5d1875c Read the attended admin token from the private helper when the MAS gate requests its writer token.
Warden's attended login strips BAO_TOKEN and VAULT_TOKEN from the child, so the
2026-11-03 run minted and proved the key and then exited writer_token_failed.
The minted key was deleted. Token-request failures now record a value-free reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 4541@bnt-lap001
Assistant-Session: 2099b3aa-d7d9-49a9-ab4e-fa8b7d0e39e8
2026-11-03 05:41:25 +01:00

384 lines
16 KiB
Python

"""MAS publisher gate records boundaries and refuses to keep secret values."""
import importlib.util
import json
import os
import pwd
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location(
"mas_gate", ROOT / "scripts/mas_evidence_publisher_gate.py"
)
gate = importlib.util.module_from_spec(spec)
spec.loader.exec_module(gate)
def test_venv_symlink_is_not_the_system_interpreter():
system = "/usr/bin/python3"
venv = "/home/operator/.local/share/railiance-platform/backup-venv/bin/python"
assert gate.same_interpreter(system, system)
assert not gate.same_interpreter(system, venv)
def test_backup_python_stays_on_the_account_home_when_login_replaces_home(monkeypatch):
monkeypatch.setenv("HOME", "/tmp/warden-session")
account = pwd.getpwuid(os.getuid()).pw_dir
assert gate.selected_backup_python() == (
Path(account) / ".local/share/railiance-platform/backup-venv/bin/python"
)
assert "/tmp/warden-session" not in str(gate.selected_backup_python())
assert "/tmp/mas-bucket-policy-venv" not in str(gate.backup_python(account))
def test_s3_failure_keeps_the_code_and_drops_secret_material():
class Fake:
response = {
"Error": {
"Code": "MalformedPolicy",
"Message": "Invalid principal secret=abc SCWABCDEFGHIJKLMNOP signature=zzz",
},
"ResponseMetadata": {"HTTPStatusCode": 400},
}
detail = gate.s3_failure(Fake())
assert detail["error_code"] == "MalformedPolicy"
assert detail["http_status"] == 400
assert "Invalid principal" in detail["message"]
assert "abc" not in detail["message"]
assert "ABCDEFGHIJKLMNOP" not in detail["message"]
def test_supplier_pair_maps_aliases_and_refuses_partial_or_equal_values():
mapped = gate.supplier_pair({"ACCESS_KEY": "access-one", "SECRET_KEY": "secret-two"})
assert list(mapped) == ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
assert mapped["AWS_ACCESS_KEY_ID"] != mapped["AWS_SECRET_ACCESS_KEY"]
labeled = gate.supplier_pair({"Access Key": "access-one", "Secret Key": "secret-two"})
assert labeled["AWS_ACCESS_KEY_ID"] == "access-one"
blob = gate.supplier_pair(
{"credentials": '[default]\naccess_key = access-one\nsecret_key = secret-two\n'}
)
assert blob["AWS_SECRET_ACCESS_KEY"] == "secret-two"
with pytest.raises(gate.GateError) as partial:
gate.supplier_pair({"ACCESS_KEY": "access-one"}, "platform/suppliers/scaleway/example")
assert partial.value.detail["keys"] == ["ACCESS_KEY"]
assert "access-one" not in json.dumps(partial.value.detail)
with pytest.raises(gate.GateError):
gate.supplier_pair({"ACCESS_KEY": "same-value", "SECRET_KEY": "same-value"})
def test_policy_observation_requires_the_prefix_statements_without_delete():
document = {
"Statement": [
{
"Sid": "MasEvidenceBackupObjects",
"Action": ["s3:GetObject", "s3:PutObject"],
},
{
"Sid": "PublisherBucketProbe",
"Principal": {"SCW": ["application_id:" + gate.APPLICATION_ID]},
"Action": list(gate.PROBE_ACTIONS),
},
{"Sid": "MasEvidenceBackupList", "Action": ["s3:ListBucket"]},
{"Sid": "Other", "Action": ["s3:DeleteObject"]},
]
}
observed = gate.policy_observation(document)
assert observed["mas_sids_present"] == list(gate.MAS_SIDS)
assert observed["object_delete_absent"] is True
assert observed["probe_includes_publisher"] is True
assert observed["principal"] == gate.APPLICATION_ID
document["Statement"][0]["Action"].append("s3:DeleteObject")
assert gate.policy_observation(document)["object_delete_absent"] is False
def test_merged_policy_replaces_separate_probes_and_stays_within_ten():
desired = json.loads(
(ROOT / "docs/object-storage/railiance-platform-pg-backup-policy.json").read_text()
)
live = {
"Version": "2023-04-17",
"Id": "resource-control-scoped-backup",
"Statement": [
item for item in desired["Statement"]
if not str(item.get("Sid", "")).startswith("Mas")
and item.get("Sid") != "PublisherBucketProbe"
] + [
{
"Sid": "CoulombSocialBackupProbe",
"Principal": {"SCW": "application_id:681d00bf-b965-4323-b657-97bfde423511"},
"Action": list(gate.PROBE_ACTIONS),
},
{
"Sid": "EmailConnectBackupProbe",
"Principal": {"SCW": "application_id:0eb0f958-77a4-4c70-9aa7-14ba46411415"},
"Action": list(gate.PROBE_ACTIONS),
},
],
}
merged = gate.merged_policy(live, desired)
sids = [item.get("Sid") for item in merged["Statement"]]
assert len(sids) == 10
assert "CoulombSocialBackupProbe" not in sids
assert "EmailConnectBackupProbe" not in sids
assert "PublisherBucketProbe" in sids
assert sids.count("MasEvidenceBackupList") == 1
assert "BackupObjectsUnderPrefix" in sids
observed = gate.policy_observation(merged)
assert observed["probe_includes_publisher"] is True
assert observed["object_delete_absent"] is True
def test_denial_observation_passes_only_when_all_three_boundaries_deny():
results = {
"sibling_prefix": {"http_status": 403, "denied": True},
"retained_delete": {"http_status": 403, "denied": True},
"administration": {"http_status": 403, "denied": True},
"own_prefix": {"http_status": 200, "denied": False},
}
passed = gate.denial_observation(results)
assert passed["passed"] is True
assert passed["application_id"] == "25a4cbca-220e-40c0-9af5-6c7713000174"
results["retained_delete"]["denied"] = False
assert gate.denial_observation(results)["passed"] is False
results["retained_delete"]["denied"] = True
results["own_prefix"] = {"http_status": 403, "denied": True, "error_code": "AccessDenied"}
rejected = gate.denial_observation(results)
assert rejected["passed"] is False
assert rejected["own_prefix_allowed"] is False
assert rejected["error_code"]["own_prefix"] == "AccessDenied"
rendered = json.dumps(passed)
assert "access-one" not in rendered
assert "secret-two" not in rendered
def test_project_scope_appends_the_bootstrap_project_once():
pair = {
"AWS_ACCESS_KEY_ID": "SCWEXAMPLEKEY",
"AWS_SECRET_ACCESS_KEY": "secret-value-not-written",
}
project = "11111111-2222-3333-4444-555555555555"
scoped, applied = gate.scoped_supplier_pair(pair, project)
assert applied is True
assert scoped["AWS_ACCESS_KEY_ID"] == f"SCWEXAMPLEKEY@{project}"
assert scoped["AWS_SECRET_ACCESS_KEY"] == pair["AWS_SECRET_ACCESS_KEY"]
again, applied_again = gate.scoped_supplier_pair(scoped, project)
assert applied_again is False
assert again["AWS_ACCESS_KEY_ID"] == scoped["AWS_ACCESS_KEY_ID"]
untouched, unused = gate.scoped_supplier_pair(pair, "")
assert unused is False
assert untouched["AWS_ACCESS_KEY_ID"] == "SCWEXAMPLEKEY"
def test_raw_create_response_keeps_num_uses_out_of_the_public_detail():
auth = gate.writer_auth({
"auth": {
"client_token": "s.secret-token-value",
"accessor": "accessor-secret",
"token_policies": ["backup-pair-writer"],
"policies": ["backup-pair-writer"],
"lease_duration": 300,
"num_uses": 2,
}
})
identity = gate._writer_identity(auth)
assert identity["num_uses"] == 2
rendered = json.dumps(identity)
assert "s.secret-token-value" not in rendered
with pytest.raises(gate.GateError):
gate.writer_auth({"auth": {"num_uses": 2}})
def test_writer_identity_accepts_token_policies_and_rejects_default():
auth = {
"client_token": "s.secret-token-value",
"accessor": "accessor-secret",
"token_policies": ["backup-pair-writer"],
"lease_duration": "300",
"num_uses": "2",
}
identity = gate._writer_identity(auth)
assert identity == {
"policies": ["backup-pair-writer"],
"identity_policies": [],
"ttl": 300,
"num_uses": 2,
}
auth["policies"] = ["default", "backup-pair-writer"]
with pytest.raises(gate.GateError) as caught:
gate._writer_identity(auth)
assert caught.value.code == "writer_identity_rejected"
rendered = json.dumps(caught.value.detail)
assert "s.secret-token-value" not in rendered
assert "accessor-secret" not in rendered
assert "client_token" not in caught.value.detail["auth_fields"]
assert "accessor" not in caught.value.detail["auth_fields"]
assert caught.value.detail["legacy_policies"] == ["default", "backup-pair-writer"]
def test_put_parser_keeps_version_and_drops_any_nested_value():
parsed = gate.version_from_put(
{"request_id": "req-1", "data": {"version": 1, "data": {"AWS_SECRET_ACCESS_KEY": "not-a-live-value"}}}
)
assert parsed == {"version": 1, "request_id": "req-1"}
assert "not-a-live-value" not in json.dumps(parsed)
with pytest.raises(gate.GateError):
gate.version_from_put({"data": {"version": 2}})
def test_logical_path_strips_only_the_openbao_storage_marker():
assert gate.logical_path("platform/metadata/workloads/mas/backup-primary") == (
"platform/workloads/mas/backup-primary"
)
assert gate.logical_path("platform/data/workloads/mas/backup-primary") == (
"platform/workloads/mas/backup-primary"
)
def test_pair_custody_preview_includes_mas_and_the_writer_policy_names_it():
spec_custody = importlib.util.spec_from_file_location(
"pair_custody_for_gate", ROOT / "scripts/application_backup_pair_custody.py"
)
custody = importlib.util.module_from_spec(spec_custody)
spec_custody.loader.exec_module(custody)
preview = custody.preview("mas")
assert preview["kv_path"] == "platform/workloads/mas/backup-primary"
assert preview["openbao_write"] is False
text = (ROOT / "openbao/policies/backup-pair-writer.hcl").read_text(encoding="utf-8")
assert 'path "platform/data/workloads/mas/backup-primary"' in text
assert "read" not in text.split('path "platform/data/workloads/mas/backup-primary"')[1].split("path ")[0]
class _FakeConsole:
"""Scaleway IAM stand-in. Records calls; never returns the supplier secret."""
def __init__(self, keys=None, issued=None, name="mas-evidence-backup-primary"):
self.name = name
self.keys = keys if keys is not None else [
{"application_id": gate.APPLICATION_ID, "default_project_id": gate.PROJECT_ID,
"description": "supplier"}
]
self.issued = issued if issued is not None else {
"application_id": gate.APPLICATION_ID, "access_key": "SCWMINTED",
"secret_key": "minted-secret", "created_at": "2026-11-02T20:00:00Z",
}
self.calls = []
def __call__(self, method, path, data=None):
self.calls.append((method, path))
if method == "GET" and path.startswith("/iam/v1alpha1/applications/"):
return {"name": self.name, "organization_id": "org-1"}
if method == "GET" and path.startswith("/iam/v1alpha1/api-keys?"):
assert "organization_id=org-1" in path and "application_id=" + gate.APPLICATION_ID in path
return {"api_keys": list(self.keys), "total_count": len(self.keys)}
if method == "POST":
assert data["application_id"] == gate.APPLICATION_ID
assert data["default_project_id"] == gate.PROJECT_ID
self.keys.append({"application_id": gate.APPLICATION_ID,
"default_project_id": gate.PROJECT_ID,
"description": data["description"]})
return dict(self.issued)
if method == "DELETE":
return {}
raise AssertionError((method, path))
def test_mint_proves_then_writes_the_minted_key_not_the_supplier():
api = _FakeConsole()
seen = {}
def prove(pair, project):
seen["prove"] = (pair["AWS_ACCESS_KEY_ID"], project)
def write(pair):
seen["write"] = dict(pair)
return {"current_version": 1}
receipt = gate.mint_workload_pair(api, prove, write, sleep=lambda _: None)
assert seen["prove"] == ("SCWMINTED", gate.PROJECT_ID)
assert seen["write"] == {"AWS_ACCESS_KEY_ID": "SCWMINTED", "AWS_SECRET_ACCESS_KEY": "minted-secret"}
assert receipt["status"] == "passed" and receipt["keys_after"]["count"] == 2
assert "minted-secret" not in json.dumps(receipt) and "SCWMINTED" not in json.dumps(receipt)
assert not any(method == "DELETE" for method, _ in api.calls)
def test_mint_retries_propagation_then_deletes_the_key_when_proof_never_passes():
api = _FakeConsole()
sleeps = []
def prove(pair, project):
raise gate.GateError("publisher_denials_failed")
with pytest.raises(gate.GateError) as caught:
gate.mint_workload_pair(api, prove, lambda pair: None, sleep=sleeps.append)
assert caught.value.code == "publisher_denials_failed"
assert caught.value.detail == {"minted_key_deleted": True}
assert len(sleeps) == gate.PROOF_ATTEMPTS - 1
assert ("DELETE", "/iam/v1alpha1/api-keys/SCWMINTED") in api.calls
def test_mint_deletes_the_key_when_the_pair_write_fails():
api = _FakeConsole()
def write(pair):
raise gate.GateError("existing_custody_requires_rotation_review")
with pytest.raises(gate.GateError) as caught:
gate.mint_workload_pair(api, lambda pair, project: None, write)
assert caught.value.code == "existing_custody_requires_rotation_review"
assert caught.value.detail["minted_key_deleted"] is True
@pytest.mark.parametrize("console", [
_FakeConsole(name="email-connect-backup-primary"),
_FakeConsole(keys=[]),
_FakeConsole(keys=[{"application_id": gate.APPLICATION_ID, "default_project_id": gate.PROJECT_ID}] * 2),
_FakeConsole(keys=[{"application_id": gate.APPLICATION_ID, "default_project_id": "other"}]),
])
def test_mint_refuses_before_issuing_when_application_or_key_state_is_unexpected(console):
with pytest.raises(gate.GateError):
gate.mint_workload_pair(console, lambda *a: None, lambda pair: None)
assert not any(method in ("POST", "DELETE") for method, _ in console.calls)
def test_incomplete_issue_is_deleted_and_never_written():
api = _FakeConsole(issued={"application_id": gate.APPLICATION_ID, "access_key": "SCWMINTED"})
written = []
with pytest.raises(gate.GateError) as caught:
gate.mint_workload_pair(api, lambda *a: None, written.append)
assert caught.value.code == "issued_key_incomplete"
assert caught.value.detail["minted_key_deleted"] is True and written == []
def test_main_refuses_without_the_mint_flag(tmp_path, monkeypatch):
import sys
monkeypatch.setattr(gate, "RECEIPT_DIR", tmp_path)
monkeypatch.setattr(sys, "stdout", sys.stdout)
monkeypatch.setattr(sys, "stderr", sys.stderr)
assert gate.main([]) == 71
assert json.loads((tmp_path / "mas-evidence-publisher-gate.json").read_text())["code"] == "usage_refused"
def test_parent_token_prefers_env_then_the_private_helper(tmp_path):
helper = tmp_path / ".vault-token"
helper.write_text("s.helpertoken\n")
helper.chmod(0o600)
assert gate.parent_token({"BAO_TOKEN": "s.env", "HOME": str(tmp_path)}) == ("s.env", "BAO_TOKEN")
assert gate.parent_token({"HOME": str(tmp_path)}) == ("s.helpertoken", "token_helper")
helper.chmod(0o644)
assert gate.parent_token({"HOME": str(tmp_path)}) == ("", "helper_rejected")
assert gate.parent_token({"HOME": str(tmp_path / "missing")}) == ("", "absent")
assert gate.parent_token({}) == ("", "absent")
def test_writer_auth_failure_names_the_reason_without_values(monkeypatch, tmp_path):
monkeypatch.delenv("BAO_TOKEN", raising=False)
monkeypatch.delenv("VAULT_TOKEN", raising=False)
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("BAO_ADDR", gate.BAO_ADDR)
with pytest.raises(gate.GateError) as caught:
gate._request_writer_auth()
assert caught.value.detail == {"reason": "parent_token_absent"}