"""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"}