From d45a6d1626cdf645a9c9300dff189421eb5574da Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 18 Apr 2017 13:36:41 -0400 Subject: [PATCH 1/4] Store policy bindings as sets, not frozensets. The legacy accessors still return frozensets, as they cannot safely be mutated in plcae. --- core/google/cloud/iam.py | 10 ++--- core/tests/unit/test_iam.py | 64 +++++++++++++++++-------------- pubsub/google/cloud/pubsub/iam.py | 4 +- pubsub/tests/unit/test_iam.py | 37 +++++++++--------- 4 files changed, 60 insertions(+), 55 deletions(-) diff --git a/core/google/cloud/iam.py b/core/google/cloud/iam.py index 4747b39bbf07..653cebda1e71 100644 --- a/core/google/cloud/iam.py +++ b/core/google/cloud/iam.py @@ -71,7 +71,7 @@ def __getitem__(self, key): return self._bindings[key] def __setitem__(self, key, value): - self._bindings[key] = frozenset(value) + self._bindings[key] = set(value) def __delitem__(self, key): del self._bindings[key] @@ -91,7 +91,7 @@ def owners(self, value): warnings.warn( _ASSIGNMENT_DEPRECATED_MSG.format('owners', OWNER_ROLE), DeprecationWarning) - self._bindings[OWNER_ROLE] = list(value) + self[OWNER_ROLE] = value @property def editors(self): @@ -108,7 +108,7 @@ def editors(self, value): warnings.warn( _ASSIGNMENT_DEPRECATED_MSG.format('editors', EDITOR_ROLE), DeprecationWarning) - self._bindings[EDITOR_ROLE] = list(value) + self[EDITOR_ROLE] = value @property def viewers(self): @@ -125,7 +125,7 @@ def viewers(self, value): warnings.warn( _ASSIGNMENT_DEPRECATED_MSG.format('viewers', VIEWER_ROLE), DeprecationWarning) - self._bindings[VIEWER_ROLE] = list(value) + self[VIEWER_ROLE] = value @staticmethod def user(email): @@ -209,7 +209,7 @@ def from_api_repr(cls, resource): for binding in resource.get('bindings', ()): role = binding['role'] members = sorted(binding['members']) - policy._bindings[role] = members + policy[role] = members return policy def to_api_repr(self): diff --git a/core/tests/unit/test_iam.py b/core/tests/unit/test_iam.py index 9f1bd9b3904f..42fac7c623c1 100644 --- a/core/tests/unit/test_iam.py +++ b/core/tests/unit/test_iam.py @@ -27,27 +27,26 @@ def _make_one(self, *args, **kw): return self._get_target_class()(*args, **kw) def test_ctor_defaults(self): + empty = frozenset() policy = self._make_one() self.assertIsNone(policy.etag) self.assertIsNone(policy.version) - self.assertIsInstance(policy.owners, frozenset) - self.assertEqual(list(policy.owners), []) - self.assertIsInstance(policy.editors, frozenset) - self.assertEqual(list(policy.editors), []) - self.assertIsInstance(policy.viewers, frozenset) - self.assertEqual(list(policy.viewers), []) + self.assertEqual(policy.owners, empty) + self.assertEqual(policy.editors, empty) + self.assertEqual(policy.viewers, empty) self.assertEqual(len(policy), 0) self.assertEqual(dict(policy), {}) def test_ctor_explicit(self): VERSION = 17 ETAG = 'ETAG' + empty = frozenset() policy = self._make_one(ETAG, VERSION) self.assertEqual(policy.etag, ETAG) self.assertEqual(policy.version, VERSION) - self.assertEqual(list(policy.owners), []) - self.assertEqual(list(policy.editors), []) - self.assertEqual(list(policy.viewers), []) + self.assertEqual(policy.owners, empty) + self.assertEqual(policy.editors, empty) + self.assertEqual(policy.viewers, empty) self.assertEqual(len(policy), 0) self.assertEqual(dict(policy), {}) @@ -58,7 +57,7 @@ def test___getitem___miss(self): def test___setitem__(self): USER = 'user:phred@example.com' - PRINCIPALS = frozenset([USER]) + PRINCIPALS = set([USER]) policy = self._make_one() policy['rolename'] = [USER] self.assertEqual(policy['rolename'], PRINCIPALS) @@ -80,54 +79,59 @@ def test___delitem___miss(self): def test_owners_getter(self): from google.cloud.iam import OWNER_ROLE MEMBER = 'user:phred@example.com' + expected = frozenset([MEMBER]) policy = self._make_one() policy[OWNER_ROLE] = [MEMBER] - self.assertIsInstance(policy.owners, frozenset) - self.assertEqual(list(policy.owners), [MEMBER]) + self.assertEqual(policy.owners, expected) def test_owners_setter(self): import warnings from google.cloud.iam import OWNER_ROLE MEMBER = 'user:phred@example.com' + expected = set([MEMBER]) policy = self._make_one() with warnings.catch_warnings(): + warnings.simplefilter('always') policy.owners = [MEMBER] - self.assertEqual(list(policy[OWNER_ROLE]), [MEMBER]) + self.assertEqual(policy[OWNER_ROLE], expected) def test_editors_getter(self): from google.cloud.iam import EDITOR_ROLE MEMBER = 'user:phred@example.com' + expected = frozenset([MEMBER]) policy = self._make_one() policy[EDITOR_ROLE] = [MEMBER] - self.assertIsInstance(policy.editors, frozenset) - self.assertEqual(list(policy.editors), [MEMBER]) + self.assertEqual(policy.editors, expected) def test_editors_setter(self): import warnings from google.cloud.iam import EDITOR_ROLE MEMBER = 'user:phred@example.com' + expected = set([MEMBER]) policy = self._make_one() with warnings.catch_warnings(): + warnings.simplefilter('always') policy.editors = [MEMBER] - self.assertEqual(list(policy[EDITOR_ROLE]), [MEMBER]) + self.assertEqual(policy[EDITOR_ROLE], expected) def test_viewers_getter(self): from google.cloud.iam import VIEWER_ROLE MEMBER = 'user:phred@example.com' + expected = frozenset([MEMBER]) policy = self._make_one() policy[VIEWER_ROLE] = [MEMBER] - self.assertIsInstance(policy.viewers, frozenset) - self.assertEqual(list(policy.viewers), [MEMBER]) + self.assertEqual(policy.viewers, expected) def test_viewers_setter(self): import warnings from google.cloud.iam import VIEWER_ROLE MEMBER = 'user:phred@example.com' + expected = set([MEMBER]) policy = self._make_one() with warnings.catch_warnings(): warnings.simplefilter('always') policy.viewers = [MEMBER] - self.assertEqual(list(policy[VIEWER_ROLE]), [MEMBER]) + self.assertEqual(policy[VIEWER_ROLE], expected) def test_user(self): EMAIL = 'phred@example.com' @@ -162,6 +166,7 @@ def test_authenticated_users(self): self.assertEqual(policy.authenticated_users(), 'allAuthenticatedUsers') def test_from_api_repr_only_etag(self): + empty = frozenset() RESOURCE = { 'etag': 'ACAB', } @@ -169,9 +174,9 @@ def test_from_api_repr_only_etag(self): policy = klass.from_api_repr(RESOURCE) self.assertEqual(policy.etag, 'ACAB') self.assertIsNone(policy.version) - self.assertEqual(list(policy.owners), []) - self.assertEqual(list(policy.editors), []) - self.assertEqual(list(policy.viewers), []) + self.assertEqual(policy.owners, empty) + self.assertEqual(policy.editors, empty) + self.assertEqual(policy.viewers, empty) self.assertEqual(dict(policy), {}) def test_from_api_repr_complete(self): @@ -196,18 +201,19 @@ def test_from_api_repr_complete(self): {'role': VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, ], } + empty = frozenset() klass = self._get_target_class() policy = klass.from_api_repr(RESOURCE) self.assertEqual(policy.etag, 'DEADBEEF') self.assertEqual(policy.version, 17) - self.assertEqual(sorted(policy.owners), [OWNER1, OWNER2]) - self.assertEqual(sorted(policy.editors), [EDITOR1, EDITOR2]) - self.assertEqual(sorted(policy.viewers), [VIEWER1, VIEWER2]) + self.assertEqual(policy.owners, frozenset([OWNER1, OWNER2])) + self.assertEqual(policy.editors, frozenset([EDITOR1, EDITOR2])) + self.assertEqual(policy.viewers, frozenset([VIEWER1, VIEWER2])) self.assertEqual( dict(policy), { - OWNER_ROLE: [OWNER1, OWNER2], - EDITOR_ROLE: [EDITOR1, EDITOR2], - VIEWER_ROLE: [VIEWER1, VIEWER2], + OWNER_ROLE: set([OWNER1, OWNER2]), + EDITOR_ROLE: set([EDITOR1, EDITOR2]), + VIEWER_ROLE: set([VIEWER1, VIEWER2]), }) def test_from_api_repr_unknown_role(self): @@ -224,7 +230,7 @@ def test_from_api_repr_unknown_role(self): policy = klass.from_api_repr(RESOURCE) self.assertEqual(policy.etag, 'DEADBEEF') self.assertEqual(policy.version, 17) - self.assertEqual(dict(policy), {'unknown': [GROUP, USER]}) + self.assertEqual(dict(policy), {'unknown': set([GROUP, USER])}) def test_to_api_repr_defaults(self): policy = self._make_one() diff --git a/pubsub/google/cloud/pubsub/iam.py b/pubsub/google/cloud/pubsub/iam.py index e92f2151dc05..9c7e46af222a 100644 --- a/pubsub/google/cloud/pubsub/iam.py +++ b/pubsub/google/cloud/pubsub/iam.py @@ -121,7 +121,7 @@ def publishers(self, value): _ASSIGNMENT_DEPRECATED_MSG.format( 'publishers', PUBSUB_PUBLISHER_ROLE), DeprecationWarning) - self._bindings[PUBSUB_PUBLISHER_ROLE] = list(value) + self[PUBSUB_PUBLISHER_ROLE] = value @property def subscribers(self): @@ -135,4 +135,4 @@ def subscribers(self, value): _ASSIGNMENT_DEPRECATED_MSG.format( 'subscribers', PUBSUB_SUBSCRIBER_ROLE), DeprecationWarning) - self._bindings[PUBSUB_SUBSCRIBER_ROLE] = list(value) + self[PUBSUB_SUBSCRIBER_ROLE] = value diff --git a/pubsub/tests/unit/test_iam.py b/pubsub/tests/unit/test_iam.py index 3bf4aaa922f0..475d375d0cd8 100644 --- a/pubsub/tests/unit/test_iam.py +++ b/pubsub/tests/unit/test_iam.py @@ -27,31 +27,28 @@ def _make_one(self, *args, **kw): return self._get_target_class()(*args, **kw) def test_ctor_defaults(self): + empty = frozenset() policy = self._make_one() self.assertIsNone(policy.etag) self.assertIsNone(policy.version) - self.assertIsInstance(policy.owners, frozenset) - self.assertEqual(list(policy.owners), []) - self.assertIsInstance(policy.editors, frozenset) - self.assertEqual(list(policy.editors), []) - self.assertIsInstance(policy.viewers, frozenset) - self.assertEqual(list(policy.viewers), []) - self.assertIsInstance(policy.publishers, frozenset) - self.assertEqual(list(policy.publishers), []) - self.assertIsInstance(policy.subscribers, frozenset) - self.assertEqual(list(policy.subscribers), []) + self.assertEqual(policy.owners, empty) + self.assertEqual(policy.editors, empty) + self.assertEqual(policy.viewers, empty) + self.assertEqual(policy.publishers, empty) + self.assertEqual(policy.subscribers, empty) def test_ctor_explicit(self): VERSION = 17 ETAG = 'ETAG' + empty = frozenset() policy = self._make_one(ETAG, VERSION) self.assertEqual(policy.etag, ETAG) self.assertEqual(policy.version, VERSION) - 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), []) + self.assertEqual(policy.owners, empty) + self.assertEqual(policy.editors, empty) + self.assertEqual(policy.viewers, empty) + self.assertEqual(policy.publishers, empty) + self.assertEqual(policy.subscribers, empty) def test_publishers_setter(self): import warnings @@ -59,13 +56,14 @@ def test_publishers_setter(self): PUBSUB_PUBLISHER_ROLE, ) PUBLISHER = 'user:phred@example.com' + expected = set([PUBLISHER]) policy = self._make_one() with warnings.catch_warnings(): policy.publishers = [PUBLISHER] - self.assertEqual(sorted(policy.publishers), [PUBLISHER]) + self.assertEqual(policy.publishers, frozenset(expected)) self.assertEqual( - dict(policy), {PUBSUB_PUBLISHER_ROLE: [PUBLISHER]}) + dict(policy), {PUBSUB_PUBLISHER_ROLE: expected}) def test_subscribers_setter(self): import warnings @@ -73,10 +71,11 @@ def test_subscribers_setter(self): PUBSUB_SUBSCRIBER_ROLE, ) SUBSCRIBER = 'serviceAccount:1234-abcdef@service.example.com' + expected = set([SUBSCRIBER]) policy = self._make_one() with warnings.catch_warnings(): policy.subscribers = [SUBSCRIBER] - self.assertEqual(sorted(policy.subscribers), [SUBSCRIBER]) + self.assertEqual(policy.subscribers, frozenset(expected)) self.assertEqual( - dict(policy), {PUBSUB_SUBSCRIBER_ROLE: [SUBSCRIBER]}) + dict(policy), {PUBSUB_SUBSCRIBER_ROLE: expected}) From 790d3e85ab4edaeaa456875e29674bb90d5e25c8 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 17 Apr 2017 17:25:33 -0400 Subject: [PATCH 2/4] Add role / permission constants for storage IAM. --- storage/google/cloud/storage/iam.py | 86 +++++++++++++++++++++++++++++ storage/tests/unit/test_iam.py | 22 ++++++++ 2 files changed, 108 insertions(+) create mode 100644 storage/google/cloud/storage/iam.py create mode 100644 storage/tests/unit/test_iam.py diff --git a/storage/google/cloud/storage/iam.py b/storage/google/cloud/storage/iam.py new file mode 100644 index 000000000000..e9e91032f864 --- /dev/null +++ b/storage/google/cloud/storage/iam.py @@ -0,0 +1,86 @@ +# Copyright 2017 Google Inc. +# +# 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. +"""Storage API IAM policy definitions + +For allowed roles / permissions, see: +https://cloud.google.com/storage/docs/access-control/iam +""" + +# Storage-specific IAM roles + +STORAGE_OBJECT_CREATOR_ROLE = 'roles/storage.objectCreator' +"""Role implying rights to create objects, but not delete or overwrite them.""" + +STORAGE_OBJECT_VIEWER_ROLE = 'roles/storage.objectViewer' +"""Role implying rights to view object properties, excluding ACLs.""" + +STORAGE_OBJECT_ADMIN_ROLE = 'roles/storage.objectViewer' +"""Role implying full control of objects.""" + +STORAGE_ADMIN_ROLE = 'roles/storage.admin' +"""Role implying full control of objects and buckets.""" + +STORAGE_VIEWER_ROLE = 'Viewer' +"""Can list buckets.""" + +STORAGE_EDITOR_ROLE = 'Editor' +"""Can create, list, and delete buckets.""" + +STORAGE_OWNER_ROLE = 'Owners' +"""Can create, list, and delete buckets.""" + + +# Storage-specific permissions + +STORAGE_BUCKETS_CREATE = 'storage.buckets.create' +"""Permission: create buckets.""" + +STORAGE_BUCKETS_DELETE = 'storage.buckets.delete' +"""Permission: delete buckets.""" + +STORAGE_BUCKETS_GET = 'storage.buckets.get' +"""Permission: read bucket metadata, excluding ACLs.""" + +STORAGE_BUCKETS_GET_IAM_POLICY = 'storage.buckets.getIamPolicy' +"""Permission: read bucket ACLs.""" + +STORAGE_BUCKETS_LIST = 'storage.buckets.list' +"""Permission: list buckets.""" + +STORAGE_BUCKETS_SET_IAM_POLICY = 'storage.buckets.setIamPolicy' +"""Permission: update bucket ACLs.""" + +STORAGE_BUCKETS_UPDATE = 'storage.buckets.list' +"""Permission: update buckets, excluding ACLS.""" + +STORAGE_OBJECTS_CREATE = 'storage.objects.create' +"""Permission: add new objects to a bucket.""" + +STORAGE_OBJECTS_DELETE = 'storage.objects.delete' +"""Permission: delete objects.""" + +STORAGE_OBJECTS_GET = 'storage.objects.get' +"""Permission: read object data / metadata, excluding ACLs.""" + +STORAGE_OBJECTS_GET_IAM_POLICY = 'storage.objects.getIamPolicy' +"""Permission: read object ACLs.""" + +STORAGE_OBJECTS_LIST = 'storage.objects.list' +"""Permission: list objects in a bucket.""" + +STORAGE_OBJECTS_SET_IAM_POLICY = 'storage.objects.setIamPolicy' +"""Permission: update object ACLs.""" + +STORAGE_OBJECTS_UPDATE = 'storage.objects.update' +"""Permission: update object metadat, excluding ACLs.""" diff --git a/storage/tests/unit/test_iam.py b/storage/tests/unit/test_iam.py new file mode 100644 index 000000000000..3b0091bf3120 --- /dev/null +++ b/storage/tests/unit/test_iam.py @@ -0,0 +1,22 @@ +# Copyright 2017 Google Inc. +# +# 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 unittest + + +class Test_constants(unittest.TestCase): + + def test_ctor_defaults(self): + from google.cloud.storage.iam import STORAGE_ADMIN_ROLE + role = STORAGE_ADMIN_ROLE From fa53208a7affa4f50ea4ee2248d95744e7c6a1c6 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 18 Apr 2017 15:46:38 -0400 Subject: [PATCH 3/4] Add IAM methods for buckets. Toward #1679. --- storage/google/cloud/storage/bucket.py | 78 +++++++++++++++ storage/tests/unit/test_bucket.py | 129 +++++++++++++++++++++++++ 2 files changed, 207 insertions(+) diff --git a/storage/google/cloud/storage/bucket.py b/storage/google/cloud/storage/bucket.py index 819b273b77d1..832345d8c493 100644 --- a/storage/google/cloud/storage/bucket.py +++ b/storage/google/cloud/storage/bucket.py @@ -26,6 +26,7 @@ from google.cloud._helpers import _NOW from google.cloud._helpers import _rfc3339_to_datetime from google.cloud.exceptions import NotFound +from google.cloud.iam import Policy from google.cloud.iterator import HTTPIterator from google.cloud.storage._helpers import _PropertyMixin from google.cloud.storage._helpers import _scalar_property @@ -803,6 +804,83 @@ def disable_website(self): """ return self.configure_website(None, None) + def get_iam_policy(self, client=None): + """Retrieve the IAM policy for the bucket. + + See: + https://cloud.google.com/storage/docs/json_api/v1/buckets/getIamPolicy + + :type client: :class:`~google.cloud.storage.client.Client` or + ``NoneType`` + :param client: Optional. The client to use. If not passed, falls back + to the ``client`` stored on the current bucket. + + :rtype: :class:`google.cloud.iam.Policy` + :returns: the policy instance, based on the resource returned from + the ``getIamPolicy`` API request. + """ + client = self._require_client(client) + info = client._connection.api_request( + method='GET', + path='%s/iam' % (self.path,), + _target_object=None) + return Policy.from_api_repr(info) + + def set_iam_policy(self, policy, client=None): + """Update the IAM policy for the bucket. + + See: + https://cloud.google.com/storage/docs/json_api/v1/buckets/setIamPolicy + + :type policy: :class:`google.cloud.iam.Policy` + :param policy: policy instance used to update bucket's IAM policy. + + :type client: :class:`~google.cloud.storage.client.Client` or + ``NoneType`` + :param client: Optional. The client to use. If not passed, falls back + to the ``client`` stored on the current bucket. + + :rtype: :class:`google.cloud.iam.Policy` + :returns: the policy instance, based on the resource returned from + the ``setIamPolicy`` API request. + """ + client = self._require_client(client) + resource = policy.to_api_repr() + resource['resourceId'] = self.path + info = client._connection.api_request( + method='PUT', + path='%s/iam' % (self.path,), + data=resource, + _target_object=None) + return Policy.from_api_repr(info) + + def test_iam_permissions(self, permissions, client=None): + """API call: test permissions + + See: + https://cloud.google.com/storage/docs/json_api/v1/buckets/testIamPermissions + + :type permissions: list of string + :param permissions: the permissions to check + + :type client: :class:`~google.cloud.storage.client.Client` or + ``NoneType`` + :param client: Optional. The client to use. If not passed, falls back + to the ``client`` stored on the current bucket. + + :rtype: list of string + :returns: the permissions returned by the ``testIamPermissions`` API + request. + """ + client = self._require_client(client) + query = {'permissions': permissions} + path = '%s/iam/testPermissions' % (self.path,) + resp = client._connection.api_request( + method='GET', + path=path, + query_params=query) + return resp.get('permissions', []) + def make_public(self, recursive=False, future=False, client=None): """Make a bucket public. diff --git a/storage/tests/unit/test_bucket.py b/storage/tests/unit/test_bucket.py index 7406e8f9205d..03119bbfdf1b 100644 --- a/storage/tests/unit/test_bucket.py +++ b/storage/tests/unit/test_bucket.py @@ -866,6 +866,135 @@ def test_disable_website(self): bucket.disable_website() self.assertEqual(bucket._properties, UNSET) + def test_get_iam_policy(self): + from google.cloud.storage.iam import STORAGE_OWNER_ROLE + from google.cloud.storage.iam import STORAGE_EDITOR_ROLE + from google.cloud.storage.iam import STORAGE_VIEWER_ROLE + from google.cloud.iam import Policy + + NAME = 'name' + PATH = '/b/%s' % (NAME,) + ETAG = 'DEADBEEF' + VERSION = 17 + 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' + RETURNED = { + 'resourceId': PATH, + 'etag': ETAG, + 'version': VERSION, + 'bindings': [ + {'role': STORAGE_OWNER_ROLE, 'members': [OWNER1, OWNER2]}, + {'role': STORAGE_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, + {'role': STORAGE_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, + ], + } + EXPECTED = { + binding['role']: set(binding['members']) + for binding in RETURNED['bindings']} + connection = _Connection(RETURNED) + client = _Client(connection, None) + bucket = self._make_one(client=client, name=NAME) + + policy = bucket.get_iam_policy() + + self.assertIsInstance(policy, Policy) + self.assertEqual(policy.etag, RETURNED['etag']) + self.assertEqual(policy.version, RETURNED['version']) + self.assertEqual(dict(policy), EXPECTED) + + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'GET') + self.assertEqual(kw[0]['path'], '%s/iam' % (PATH,)) + + def test_set_iam_policy(self): + import operator + from google.cloud.storage.iam import STORAGE_OWNER_ROLE + from google.cloud.storage.iam import STORAGE_EDITOR_ROLE + from google.cloud.storage.iam import STORAGE_VIEWER_ROLE + from google.cloud.iam import Policy + + NAME = 'name' + PATH = '/b/%s' % (NAME,) + ETAG = 'DEADBEEF' + VERSION = 17 + 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' + BINDINGS = [ + {'role': STORAGE_OWNER_ROLE, 'members': [OWNER1, OWNER2]}, + {'role': STORAGE_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, + {'role': STORAGE_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, + ] + RETURNED = { + 'etag': ETAG, + 'version': VERSION, + 'bindings': BINDINGS, + } + policy = Policy() + for binding in BINDINGS: + policy[binding['role']] = binding['members'] + + connection = _Connection(RETURNED) + client = _Client(connection, None) + bucket = self._make_one(client=client, name=NAME) + + returned = bucket.set_iam_policy(policy) + + self.assertEqual(returned.etag, ETAG) + self.assertEqual(returned.version, VERSION) + self.assertEqual(dict(returned), dict(policy)) + + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PUT') + self.assertEqual(kw[0]['path'], '%s/iam' % (PATH,)) + sent = kw[0]['data'] + self.assertEqual(sent['resourceId'], PATH) + self.assertEqual(len(sent['bindings']), len(BINDINGS)) + key = operator.itemgetter('role') + for found, expected in zip( + sorted(sent['bindings'], key=key), + sorted(BINDINGS, key=key)): + self.assertEqual(found['role'], expected['role']) + self.assertEqual( + sorted(found['members']), sorted(expected['members'])) + + def test_test_iam_permissions(self): + from google.cloud.storage.iam import STORAGE_OBJECTS_LIST + from google.cloud.storage.iam import STORAGE_BUCKETS_GET + from google.cloud.storage.iam import STORAGE_BUCKETS_UPDATE + + NAME = 'name' + PATH = '/b/%s' % (NAME,) + PERMISSIONS = [ + STORAGE_OBJECTS_LIST, + STORAGE_BUCKETS_GET, + STORAGE_BUCKETS_UPDATE, + ] + ALLOWED = PERMISSIONS[1:] + RETURNED = {'permissions': ALLOWED} + connection = _Connection(RETURNED) + client = _Client(connection, None) + bucket = self._make_one(client=client, name=NAME) + + allowed = bucket.test_iam_permissions(PERMISSIONS) + + self.assertEqual(allowed, ALLOWED) + + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'GET') + self.assertEqual(kw[0]['path'], '%s/iam/testPermissions' % (PATH,)) + self.assertEqual(kw[0]['query_params'], {'permissions': PERMISSIONS}) + def test_make_public_defaults(self): from google.cloud.storage.acl import _ACLEntity From f94c1afa6c5a106ba403b59f306f58db2913cb81 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Tue, 18 Apr 2017 17:38:12 -0400 Subject: [PATCH 4/4] Add IAM methods for blobs. Closes #1679. --- storage/google/cloud/storage/blob.py | 78 +++++++++++++++ storage/tests/unit/test_blob.py | 138 +++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) diff --git a/storage/google/cloud/storage/blob.py b/storage/google/cloud/storage/blob.py index 2c0ddc8d71e4..04abbac71beb 100644 --- a/storage/google/cloud/storage/blob.py +++ b/storage/google/cloud/storage/blob.py @@ -36,6 +36,7 @@ from google.cloud.credentials import generate_signed_url from google.cloud.exceptions import NotFound from google.cloud.exceptions import make_exception +from google.cloud.iam import Policy from google.cloud.storage._helpers import _PropertyMixin from google.cloud.storage._helpers import _scalar_property from google.cloud.storage.acl import ObjectACL @@ -794,6 +795,83 @@ def create_resumable_upload_session( return resumable_upload_session_url + def get_iam_policy(self, client=None): + """Retrieve the IAM policy for the object. + + See: + https://cloud.google.com/storage/docs/json_api/v1/objects/getIamPolicy + + :type client: :class:`~google.cloud.storage.client.Client` or + ``NoneType`` + :param client: Optional. The client to use. If not passed, falls back + to the ``client`` stored on the current object's bucket. + + :rtype: :class:`google.cloud.iam.Policy` + :returns: the policy instance, based on the resource returned from + the ``getIamPolicy`` API request. + """ + client = self._require_client(client) + info = client._connection.api_request( + method='GET', + path='%s/iam' % (self.path,), + _target_object=None) + return Policy.from_api_repr(info) + + def set_iam_policy(self, policy, client=None): + """Update the IAM policy for the bucket. + + See: + https://cloud.google.com/storage/docs/json_api/v1/objects/setIamPolicy + + :type policy: :class:`google.cloud.iam.Policy` + :param policy: policy instance used to update bucket's IAM policy. + + :type client: :class:`~google.cloud.storage.client.Client` or + ``NoneType`` + :param client: Optional. The client to use. If not passed, falls back + to the ``client`` stored on the current bucket. + + :rtype: :class:`google.cloud.iam.Policy` + :returns: the policy instance, based on the resource returned from + the ``setIamPolicy`` API request. + """ + client = self._require_client(client) + resource = policy.to_api_repr() + resource['resourceId'] = self.path + info = client._connection.api_request( + method='PUT', + path='%s/iam' % (self.path,), + data=resource, + _target_object=None) + return Policy.from_api_repr(info) + + def test_iam_permissions(self, permissions, client=None): + """API call: test permissions + + See: + https://cloud.google.com/storage/docs/json_api/v1/objects/testIamPermissions + + :type permissions: list of string + :param permissions: the permissions to check + + :type client: :class:`~google.cloud.storage.client.Client` or + ``NoneType`` + :param client: Optional. The client to use. If not passed, falls back + to the ``client`` stored on the current bucket. + + :rtype: list of string + :returns: the permissions returned by the ``testIamPermissions`` API + request. + """ + client = self._require_client(client) + query = {'permissions': permissions} + path = '%s/iam/testPermissions' % (self.path,) + resp = client._connection.api_request( + method='GET', + path=path, + query_params=query) + return resp.get('permissions', []) + def make_public(self, client=None): """Make this blob public giving all users read access. diff --git a/storage/tests/unit/test_blob.py b/storage/tests/unit/test_blob.py index 45bfc901ee32..9f0722868a27 100644 --- a/storage/tests/unit/test_blob.py +++ b/storage/tests/unit/test_blob.py @@ -1173,6 +1173,144 @@ def test_create_resumable_upload_session_args(self): self.assertEqual( headers['Origin'], ORIGIN) + def test_get_iam_policy(self): + from six.moves.http_client import OK + from google.cloud.storage.iam import STORAGE_OWNER_ROLE + from google.cloud.storage.iam import STORAGE_EDITOR_ROLE + from google.cloud.storage.iam import STORAGE_VIEWER_ROLE + from google.cloud.iam import Policy + + BLOB_NAME = 'blob-name' + PATH = '/b/name/o/%s' % (BLOB_NAME,) + ETAG = 'DEADBEEF' + VERSION = 17 + 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' + RETURNED = { + 'resourceId': PATH, + 'etag': ETAG, + 'version': VERSION, + 'bindings': [ + {'role': STORAGE_OWNER_ROLE, 'members': [OWNER1, OWNER2]}, + {'role': STORAGE_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, + {'role': STORAGE_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, + ], + } + after = ({'status': OK}, RETURNED) + EXPECTED = { + binding['role']: set(binding['members']) + for binding in RETURNED['bindings']} + connection = _Connection(after) + client = _Client(connection) + bucket = _Bucket(client=client) + blob = self._make_one(BLOB_NAME, bucket=bucket) + + policy = blob.get_iam_policy() + + self.assertIsInstance(policy, Policy) + self.assertEqual(policy.etag, RETURNED['etag']) + self.assertEqual(policy.version, RETURNED['version']) + self.assertEqual(dict(policy), EXPECTED) + + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'GET') + self.assertEqual(kw[0]['path'], '%s/iam' % (PATH,)) + + def test_set_iam_policy(self): + import operator + from six.moves.http_client import OK + from google.cloud.storage.iam import STORAGE_OWNER_ROLE + from google.cloud.storage.iam import STORAGE_EDITOR_ROLE + from google.cloud.storage.iam import STORAGE_VIEWER_ROLE + from google.cloud.iam import Policy + + BLOB_NAME = 'blob-name' + PATH = '/b/name/o/%s' % (BLOB_NAME,) + ETAG = 'DEADBEEF' + VERSION = 17 + 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' + BINDINGS = [ + {'role': STORAGE_OWNER_ROLE, 'members': [OWNER1, OWNER2]}, + {'role': STORAGE_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]}, + {'role': STORAGE_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]}, + ] + RETURNED = { + 'etag': ETAG, + 'version': VERSION, + 'bindings': BINDINGS, + } + after = ({'status': OK}, RETURNED) + policy = Policy() + for binding in BINDINGS: + policy[binding['role']] = binding['members'] + + connection = _Connection(after) + client = _Client(connection) + bucket = _Bucket(client=client) + blob = self._make_one(BLOB_NAME, bucket=bucket) + + returned = blob.set_iam_policy(policy) + + self.assertEqual(returned.etag, ETAG) + self.assertEqual(returned.version, VERSION) + self.assertEqual(dict(returned), dict(policy)) + + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'PUT') + self.assertEqual(kw[0]['path'], '%s/iam' % (PATH,)) + sent = kw[0]['data'] + self.assertEqual(sent['resourceId'], PATH) + self.assertEqual(len(sent['bindings']), len(BINDINGS)) + key = operator.itemgetter('role') + for found, expected in zip( + sorted(sent['bindings'], key=key), + sorted(BINDINGS, key=key)): + self.assertEqual(found['role'], expected['role']) + self.assertEqual( + sorted(found['members']), sorted(expected['members'])) + + def test_test_iam_permissions(self): + from six.moves.http_client import OK + from google.cloud.storage.iam import STORAGE_OBJECTS_LIST + from google.cloud.storage.iam import STORAGE_BUCKETS_GET + from google.cloud.storage.iam import STORAGE_BUCKETS_UPDATE + + BLOB_NAME = 'blob-name' + PATH = '/b/name/o/%s' % (BLOB_NAME,) + PERMISSIONS = [ + STORAGE_OBJECTS_LIST, + STORAGE_BUCKETS_GET, + STORAGE_BUCKETS_UPDATE, + ] + ALLOWED = PERMISSIONS[1:] + RETURNED = {'permissions': ALLOWED} + after = ({'status': OK}, RETURNED) + connection = _Connection(after) + client = _Client(connection) + bucket = _Bucket(client=client) + blob = self._make_one(BLOB_NAME, bucket=bucket) + + allowed = blob.test_iam_permissions(PERMISSIONS) + + self.assertEqual(allowed, ALLOWED) + + kw = connection._requested + self.assertEqual(len(kw), 1) + self.assertEqual(kw[0]['method'], 'GET') + self.assertEqual(kw[0]['path'], '%s/iam/testPermissions' % (PATH,)) + self.assertEqual(kw[0]['query_params'], {'permissions': PERMISSIONS}) + def test_make_public(self): from six.moves.http_client import OK from google.cloud.storage.acl import _ACLEntity