From cbd59157d6a740940d05a74ba53a0af8393b0e64 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 5 May 2016 17:23:54 -0400 Subject: [PATCH] Enumerate all roles / permissions for Pubsub IAM. Capture pubsub-specific 'publisher' and 'subscriber' roles. See: https://cloud.google.com/pubsub/access_control#permissions --- gcloud/pubsub/iam.py | 116 +++++++++++++++++++++++++---- gcloud/pubsub/test_iam.py | 38 ++++++++-- gcloud/pubsub/test_subscription.py | 42 +++++++++-- gcloud/pubsub/test_topic.py | 47 ++++++++++-- 4 files changed, 209 insertions(+), 34 deletions(-) diff --git a/gcloud/pubsub/iam.py b/gcloud/pubsub/iam.py index 5c9b2eeac603..72fb07957cb8 100644 --- a/gcloud/pubsub/iam.py +++ b/gcloud/pubsub/iam.py @@ -11,16 +11,87 @@ # 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""" +"""PubSub API IAM policy definitions + +For allowed roles / permissions, see: +https://cloud.google.com/pubsub/access_control#permissions +""" + +# Generic IAM roles OWNER_ROLE = 'roles/owner' -"""IAM permission implying all rights to an object.""" +"""Generic role implying all rights to an object.""" EDITOR_ROLE = 'roles/editor' -"""IAM permission implying rights to modify an object.""" +"""Generic role implying rights to modify an object.""" VIEWER_ROLE = 'roles/viewer' -"""IAM permission implying rights to access an object without modifying it.""" +"""Generic role implying rights to access an object.""" + +# Pubsub-specific IAM roles + +PUBSUB_ADMIN_ROLE = 'roles/pubsub.admin' +"""Role implying all rights to an object.""" + +PUBSUB_EDITOR_ROLE = 'roles/pubsub.editor' +"""Role implying rights to modify an object.""" + +PUBSUB_VIEWER_ROLE = 'roles/pubsub.viewer' +"""Role implying rights to access an object.""" + +PUBSUB_PUBLISHER_ROLE = 'roles/pubsub.publisher' +"""Role implying rights to publish to a topic.""" + +PUBSUB_SUBSCRIBER_ROLE = 'roles/pubsub.subscriber' +"""Role implying rights to subscribe to a topic.""" + + +# Pubsub-specific permissions + +PUBSUB_TOPICS_CONSUME = 'pubsub.topics.consume' +"""Permission: consume events from a subscription.""" + +PUBSUB_TOPICS_CREATE = 'pubsub.topics.create' +"""Permission: create topics.""" + +PUBSUB_TOPICS_DELETE = 'pubsub.topics.delete' +"""Permission: delete topics.""" + +PUBSUB_TOPICS_GET = 'pubsub.topics.get' +"""Permission: retrieve topics.""" + +PUBSUB_TOPICS_GET_IAM_POLICY = 'pubsub.topics.getIamPolicy' +"""Permission: retrieve subscription IAM policies.""" + +PUBSUB_TOPICS_LIST = 'pubsub.topics.list' +"""Permission: list topics.""" + +PUBSUB_TOPICS_SET_IAM_POLICY = 'pubsub.topics.setIamPolicy' +"""Permission: update subscription IAM policies.""" + +PUBSUB_SUBSCRIPTIONS_CONSUME = 'pubsub.subscriptions.consume' +"""Permission: consume events from a subscription.""" + +PUBSUB_SUBSCRIPTIONS_CREATE = 'pubsub.subscriptions.create' +"""Permission: create subscriptions.""" + +PUBSUB_SUBSCRIPTIONS_DELETE = 'pubsub.subscriptions.delete' +"""Permission: delete subscriptions.""" + +PUBSUB_SUBSCRIPTIONS_GET = 'pubsub.subscriptions.get' +"""Permission: retrieve subscriptions.""" + +PUBSUB_SUBSCRIPTIONS_GET_IAM_POLICY = 'pubsub.subscriptions.getIamPolicy' +"""Permission: retrieve subscription IAM policies.""" + +PUBSUB_SUBSCRIPTIONS_LIST = 'pubsub.subscriptions.list' +"""Permission: list subscriptions.""" + +PUBSUB_SUBSCRIPTIONS_SET_IAM_POLICY = 'pubsub.subscriptions.setIamPolicy' +"""Permission: update subscription IAM policies.""" + +PUBSUB_SUBSCRIPTIONS_UPDATE = 'pubsub.subscriptions.update' +"""Permission: update subscriptions.""" class Policy(object): @@ -42,6 +113,8 @@ def __init__(self, etag=None, version=None): self.owners = set() self.editors = set() self.viewers = set() + self.publishers = set() + self.subscribers = set() @staticmethod def user(email): @@ -125,12 +198,16 @@ def from_api_repr(cls, resource): for binding in resource.get('bindings', ()): role = binding['role'] members = set(binding['members']) - if role == OWNER_ROLE: - policy.owners = members - elif role == EDITOR_ROLE: - policy.editors = members - elif role == VIEWER_ROLE: - policy.viewers = members + if role in (OWNER_ROLE, PUBSUB_ADMIN_ROLE): + policy.owners |= members + elif role in (EDITOR_ROLE, PUBSUB_EDITOR_ROLE): + policy.editors |= members + elif role in (VIEWER_ROLE, PUBSUB_VIEWER_ROLE): + policy.viewers |= members + elif role == PUBSUB_PUBLISHER_ROLE: + policy.publishers |= members + elif role == PUBSUB_SUBSCRIBER_ROLE: + policy.subscribers |= members else: raise ValueError('Unknown role: %s' % (role,)) return policy @@ -153,15 +230,28 @@ def to_api_repr(self): if self.owners: bindings.append( - {'role': OWNER_ROLE, 'members': sorted(self.owners)}) + {'role': PUBSUB_ADMIN_ROLE, + 'members': sorted(self.owners)}) if self.editors: bindings.append( - {'role': EDITOR_ROLE, 'members': sorted(self.editors)}) + {'role': PUBSUB_EDITOR_ROLE, + 'members': sorted(self.editors)}) if self.viewers: bindings.append( - {'role': VIEWER_ROLE, 'members': sorted(self.viewers)}) + {'role': PUBSUB_VIEWER_ROLE, + 'members': sorted(self.viewers)}) + + if self.publishers: + bindings.append( + {'role': PUBSUB_PUBLISHER_ROLE, + 'members': sorted(self.publishers)}) + + if self.subscribers: + bindings.append( + {'role': PUBSUB_SUBSCRIBER_ROLE, + 'members': sorted(self.subscribers)}) if bindings: resource['bindings'] = bindings diff --git a/gcloud/pubsub/test_iam.py b/gcloud/pubsub/test_iam.py index 4aec6ad14130..90cd179253c0 100644 --- a/gcloud/pubsub/test_iam.py +++ b/gcloud/pubsub/test_iam.py @@ -31,6 +31,8 @@ def test_ctor_defaults(self): self.assertEqual(list(policy.owners), []) self.assertEqual(list(policy.editors), []) self.assertEqual(list(policy.viewers), []) + self.assertEqual(list(policy.publishers), []) + self.assertEqual(list(policy.subscribers), []) def test_ctor_explicit(self): VERSION = 17 @@ -41,6 +43,8 @@ def test_ctor_explicit(self): self.assertEqual(list(policy.owners), []) self.assertEqual(list(policy.editors), []) self.assertEqual(list(policy.viewers), []) + self.assertEqual(list(policy.publishers), []) + self.assertEqual(list(policy.subscribers), []) def test_user(self): EMAIL = 'phred@example.com' @@ -87,13 +91,21 @@ def test_from_api_repr_only_etag(self): self.assertEqual(list(policy.viewers), []) def test_from_api_repr_complete(self): - from gcloud.pubsub.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE + from gcloud.pubsub.iam import ( + OWNER_ROLE, + EDITOR_ROLE, + VIEWER_ROLE, + PUBSUB_PUBLISHER_ROLE, + PUBSUB_SUBSCRIBER_ROLE, + ) OWNER1 = 'user:phred@example.com' OWNER2 = 'group:cloud-logs@google.com' EDITOR1 = 'domain:google.com' EDITOR2 = 'user:phred@example.com' VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com' VIEWER2 = 'user:phred@example.com' + PUBLISHER = 'user:phred@example.com' + SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com' RESOURCE = { 'etag': 'DEADBEEF', 'version': 17, @@ -101,6 +113,8 @@ def test_from_api_repr_complete(self): {'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]}, {'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, {'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, + {'role': PUBSUB_PUBLISHER_ROLE, 'members': [PUBLISHER]}, + {'role': PUBSUB_SUBSCRIBER_ROLE, 'members': [SUBSCRIBER]}, ], } klass = self._getTargetClass() @@ -110,6 +124,8 @@ def test_from_api_repr_complete(self): self.assertEqual(sorted(policy.owners), [OWNER2, OWNER1]) self.assertEqual(sorted(policy.editors), [EDITOR1, EDITOR2]) self.assertEqual(sorted(policy.viewers), [VIEWER1, VIEWER2]) + self.assertEqual(sorted(policy.publishers), [PUBLISHER]) + self.assertEqual(sorted(policy.subscribers), [SUBSCRIBER]) def test_from_api_repr_bad_role(self): BOGUS1 = 'user:phred@example.com' @@ -134,20 +150,30 @@ 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, EDITOR_ROLE, VIEWER_ROLE + from gcloud.pubsub.iam import ( + PUBSUB_ADMIN_ROLE, + PUBSUB_EDITOR_ROLE, + PUBSUB_VIEWER_ROLE, + PUBSUB_PUBLISHER_ROLE, + PUBSUB_SUBSCRIBER_ROLE, + ) OWNER1 = 'group:cloud-logs@google.com' OWNER2 = 'user:phred@example.com' EDITOR1 = 'domain:google.com' EDITOR2 = 'user:phred@example.com' VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com' VIEWER2 = 'user:phred@example.com' + PUBLISHER = 'user:phred@example.com' + SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com' EXPECTED = { 'etag': 'DEADBEEF', 'version': 17, 'bindings': [ - {'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]}, - {'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, - {'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, + {'role': PUBSUB_ADMIN_ROLE, 'members': [OWNER1, OWNER2]}, + {'role': PUBSUB_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, + {'role': PUBSUB_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, + {'role': PUBSUB_PUBLISHER_ROLE, 'members': [PUBLISHER]}, + {'role': PUBSUB_SUBSCRIBER_ROLE, 'members': [SUBSCRIBER]}, ], } policy = self._makeOne('DEADBEEF', 17) @@ -157,4 +183,6 @@ def test_to_api_repr_full(self): policy.editors.add(EDITOR2) policy.viewers.add(VIEWER1) policy.viewers.add(VIEWER2) + policy.publishers.add(PUBLISHER) + policy.subscribers.add(SUBSCRIBER) self.assertEqual(policy.to_api_repr(), EXPECTED) diff --git a/gcloud/pubsub/test_subscription.py b/gcloud/pubsub/test_subscription.py index fa1bd0f19696..700dbb82bff5 100644 --- a/gcloud/pubsub/test_subscription.py +++ b/gcloud/pubsub/test_subscription.py @@ -426,20 +426,30 @@ def test_modify_ack_deadline_w_alternate_client(self): (self.SUB_PATH, [ACK_ID1, ACK_ID2], self.DEADLINE)) def test_get_iam_policy_w_bound_client(self): - from gcloud.pubsub.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE + from gcloud.pubsub.iam import ( + PUBSUB_ADMIN_ROLE, + PUBSUB_EDITOR_ROLE, + PUBSUB_VIEWER_ROLE, + PUBSUB_PUBLISHER_ROLE, + PUBSUB_SUBSCRIBER_ROLE, + ) OWNER1 = 'user:phred@example.com' OWNER2 = 'group:cloud-logs@google.com' EDITOR1 = 'domain:google.com' EDITOR2 = 'user:phred@example.com' VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com' VIEWER2 = 'user:phred@example.com' + PUBLISHER = 'user:phred@example.com' + SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com' POLICY = { 'etag': 'DEADBEEF', 'version': 17, 'bindings': [ - {'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]}, - {'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, - {'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, + {'role': PUBSUB_ADMIN_ROLE, 'members': [OWNER1, OWNER2]}, + {'role': PUBSUB_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, + {'role': PUBSUB_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, + {'role': PUBSUB_PUBLISHER_ROLE, 'members': [PUBLISHER]}, + {'role': PUBSUB_SUBSCRIBER_ROLE, 'members': [SUBSCRIBER]}, ], } client = _Client(project=self.PROJECT) @@ -455,6 +465,8 @@ def test_get_iam_policy_w_bound_client(self): self.assertEqual(sorted(policy.owners), [OWNER2, OWNER1]) self.assertEqual(sorted(policy.editors), [EDITOR1, EDITOR2]) self.assertEqual(sorted(policy.viewers), [VIEWER1, VIEWER2]) + self.assertEqual(sorted(policy.publishers), [PUBLISHER]) + self.assertEqual(sorted(policy.subscribers), [SUBSCRIBER]) self.assertEqual(api._got_iam_policy, self.SUB_PATH) def test_get_iam_policy_w_alternate_client(self): @@ -479,21 +491,31 @@ def test_get_iam_policy_w_alternate_client(self): self.assertEqual(api._got_iam_policy, self.SUB_PATH) def test_set_iam_policy_w_bound_client(self): - from gcloud.pubsub.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE from gcloud.pubsub.iam import Policy + from gcloud.pubsub.iam import ( + PUBSUB_ADMIN_ROLE, + PUBSUB_EDITOR_ROLE, + PUBSUB_VIEWER_ROLE, + PUBSUB_PUBLISHER_ROLE, + PUBSUB_SUBSCRIBER_ROLE, + ) OWNER1 = 'group:cloud-logs@google.com' OWNER2 = 'user:phred@example.com' EDITOR1 = 'domain:google.com' EDITOR2 = 'user:phred@example.com' VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com' VIEWER2 = 'user:phred@example.com' + PUBLISHER = 'user:phred@example.com' + SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com' POLICY = { 'etag': 'DEADBEEF', 'version': 17, 'bindings': [ - {'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]}, - {'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, - {'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, + {'role': PUBSUB_ADMIN_ROLE, 'members': [OWNER1, OWNER2]}, + {'role': PUBSUB_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, + {'role': PUBSUB_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, + {'role': PUBSUB_PUBLISHER_ROLE, 'members': [PUBLISHER]}, + {'role': PUBSUB_SUBSCRIBER_ROLE, 'members': [SUBSCRIBER]}, ], } RESPONSE = POLICY.copy() @@ -511,6 +533,8 @@ def test_set_iam_policy_w_bound_client(self): policy.editors.add(EDITOR2) policy.viewers.add(VIEWER1) policy.viewers.add(VIEWER2) + policy.publishers.add(PUBLISHER) + policy.subscribers.add(SUBSCRIBER) new_policy = subscription.set_iam_policy(policy) @@ -519,6 +543,8 @@ def test_set_iam_policy_w_bound_client(self): self.assertEqual(sorted(new_policy.owners), [OWNER1, OWNER2]) self.assertEqual(sorted(new_policy.editors), [EDITOR1, EDITOR2]) self.assertEqual(sorted(new_policy.viewers), [VIEWER1, VIEWER2]) + self.assertEqual(sorted(new_policy.publishers), [PUBLISHER]) + self.assertEqual(sorted(new_policy.subscribers), [SUBSCRIBER]) self.assertEqual(api._set_iam_policy, (self.SUB_PATH, POLICY)) def test_set_iam_policy_w_alternate_client(self): diff --git a/gcloud/pubsub/test_topic.py b/gcloud/pubsub/test_topic.py index 20a44141ffcd..7bce0e3c3198 100644 --- a/gcloud/pubsub/test_topic.py +++ b/gcloud/pubsub/test_topic.py @@ -370,20 +370,30 @@ def test_list_subscriptions_missing_key(self): (self.TOPIC_PATH, None, None)) def test_get_iam_policy_w_bound_client(self): - from gcloud.pubsub.iam import OWNER_ROLE, EDITOR_ROLE, VIEWER_ROLE + from gcloud.pubsub.iam import ( + PUBSUB_ADMIN_ROLE, + PUBSUB_EDITOR_ROLE, + PUBSUB_VIEWER_ROLE, + PUBSUB_PUBLISHER_ROLE, + PUBSUB_SUBSCRIBER_ROLE, + ) OWNER1 = 'user:phred@example.com' OWNER2 = 'group:cloud-logs@google.com' EDITOR1 = 'domain:google.com' EDITOR2 = 'user:phred@example.com' VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com' VIEWER2 = 'user:phred@example.com' + PUBLISHER = 'user:phred@example.com' + SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com' POLICY = { 'etag': 'DEADBEEF', 'version': 17, 'bindings': [ - {'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]}, - {'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, - {'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, + {'role': PUBSUB_ADMIN_ROLE, 'members': [OWNER1, OWNER2]}, + {'role': PUBSUB_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, + {'role': PUBSUB_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, + {'role': PUBSUB_PUBLISHER_ROLE, 'members': [PUBLISHER]}, + {'role': PUBSUB_SUBSCRIBER_ROLE, 'members': [SUBSCRIBER]}, ], } @@ -399,6 +409,8 @@ def test_get_iam_policy_w_bound_client(self): self.assertEqual(sorted(policy.owners), [OWNER2, OWNER1]) self.assertEqual(sorted(policy.editors), [EDITOR1, EDITOR2]) self.assertEqual(sorted(policy.viewers), [VIEWER1, VIEWER2]) + self.assertEqual(sorted(policy.publishers), [PUBLISHER]) + self.assertEqual(sorted(policy.subscribers), [SUBSCRIBER]) self.assertEqual(api._got_iam_policy, self.TOPIC_PATH) def test_get_iam_policy_w_alternate_client(self): @@ -424,20 +436,35 @@ 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, EDITOR_ROLE, VIEWER_ROLE + from gcloud.pubsub.iam import ( + PUBSUB_ADMIN_ROLE, + PUBSUB_EDITOR_ROLE, + PUBSUB_VIEWER_ROLE, + PUBSUB_PUBLISHER_ROLE, + PUBSUB_SUBSCRIBER_ROLE, + ) OWNER1 = 'group:cloud-logs@google.com' OWNER2 = 'user:phred@example.com' EDITOR1 = 'domain:google.com' EDITOR2 = 'user:phred@example.com' VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com' VIEWER2 = 'user:phred@example.com' + PUBLISHER = 'user:phred@example.com' + SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com' POLICY = { 'etag': 'DEADBEEF', 'version': 17, 'bindings': [ - {'role': OWNER_ROLE, 'members': [OWNER1, OWNER2]}, - {'role': EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, - {'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, + {'role': PUBSUB_ADMIN_ROLE, + 'members': [OWNER1, OWNER2]}, + {'role': PUBSUB_EDITOR_ROLE, + 'members': [EDITOR1, EDITOR2]}, + {'role': PUBSUB_VIEWER_ROLE, + 'members': [VIEWER1, VIEWER2]}, + {'role': PUBSUB_PUBLISHER_ROLE, + 'members': [PUBLISHER]}, + {'role': PUBSUB_SUBSCRIBER_ROLE, + 'members': [SUBSCRIBER]}, ], } RESPONSE = POLICY.copy() @@ -455,6 +482,8 @@ def test_set_iam_policy_w_bound_client(self): policy.editors.add(EDITOR2) policy.viewers.add(VIEWER1) policy.viewers.add(VIEWER2) + policy.publishers.add(PUBLISHER) + policy.subscribers.add(SUBSCRIBER) new_policy = topic.set_iam_policy(policy) @@ -463,6 +492,8 @@ def test_set_iam_policy_w_bound_client(self): self.assertEqual(sorted(new_policy.owners), [OWNER1, OWNER2]) self.assertEqual(sorted(new_policy.editors), [EDITOR1, EDITOR2]) self.assertEqual(sorted(new_policy.viewers), [VIEWER1, VIEWER2]) + self.assertEqual(sorted(new_policy.publishers), [PUBLISHER]) + self.assertEqual(sorted(new_policy.subscribers), [SUBSCRIBER]) self.assertEqual(api._set_iam_policy, (self.TOPIC_PATH, POLICY)) def test_set_iam_policy_w_alternate_client(self):