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