Skip to content

Commit 02a2361

Browse files
committed
Merge pull request #1776 from tseaver/pubsub-elaborate_iam_roles_permissions
Enumerate all roles / permissions for Pubsub IAM.
2 parents 77123bb + cbd5915 commit 02a2361

4 files changed

Lines changed: 209 additions & 34 deletions

File tree

gcloud/pubsub/iam.py

Lines changed: 103 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,87 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
"""PubSub API IAM policy definitions"""
14+
"""PubSub API IAM policy definitions
15+
16+
For allowed roles / permissions, see:
17+
https://cloud.google.com/pubsub/access_control#permissions
18+
"""
19+
20+
# Generic IAM roles
1521

1622
OWNER_ROLE = 'roles/owner'
17-
"""IAM permission implying all rights to an object."""
23+
"""Generic role implying all rights to an object."""
1824

1925
EDITOR_ROLE = 'roles/editor'
20-
"""IAM permission implying rights to modify an object."""
26+
"""Generic role implying rights to modify an object."""
2127

2228
VIEWER_ROLE = 'roles/viewer'
23-
"""IAM permission implying rights to access an object without modifying it."""
29+
"""Generic role implying rights to access an object."""
30+
31+
# Pubsub-specific IAM roles
32+
33+
PUBSUB_ADMIN_ROLE = 'roles/pubsub.admin'
34+
"""Role implying all rights to an object."""
35+
36+
PUBSUB_EDITOR_ROLE = 'roles/pubsub.editor'
37+
"""Role implying rights to modify an object."""
38+
39+
PUBSUB_VIEWER_ROLE = 'roles/pubsub.viewer'
40+
"""Role implying rights to access an object."""
41+
42+
PUBSUB_PUBLISHER_ROLE = 'roles/pubsub.publisher'
43+
"""Role implying rights to publish to a topic."""
44+
45+
PUBSUB_SUBSCRIBER_ROLE = 'roles/pubsub.subscriber'
46+
"""Role implying rights to subscribe to a topic."""
47+
48+
49+
# Pubsub-specific permissions
50+
51+
PUBSUB_TOPICS_CONSUME = 'pubsub.topics.consume'
52+
"""Permission: consume events from a subscription."""
53+
54+
PUBSUB_TOPICS_CREATE = 'pubsub.topics.create'
55+
"""Permission: create topics."""
56+
57+
PUBSUB_TOPICS_DELETE = 'pubsub.topics.delete'
58+
"""Permission: delete topics."""
59+
60+
PUBSUB_TOPICS_GET = 'pubsub.topics.get'
61+
"""Permission: retrieve topics."""
62+
63+
PUBSUB_TOPICS_GET_IAM_POLICY = 'pubsub.topics.getIamPolicy'
64+
"""Permission: retrieve subscription IAM policies."""
65+
66+
PUBSUB_TOPICS_LIST = 'pubsub.topics.list'
67+
"""Permission: list topics."""
68+
69+
PUBSUB_TOPICS_SET_IAM_POLICY = 'pubsub.topics.setIamPolicy'
70+
"""Permission: update subscription IAM policies."""
71+
72+
PUBSUB_SUBSCRIPTIONS_CONSUME = 'pubsub.subscriptions.consume'
73+
"""Permission: consume events from a subscription."""
74+
75+
PUBSUB_SUBSCRIPTIONS_CREATE = 'pubsub.subscriptions.create'
76+
"""Permission: create subscriptions."""
77+
78+
PUBSUB_SUBSCRIPTIONS_DELETE = 'pubsub.subscriptions.delete'
79+
"""Permission: delete subscriptions."""
80+
81+
PUBSUB_SUBSCRIPTIONS_GET = 'pubsub.subscriptions.get'
82+
"""Permission: retrieve subscriptions."""
83+
84+
PUBSUB_SUBSCRIPTIONS_GET_IAM_POLICY = 'pubsub.subscriptions.getIamPolicy'
85+
"""Permission: retrieve subscription IAM policies."""
86+
87+
PUBSUB_SUBSCRIPTIONS_LIST = 'pubsub.subscriptions.list'
88+
"""Permission: list subscriptions."""
89+
90+
PUBSUB_SUBSCRIPTIONS_SET_IAM_POLICY = 'pubsub.subscriptions.setIamPolicy'
91+
"""Permission: update subscription IAM policies."""
92+
93+
PUBSUB_SUBSCRIPTIONS_UPDATE = 'pubsub.subscriptions.update'
94+
"""Permission: update subscriptions."""
2495

