Skip to content

Commit b625017

Browse files
committed
Accomodate (future) user-defined roles.
- google.cloud.iam.Policy holds a 'bindings' mapping, which doesn't enforce using known roles. - Its 'owners', 'editors', and 'viewers' are now properties which indirect over that 'bindings' attribute. Note that this is a breaking change, as users who relied on mutating one of those sets (rather than re-assigning it) will need to update.
1 parent 399a6dd commit b625017

File tree

7 files changed

+171
-237
lines changed

7 files changed

+171
-237
lines changed

core/google/cloud/iam.py

Lines changed: 49 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,49 @@ class Policy(object):
5353
def __init__(self, etag=None, version=None):
5454
self.etag = etag
5555
self.version = version
56-
self.owners = set()
57-
self.editors = set()
58-
self.viewers = set()
56+
self.bindings = {}
57+
58+
@property
59+
def owners(self):
60+
"""Legacy access to owner role."""
61+
result = set()
62+
for role in self._OWNER_ROLES:
63+
for member in self.bindings.get(role, ()):
64+
result.add(member)
65+
return result
66+
67+
@owners.setter
68+
def owners(self, value):
69+
"""Update owners."""
70+
self.bindings[OWNER_ROLE] = list(value)
71+
72+
@property
73+
def editors(self):
74+
"""Legacy access to editor role."""
75+
result = set()
76+
for role in self._EDITOR_ROLES:
77+
for member in self.bindings.get(role, ()):
78+
result.add(member)
79+
return result
80+
81+
@editors.setter
82+
def editors(self, value):
83+
"""Update editors."""
84+
self.bindings[EDITOR_ROLE] = list(value)
85+
86+
@property
87+
def viewers(self):
88+
"""Legacy access to viewer role."""
89+
result = set()
90+
for role in self._VIEWER_ROLES:
91+
for member in self.bindings.get(role, ()):
92+
result.add(member)
93+
return result
94+
95+
@viewers.setter
96+
def viewers(self, value):
97+
"""Update viewers."""
98+
self.bindings[VIEWER_ROLE] = list(value)
5999

60100
@staticmethod
61101
def user(email):
@@ -123,22 +163,6 @@ def authenticated_users():
123163
"""
124164
return 'allAuthenticatedUsers'
125165

126-
def _bind_custom_role(self, role, members): # pylint: disable=no-self-use
127-
"""Bind an API-specific role to members.
128-
129-
Helper for :meth:`from_api_repr`.
130-
131-
:type role: str
132-
:param role: role to bind.
133-
134-
:type members: set of str
135-
:param members: member IDs to be bound to the role.
136-
137-
Subclasses may override.
138-
"""
139-
raise ValueError(
140-
'Unknown binding: role=%s, members=%s' % (role, members))
141-
142166
@classmethod
143167
def from_api_repr(cls, resource):
144168
"""Create a policy from the resource returned from the API.
@@ -154,47 +178,10 @@ def from_api_repr(cls, resource):
154178
policy = cls(etag, version)
155179
for binding in resource.get('bindings', ()):
156180
role = binding['role']
157-
members = set(binding['members'])
158-
if role in cls._OWNER_ROLES:
159-
policy.owners |= members
160-
elif role in cls._EDITOR_ROLES:
161-
policy.editors |= members
162-
elif role in cls._VIEWER_ROLES:
163-
policy.viewers |= members
164-
else:
165-
policy._bind_custom_role(role, members)
181+
members = sorted(binding['members'])
182+
policy.bindings[role] = members
166183
return policy
167184

