Skip to content

Commit dbb22a7

Browse files
committed
Merge pull request #1646 from tseaver/pubsub-subscription-test_iam_permissions
Add 'Subscription.test_iam_permissions' API wrapper.
2 parents efc1d41 + 537424e commit dbb22a7

File tree

7 files changed

+142
-40
lines changed

7 files changed

+142
-40
lines changed

docs/pubsub-iam.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ IAM Policy
33

44
.. automodule:: gcloud.pubsub.iam
55
:members:
6+
:member-order: bysource
67
:undoc-members:
78
:show-inheritance:
89

docs/pubsub-usage.rst

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,13 @@ Test permissions allowed by the current IAM policy on a topic:
9999
.. doctest::
100100

101101
>>> from gcloud import pubsub
102+
>>> from gcloud.pubsub.iam import OWNER_ROLE, WRITER_ROLE, READER_ROLE
102103
>>> client = pubsub.Client()
103104
>>> topic = client.topic('topic_name')
104-
>>> topic.test_iam_permissions(
105-
... ['roles/reader', 'roles/writer', 'roles/owner']) # API request
106-
['roles/reader', 'roles/writer']
105+
>>> allowed = topic.test_iam_permissions(
106+
... [READER_ROLE, WRITER_ROLE, OWNER_ROLE]) # API request
107+
>>> allowed == [READER_ROLE, WRITER_ROLE]
108+
True
107109

108110

109111
Publish messages to a topic
@@ -341,3 +343,17 @@ Update the IAM policy for a subscription:
341343
>>> policy = subscription.get_iam_policy() # API request
342344
>>> policy.writers.add(policy.group('[email protected]'))
343345
>>> subscription.set_iam_policy(policy) # API request
346+
347+
Test permissions allowed by the current IAM policy on a subscription:
348+
349+
.. doctest::
350+
351+
>>> from gcloud import pubsub
352+
>>> from gcloud.pubsub.iam import OWNER_ROLE, WRITER_ROLE, READER_ROLE
353+
>>> client = pubsub.Client()
354+
>>> topic = client.topic('topic_name')
355+
>>> subscription = topic.subscription('subscription_name')
356+
>>> allowed = subscription.test_iam_permissions(
357+
... [READER_ROLE, WRITER_ROLE, OWNER_ROLE]) # API request
358+
>>> allowed == [READER_ROLE, WRITER_ROLE]
359+
True

gcloud/pubsub/iam.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@
1313
# limitations under the License.
1414
"""PubSub API IAM policy definitions"""
1515

16-
_OWNER_ROLE = 'roles/owner'
17-
_WRITER_ROLE = 'roles/writer'
18-
_READER_ROLE = 'roles/reader'
16+
OWNER_ROLE = 'roles/owner'
17+
"""IAM permission implying all rights to an object."""
18+
19+
WRITER_ROLE = 'roles/writer'
20+
"""IAM permission implying rights to modify an object."""
21+
22+
READER_ROLE = 'roles/reader'
23+
"""IAM permission implying rights to access an object without modifying it."""
1924

2025

2126
class Policy(object):
@@ -120,11 +125,11 @@ def from_api_repr(cls, resource):
120125
for binding in resource.get('bindings', ()):
121126
role = binding['role']
122127
members = set(binding['members'])
123-
if role == _OWNER_ROLE:
128+
if role == OWNER_ROLE:
124129
policy.owners = members
125-
elif role == _WRITER_ROLE:
130+
elif role == WRITER_ROLE:
126131
policy.writers = members
127-
elif role == _READER_ROLE:
132+
elif role == READER_ROLE:
128133
policy.readers = members
129134
else:
130135
raise ValueError('Unknown role: %s' % (role,))
@@ -148,15 +153,15 @@ def to_api_repr(self):
148153

149154
if self.owners:
150155
bindings.append(
151-
{'role': _OWNER_ROLE, 'members': sorted(self.owners)})
156+
{'role': OWNER_ROLE, 'members': sorted(self.owners)})
152157

153158
if self.writers:
154159
bindings.append(
155-
{'role': _WRITER_ROLE, 'members': sorted(self.writers)})
160+
{'role': WRITER_ROLE, 'members': sorted(self.writers)})
156161