2596

2697
class Policy(object):
@@ -42,6 +113,8 @@ def __init__(self, etag=None, version=None):
42113
self.owners = set()
43114
self.editors = set()
44115
self.viewers = set()
116+
self.publishers = set()
117+
self.subscribers = set()
45118

46119
@staticmethod
47120
def user(email):
@@ -125,12 +198,16 @@ def from_api_repr(cls, resource):
125198
for binding in resource.get('bindings', ()):
126199
role = binding['role']
127200
members = set(binding['members'])
128-
if role == OWNER_ROLE:
129-
policy.owners = members
130-
elif role == EDITOR_ROLE:
131-
policy.editors = members
132-
elif role == VIEWER_ROLE:
133-
policy.viewers = members
201+
if role in (OWNER_ROLE, PUBSUB_ADMIN_ROLE):
202+
policy.owners |= members
203+
elif role in (EDITOR_ROLE, PUBSUB_EDITOR_ROLE):
204+
policy.editors |= members
205+
elif role in (VIEWER_ROLE, PUBSUB_VIEWER_ROLE):
206+
policy.viewers |= members
207+
elif role == PUBSUB_PUBLISHER_ROLE:
208+
policy.publishers |= members
209+
elif role == PUBSUB_SUBSCRIBER_ROLE:
210+
policy.subscribers |= members
134211
else:
135212
raise ValueError('Unknown role: %s' % (role,))
136213
return policy
@@ -153,15 +230,28 @@ def to_api_repr(self):
153230

154231
if self.owners:
155232
bindings.append(
156-
{'role': OWNER_ROLE, 'members': sorted(self.owners)})
233+
{'role': PUBSUB_ADMIN_ROLE,
234+
'members': sorted(self.owners)})
157235

158236
if self.editors:
159237
bindings.append(
160-
{'role': EDITOR_ROLE, 'members': sorted(self.editors)})
238+
{'role': PUBSUB_EDITOR_ROLE,
239+
'members': sorted(self.editors)})
161240

162241
if self.viewers:
163242
bindings.append(
164-
{'role': VIEWER_ROLE, 'members': sorted(self.viewers)})
243+
{'role': PUBSUB_VIEWER_ROLE,
244+
'members': sorted(self.viewers)})
245+
246+
if self.publishers:
247+
bindings.append(
248+
{'role': PUBSUB_PUBLISHER_ROLE,
249+
'members': sorted(self.publishers)})
250+
251+
if self.subscribers:
252+
bindings.append(
253+
{'role': PUBSUB_SUBSCRIBER_ROLE,
254+
'members': sorted(self.subscribers)})
165255

166256
if bindings:
167257
resource['bindings'] = bindings

gcloud/pubsub/test_iam.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ def test_ctor_defaults(self):
3131
self.assertEqual(list(policy.owners), [])
3232
self.assertEqual(list(policy.editors), [])
3333
self.assertEqual(list(policy.viewers), [])
34+
self.assertEqual(list(policy.publishers), [])
35+
self.assertEqual(list(policy.subscribers), [])
3436

3537
def test_ctor_explicit(self):
3638
VERSION = 17
@@ -41,6 +43,8 @@ def test_ctor_explicit(self):
4143
self.assertEqual(list(policy.owners), [])
4244
self.assertEqual(list(policy.editors), [])
4345
self.assertEqual(list(policy.viewers), [])
46+
self.assertEqual(list(policy.publishers), [])
47+
self.assertEqual(list(policy.subscribers), [])
4448

4549
def test_user(self):
4650
EMAIL = 'phred@example.com'
@@ -87,20 +91,30 @@ def test_from_api_repr_only_etag(self):
8791
self.assertEqual(list(policy.viewers), [])
8892

