Skip to content

Commit 60b4c38

Browse files
tseaverlukesneeringer
authored andcommitted
Add IAM methods for buckets. (googleapis#3309)
* Add role / permission constants for storage IAM. * Add IAM methods for buckets.
1 parent e89cda1 commit 60b4c38

File tree

4 files changed

+315
-0
lines changed

4 files changed

+315
-0
lines changed

storage/google/cloud/storage/bucket.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from google.cloud._helpers import _NOW
2727
from google.cloud._helpers import _rfc3339_to_datetime
2828
from google.cloud.exceptions import NotFound
29+
from google.cloud.iam import Policy
2930
from google.cloud.iterator import HTTPIterator
3031
from google.cloud.storage._helpers import _PropertyMixin
3132
from google.cloud.storage._helpers import _scalar_property
@@ -803,6 +804,83 @@ def disable_website(self):
803804
"""
804805
return self.configure_website(None, None)
805806

807+
def get_iam_policy(self, client=None):
808+
"""Retrieve the IAM policy for the bucket.
809+
810+
See:
811+
https://cloud.google.com/storage/docs/json_api/v1/buckets/getIamPolicy
812+
813+
:type client: :class:`~google.cloud.storage.client.Client` or
814+
``NoneType``
815+
:param client: Optional. The client to use. If not passed, falls back
816+
to the ``client`` stored on the current bucket.
817+
818+
:rtype: :class:`google.cloud.iam.Policy`
819+
:returns: the policy instance, based on the resource returned from
820+
the ``getIamPolicy`` API request.
821+
"""
822+
client = self._require_client(client)
823+
info = client._connection.api_request(
824+
method='GET',
825+
path='%s/iam' % (self.path,),
826+
_target_object=None)
827+
return Policy.from_api_repr(info)
828+
829+
def set_iam_policy(self, policy, client=None):
830+
"""Update the IAM policy for the bucket.
831+
832+
See:
833+
https://cloud.google.com/storage/docs/json_api/v1/buckets/setIamPolicy
834+
835+
:type policy: :class:`google.cloud.iam.Policy`
836+
:param policy: policy instance used to update bucket's IAM policy.
837+
838+
:type client: :class:`~google.cloud.storage.client.Client` or
839+
``NoneType``
840+
:param client: Optional. The client to use. If not passed, falls back
841+
to the ``client`` stored on the current bucket.
842+
843+
:rtype: :class:`google.cloud.iam.Policy`
844+
:returns: the policy instance, based on the resource returned from
845+
the ``setIamPolicy`` API request.
846+
"""
847+
client = self._require_client(client)
848+
resource = policy.to_api_repr()
849+
resource['resourceId'] = self.path
850+
info = client._connection.api_request(
851+
method='PUT',
852+
path='%s/iam' % (self.path,),
853+
data=resource,
854+
_target_object=None)
855+
return Policy.from_api_repr(info)
856+
857+
def test_iam_permissions(self, permissions, client=None):
858+
"""API call: test permissions
859+
860+
See:
861+
https://cloud.google.com/storage/docs/json_api/v1/buckets/testIamPermissions
862+
863+
:type permissions: list of string
864+
:param permissions: the permissions to check
865+
866+
:type client: :class:`~google.cloud.storage.client.Client` or
867+
``NoneType``
868+
:param client: Optional. The client to use. If not passed, falls back
869+
to the ``client`` stored on the current bucket.
870+
871+
:rtype: list of string
872+
:returns: the permissions returned by the ``testIamPermissions`` API
873+
request.
874+
"""
875+
client = self._require_client(client)
876+
query = {'permissions': permissions}
877+
path = '%s/iam/testPermissions' % (self.path,)
878+
resp = client._connection.api_request(
879+
method='GET',
880+
path=path,
881+
query_params=query)
882+
return resp.get('permissions', [])
883+
806884
def make_public(self, recursive=False, future=False, client=None):
807885
"""Make a bucket public.
808886

storage/google/cloud/storage/iam.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright 2017 Google Inc.
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+
"""Storage API IAM policy definitions
15+
16+
For allowed roles / permissions, see:
17+
https://cloud.google.com/storage/docs/access-control/iam
18+
"""
19+
20+
# Storage-specific IAM roles
21+
22+
STORAGE_OBJECT_CREATOR_ROLE = 'roles/storage.objectCreator'
23+
"""Role implying rights to create objects, but not delete or overwrite them."""
24+
25+
STORAGE_OBJECT_VIEWER_ROLE = 'roles/storage.objectViewer'
26+
"""Role implying rights to view object properties, excluding ACLs."""
27+
28+
STORAGE_OBJECT_ADMIN_ROLE = 'roles/storage.objectViewer'
29+
"""Role implying full control of objects."""
30+
31+
STORAGE_ADMIN_ROLE = 'roles/storage.admin'
32+
"""Role implying full control of objects and buckets."""
33+
34+
STORAGE_VIEWER_ROLE = 'Viewer'
35+
"""Can list buckets."""
36+
37+
STORAGE_EDITOR_ROLE = 'Editor'
38+
"""Can create, list, and delete buckets."""
39+
40+
STORAGE_OWNER_ROLE = 'Owners'
41+
"""Can create, list, and delete buckets."""
42+
43+
44+
# Storage-specific permissions
45+
46+
STORAGE_BUCKETS_CREATE = 'storage.buckets.create'
47+
"""Permission: create buckets."""
48+
49+
STORAGE_BUCKETS_DELETE = 'storage.buckets.delete'
50+
"""Permission: delete buckets."""
51+
52+
STORAGE_BUCKETS_GET = 'storage.buckets.get'
53+
"""Permission: read bucket metadata, excluding ACLs."""
54+
55+
STORAGE_BUCKETS_GET_IAM_POLICY = 'storage.buckets.getIamPolicy'
56+
"""Permission: read bucket ACLs."""
57+
58+
STORAGE_BUCKETS_LIST = 'storage.buckets.list'
59+
"""Permission: list buckets."""
60+
61+
STORAGE_BUCKETS_SET_IAM_POLICY = 'storage.buckets.setIamPolicy'
62+
"""Permission: update bucket ACLs."""
63+
64+
STORAGE_BUCKETS_UPDATE = 'storage.buckets.list'
65+
"""Permission: update buckets, excluding ACLS."""
66+
67+
STORAGE_OBJECTS_CREATE = 'storage.objects.create'
68+
"""Permission: add new objects to a bucket."""
69+
70+
STORAGE_OBJECTS_DELETE = 'storage.objects.delete'
71+
"""Permission: delete objects."""
72+
73+
STORAGE_OBJECTS_GET = 'storage.objects.get'
74+
"""Permission: read object data / metadata, excluding ACLs."""
75+
76+
STORAGE_OBJECTS_GET_IAM_POLICY = 'storage.objects.getIamPolicy'
77+
"""Permission: read object ACLs."""
78+
79+
STORAGE_OBJECTS_LIST = 'storage.objects.list'
80+
"""Permission: list objects in a bucket."""
81+
82+
STORAGE_OBJECTS_SET_IAM_POLICY = 'storage.objects.setIamPolicy'
83+
"""Permission: update object ACLs."""
84+
85+
STORAGE_OBJECTS_UPDATE = 'storage.objects.update'
86+
"""Permission: update object metadat, excluding ACLs."""

storage/tests/unit/test_bucket.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,135 @@ def test_disable_website(self):
866866
bucket.disable_website()
867867
self.assertEqual(bucket._properties, UNSET)
868868

869+
def test_get_iam_policy(self):
870+
from google.cloud.storage.iam import STORAGE_OWNER_ROLE
871+
from google.cloud.storage.iam import STORAGE_EDITOR_ROLE
872+
from google.cloud.storage.iam import STORAGE_VIEWER_ROLE
873+
from google.cloud.iam import Policy
874+
875+
NAME = 'name'
876+
PATH = '/b/%s' % (NAME,)
877+
ETAG = 'DEADBEEF'
878+
VERSION = 17
879+
OWNER1 = 'user:[email protected]'
880+
OWNER2 = 'group:[email protected]'
881+
EDITOR1 = 'domain:google.com'
882+
EDITOR2 = 'user:[email protected]'
883+
VIEWER1 = 'serviceAccount:[email protected]'
884+
VIEWER2 = 'user:[email protected]'
885+
RETURNED = {
886+
'resourceId': PATH,
887+
'etag': ETAG,
888+
'version': VERSION,
889+
'bindings': [
890+
{'role': STORAGE_OWNER_ROLE, 'members': [OWNER1, OWNER2]},
891+
{'role': STORAGE_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
892+
{'role': STORAGE_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
893+
],
894+
}
895+
EXPECTED = {
896+
binding['role']: set(binding['members'])
897+
for binding in RETURNED['bindings']}
898+
connection = _Connection(RETURNED)
899+
client = _Client(connection, None)
900+
bucket = self._make_one(client=client, name=NAME)
901+
902+
policy = bucket.get_iam_policy()
903+
904+
self.assertIsInstance(policy, Policy)
905+
self.assertEqual(policy.etag, RETURNED['etag'])
906+
self.assertEqual(policy.version, RETURNED['version'])
907+
self.assertEqual(dict(policy), EXPECTED)
908+
909+
kw = connection._requested
910+
self.assertEqual(len(kw), 1)
911+
self.assertEqual(kw[0]['method'], 'GET')
912+
self.assertEqual(kw[0]['path'], '%s/iam' % (PATH,))
913+
914+
def test_set_iam_policy(self):
915+
import operator
916+
from google.cloud.storage.iam import STORAGE_OWNER_ROLE
917+
from google.cloud.storage.iam import STORAGE_EDITOR_ROLE
918+
from google.cloud.storage.iam import STORAGE_VIEWER_ROLE
919+
from google.cloud.iam import Policy
920+
921+
NAME = 'name'
922+
PATH = '/b/%s' % (NAME,)
923+
ETAG = 'DEADBEEF'
924+
VERSION = 17
925+
OWNER1 = 'user:[email protected]'
926+
OWNER2 = 'group:[email protected]'
927+
EDITOR1 = 'domain:google.com'
928+
EDITOR2 = 'user:[email protected]'
929+
VIEWER1 = 'serviceAccount:[email protected]'
930+
VIEWER2 = 'user:[email protected]'
931+
BINDINGS = [
932+
{'role': STORAGE_OWNER_ROLE, 'members': [OWNER1, OWNER2]},
933+
{'role': STORAGE_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
934+
{'role': STORAGE_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
935+
]
936+
RETURNED = {
937+
'etag': ETAG,
938+
'version': VERSION,
939+
'bindings': BINDINGS,
940+
}
941+
policy = Policy()
942+
for binding in BINDINGS:
943+
policy[binding['role']] = binding['members']
944+
945+
connection = _Connection(RETURNED)
946+
client = _Client(connection, None)
947+
bucket = self._make_one(client=client, name=NAME)
948+
949+
returned = bucket.set_iam_policy(policy)
950+
951+
self.assertEqual(returned.etag, ETAG)
952+
self.assertEqual(returned.version, VERSION)
953+
self.assertEqual(dict(returned), dict(policy))
954+
955+
kw = connection._requested
956+
self.assertEqual(len(kw), 1)
957+
self.assertEqual(kw[0]['method'], 'PUT')
958+
self.assertEqual(kw[0]['path'], '%s/iam' % (PATH,))
959+
sent = kw[0]['data']
960+
self.assertEqual(sent['resourceId'], PATH)
961+
self.assertEqual(len(sent['bindings']), len(BINDINGS))
962+
key = operator.itemgetter('role')
963+
for found, expected in zip(
964+
sorted(sent['bindings'], key=key),
965+
sorted(BINDINGS, key=key)):
966+
self.assertEqual(found['role'], expected['role'])
967+
self.assertEqual(
968+
sorted(found['members']), sorted(expected['members']))
969+
970+
def test_test_iam_permissions(self):
971+
from google.cloud.storage.iam import STORAGE_OBJECTS_LIST
972+
from google.cloud.storage.iam import STORAGE_BUCKETS_GET
973+
from google.cloud.storage.iam import STORAGE_BUCKETS_UPDATE
974+
975+
NAME = 'name'
976+
PATH = '/b/%s' % (NAME,)
977+
PERMISSIONS = [
978+
STORAGE_OBJECTS_LIST,
979+
STORAGE_BUCKETS_GET,
980+
STORAGE_BUCKETS_UPDATE,
981+
]
982+
ALLOWED = PERMISSIONS[1:]
983+
RETURNED = {'permissions': ALLOWED}
984+
connection = _Connection(RETURNED)
985+
client = _Client(connection, None)
986+
bucket = self._make_one(client=client, name=NAME)
987+
988+
allowed = bucket.test_iam_permissions(PERMISSIONS)
989+
990+
self.assertEqual(allowed, ALLOWED)
991+
992+
kw = connection._requested
993+
self.assertEqual(len(kw), 1)
994+
self.assertEqual(kw[0]['method'], 'GET')
995+
self.assertEqual(kw[0]['path'], '%s/iam/testPermissions' % (PATH,))
996+
self.assertEqual(kw[0]['query_params'], {'permissions': PERMISSIONS})
997+
869998
def test_make_public_defaults(self):
870999
from google.cloud.storage.acl import _ACLEntity
8711000

storage/tests/unit/test_iam.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2017 Google Inc.
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 unittest
16+
17+
18+
class Test_constants(unittest.TestCase):
19+
20+
def test_ctor_defaults(self):
21+
from google.cloud.storage.iam import STORAGE_ADMIN_ROLE
22+
role = STORAGE_ADMIN_ROLE

0 commit comments

Comments
 (0)