Skip to content

Commit

Permalink
Merge pull request #1640 from tseaver/pubsub-topic-get_iam_policy
Browse files Browse the repository at this point in the history
Add 'Topic.get_iam_policy' API wrapper.
  • Loading branch information
tseaver committed Mar 22, 2016
2 parents 6b46045 + ebf5051 commit b162a81
Show file tree
Hide file tree
Showing 7 changed files with 437 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
pubsub-topic
pubsub-subscription
pubsub-message
pubsub-iam

.. toctree::
:maxdepth: 0
Expand Down
8 changes: 8 additions & 0 deletions docs/pubsub-iam.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
IAM Policy
~~~~~~~~~~

.. automodule:: gcloud.pubsub.iam
:members:
:undoc-members:
:show-inheritance:

17 changes: 17 additions & 0 deletions docs/pubsub-usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,23 @@ Delete a topic:
>>> topic = client.topic('topic_name')
>>> topic.delete() # API request

Fetch the IAM policy for a topic:

.. doctest::

>>> from gcloud import pubsub
>>> client = pubsub.Client()
>>> topic = client.topic('topic_name')
>>> policy = topic.get_iam_policy() # API request
>>> policy.etag
'DEADBEEF'
>>> policy.owners
['user:phred@example.com']
>>> policy.writers
['systemAccount:abc-1234@systemaccounts.example.com']
>>> policy.readers
['domain:example.com']


