Skip to content

Commit b162a81

Browse files
committed
Merge pull request #1640 from tseaver/pubsub-topic-get_iam_policy
Add 'Topic.get_iam_policy' API wrapper.
2 parents 6b46045 + ebf5051 commit b162a81

File tree

7 files changed

+437
-0
lines changed

7 files changed

+437
-0
lines changed

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
pubsub-topic
4141
pubsub-subscription
4242
pubsub-message
43+
pubsub-iam
4344

4445
.. toctree::
4546
:maxdepth: 0

docs/pubsub-iam.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
IAM Policy
2+
~~~~~~~~~~
3+
4+
.. automodule:: gcloud.pubsub.iam
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:
8+

docs/pubsub-usage.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,23 @@ Delete a topic:
6666
>>> topic = client.topic('topic_name')
6767
>>> topic.delete() # API request
6868

69+
Fetch the IAM policy for a topic:
70+
71+
.. doctest::
72+
73+
>>> from gcloud import pubsub
74+
>>> client = pubsub.Client()
75+
>>> topic = client.topic('topic_name')
76+
>>> policy = topic.get_iam_policy() # API request
77+
>>> policy.etag
78+
'DEADBEEF'
79+
>>> policy.owners
80+
81+
>>> policy.writers
82+
['systemAccount:[email protected]']
83+
>>> policy.readers
84+
['domain:example.com']
85+
6986

7087
Publish messages to a topic
7188
---------------------------

gcloud/pubsub/iam.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# Copyright 2016 Google Inc. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""PubSub API IAM policy definitions"""
15+
16+
_OWNER_ROLE = 'roles/owner'
17+
_WRITER_ROLE = 'roles/writer'
18+
_READER_ROLE = 'roles/reader'
19+
20+
21+
class Policy(object):
22+
"""Combined IAM Policy / Bindings.
23+
24+
See:
25+
https://cloud.google.com/pubsub/reference/rest/Shared.Types/Policy
26+
https://cloud.google.com/pubsub/reference/rest/Shared.Types/Binding
27+
28+
:type etag: string
29+
:param etag: ETag used to identify a unique of the policy
30+
31+
:type version: int
32+
:param version: unique version of the policy
33+
"""
34+
def __init__(self, etag=None, version=None):
35+
self.etag = etag
36+
self.version = version
37+
self.owners = set()
38+
self.writers = set()
39+
self.readers = set()
40+
41+
@staticmethod
42+
def user(email):
43+
"""Factory method for a user member.
44+
45+
:type email: string
46+
:param email: E-mail for this particular user.
47+
48+
:rtype: string
49+
:returns: A member string corresponding to the given user.
50+
"""
51+
return 'user:%s' % (email,)
52+
53+
@staticmethod
54+
def service_account(email):
55+
"""Factory method for a service account member.
56+
57+
:type email: string
58+
:param email: E-mail for this particular service account.
59+
60+
:rtype: string
61+
:returns: A member string corresponding to the given service account.
62+
"""
63+
return 'serviceAccount:%s' % (email,)
64+
65+
@staticmethod
66+
def group(email):
67+
"""Factory method for a group member.
68+
69+
:type email: string
70+
:param email: An id or e-mail for this particular group.
71+
72+
:rtype: string
73+
:returns: A member string corresponding to the given group.
74+
"""
75+
return 'group:%s' % (email,)
76+
77+
@staticmethod
78+
def domain(domain):
79+
"""Factory method for a domain member.
80+
81+
:type domain: string
82+
:param domain: The domain for this member.
83+
84+
:rtype: string
85+
:returns: A member string corresponding to the given domain.
86+
"""
87+
return 'domain:%s' % (domain,)
88+
89+
@staticmethod
90+
def all_users():
91+
"""Factory method for a member representing all users.
92+
93+
:rtype: string
94+
:returns: A member string representing all users.
95+
"""
96+
return 'allUsers'
97+
98+
@staticmethod
99+
def authenticated_users():
100+
"""Factory method for a member representing all authenticated users.
101+
102+
:rtype: string
103+
:returns: A member string representing all authenticated users.
104+
"""
105+
return 'allAuthenticatedUsers'
106+
107+
@classmethod
108+
def from_api_repr(cls, resource):
109+
"""Create a policy from the resource returned from the API.
110+
111+
:type resource: dict
112+
:param resource: resource returned from the ``getIamPolicy`` API.
113+
114+
:rtype: :class:`Policy`
115+
:returns: the parsed policy
116+
"""
117+
version = resource.get('version')
118+
etag = resource.get('etag')
119+
policy = cls(etag, version)
120+
for binding in resource.get('bindings', ()):
121+
role = binding['role']
122+
members = set(binding['members'])
123+
if role == _OWNER_ROLE:
124+
policy.owners = members
125+
elif role == _WRITER_ROLE:
126+
policy.writers = members
127+
elif role == _READER_ROLE:
128+
policy.readers = members
129+
else:
130+
raise ValueError('Unknown role: %s' % (role,))
131+
return policy
132+
133+
def to_api_repr(self):
134+
"""Construct a Policy resource.
135+
136+
:rtype: dict
137+
:returns: a resource to be passed to the ``setIamPolicy`` API.
138+
"""
139+
resource = {}
140+
141+
if self.etag is not None:
142+
resource['etag'] = self.etag
143+
144+
if self.version is not None:
145+
resource['version'] = self.version
146+
147+
bindings = []
148+
149+
if self.owners:
150+
bindings.append(
151+
{'role': _OWNER_ROLE, 'members': sorted(self.owners)})
152+
153+
if self.writers:
154+
bindings.append(
155+
{'role': _WRITER_ROLE, 'members': sorted(self.writers)})
156+
157+
if self.readers:
158+
bindings.append(
159+
{'role': _READER_ROLE, 'members': sorted(self.readers)})
160+
161+
if bindings:
162+
resource['bindings'] = bindings
163+
164+
return resource