157162
if self.readers:
158163
bindings.append(
159-
{'role': _READER_ROLE, 'members': sorted(self.readers)})
164+
{'role': READER_ROLE, 'members': sorted(self.readers)})
160165

161166
if bindings:
162167
resource['bindings'] = bindings

gcloud/pubsub/subscription.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,28 @@ def set_iam_policy(self, policy, client=None):
308308
resp = client.connection.api_request(
309309
method='POST', path=path, data=resource)
310310
return Policy.from_api_repr(resp)
311+
312+
def test_iam_permissions(self, permissions, client=None):
313+
"""Permissions allowed for the current user by the effective IAM policy.
314+
315+
See:
316+
https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/testIamPermissions
317+
318+
:type permissions: list of string
319+
:param permissions: list of permissions to be tested
320+
321+
:type client: :class:`gcloud.pubsub.client.Client` or ``NoneType``
322+
:param client: the client to use. If not passed, falls back to the
323+
``client`` stored on the current subscription's topic.
324+
325+
:rtype: sequence of string
326+
:returns: subset of ``permissions`` allowed by current IAM policy.
327+
"""
328+
client = self._require_client(client)
329+
path = '%s:testIamPermissions' % (self.path,)
330+
data = {
331+
'permissions': list(permissions),
332+
}
333+
resp = client.connection.api_request(
334+
method='POST', path=path, data=data)
335+
return resp.get('permissions', ())

gcloud/pubsub/test_iam.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def test_from_api_repr_only_etag(self):
8787
self.assertEqual(list(policy.readers), [])
8888