168-
def _role_bindings(self):
169-
"""Enumerate members bound to roles for the policy.
170-
171-
Helper for :meth:`to_api_repr`.
172-
173-
:rtype: list of mapping
174-
:returns: zero or more mappings describing roles / members bound by
175-
the policy.
176-
177-
Subclasses may override.
178-
"""
179-
bindings = []
180-
181-
if self.owners:
182-
bindings.append(
183-
{'role': OWNER_ROLE,
184-
'members': sorted(self.owners)})
185-
186-
if self.editors:
187-
bindings.append(
188-
{'role': EDITOR_ROLE,
189-
'members': sorted(self.editors)})
190-
191-
if self.viewers:
192-
bindings.append(
193-
{'role': VIEWER_ROLE,
194-
'members': sorted(self.viewers)})
195-
196-
return bindings
197-
198185
def to_api_repr(self):
199186
"""Construct a Policy resource.
200187
@@ -209,9 +196,9 @@ def to_api_repr(self):
209196
if self.version is not None:
210197
resource['version'] = self.version
211198

212-
bindings = self._role_bindings()
213-
214-
if bindings:
215-
resource['bindings'] = bindings
199+
if len(self.bindings) > 0:
200+
resource['bindings'] = [
201+
{'role': role, 'members': members}
202+
for role, members in sorted(self.bindings.items())]
216203

217204
return resource

core/unit_tests/test_iam.py

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def test_ctor_defaults(self):
3333
self.assertEqual(list(policy.owners), [])
3434
self.assertEqual(list(policy.editors), [])
3535
self.assertEqual(list(policy.viewers), [])
36+
self.assertEqual(dict(policy.bindings), {})
3637

3738
def test_ctor_explicit(self):
3839
VERSION = 17
@@ -43,6 +44,7 @@ def test_ctor_explicit(self):
4344
self.assertEqual(list(policy.owners), [])
4445
self.assertEqual(list(policy.editors), [])
4546
self.assertEqual(list(policy.viewers), [])
47+
self.assertEqual(dict(policy.bindings), {})
4648

4749
def test_user(self):
4850
EMAIL = 'phred@example.com'
@@ -87,6 +89,7 @@ def test_from_api_repr_only_etag(self):
8789
self.assertEqual(list(policy.owners), [])
8890
self.assertEqual(list(policy.editors), [])
8991
self.assertEqual(list(policy.viewers), [])
92+
self.assertEqual(dict(policy.bindings), {})
9093

9194
def test_from_api_repr_complete(self):
9295
from google.cloud.iam import (
@@ -95,8 +98,8 @@ def test_from_api_repr_complete(self):
9598
VIEWER_ROLE,
9699
)
97100

98-
OWNER1 = 'user:phred@example.com'
99-
OWNER2 = 'group:cloud-logs@google.com'
101+
OWNER1 = 'group:cloud-logs@google.com'
102+
OWNER2 = 'user:phred@example.com'
100103
EDITOR1 = 'domain:google.com'
101104
EDITOR2 = 'user:phred@example.com'
102105
VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
@@ -114,23 +117,31 @@ def test_from_api_repr_complete(self):
114117
policy = klass.from_api_repr(RESOURCE)
115118
self.assertEqual(policy.etag, 'DEADBEEF')
116119
self.assertEqual(policy.version, 17)
117-
self.assertEqual(sorted(policy.owners), [OWNER2, OWNER1])
120+
self.assertEqual(sorted(policy.owners), [OWNER1, OWNER2])
118121
self.assertEqual(sorted(policy.editors), [EDITOR1, EDITOR2])
119122
self.assertEqual(sorted(policy.viewers), [VIEWER1, VIEWER2])
120-
121-
def test_from_api_repr_bad_role(self):
122-
BOGUS1 = 'user:phred@example.com'
123-
BOGUS2 = 'group:cloud-logs@google.com'
123+
self.assertEqual(
124+
dict(policy.bindings), {
125+
OWNER_ROLE: [OWNER1, OWNER2],
126+
EDITOR_ROLE: [EDITOR1, EDITOR2],
127+
VIEWER_ROLE: [VIEWER1, VIEWER2],
128+
})
129+
130+
def test_from_api_repr_unknown_role(self):
131+
USER = 'user:phred@example.com'
132+
GROUP = 'group:cloud-logs@google.com'
124133
RESOURCE = {
125134
'etag': 'DEADBEEF',
126135
'version': 17,
127136
'bindings': [
128-
{'role': 'nonesuch', 'members': [BOGUS1, BOGUS2]},
137+
{'role': 'unknown', 'members': [USER, GROUP]},
129138
],
130139
}
131140
klass = self._get_target_class()
132-
with self.assertRaises(ValueError):
133-
klass.from_api_repr(RESOURCE)
141+
policy = klass.from_api_repr(RESOURCE)
142+
self.assertEqual(policy.etag, 'DEADBEEF')
143+
self.assertEqual(policy.version, 17)
144+
self.assertEqual(policy.bindings, {'unknown': [GROUP, USER]})
134145

135146
def test_to_api_repr_defaults(self):
136147
policy = self._make_one()
@@ -141,6 +152,7 @@ def test_to_api_repr_only_etag(self):
141152
self.assertEqual(policy.to_api_repr(), {'etag': 'DEADBEEF'})
142153

143154
def test_to_api_repr_full(self):
155+
import operator
144156
from google.cloud.iam import (
145157
OWNER_ROLE,
146158
EDITOR_ROLE,
@@ -153,20 +165,18 @@ def test_to_api_repr_full(self):
153165
EDITOR2 = 'user:phred@example.com'
154166
VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
155167
VIEWER2 = 'user:phred@example.com'
156-
EXPECTED = {
157-
'etag': 'DEADBEEF',
158-
'version': 17,
159-
'bindings': [
160-
{'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]},
161-
{'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
162-
{'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
163-
],
164-
}
168+
BINDINGS = [
169+
{'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]},
170+
{'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
171+
{'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
172+
]
165173
policy = self._make_one('DEADBEEF', 17)
166-
policy.owners.add(OWNER1)
167-
policy.owners.add(OWNER2)
168-
policy.editors.add(EDITOR1)
169-
policy.editors.add(EDITOR2)
170-
policy.viewers.add(VIEWER1)
171-
policy.viewers.add(VIEWER2)
172-
self.assertEqual(policy.to_api_repr(), EXPECTED)
174+
policy.owners = [OWNER1, OWNER2]
175+
policy.editors = [EDITOR1, EDITOR2]
176+
policy.viewers = [VIEWER1, VIEWER2]
177+
resource = policy.to_api_repr()
178+
self.assertEqual(resource['etag'], 'DEADBEEF')
179+
self.assertEqual(resource['version'], 17)
180+
key = operator.itemgetter('role')
181+
self.assertEqual(
182+
sorted(resource['bindings'], key=key), sorted(BINDINGS, key=key))

pubsub/google/cloud/pubsub/iam.py

Lines changed: 30 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
https://cloud.google.com/pubsub/access_control#permissions
1818
"""
1919

