From 2010097542c6b1035575fad015069b56baba9734 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 21 Mar 2016 17:14:08 -0400 Subject: [PATCH 1/3] Add 'Subscription.test_iam_permissions' API wrapper. Closes #1073. --- docs/pubsub-usage.rst | 12 +++++++ gcloud/pubsub/subscription.py | 25 ++++++++++++++ gcloud/pubsub/test_subscription.py | 55 ++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/docs/pubsub-usage.rst b/docs/pubsub-usage.rst index d3dbba1a94ff..8100a8a2c090 100644 --- a/docs/pubsub-usage.rst +++ b/docs/pubsub-usage.rst @@ -341,3 +341,15 @@ Update the IAM policy for a subscription: >>> policy = subscription.get_iam_policy() # API request >>> policy.writers.add(policy.group('editors-list@example.com')) >>> subscription.set_iam_policy(policy) # API request + +Test permissions allowed by the current IAM policy on a subscription: + +.. doctest:: + + >>> from gcloud import pubsub + >>> client = pubsub.Client() + >>> topic = client.topic('topic_name') + >>> subscription = topic.subscription('subscription_name') + >>> subscription.test_iam_permissions( + ... ['roles/reader', 'roles/writer', 'roles/owner']) # API request + ['roles/reader', 'roles/writer'] diff --git a/gcloud/pubsub/subscription.py b/gcloud/pubsub/subscription.py index 56fb0d337f49..14d8c8019e8a 100644 --- a/gcloud/pubsub/subscription.py +++ b/gcloud/pubsub/subscription.py @@ -308,3 +308,28 @@ def set_iam_policy(self, policy, client=None): resp = client.connection.api_request( method='POST', path=path, data=resource) return Policy.from_api_repr(resp) + + def test_iam_permissions(self, permissions, client=None): + """Permissions allowed for the current user by the effective IAM policy. + + See: + https://cloud.google.com/pubsub/reference/rest/v1/projects.subscriptions/testIamPermissions + + :type permissions: list of string + :param permissions: list of permissions to be tested + + :type client: :class:`gcloud.pubsub.client.Client` or ``NoneType`` + :param client: the client to use. If not passed, falls back to the + ``client`` stored on the current subscription's topic. + + :rtype: sequence of string + :returns: subset of ``permissions`` allowed by current IAM policy. + """ + client = self._require_client(client) + path = '%s:testIamPermissions' % (self.path,) + data = { + 'permissions': list(permissions), + } + resp = client.connection.api_request( + method='POST', path=path, data=data) + return resp.get('permissions', ()) diff --git a/gcloud/pubsub/test_subscription.py b/gcloud/pubsub/test_subscription.py index 9d18d28eb3fe..3438ae68db79 100644 --- a/gcloud/pubsub/test_subscription.py +++ b/gcloud/pubsub/test_subscription.py @@ -641,6 +641,61 @@ def test_set_iam_policy_w_alternate_client(self): self.assertEqual(req['path'], '/%s' % PATH) self.assertEqual(req['data'], {}) + def test_test_iam_permissions_w_bound_client(self): + PROJECT = 'PROJECT' + TOPIC_NAME = 'topic_name' + SUB_NAME = 'sub_name' + PATH = 'projects/%s/subscriptions/%s:testIamPermissions' % ( + PROJECT, SUB_NAME) + ROLES = ['roles/reader', 'roles/writer', 'roles/owner'] + REQUESTED = { + 'permissions': ROLES, + } + RESPONSE = { + 'permissions': ROLES[:-1], + } + conn = _Connection(RESPONSE) + CLIENT = _Client(project=PROJECT, connection=conn) + topic = _Topic(TOPIC_NAME, client=CLIENT) + subscription = self._makeOne(SUB_NAME, topic) + + allowed = subscription.test_iam_permissions(ROLES) + + self.assertEqual(allowed, ROLES[:-1]) + self.assertEqual(len(conn._requested), 1) + req = conn._requested[0] + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/%s' % PATH) + self.assertEqual(req['data'], REQUESTED) + + def test_test_iam_permissions_w_alternate_client(self): + PROJECT = 'PROJECT' + TOPIC_NAME = 'topic_name' + SUB_NAME = 'sub_name' + PATH = 'projects/%s/subscriptions/%s:testIamPermissions' % ( + PROJECT, SUB_NAME) + ROLES = ['roles/reader', 'roles/writer', 'roles/owner'] + REQUESTED = { + 'permissions': ROLES, + } + RESPONSE = {} + conn1 = _Connection() + CLIENT1 = _Client(project=PROJECT, connection=conn1) + conn2 = _Connection(RESPONSE) + CLIENT2 = _Client(project=PROJECT, connection=conn2) + topic = _Topic(TOPIC_NAME, client=CLIENT1) + subscription = self._makeOne(SUB_NAME, topic) + + allowed = subscription.test_iam_permissions(ROLES, client=CLIENT2) + + self.assertEqual(len(allowed), 0) + self.assertEqual(len(conn1._requested), 0) + self.assertEqual(len(conn2._requested), 1) + req = conn2._requested[0] + self.assertEqual(req['method'], 'POST') + self.assertEqual(req['path'], '/%s' % PATH) + self.assertEqual(req['data'], REQUESTED) + class _Connection(object): From 53f5ebfdaef34c851b898ec608eebd17f0c66a03 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 22 Mar 2016 17:01:30 -0400 Subject: [PATCH 2/3] Expose role-like permission constants as public module attrs. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1646#discussion_r57066712 --- docs/pubsub-iam.rst | 1 + gcloud/pubsub/iam.py | 23 ++++++++++++++--------- gcloud/pubsub/test_iam.py | 16 ++++++++-------- gcloud/pubsub/test_subscription.py | 16 ++++++++-------- gcloud/pubsub/test_topic.py | 24 ++++++++++++------------ 5 files changed, 43 insertions(+), 37 deletions(-) diff --git a/docs/pubsub-iam.rst b/docs/pubsub-iam.rst index 701dad385ece..4fb79d216139 100644 --- a/docs/pubsub-iam.rst +++ b/docs/pubsub-iam.rst @@ -3,6 +3,7 @@ IAM Policy .. automodule:: gcloud.pubsub.iam :members: + :member-order: bysource :undoc-members: :show-inheritance: diff --git a/gcloud/pubsub/iam.py b/gcloud/pubsub/iam.py index 0b85b6b5f96c..1ead9d05ce92 100644 --- a/gcloud/pubsub/iam.py +++ b/gcloud/pubsub/iam.py @@ -13,9 +13,14 @@ # limitations under the License. """PubSub API IAM policy definitions""" -_OWNER_ROLE = 'roles/owner' -_WRITER_ROLE = 'roles/writer' -_READER_ROLE = 'roles/reader' +OWNER_ROLE = 'roles/owner' +"""IAM permission implying all rights to an object.""" + +WRITER_ROLE = 'roles/writer' +"""IAM permission implying rights to modify an object.""" + +READER_ROLE = 'roles/reader' +"""IAM permission implying rights to access an object without modifying it.""" class Policy(object): @@ -120,11 +125,11 @@ def from_api_repr(cls, resource): for binding in resource.get('bindings', ()): role = binding['role'] members = set(binding['members']) - if role == _OWNER_ROLE: + if role == OWNER_ROLE: policy.owners = members - elif role == _WRITER_ROLE: + elif role == WRITER_ROLE: policy.writers = members - elif role == _READER_ROLE: + elif role == READER_ROLE: policy.readers = members else: raise ValueError('Unknown role: %s' % (role,)) @@ -148,15 +153,15 @@ def to_api_repr(self): if self.owners: bindings.append( - {'role': _OWNER_ROLE, 'members': sorted(self.owners)}) + {'role': OWNER_ROLE, 'members': sorted(self.owners)}) if self.writers: bindings.append( - {'role': _WRITER_ROLE, 'members': sorted(self.writers)}) + {'role': WRITER_ROLE, 'members': sorted(self.writers)}) if self.readers: bindings.append( - {'role': _READER_ROLE, 'members': sorted(self.readers)}) + {'role': READER_ROLE, 'members': sorted(self.readers)}) if bindings: resource['bindings'] = bindings diff --git a/gcloud/pubsub/test_iam.py b/gcloud/pubsub/test_iam.py index 3efd6df5c49e..d6a6e165e715 100644 --- a/gcloud/pubsub/test_iam.py +++ b/gcloud/pubsub/test_iam.py @@ -87,7 +87,7 @@ def test_from_api_repr_only_etag(self): self.assertEqual(list(policy.readers), []) def test_from_api_repr_complete(self): - from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE + 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' @@ -98,9 +98,9 @@ def test_from_api_repr_complete(self): 'etag': 'DEADBEEF', 'version': 17, 'bindings': [ - {'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]}, - {'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]}, - {'role': _READER_ROLE, 'members': [READER1, READER2]}, + {'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]}, + {'role': WRITER_ROLE, 'members': [WRITER1, WRITER2]}, + {'role': READER_ROLE, 'members': [READER1, READER2]}, ], } klass = self._getTargetClass() @@ -134,7 +134,7 @@ def test_to_api_repr_only_etag(self): 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 + 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' @@ -145,9 +145,9 @@ def test_to_api_repr_full(self): 'etag': 'DEADBEEF', 'version': 17, 'bindings': [ - {'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]}, - {'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]}, - {'role': _READER_ROLE, 'members': [READER1, READER2]}, + {'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]}, + {'role': WRITER_ROLE, 'members': [WRITER1, WRITER2]}, + {'role': READER_ROLE, 'members': [READER1, READER2]}, ], } policy = self._makeOne('DEADBEEF', 17) diff --git a/gcloud/pubsub/test_subscription.py b/gcloud/pubsub/test_subscription.py index 3438ae68db79..0165814b0e13 100644 --- a/gcloud/pubsub/test_subscription.py +++ b/gcloud/pubsub/test_subscription.py @@ -485,7 +485,7 @@ def test_delete_w_alternate_client(self): self.assertEqual(req['path'], '/%s' % SUB_PATH) def test_get_iam_policy_w_bound_client(self): - from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE + 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' @@ -496,9 +496,9 @@ def test_get_iam_policy_w_bound_client(self): 'etag': 'DEADBEEF', 'version': 17, 'bindings': [ - {'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]}, - {'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]}, - {'role': _READER_ROLE, 'members': [READER1, READER2]}, + {'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]}, + {'role': WRITER_ROLE, 'members': [WRITER1, WRITER2]}, + {'role': READER_ROLE, 'members': [READER1, READER2]}, ], } PROJECT = 'PROJECT' @@ -557,7 +557,7 @@ def test_get_iam_policy_w_alternate_client(self): self.assertEqual(req['path'], '/%s' % PATH) def test_set_iam_policy_w_bound_client(self): - from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE + from gcloud.pubsub.iam import OWNER_ROLE, WRITER_ROLE, READER_ROLE from gcloud.pubsub.iam import Policy OWNER1 = 'group:cloud-logs@google.com' OWNER2 = 'user:phred@example.com' @@ -569,9 +569,9 @@ def test_set_iam_policy_w_bound_client(self): 'etag': 'DEADBEEF', 'version': 17, 'bindings': [ - {'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]}, - {'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]}, - {'role': _READER_ROLE, 'members': [READER1, READER2]}, + {'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]}, + {'role': WRITER_ROLE, 'members': [WRITER1, WRITER2]}, + {'role': READER_ROLE, 'members': [READER1, READER2]}, ], } RESPONSE = POLICY.copy() diff --git a/gcloud/pubsub/test_topic.py b/gcloud/pubsub/test_topic.py index fc9684c5f829..74f00674662d 100644 --- a/gcloud/pubsub/test_topic.py +++ b/gcloud/pubsub/test_topic.py @@ -453,7 +453,7 @@ def test_list_subscriptions_missing_key(self): self.assertEqual(req['query_params'], {}) def test_get_iam_policy_w_bound_client(self): - from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE + 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' @@ -464,9 +464,9 @@ def test_get_iam_policy_w_bound_client(self): 'etag': 'DEADBEEF', 'version': 17, 'bindings': [ - {'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]}, - {'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]}, - {'role': _READER_ROLE, 'members': [READER1, READER2]}, + {'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]}, + {'role': WRITER_ROLE, 'members': [WRITER1, WRITER2]}, + {'role': READER_ROLE, 'members': [READER1, READER2]}, ], } TOPIC_NAME = 'topic_name' @@ -522,7 +522,7 @@ def test_get_iam_policy_w_alternate_client(self): def test_set_iam_policy_w_bound_client(self): from gcloud.pubsub.iam import Policy - from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE + 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' @@ -533,9 +533,9 @@ def test_set_iam_policy_w_bound_client(self): 'etag': 'DEADBEEF', 'version': 17, 'bindings': [ - {'role': _OWNER_ROLE, 'members': [OWNER1, OWNER2]}, - {'role': _WRITER_ROLE, 'members': [WRITER1, WRITER2]}, - {'role': _READER_ROLE, 'members': [READER1, READER2]}, + {'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]}, + {'role': WRITER_ROLE, 'members': [WRITER1, WRITER2]}, + {'role': READER_ROLE, 'members': [READER1, READER2]}, ], } RESPONSE = POLICY.copy() @@ -602,12 +602,12 @@ def test_set_iam_policy_w_alternate_client(self): self.assertEqual(req['data'], {}) def test_test_iam_permissions_w_bound_client(self): - from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE + from gcloud.pubsub.iam import OWNER_ROLE, WRITER_ROLE, READER_ROLE TOPIC_NAME = 'topic_name' PROJECT = 'PROJECT' PATH = 'projects/%s/topics/%s:testIamPermissions' % ( PROJECT, TOPIC_NAME) - ROLES = [_READER_ROLE, _WRITER_ROLE, _OWNER_ROLE] + ROLES = [READER_ROLE, WRITER_ROLE, OWNER_ROLE] REQUESTED = { 'permissions': ROLES, } @@ -628,12 +628,12 @@ def test_test_iam_permissions_w_bound_client(self): self.assertEqual(req['data'], REQUESTED) def test_test_iam_permissions_w_alternate_client(self): - from gcloud.pubsub.iam import _OWNER_ROLE, _WRITER_ROLE, _READER_ROLE + from gcloud.pubsub.iam import OWNER_ROLE, WRITER_ROLE, READER_ROLE TOPIC_NAME = 'topic_name' PROJECT = 'PROJECT' PATH = 'projects/%s/topics/%s:testIamPermissions' % ( PROJECT, TOPIC_NAME) - ROLES = [_READER_ROLE, _WRITER_ROLE, _OWNER_ROLE] + ROLES = [READER_ROLE, WRITER_ROLE, OWNER_ROLE] REQUESTED = { 'permissions': ROLES, } From 537424e2e7a6b7026c0cb8aa00fa404223c3351e Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 22 Mar 2016 20:19:44 -0400 Subject: [PATCH 3/3] Use permission constants in usage docs. Addresses: https://github.com/GoogleCloudPlatform/gcloud-python/pull/1646#discussion_r57086780 --- docs/pubsub-usage.rst | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/pubsub-usage.rst b/docs/pubsub-usage.rst index 8100a8a2c090..075dcc611b18 100644 --- a/docs/pubsub-usage.rst +++ b/docs/pubsub-usage.rst @@ -99,11 +99,13 @@ Test permissions allowed by the current IAM policy on a topic: .. doctest:: >>> from gcloud import pubsub + >>> from gcloud.pubsub.iam import OWNER_ROLE, WRITER_ROLE, READER_ROLE >>> client = pubsub.Client() >>> topic = client.topic('topic_name') - >>> topic.test_iam_permissions( - ... ['roles/reader', 'roles/writer', 'roles/owner']) # API request - ['roles/reader', 'roles/writer'] + >>> allowed = topic.test_iam_permissions( + ... [READER_ROLE, WRITER_ROLE, OWNER_ROLE]) # API request + >>> allowed == [READER_ROLE, WRITER_ROLE] + True Publish messages to a topic @@ -347,9 +349,11 @@ Test permissions allowed by the current IAM policy on a subscription: .. doctest:: >>> from gcloud import pubsub + >>> from gcloud.pubsub.iam import OWNER_ROLE, WRITER_ROLE, READER_ROLE >>> client = pubsub.Client() >>> topic = client.topic('topic_name') >>> subscription = topic.subscription('subscription_name') - >>> subscription.test_iam_permissions( - ... ['roles/reader', 'roles/writer', 'roles/owner']) # API request - ['roles/reader', 'roles/writer'] + >>> allowed = subscription.test_iam_permissions( + ... [READER_ROLE, WRITER_ROLE, OWNER_ROLE]) # API request + >>> allowed == [READER_ROLE, WRITER_ROLE] + True