Publish messages to a topic
---------------------------
Expand Down
164 changes: 164 additions & 0 deletions gcloud/pubsub/iam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""PubSub API IAM policy definitions"""

_OWNER_ROLE = 'roles/owner'
_WRITER_ROLE = 'roles/writer'
_READER_ROLE = 'roles/reader'


class Policy(object):
"""Combined IAM Policy / Bindings.
See:
https://cloud.google.com/pubsub/reference/rest/Shared.Types/Policy
https://cloud.google.com/pubsub/reference/rest/Shared.Types/Binding
:type etag: string
:param etag: ETag used to identify a unique of the policy
:type version: int
:param version: unique version of the policy
"""
def __init__(self, etag=None, version=None):
self.etag = etag
self.version = version
self.owners = set()
self.writers = set()
self.readers = set()

@staticmethod
def user(email):
"""Factory method for a user member.
:type email: string
:param email: E-mail for this particular user.
:rtype: string
:returns: A member string corresponding to the given user.
"""
return 'user:%s' % (email,)

@staticmethod
def service_account(email):
"""Factory method for a service account member.
:type email: string
:param email: E-mail for this particular service account.
:rtype: string
:returns: A member string corresponding to the given service account.
"""
return 'serviceAccount:%s' % (email,)

@staticmethod
def group(email):
"""Factory method for a group member.
:type email: string
:param email: An id or e-mail for this particular group.
:rtype: string
:returns: A member string corresponding to the given group.
"""
return 'group:%s' % (email,)

@staticmethod
def domain(domain):
"""Factory method for a domain member.
:type domain: string
:param domain: The domain for this member.
:rtype: string
:returns: A member string corresponding to the given domain.
"""
return 'domain:%s' % (domain,)

@staticmethod
def all_users():
"""Factory method for a member representing all users.
:rtype: string
:returns: A member string representing all users.
"""
return 'allUsers'

@staticmethod
def authenticated_users():
"""Factory method for a member representing all authenticated users.
:rtype: string
:returns: A member string representing all authenticated users.
"""
return 'allAuthenticatedUsers'

@classmethod
def from_api_repr(cls, resource):
"""Create a policy from the resource returned from the API.
:type resource: dict
:param resource: resource returned from the ``getIamPolicy`` API.
:rtype: :class:`Policy`
:returns: the parsed policy
"""
version = resource.get('version')
etag = resource.get('etag')
policy = cls(etag, version)
for binding in resource.get('bindings', ()):
role = binding['role']
members = set(binding['members'])
if role == _OWNER_ROLE:
policy.owners = members
elif role == _WRITER_ROLE:
policy.writers = members
elif role == _READER_ROLE:
policy.readers = members
else:
raise ValueError('Unknown role: %s' % (role,))
return policy

def to_api_repr(self):
"""Construct a Policy resource.
:rtype: dict
:returns: a resource to be passed to the ``setIamPolicy`` API.
"""
resource = {}

if self.etag is not None:
resource['etag'] = self.etag

if self.version is not None:
resource['version'] = self.version

bindings = []

if self.owners:
bindings.append(
{'role': _OWNER_ROLE, 'members': sorted(self.owners)})

if self.writers:
bindings.append(
{'role': _WRITER_ROLE, 'members': sorted(self.writers)})

if self.readers:
bindings.append(
{'role': _READER_ROLE, 'members': sorted(self.readers)})

if bindings:
resource['bindings'] = bindings

return resource
160 changes: 160 additions & 0 deletions gcloud/pubsub/test_iam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import unittest2


class TestPolicy(unittest2.TestCase):

def _getTargetClass(self):
from gcloud.pubsub.iam import Policy
return Policy

def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)

def test_ctor_defaults(self):
policy = self._makeOne()
self.assertEqual(policy.etag, None)
self.assertEqual(policy.version, None)
self.assertEqual(list(policy.owners), [])
self.assertEqual(list(policy.writers), [])
self.assertEqual(list(policy.readers), [])

def test_ctor_explicit(self):
VERSION = 17
ETAG = 'ETAG'
policy = self._makeOne(ETAG, VERSION)
self.assertEqual(policy.etag, ETAG)
self.assertEqual(policy.version, VERSION)
self.assertEqual(list(policy.owners), [])
self.assertEqual(list(policy.writers), [])
self.assertEqual(list(policy.readers), [])

def test_user(self):
EMAIL = 'phred@example.com'
MEMBER = 'user:%s' % (EMAIL,)
policy = self._makeOne()
self.assertEqual(policy.user(EMAIL), MEMBER)

def test_service_account(self):
EMAIL = 'phred@example.com'
MEMBER = 'serviceAccount:%s' % (EMAIL,)
policy = self._makeOne()
self.assertEqual(policy.service_account(EMAIL), MEMBER)

def test_group(self):
EMAIL = 'phred@example.com'
MEMBER = 'group:%s' % (EMAIL,)
policy = self._makeOne()
self.assertEqual(policy.group(EMAIL), MEMBER)

def test_domain(self):
DOMAIN = 'example.com'
MEMBER = 'domain:%s' % (DOMAIN,)
policy = self._makeOne()
self.assertEqual(policy.domain(DOMAIN), MEMBER)

def test_all_users(self):
policy = self._makeOne()
self.assertEqual(policy.all_users(), 'allUsers')

def test_authenticated_users(self):
policy = self._makeOne()
self.assertEqual(policy.authenticated_users(), 'allAuthenticatedUsers')

def test_from_api_repr_only_etag(self):
RESOURCE = {
'etag': 'ACAB',
}
klass = self._getTargetClass()
policy = klass.from_api_repr(RESOURCE)
self.assertEqual(policy.etag, 'ACAB')
self.assertEqual(policy.version, None)
self.assertEqual(list(policy.owners), [])
self.assertEqual(list(policy.writers), [])
self.assertEqual(list(policy.readers), [])

def test_from_api_repr_complete(self):
from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE
OWNER1 = 'user:phred@example.com'
OWNER2 = 'group:cloud-logs@google.com'
WRITER1 = 'domain:google.com'
WRITER2 = 'user:phred@example.com'
READER1 = 'serviceAccount:1234-abcdef@service.example.com'
READER2 = 'user:phred@example.com'
RESOURCE = {
'etag': 'DEADBEEF',
'version': 17,
'bindings': [
{'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]},
{'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]},
{'role': _READER_ROLE, 'members': [READER1, READER2]},
],
}
klass = self._getTargetClass()
policy = klass.from_api_repr(RESOURCE)
self.assertEqual(policy.etag, 'DEADBEEF')
self.assertEqual(policy.version, 17)
self.assertEqual(sorted(policy.owners), [OWNER2, OWNER1])
self.assertEqual(sorted(policy.writers), [WRITER1, WRITER2])
self.assertEqual(sorted(policy.readers), [READER1, READER2])

def test_from_api_repr_bad_role(self):
BOGUS1 = 'user:phred@example.com'
BOGUS2 = 'group:cloud-logs@google.com'
RESOURCE = {
'etag': 'DEADBEEF',
'version': 17,
'bindings': [
{'role': 'nonesuch', 'members': [BOGUS1, BOGUS2]},
],
}
klass = self._getTargetClass()
with self.assertRaises(ValueError):
klass.from_api_repr(RESOURCE)

def test_to_api_repr_defaults(self):
policy = self._makeOne()
self.assertEqual(policy.to_api_repr(), {})

def test_to_api_repr_only_etag(self):
policy = self._makeOne('DEADBEEF')
self.assertEqual(policy.to_api_repr(), {'etag': 'DEADBEEF'})

def test_to_api_repr_full(self):
from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE
OWNER1 = 'group:cloud-logs@google.com'
OWNER2 = 'user:phred@example.com'
WRITER1 = 'domain:google.com'
WRITER2 = 'user:phred@example.com'
READER1 = 'serviceAccount:1234-abcdef@service.example.com'
READER2 = 'user:phred@example.com'
EXPECTED = {
'etag': 'DEADBEEF',
'version': 17,
'bindings': [
{'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]},
{'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]},
{'role': _READER_ROLE, 'members': [READER1, READER2]},
],
}
policy = self._makeOne('DEADBEEF', 17)
policy.owners.add(OWNER1)
policy.owners.add(OWNER2)
policy.writers.add(WRITER1)
policy.writers.add(WRITER2)
policy.readers.add(READER1)
policy.readers.add(READER2)
self.assertEqual(policy.to_api_repr(), EXPECTED)
Loading

0 comments on commit b162a81

Please sign in to comment.