gcloud/pubsub/test_iam.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Copyright 2016 Google Inc. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import unittest2
16+
17+
18+
class TestPolicy(unittest2.TestCase):
19+
20+
def _getTargetClass(self):
21+
from gcloud.pubsub.iam import Policy
22+
return Policy
23+
24+
def _makeOne(self, *args, **kw):
25+
return self._getTargetClass()(*args, **kw)
26+
27+
def test_ctor_defaults(self):
28+
policy = self._makeOne()
29+
self.assertEqual(policy.etag, None)
30+
self.assertEqual(policy.version, None)
31+
self.assertEqual(list(policy.owners), [])
32+
self.assertEqual(list(policy.writers), [])
33+
self.assertEqual(list(policy.readers), [])
34+
35+
def test_ctor_explicit(self):
36+
VERSION = 17
37+
ETAG = 'ETAG'
38+
policy = self._makeOne(ETAG, VERSION)
39+
self.assertEqual(policy.etag, ETAG)
40+
self.assertEqual(policy.version, VERSION)
41+
self.assertEqual(list(policy.owners), [])
42+
self.assertEqual(list(policy.writers), [])
43+
self.assertEqual(list(policy.readers), [])
44+
45+
def test_user(self):
46+
47+
MEMBER = 'user:%s' % (EMAIL,)
48+
policy = self._makeOne()
49+
self.assertEqual(policy.user(EMAIL), MEMBER)
50+
51+
def test_service_account(self):
52+
53+
MEMBER = 'serviceAccount:%s' % (EMAIL,)
54+
policy = self._makeOne()
55+
self.assertEqual(policy.service_account(EMAIL), MEMBER)
56+
57+
def test_group(self):
58+
59+
MEMBER = 'group:%s' % (EMAIL,)
60+
policy = self._makeOne()
61+
self.assertEqual(policy.group(EMAIL), MEMBER)
62+
63+
def test_domain(self):
64+
DOMAIN = 'example.com'
65+
MEMBER = 'domain:%s' % (DOMAIN,)
66+
policy = self._makeOne()
67+
self.assertEqual(policy.domain(DOMAIN), MEMBER)
68+
69+
def test_all_users(self):
70+
policy = self._makeOne()
71+
self.assertEqual(policy.all_users(), 'allUsers')
72+
73+
def test_authenticated_users(self):
74+
policy = self._makeOne()
75+
self.assertEqual(policy.authenticated_users(), 'allAuthenticatedUsers')
76+
77+
def test_from_api_repr_only_etag(self):
78+
RESOURCE = {
79+
'etag': 'ACAB',
80+
}
81+
klass = self._getTargetClass()
82+
policy = klass.from_api_repr(RESOURCE)
83+
self.assertEqual(policy.etag, 'ACAB')
84+
self.assertEqual(policy.version, None)
85+
self.assertEqual(list(policy.owners), [])
86+
self.assertEqual(list(policy.writers), [])
87+
self.assertEqual(list(policy.readers), [])
88+
89+
def test_from_api_repr_complete(self):
90+
from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE
91+
OWNER1 = 'user:[email protected]'
92+
OWNER2 = 'group:[email protected]'
93+
WRITER1 = 'domain:google.com'
94+
WRITER2 = 'user:[email protected]'
95+
READER1 = 'serviceAccount:[email protected]'
96+
READER2 = 'user:[email protected]'
97+
RESOURCE = {
98+
'etag': 'DEADBEEF',
99+
'version': 17,
100+
'bindings': [
101+
{'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]},
102+
{'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]},
103+
{'role': _READER_ROLE, 'members': [READER1, READER2]},
104+
],
105+
}
106+
klass = self._getTargetClass()
107+
policy = klass.from_api_repr(RESOURCE)
108+
self.assertEqual(policy.etag, 'DEADBEEF')
109+
self.assertEqual(policy.version, 17)
110+
self.assertEqual(sorted(policy.owners), [OWNER2, OWNER1])
111+
self.assertEqual(sorted(policy.writers), [WRITER1, WRITER2])
112+
self.assertEqual(sorted(policy.readers), [READER1, READER2])
113+
114+
def test_from_api_repr_bad_role(self):
115+
BOGUS1 = 'user:[email protected]'
116+
BOGUS2 = 'group:[email protected]'
117+
RESOURCE = {
118+
'etag': 'DEADBEEF',
119+
'version': 17,
120+
'bindings': [
121+
{'role': 'nonesuch', 'members': [BOGUS1, BOGUS2]},
122+
],
123+
}
124+
klass = self._getTargetClass()
125+
with self.assertRaises(ValueError):
126+
klass.from_api_repr(RESOURCE)
127+
128+
def test_to_api_repr_defaults(self):
129+
policy = self._makeOne()
130+
self.assertEqual(policy.to_api_repr(), {})
131+
132+
def test_to_api_repr_only_etag(self):
133+
policy = self._makeOne('DEADBEEF')
134+
self.assertEqual(policy.to_api_repr(), {'etag': 'DEADBEEF'})
135+
136+
def test_to_api_repr_full(self):
137+
from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE
138+
OWNER1 = 'group:[email protected]'
139+
OWNER2 = 'user:[email protected]'
140+
WRITER1 = 'domain:google.com'
141+
WRITER2 = 'user:[email protected]'
142+
READER1 = 'serviceAccount:[email protected]'
143+
READER2 = 'user:[email protected]'
144+
EXPECTED = {
145+
'etag': 'DEADBEEF',
146+
'version': 17,
147+
'bindings': [
148+
{'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]},
149+
{'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]},
150+
{'role': _READER_ROLE, 'members': [READER1, READER2]},
151+
],
152+
}
153+
policy = self._makeOne('DEADBEEF', 17)
154+
policy.owners.add(OWNER1)
155+
policy.owners.add(OWNER2)
156+
policy.writers.add(WRITER1)
157+
policy.writers.add(WRITER2)
158+
policy.readers.add(READER1)
159+
policy.readers.add(READER2)
160+
self.assertEqual(policy.to_api_repr(), EXPECTED)

0 commit comments

Comments
 (0)