railiance-platform/tests/test_mas_evidence_iam_grant.py
codex ca7c46b775 Align the MAS IAM rule and read num_uses from the raw writer response.
Adds the one-session Scaleway IAM grant script, merges the publisher
bucket probes into one statement to fit the 10-statement limit, keeps
the Playwright cache under the account home during attended login, and
records the publisher-gate runs through the num_uses rejection.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 49552@bnt-lap001
Assistant-Session: 575802ee-c0c0-42a9-b2a1-9204c9f5b3f4
2026-11-02 19:28:28 +01:00

205 lines
7.9 KiB
Python

"""Synthetic Scaleway responses only. No live IAM call and no credential values."""
import importlib.util
import json
from pathlib import Path
from urllib.parse import parse_qs, urlsplit
import pytest
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location(
"mas_iam_grant", ROOT / "scripts/mas_evidence_iam_grant.py"
)
grant = importlib.util.module_from_spec(spec)
spec.loader.exec_module(grant)
ORG = "org-1"
REEF = "reef-project"
MAIL = "project-mas"
ACCESS = "SCWACCESS-NOT-A-RECEIPT-FIELD"
class Api:
def __init__(self):
self.calls = []
self.policies = {}
self.rules = {}
self.keys = [{
"access_key": ACCESS,
"application_id": grant.MAS_APPLICATION,
"default_project_id": "other-project",
}]
self.mas_rules = []
self.groups = []
self.project_rows = [
{"id": REEF, "name": "reef-storage", "organization_id": ORG},
{"id": MAIL, "name": "mail-address-semantics", "organization_id": ORG},
]
self.policies["coulomb-live"] = {
"id": "coulomb-live",
"name": "coulomb-social-backup-primary",
"editable": True,
"application_id": grant.COULOMB_APPLICATION,
}
self.rules["coulomb-live"] = [{
"permission_set_names": list(grant.PERMISSIONS),
"project_ids": [REEF],
}]
self.policies["coulomb-recovery"] = {
"id": "coulomb-recovery",
"name": "coulomb-recovery-project-publisher",
"editable": True,
"application_id": grant.COULOMB_APPLICATION,
}
self.rules["coulomb-recovery"] = [{
"permission_set_names": list(grant.PERMISSIONS),
"project_ids": ["recovery-project"],
}]
def __call__(self, method, path, data=None):
self.calls.append((method, path.split("?", 1)[0], data))
route = urlsplit(path).path
query = parse_qs(urlsplit(path).query)
if route == f"/iam/v1alpha1/applications/{grant.COULOMB_APPLICATION}":
return {
"id": grant.COULOMB_APPLICATION,
"name": "coulomb-social-backup-primary",
"organization_id": ORG,
}
if route == f"/iam/v1alpha1/applications/{grant.MAS_APPLICATION}":
return {
"id": grant.MAS_APPLICATION,
"name": "mas-evidence-backup-primary",
"organization_id": ORG,
}
if route == "/iam/v1alpha1/permission-sets":
names = list(grant.PERMISSIONS)
return {"permission_sets": [{"name": name} for name in names], "total_count": len(names)}
if route == "/account/v3/projects":
return {"projects": self.project_rows, "total_count": len(self.project_rows)}
if route == "/iam/v1alpha1/groups":
return {"groups": self.groups, "total_count": len(self.groups)}
if route == "/iam/v1alpha1/policies" and method == "GET":
rows = [
item for item in self.policies.values()
if item["application_id"] == query["application_id"][0]
]
return {"policies": rows, "total_count": len(rows)}
if route == "/iam/v1alpha1/policies" and method == "POST":
created = {"id": "policy-new", "editable": True, **data}
self.policies[created["id"]] = created
self.rules[created["id"]] = data["rules"]
return created
if route == "/iam/v1alpha1/rules" and method == "GET":
rows = self.rules[query["policy_id"][0]]
return {"rules": rows, "total_count": len(rows)}
if route == "/iam/v1alpha1/rules" and method == "PUT":
self.rules[data["policy_id"]] = data["rules"]
return {"rules": data["rules"]}
if route == "/iam/v1alpha1/api-keys" and method == "GET":
rows = [item for item in self.keys if item["application_id"] == query["application_id"][0]]
return {"api_keys": rows, "total_count": len(rows)}
if route.startswith("/iam/v1alpha1/api-keys/") and method == "PATCH":
access_key = route.rsplit("/", 1)[1]
for item in self.keys:
if item["access_key"] == access_key:
item["default_project_id"] = data["default_project_id"]
return {"default_project_id": item["default_project_id"]}
raise AssertionError(route)
def _rendered(receipt):
return json.dumps(receipt)
def test_one_wrong_rule_is_replaced_and_the_single_key_project_follows():
api = Api()
api.policies["policy-1"] = {
"id": "policy-1",
"name": "hand-made",
"editable": True,
"application_id": grant.MAS_APPLICATION,
}
api.rules["policy-1"] = [{
"permission_set_names": ["ObjectStorageReadOnly"],
"project_ids": [MAIL],
}]
receipt = grant.align(api)
assert receipt["outcome"] == "rule_replaced"
assert receipt["project_name"] == "reef-storage"
assert receipt["project_id"] == REEF
assert receipt["changed"] is True
assert receipt["preferred_project_updated"] is True
assert receipt["mas_keys"]["preferred_project_matches"] is True
assert api.rules["policy-1"] == [{
"permission_set_names": list(grant.PERMISSIONS),
"project_ids": [REEF],
}]
rendered = _rendered(receipt)
assert ACCESS not in rendered
assert "secret" not in rendered
recovery = next(item for item in receipt["coulomb_policies"] if "recovery" in item["name"])
assert recovery["rules"][0]["project_ids"] == ["recovery-project"]
def test_matching_rule_and_matching_key_make_no_provider_change():
api = Api()
api.keys[0]["default_project_id"] = REEF
api.policies["policy-1"] = {
"id": "policy-1", "name": grant.POLICY_NAME, "editable": True,
"application_id": grant.MAS_APPLICATION,
}
api.rules["policy-1"] = [{
"permission_set_names": list(reversed(grant.PERMISSIONS)),
"project_ids": [REEF],
}]
before = len(api.calls)
receipt = grant.align(api)
assert receipt["outcome"] == "already_matches"
assert receipt["changed"] is False
assert receipt["preferred_project_updated"] is False
assert not any(method in {"PUT", "POST", "PATCH"} for method, _, _ in api.calls[before:])
def test_missing_policy_is_created_on_the_application():
api = Api()
api.keys = []
receipt = grant.align(api)
assert receipt["outcome"] == "policy_created"
posted = [data for method, path, data in api.calls if method == "POST" and path.endswith("/policies")]
assert posted[0]["application_id"] == grant.MAS_APPLICATION
assert posted[0]["rules"][0]["project_ids"] == [REEF]
assert "organization_id" not in posted[0]["rules"][0]
def test_several_rules_are_left_unchanged():
api = Api()
api.policies["policy-1"] = {
"id": "policy-1", "name": "hand-made", "editable": True,
"application_id": grant.MAS_APPLICATION,
}
api.rules["policy-1"] = [
{"permission_set_names": list(grant.PERMISSIONS), "project_ids": [REEF]},
{"permission_set_names": ["IAMReadOnly"], "organization_id": ORG},
]
receipt = grant.align(api)
assert receipt["status"] == "review"
assert receipt["outcome"] == "multiple_rules_require_review"
assert not any(method in {"PUT", "POST", "PATCH"} for method, _, _ in api.calls)
def test_unresolved_project_refuses_before_a_change():
api = Api()
del api.policies["coulomb-live"]
del api.rules["coulomb-live"]
with pytest.raises(grant.Refused, match="coulomb_backup_project_unresolved"):
grant.align(api)
assert not any(method in {"PUT", "POST", "PATCH"} for method, _, _ in api.calls)
def test_preview_contacts_nothing(capsys):
assert grant.main([]) == 0
preview = json.loads(capsys.readouterr().out)
assert preview["provider_contacted"] is False
assert preview["project_source"] == "coulomb-social-backup-primary"