8993
def test_from_api_repr_complete(self):
90-
from gcloud.pubsub.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE
94+
from gcloud.pubsub.iam import (
95+
OWNER_ROLE,
96+
EDITOR_ROLE,
97+
VIEWER_ROLE,
98+
PUBSUB_PUBLISHER_ROLE,
99+
PUBSUB_SUBSCRIBER_ROLE,
100+
)
91101
OWNER1 = 'user:phred@example.com'
92102
OWNER2 = 'group:cloud-logs@google.com'
93103
EDITOR1 = 'domain:google.com'
94104
EDITOR2 = 'user:phred@example.com'
95105
VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
96106
VIEWER2 = 'user:phred@example.com'
107+
PUBLISHER = 'user:phred@example.com'
108+
SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com'
97109
RESOURCE = {
98110
'etag': 'DEADBEEF',
99111
'version': 17,
100112
'bindings': [
101113
{'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]},
102114
{'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
103115
{'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
116+
{'role': PUBSUB_PUBLISHER_ROLE, 'members': [PUBLISHER]},
117+
{'role': PUBSUB_SUBSCRIBER_ROLE, 'members': [SUBSCRIBER]},
104118
],
105119
}
106120
klass = self._getTargetClass()
@@ -110,6 +124,8 @@ def test_from_api_repr_complete(self):
110124
self.assertEqual(sorted(policy.owners), [OWNER2, OWNER1])
111125
self.assertEqual(sorted(policy.editors), [EDITOR1, EDITOR2])
112126
self.assertEqual(sorted(policy.viewers), [VIEWER1, VIEWER2])
127+
self.assertEqual(sorted(policy.publishers), [PUBLISHER])
128+
self.assertEqual(sorted(policy.subscribers), [SUBSCRIBER])
113129

114130
def test_from_api_repr_bad_role(self):
115131
BOGUS1 = 'user:phred@example.com'
@@ -134,20 +150,30 @@ def test_to_api_repr_only_etag(self):
134150
self.assertEqual(policy.to_api_repr(), {'etag': 'DEADBEEF'})
135151

136152
def test_to_api_repr_full(self):
137-
from gcloud.pubsub.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE
153+
from gcloud.pubsub.iam import (
154+
PUBSUB_ADMIN_ROLE,
155+
PUBSUB_EDITOR_ROLE,
156+
PUBSUB_VIEWER_ROLE,
157+
PUBSUB_PUBLISHER_ROLE,
158+
PUBSUB_SUBSCRIBER_ROLE,
159+
)
138160
OWNER1 = 'group:cloud-logs@google.com'
139161
OWNER2 = 'user:phred@example.com'
140162
EDITOR1 = 'domain:google.com'
141163
EDITOR2 = 'user:phred@example.com'
142164
VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
143165
VIEWER2 = 'user:phred@example.com'
166+
PUBLISHER = 'user:phred@example.com'
167+
SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com'
144168
EXPECTED = {
145169
'etag': 'DEADBEEF',
146170
'version': 17,
147171
'bindings': [
148-
{'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]},
149-
{'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
150-
{'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
172+
{'role': PUBSUB_ADMIN_ROLE, 'members': [OWNER1, OWNER2]},
173+
{'role': PUBSUB_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
174+
{'role': PUBSUB_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
175+
{'role': PUBSUB_PUBLISHER_ROLE, 'members': [PUBLISHER]},
176+
{'role': PUBSUB_SUBSCRIBER_ROLE, 'members': [SUBSCRIBER]},
151177
],
152178
}
153179
policy = self._makeOne('DEADBEEF', 17)
@@ -157,4 +183,6 @@ def test_to_api_repr_full(self):
157183
policy.editors.add(EDITOR2)
158184
policy.viewers.add(VIEWER1)
159185
policy.viewers.add(VIEWER2)
186+
policy.publishers.add(PUBLISHER)
187+
policy.subscribers.add(SUBSCRIBER)
160188
self.assertEqual(policy.to_api_repr(), EXPECTED)

gcloud/pubsub/test_subscription.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -426,20 +426,30 @@ def test_modify_ack_deadline_w_alternate_client(self):
426426
(self.SUB_PATH, [ACK_ID1, ACK_ID2], self.DEADLINE))
427427

428428
def test_get_iam_policy_w_bound_client(self):
429-
from gcloud.pubsub.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE
429+
from gcloud.pubsub.iam import (
430+
PUBSUB_ADMIN_ROLE,
431+
PUBSUB_EDITOR_ROLE,
432+
PUBSUB_VIEWER_ROLE,
433+
PUBSUB_PUBLISHER_ROLE,
434+
PUBSUB_SUBSCRIBER_ROLE,
435+
)
430436
OWNER1 = 'user:phred@example.com'
431437
OWNER2 = 'group:cloud-logs@google.com'
432438
EDITOR1 = 'domain:google.com'
433439
EDITOR2 = 'user:phred@example.com'
434440
VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
435441
VIEWER2 = 'user:phred@example.com'
442+
PUBLISHER = 'user:phred@example.com'
443+
SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com'
436444
POLICY = {
437445
'etag': 'DEADBEEF',
438446
'version': 17,
439447
'bindings': [
440-
{'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]},
441-
{'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
442-
{'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
448+
{'role': PUBSUB_ADMIN_ROLE, 'members': [OWNER1, OWNER2]},
449+
{'role': PUBSUB_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
450+
{'role': PUBSUB_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
451+
{'role': PUBSUB_PUBLISHER_ROLE, 'members': [PUBLISHER]},
452+
{'role': PUBSUB_SUBSCRIBER_ROLE, 'members': [SUBSCRIBER]},
443453
],
444454
}
445455
client = _Client(project=self.PROJECT)
@@ -455,6 +465,8 @@ def test_get_iam_policy_w_bound_client(self):
455465
self.assertEqual(sorted(policy.owners), [OWNER2, OWNER1])
456466
self.assertEqual(sorted(policy.editors), [EDITOR1, EDITOR2])
457467
self.assertEqual(sorted(policy.viewers), [VIEWER1, VIEWER2])
468+
self.assertEqual(sorted(policy.publishers), [PUBLISHER])
469+
self.assertEqual(sorted(policy.subscribers), [SUBSCRIBER])
458470
self.assertEqual(api._got_iam_policy, self.SUB_PATH)
459471

460472
def test_get_iam_policy_w_alternate_client(self):
@@ -479,21 +491,31 @@ def test_get_iam_policy_w_alternate_client(self):
479491
self.assertEqual(api._got_iam_policy, self.SUB_PATH)
480492

481493
def test_set_iam_policy_w_bound_client(self):
482-
from gcloud.pubsub.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE
483494
from gcloud.pubsub.iam import Policy
495+
from gcloud.pubsub.iam import (
496+
PUBSUB_ADMIN_ROLE,
497+
PUBSUB_EDITOR_ROLE,
498+
PUBSUB_VIEWER_ROLE,
499+
PUBSUB_PUBLISHER_ROLE,
500+
PUBSUB_SUBSCRIBER_ROLE,
501+
)
484502
OWNER1 = 'group:cloud-logs@google.com'
485503
OWNER2 = 'user:phred@example.com'
486504
EDITOR1 = 'domain:google.com'
487505
EDITOR2 = 'user:phred@example.com'
488506
VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
489507
VIEWER2 = 'user:phred@example.com'
508+
PUBLISHER = 'user:phred@example.com'
509+
SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com'
490510
POLICY = {
491511
'etag': 'DEADBEEF',
492512
'version': 17,
493513
'bindings': [
494-
{'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]},
495-
{'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
496-
{'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
514+
{'role': PUBSUB_ADMIN_ROLE, 'members': [OWNER1, OWNER2]},
515+
{'role': PUBSUB_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
516+
{'role': PUBSUB_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
517+
{'role': PUBSUB_PUBLISHER_ROLE, 'members': [PUBLISHER]},
518+
{'role': PUBSUB_SUBSCRIBER_ROLE, 'members': [SUBSCRIBER]},
497519
],
498520
}
499521
RESPONSE = POLICY.copy()
@@ -511,6 +533,8 @@ def test_set_iam_policy_w_bound_client(self):
511533
policy.editors.add(EDITOR2)
512534
policy.viewers.add(VIEWER1)
513535
policy.viewers.add(VIEWER2)
536+
policy.publishers.add(PUBLISHER)
537+
policy.subscribers.add(SUBSCRIBER)
514538

515539
new_policy = subscription.set_iam_policy(policy)
516540

@@ -519,6 +543,8 @@ def test_set_iam_policy_w_bound_client(self):
519543
self.assertEqual(sorted(new_policy.owners), [OWNER1, OWNER2])
520544
self.assertEqual(sorted(new_policy.editors), [EDITOR1, EDITOR2])
521545
self.assertEqual(sorted(new_policy.viewers), [VIEWER1, VIEWER2])
546+
self.assertEqual(sorted(new_policy.publishers), [PUBLISHER])
547+
self.assertEqual(sorted(new_policy.subscribers), [SUBSCRIBER])
522548
self.assertEqual(api._set_iam_policy, (self.SUB_PATH, POLICY))
523549

524550
def test_set_iam_policy_w_alternate_client(self):

0 commit comments

Comments
 (0)