8989
def test_from_api_repr_complete(self):
90-
from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE
90+
from gcloud.pubsub.iam import OWNER_ROLE, WRITER_ROLE, READER_ROLE
9191
OWNER1 = 'user:[email protected]'
9292
OWNER2 = 'group:[email protected]'
9393
WRITER1 = 'domain:google.com'
@@ -98,9 +98,9 @@ def test_from_api_repr_complete(self):
9898
'etag': 'DEADBEEF',
9999
'version': 17,
100100
'bindings': [
101-
{'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]},
102-
{'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]},
103-
{'role': _READER_ROLE, 'members': [READER1, READER2]},
101+
{'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]},
102+
{'role': WRITER_ROLE, 'members': [WRITER1, WRITER2]},
103+
{'role': READER_ROLE, 'members': [READER1, READER2]},
104104
],
105105
}
106106
klass = self._getTargetClass()
@@ -134,7 +134,7 @@ def test_to_api_repr_only_etag(self):
134134
self.assertEqual(policy.to_api_repr(), {'etag': 'DEADBEEF'})
135135

136136
def test_to_api_repr_full(self):
137-
from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE
137+
from gcloud.pubsub.iam import OWNER_ROLE, WRITER_ROLE, READER_ROLE
138138
OWNER1 = 'group:[email protected]'
139139
OWNER2 = 'user:[email protected]'
140140
WRITER1 = 'domain:google.com'
@@ -145,9 +145,9 @@ def test_to_api_repr_full(self):
145145
'etag': 'DEADBEEF',
146146
'version': 17,
147147
'bindings': [
148-
{'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]},
149-
{'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]},
150-
{'role': _READER_ROLE, 'members': [READER1, READER2]},
148+
{'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]},
149+
{'role': WRITER_ROLE, 'members': [WRITER1, WRITER2]},
150+
{'role': READER_ROLE, 'members': [READER1, READER2]},
151151
],
152152
}
153153
policy = self._makeOne('DEADBEEF', 17)

gcloud/pubsub/test_subscription.py

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,7 @@ def test_delete_w_alternate_client(self):
485485
self.assertEqual(req['path'], '/%s' % SUB_PATH)
486486

487487
def test_get_iam_policy_w_bound_client(self):
488-
from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE
488+
from gcloud.pubsub.iam import OWNER_ROLE, WRITER_ROLE, READER_ROLE
489489
OWNER1 = 'user:[email protected]'
490490
OWNER2 = 'group:[email protected]'
491491
WRITER1 = 'domain:google.com'
@@ -496,9 +496,9 @@ def test_get_iam_policy_w_bound_client(self):
496496
'etag': 'DEADBEEF',
497497
'version': 17,
498498
'bindings': [
499-
{'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]},
500-
{'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]},
501-
{'role': _READER_ROLE, 'members': [READER1, READER2]},
499+
{'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]},
500+
{'role': WRITER_ROLE, 'members': [WRITER1, WRITER2]},
501+
{'role': READER_ROLE, 'members': [READER1, READER2]},
502502
],
503503
}
504504
PROJECT = 'PROJECT'
@@ -557,7 +557,7 @@ def test_get_iam_policy_w_alternate_client(self):
557557
self.assertEqual(req['path'], '/%s' % PATH)
558558

559559
def test_set_iam_policy_w_bound_client(self):
560-
from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE
560+
from gcloud.pubsub.iam import OWNER_ROLE, WRITER_ROLE, READER_ROLE
561561
from gcloud.pubsub.iam import Policy
562562
OWNER1 = 'group:[email protected]'
563563
OWNER2 = 'user:[email protected]'
@@ -569,9 +569,9 @@ def test_set_iam_policy_w_bound_client(self):
569569
'etag': 'DEADBEEF',
570570
'version': 17,
571571
'bindings': [
572-
{'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]},
573-
{'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]},
574-
{'role': _READER_ROLE, 'members': [READER1, READER2]},
572+
{'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]},
573+
{'role': WRITER_ROLE, 'members': [WRITER1, WRITER2]},
574+
{'role': READER_ROLE, 'members': [READER1, READER2]},
575575
],
576576
}
577577
RESPONSE = POLICY.copy()
@@ -641,6 +641,61 @@ def test_set_iam_policy_w_alternate_client(self):
641641
self.assertEqual(req['path'], '/%s' % PATH)
642642
self.assertEqual(req['data'], {})
643643

644+
def test_test_iam_permissions_w_bound_client(self):
645+
PROJECT = 'PROJECT'
646+
TOPIC_NAME = 'topic_name'
647+
SUB_NAME = 'sub_name'
648+
PATH = 'projects/%s/subscriptions/%s:testIamPermissions' % (
649+
PROJECT, SUB_NAME)
650+
ROLES = ['roles/reader', 'roles/writer', 'roles/owner']
651+
REQUESTED = {
652+
'permissions': ROLES,
653+
}
654+
RESPONSE = {
655+
'permissions': ROLES[:-1],
656+
}
657+
conn = _Connection(RESPONSE)
658+
CLIENT = _Client(project=PROJECT, connection=conn)
659+
topic = _Topic(TOPIC_NAME, client=CLIENT)
660+
subscription = self._makeOne(SUB_NAME, topic)
661+
662+
allowed = subscription.test_iam_permissions(ROLES)
663+
664+
self.assertEqual(allowed, ROLES[:-1])
665+
self.assertEqual(len(conn._requested), 1)
666+
req = conn._requested[0]
667+
self.assertEqual(req['method'], 'POST')
668+
self.assertEqual(req['path'], '/%s' % PATH)
669+
self.assertEqual(req['data'], REQUESTED)
670+
671+
def test_test_iam_permissions_w_alternate_client(self):
672+
PROJECT = 'PROJECT'
673+
TOPIC_NAME = 'topic_name'
674+
SUB_NAME = 'sub_name'
675+
PATH = 'projects/%s/subscriptions/%s:testIamPermissions' % (
676+
PROJECT, SUB_NAME)
677+
ROLES = ['roles/reader', 'roles/writer', 'roles/owner']
678+
REQUESTED = {
679+
'permissions': ROLES,
680+
}
681+
RESPONSE = {}
682+
conn1 = _Connection()
683+
CLIENT1 = _Client(project=PROJECT, connection=conn1)
684+
conn2 = _Connection(RESPONSE)
685+
CLIENT2 = _Client(project=PROJECT, connection=conn2)
686+
topic = _Topic(TOPIC_NAME, client=CLIENT1)
687+
subscription = self._makeOne(SUB_NAME, topic)
688+
689+
allowed = subscription.test_iam_permissions(ROLES, client=CLIENT2)
690+
691+
self.assertEqual(len(allowed), 0)
692+
self.assertEqual(len(conn1._requested), 0)
693+
self.assertEqual(len(conn2._requested), 1)
694+
req = conn2._requested[0]
695+
self.assertEqual(req['method'], 'POST')
696+
self.assertEqual(req['path'], '/%s' % PATH)
697+
self.assertEqual(req['data'], REQUESTED)
698+
644699

645700
class _Connection(object):
646701

gcloud/pubsub/test_topic.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ def test_list_subscriptions_missing_key(self):
453453
self.assertEqual(req['query_params'], {})
454454

455455
def test_get_iam_policy_w_bound_client(self):
456-
from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE
456+
from gcloud.pubsub.iam import OWNER_ROLE, WRITER_ROLE, READER_ROLE
457457
OWNER1 = 'user:[email protected]'
458458
OWNER2 = 'group:[email protected]'
459459
WRITER1 = 'domain:google.com'
@@ -464,9 +464,9 @@ def test_get_iam_policy_w_bound_client(self):
464464
'etag': 'DEADBEEF',
465465
'version': 17,
466466
'bindings': [
467-
{'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]},
468-
{'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]},
469-
{'role': _READER_ROLE, 'members': [READER1, READER2]},
467+
{'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]},
468+
{'role': WRITER_ROLE, 'members': [WRITER1, WRITER2]},
469+
{'role': READER_ROLE, 'members': [READER1, READER2]},
470470
],
471471
}
472472
TOPIC_NAME = 'topic_name'
@@ -522,7 +522,7 @@ def test_get_iam_policy_w_alternate_client(self):
522522

523523
def test_set_iam_policy_w_bound_client(self):
524524
from gcloud.pubsub.iam import Policy
525-
from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE
525+
from gcloud.pubsub.iam import OWNER_ROLE, WRITER_ROLE, READER_ROLE
526526
OWNER1 = 'group:[email protected]'
527527
OWNER2 = 'user:[email protected]'
528528
WRITER1 = 'domain:google.com'
@@ -533,9 +533,9 @@ def test_set_iam_policy_w_bound_client(self):
533533
'etag': 'DEADBEEF',
534534
'version': 17,
535535
'bindings': [
536-
{'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]},
537-
{'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]},
538-
{'role': _READER_ROLE, 'members': [READER1, READER2]},
536+
{'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]},
537+
{'role': WRITER_ROLE, 'members': [WRITER1, WRITER2]},
538+
{'role': READER_ROLE, 'members': [READER1, READER2]},
539539
],
540540
}
541541
RESPONSE = POLICY.copy()
@@ -602,12 +602,12 @@ def test_set_iam_policy_w_alternate_client(self):
602602
self.assertEqual(req['data'], {})
603603

604604
def test_test_iam_permissions_w_bound_client(self):
605-
from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE
605+
from gcloud.pubsub.iam import OWNER_ROLE, WRITER_ROLE, READER_ROLE
606606
TOPIC_NAME = 'topic_name'
607607
PROJECT = 'PROJECT'
608608
PATH = 'projects/%s/topics/%s:testIamPermissions' % (
609609
PROJECT, TOPIC_NAME)
610-
ROLES = [_READER_ROLE, _WRITER_ROLE, _OWNER_ROLE]
610+
ROLES = [READER_ROLE, WRITER_ROLE, OWNER_ROLE]
611611
REQUESTED = {
612612
'permissions': ROLES,
613613
}
@@ -628,12 +628,12 @@ def test_test_iam_permissions_w_bound_client(self):
628628
self.assertEqual(req['data'], REQUESTED)
629629

630630
def test_test_iam_permissions_w_alternate_client(self):
631-
from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE
631+
from gcloud.pubsub.iam import OWNER_ROLE, WRITER_ROLE, READER_ROLE
632632
TOPIC_NAME = 'topic_name'
633633
PROJECT = 'PROJECT'
634634
PATH = 'projects/%s/topics/%s:testIamPermissions' % (
635635
PROJECT, TOPIC_NAME)
636-
ROLES = [_READER_ROLE, _WRITER_ROLE, _OWNER_ROLE]
636+
ROLES = [READER_ROLE, WRITER_ROLE, OWNER_ROLE]
637637
REQUESTED = {
638638
'permissions': ROLES,
639639
}

0 commit comments

Comments
 (0)