From ccc0f8b5782f730f2b75921ae48169661c8a8faa Mon Sep 17 00:00:00 2001 From: codex Date: Thu, 29 Oct 2026 21:56:10 +0100 Subject: [PATCH] Compare full native bucket policies independent of array order Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a17f16-d99c-7ea3-95f0-8212897fba6b --- scripts/application_recovery_resources.py | 15 +++++++++++++-- tests/test_application_recovery_resources.py | 12 ++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/scripts/application_recovery_resources.py b/scripts/application_recovery_resources.py index e7aec97..78b6a33 100644 --- a/scripts/application_recovery_resources.py +++ b/scripts/application_recovery_resources.py @@ -97,6 +97,17 @@ def denied(call): raise Refused("native_boundary_unexpectedly_allowed") +def canonical_policy(value): + # Policy array ordering is not significant. Scaleway sorts actions and + # principal arrays on write. Preserve every value (including duplicates) + # and compare all fields; never discard unknown permission/condition data. + if isinstance(value, dict): + return {k: canonical_policy(v) for k, v in value.items()} + if isinstance(value, list): + return sorted((canonical_policy(v) for v in value), key=lambda v: json.dumps(v, sort_keys=True)) + return value + + def policy(bucket, prefix, apps, owner): def statement(sid, principal, actions, resources, condition=None): value = {"Sid": sid, "Effect": "Allow", "Principal": {"SCW": principal}, @@ -200,13 +211,13 @@ class Resources: if status(error) != 404: raise live = None - if live is not None and live != intended: + if live is not None and canonical_policy(live) != canonical_policy(intended): raise Refused("existing_policy_drift_requires_review") if live is None: self.mutate("put_policy:" + bucket, lambda: client.put_bucket_policy( Bucket=bucket, Policy=json.dumps(intended))) self.save() - if json.loads(client.get_bucket_policy(Bucket=bucket)["Policy"]) != intended: + if canonical_policy(json.loads(client.get_bucket_policy(Bucket=bucket)["Policy"])) != canonical_policy(intended): raise Refused("policy_readback_differs") if client.get_bucket_versioning(Bucket=bucket).get("Status") != "Enabled": self.mutate("versioning:" + bucket, lambda: client.put_bucket_versioning( diff --git a/tests/test_application_recovery_resources.py b/tests/test_application_recovery_resources.py index 7a93a11..381e00c 100644 --- a/tests/test_application_recovery_resources.py +++ b/tests/test_application_recovery_resources.py @@ -1,3 +1,4 @@ +import copy import importlib.util import sys import unittest @@ -69,6 +70,17 @@ class ResourcesTests(unittest.TestCase): {"application_id:" + self.apps[r] for r in ("reader", "custodian", "publisher")}) self.assertTrue(all(a.startswith("s3:Get") and "Object" not in a for a in shared["Action"])) + def test_provider_policy_reordering_allowed_but_extra_permission_refused(self): + original = m.policy("b", "archives", self.apps, "o") + returned = copy.deepcopy(original) + returned["Statement"].reverse() + for row in returned["Statement"]: + if isinstance(row["Action"], list): + row["Action"].reverse() + self.assertEqual(m.canonical_policy(original), m.canonical_policy(returned)) + returned["Statement"][0]["Action"].append("s3:DeleteObject") + self.assertNotEqual(m.canonical_policy(original), m.canonical_policy(returned)) + if __name__ == "__main__": unittest.main()