20-
from google.cloud.iam import OWNER_ROLE
21-
from google.cloud.iam import EDITOR_ROLE
22-
from google.cloud.iam import VIEWER_ROLE
20+
# pylint: disable=unused-import
21+
from google.cloud.iam import OWNER_ROLE # noqa - backward compat
22+
from google.cloud.iam import EDITOR_ROLE # noqa - backward compat
23+
from google.cloud.iam import VIEWER_ROLE # noqa - backward compat
24+
# pylint: enable=unused-import
2325
from google.cloud.iam import Policy as _BasePolicy
2426

2527
# Pubsub-specific IAM roles
@@ -94,76 +96,32 @@ class Policy(_BasePolicy):
9496
See:
9597
https://cloud.google.com/pubsub/docs/reference/rest/Shared.Types/Policy
9698
https://cloud.google.com/pubsub/docs/reference/rest/Shared.Types/Binding
97-
98-
:type etag: str
99-
:param etag: ETag used to identify a unique of the policy
100-
101-
:type version: int
102-
:param version: unique version of the policy
10399
"""
104100
_OWNER_ROLES = (OWNER_ROLE, PUBSUB_ADMIN_ROLE)
105-
_EDITOR_ROLES = (EDITOR_ROLE, PUBSUB_EDITOR_ROLE)
106-
_VIEWER_ROLES = (VIEWER_ROLE, PUBSUB_VIEWER_ROLE)
107-
108-
def __init__(self, etag=None, version=None):
109-
super(Policy, self).__init__(etag, version)
110-
self.publishers = set()
111-
self.subscribers = set()
112-
113-
def _bind_custom_role(self, role, members):
114-
"""Bind an API-specific role to members.
115-
116-
Helper for :meth:`from_api_repr`.
117-
118-
:type role: str
119-
:param role: role to bind.
120-
121-
:type members: set of str
122-
:param members: member IDs to be bound to the role.
123-
124-
Subclasses may override.
125-
"""
126-
if role == PUBSUB_PUBLISHER_ROLE:
127-
self.publishers |= members
128-
elif role == PUBSUB_SUBSCRIBER_ROLE:
129-
self.subscribers |= members
130-
else:
131-
super(Policy, self)._bind_custom_role(role, members)
101+
"""Roles mapped onto our ``owners`` attribute."""
132102

133-
def _role_bindings(self):
134-
"""Enumerate members bound to roles for the policy.
135-
136-
Helper for :meth:`to_api_repr`.
137-
138-
:rtype: list of mapping
139-
:returns: zero or more mappings describing roles / members bound by
140-
the policy.
141-
"""
142-
bindings = []
143-
144-
if self.owners:
145-
bindings.append(
146-
{'role': PUBSUB_ADMIN_ROLE,
147-
'members': sorted(self.owners)})
148-
149-
if self.editors:
150-
bindings.append(
151-
{'role': PUBSUB_EDITOR_ROLE,
152-
'members': sorted(self.editors)})
153-
154-
if self.viewers:
155-
bindings.append(
156-
{'role': PUBSUB_VIEWER_ROLE,
157-
'members': sorted(self.viewers)})
158-
159-
if self.publishers:
160-
bindings.append(
161-
{'role': PUBSUB_PUBLISHER_ROLE,
162-
'members': sorted(self.publishers)})
163-
164-
if self.subscribers:
165-
bindings.append(
166-
{'role': PUBSUB_SUBSCRIBER_ROLE,
167-
'members': sorted(self.subscribers)})
103+
_EDITOR_ROLES = (EDITOR_ROLE, PUBSUB_EDITOR_ROLE)
104+
"""Roles mapped onto our ``editors`` attribute."""
168105

169-
return bindings
106+
_VIEWER_ROLES = (VIEWER_ROLE, PUBSUB_VIEWER_ROLE)
107+
"""Roles mapped onto our ``viewers`` attribute."""
108+
109+
@property
110+
def publishers(self):
111+
"""Legacy access to owner role."""
112+
return self.bindings.get(PUBSUB_PUBLISHER_ROLE, ())
113+
114+
@publishers.setter
115+
def publishers(self, value):
116+
"""Update publishers."""
117+
self.bindings[PUBSUB_PUBLISHER_ROLE] = list(value)
118+
119+
@property
120+
def subscribers(self):
121+
"""Legacy access to owner role."""
122+
return self.bindings.get(PUBSUB_SUBSCRIBER_ROLE, ())
123+
124+
@subscribers.setter
125+
def subscribers(self, value):
126+
"""Update subscribers."""
127+
self.bindings[PUBSUB_SUBSCRIBER_ROLE] = list(value)

0 commit comments

Comments
 (0)