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
205 lines
7.9 KiB
Python
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"
|