Repository: googleapis/python-api-core

Issue:
google.api_core.iam.Policy.__getitem__ does not correctly save empty bindings
We recently tried to upgrade a tool from 1.15.0 to 1.26.1 and found it breaks some code that handles IAM policies.  The commit that introduces the bug was released in 1.16.0: https://github.com/googleapis/python-api-core/commit/fd47fda5e3f5eca63522c8d81cffa22bc2a29ab6#diff-7cc73ea72342c139ff54060be9ff25b2f792f9225e0cc0f501dca9dbed9c4741 - 

The new `__getitem__` implementation returns a new empty `set()` for roles not in the current policy.  But it doesn't save that set in the bindings.  So if the user manipulates it, the policy isn't actually updated.  That breaks code written like this:
```python
policy = resource.get_iam_policy()
policy['roles/storage.objectAdmin'].add(principal)
bucket.set_iam_policy(policy)
```
This worked fine on v1.15.0 because of the use of `defaultdict`.  But now, this adds the principal to a set that's not used by the policy.

Something like the following (untested) patch should do the trick:
```diff
diff --git a/google/api_core/iam.py b/google/api_core/iam.py
index f130936..d650336 100644
--- a/google/api_core/iam.py
+++ b/google/api_core/iam.py
@@ -136,7 +136,9 @@ class Policy(collections_abc.MutableMapping):
         for b in self._bindings:
             if b["role"] == key:
                 return b["members"]
-        return set()
+
+        self[key] = set()
+        return self[key]

     def __setitem__(self, key, value):
         self.__check_version__()

```

Comments:

Patch:
diff --git a/google/api_core/iam.py b/google/api_core/iam.py
index f130936..d83cbf3 100644
--- a/google/api_core/iam.py
+++ b/google/api_core/iam.py
@@ -136,7 +136,10 @@ class Policy(collections_abc.MutableMapping):
         for b in self._bindings:
             if b["role"] == key:
                 return b["members"]
-        return set()
+        # binding does not yet exist, create one
+        new_binding = {"role": key, "members": set()}
+        self._bindings.append(new_binding)
+        return new_binding["members"]
 
     def __setitem__(self, key, value):
         self.__check_version__()


Test patch:
diff --git a/tests/unit/test_iam.py b/tests/unit/test_iam.py
index 896e10d..f9771f0 100644
--- a/tests/unit/test_iam.py
+++ b/tests/unit/test_iam.py
@@ -32,11 +32,11 @@ class TestPolicy:
         policy = self._make_one()
         assert policy.etag is None
         assert policy.version is None
+        assert len(policy) == 0
+        assert dict(policy) == {}
         assert policy.owners == empty
         assert policy.editors == empty
         assert policy.viewers == empty
-        assert len(policy) == 0
-        assert dict(policy) == {}
 
     def test_ctor_explicit(self):
         VERSION = 1
@@ -45,16 +45,24 @@ class TestPolicy:
         policy = self._make_one(ETAG, VERSION)
         assert policy.etag == ETAG
         assert policy.version == VERSION
+        assert len(policy) == 0
+        assert dict(policy) == {}
         assert policy.owners == empty
         assert policy.editors == empty
         assert policy.viewers == empty
-        assert len(policy) == 0
-        assert dict(policy) == {}
 
     def test___getitem___miss(self):
         policy = self._make_one()
         assert policy["nonesuch"] == set()
 
+    def test__getitem___and_set(self):
+        from google.api_core.iam import OWNER_ROLE
+        policy = self._make_one()
+
+        # get the policy using the getter and then modify it
+        policy[OWNER_ROLE].add("user:phred@example.com")
+        assert dict(policy) == {OWNER_ROLE: {"user:phred@example.com"}}
+
     def test___getitem___version3(self):
         policy = self._make_one("DEADBEEF", 3)
         with pytest.raises(InvalidOperationException, match=_DICT_ACCESS_MSG):
@@ -293,10 +301,10 @@ class TestPolicy:
         policy = klass.from_api_repr(RESOURCE)
         assert policy.etag == "ACAB"
         assert policy.version is None
+        assert dict(policy) == {}
         assert policy.owners == empty
         assert policy.editors == empty
         assert policy.viewers == empty
-        assert dict(policy) == {}
 
     def test_from_api_repr_complete(self):
         from google.api_core.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE