diff --git a/scripts/credential-change.py b/scripts/credential-change.py index db612a0..20397e5 100755 --- a/scripts/credential-change.py +++ b/scripts/credential-change.py @@ -838,20 +838,30 @@ def applier_policy_violations(ccr: dict[str, Any]) -> list[str]: # WARDEN-WP-0028: platform fleet lanes stay under platform/workloads/; # client/tenant commercial secrets live on mount tenants/ (exact-path policies). + # WARDEN-WP-0043: supplier account credentials live at + # platform/suppliers// (exactly two segments). allowed_mount_prefixes = { - "platform": "platform/workloads/", - "tenants": "tenants/", + "platform": ("platform/workloads/", "platform/suppliers/"), + "tenants": ("tenants/",), } if mount not in allowed_mount_prefixes: violations.append( f"openbao.mount must be one of {sorted(allowed_mount_prefixes)}, got {mount}" ) else: - prefix = allowed_mount_prefixes[mount] - if not kv_path.startswith(prefix): + prefixes = allowed_mount_prefixes[mount] + if not kv_path.startswith(prefixes): violations.append( - f"openbao.kv_path for mount {mount!r} must stay under {prefix}" + f"openbao.kv_path for mount {mount!r} must stay under " + + " or ".join(prefixes) ) + elif kv_path.startswith("platform/suppliers/"): + segments = kv_path[len("platform/suppliers/"):].split("/") + if len(segments) != 2 or not all(segments): + violations.append( + "openbao.kv_path under platform/suppliers/ must be exactly " + "/" + ) if any(fragment in kv_path for fragment in ("*", "..", "//")): violations.append("openbao.kv_path must not contain wildcard, parent, or empty segments") if "/data/" in kv_path or "/metadata/" in kv_path: @@ -1888,7 +1898,7 @@ def decision_templates(ccr: dict[str, Any] | None = None) -> dict[str, str]: else: context = { "id": "", - "kv_path": " or /...>", + "kv_path": ", /> or /...>", "policy_name": "", "auth_role_path": "auth//role/", "decision_link": "", diff --git a/tests/test_credential_change.py b/tests/test_credential_change.py index f111667..3e21cc8 100644 --- a/tests/test_credential_change.py +++ b/tests/test_credential_change.py @@ -771,6 +771,37 @@ class CredentialChangeTests(unittest.TestCase): violations = credential_change.applier_policy_violations(ccr) self.assertEqual(violations, [], violations) + def _supplier_ccr(self, kv_path: str) -> dict: + ccr, errors, _warnings = credential_change.validate_ccr(self.sample) + self.assertEqual(errors, []) + ccr["status"] = "approved" + ccr["openbao"]["mount"] = "platform" + ccr["openbao"]["kv_path"] = kv_path + ccr["openbao"]["policy_file"] = "" + return ccr + + def test_applier_accepts_platform_suppliers_path(self) -> None: + """WARDEN-WP-0043-T02 — supplier accounts at platform/suppliers//.""" + ccr = self._supplier_ccr("platform/suppliers/ionos/main") + violations = credential_change.applier_policy_violations(ccr) + self.assertEqual(violations, [], violations) + + def test_applier_rejects_malformed_platform_suppliers_paths(self) -> None: + for kv_path in ( + "platform/suppliers/ionos", + "platform/suppliers/ionos/main/extra", + "platform/suppliers/*/main", + "platform/suppliers/../workloads/x", + "platform/suppliers/ionos//main", + "platform/data/suppliers/ionos/main", + "platform/other/ionos/main", + ): + with self.subTest(kv_path=kv_path): + violations = credential_change.applier_policy_violations( + self._supplier_ccr(kv_path) + ) + self.assertNotEqual(violations, [], kv_path) + def test_nonprod_applier_policy_remains_metadata_only(self) -> None: policy = ( REPO_DIR / "openbao/policies/credential-change-nonprod-applier.hcl"