Skip to content

Commit c7f36cb

Browse files
tseaverlukesneeringer
authored andcommitted
Add IAM handlers to blobs (googleapis#3311)
1 parent 60b4c38 commit c7f36cb

File tree

2 files changed

+216
-0
lines changed

2 files changed

+216
-0
lines changed

storage/google/cloud/storage/blob.py

+78
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from google.cloud.credentials import generate_signed_url
3737
from google.cloud.exceptions import NotFound
3838
from google.cloud.exceptions import make_exception
39+
from google.cloud.iam import Policy
3940
from google.cloud.storage._helpers import _PropertyMixin
4041
from google.cloud.storage._helpers import _scalar_property
4142
from google.cloud.storage.acl import ObjectACL
@@ -794,6 +795,83 @@ def create_resumable_upload_session(
794795

795796
return resumable_upload_session_url
796797

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

storage/tests/unit/test_blob.py

+138
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,144 @@ def test_create_resumable_upload_session_args(self):
11731173
self.assertEqual(
11741174
headers['Origin'], ORIGIN)
11751175

1176+
def test_get_iam_policy(self):
1177+
from six.moves.http_client import OK
1178+
from google.cloud.storage.iam import STORAGE_OWNER_ROLE
1179+
from google.cloud.storage.iam import STORAGE_EDITOR_ROLE
1180+
from google.cloud.storage.iam import STORAGE_VIEWER_ROLE
1181+
from google.cloud.iam import Policy
1182+
1183+
BLOB_NAME = 'blob-name'
1184+
PATH = '/b/name/o/%s' % (BLOB_NAME,)
1185+
ETAG = 'DEADBEEF'
1186+
VERSION = 17
1187+
OWNER1 = 'user:phred@example.com'
1188+
OWNER2 = 'group:cloud-logs@google.com'
1189+
EDITOR1 = 'domain:google.com'
1190+
EDITOR2 = 'user:phred@example.com'
1191+
VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
1192+
VIEWER2 = 'user:phred@example.com'
1193+
RETURNED = {
1194+
'resourceId': PATH,
1195+
'etag': ETAG,
1196+
'version': VERSION,
1197+
'bindings': [
1198+
{'role': STORAGE_OWNER_ROLE, 'members': [OWNER1, OWNER2]},
1199+
{'role': STORAGE_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
1200+
{'role': STORAGE_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
1201+
],
1202+
}
1203+
after = ({'status': OK}, RETURNED)
1204+
EXPECTED = {
1205+
binding['role']: set(binding['members'])
1206+
for binding in RETURNED['bindings']}
1207+
connection = _Connection(after)
1208+
client = _Client(connection)
1209+
bucket = _Bucket(client=client)
1210+
blob = self._make_one(BLOB_NAME, bucket=bucket)
1211+
1212+
policy = blob.get_iam_policy()
1213+
1214+
self.assertIsInstance(policy, Policy)
1215+
self.assertEqual(policy.etag, RETURNED['etag'])
1216+
self.assertEqual(policy.version, RETURNED['version'])
1217+
self.assertEqual(dict(policy), EXPECTED)
1218+
1219+
kw = connection._requested
1220+
self.assertEqual(len(kw), 1)
1221+
self.assertEqual(kw[0]['method'], 'GET')
1222+
self.assertEqual(kw[0]['path'], '%s/iam' % (PATH,))
1223+
1224+
def test_set_iam_policy(self):
1225+
import operator
1226+
from six.moves.http_client import OK
1227+
from google.cloud.storage.iam import STORAGE_OWNER_ROLE
1228+
from google.cloud.storage.iam import STORAGE_EDITOR_ROLE
1229+
from google.cloud.storage.iam import STORAGE_VIEWER_ROLE
1230+
from google.cloud.iam import Policy
1231+
1232+
BLOB_NAME = 'blob-name'
1233+
PATH = '/b/name/o/%s' % (BLOB_NAME,)
1234+
ETAG = 'DEADBEEF'
1235+
VERSION = 17
1236+
OWNER1 = 'user:phred@example.com'
1237+
OWNER2 = 'group:cloud-logs@google.com'
1238+
EDITOR1 = 'domain:google.com'
1239+
EDITOR2 = 'user:phred@example.com'
1240+
VIEWER1 = 'serviceAccount:1234-abcdef@service.example.com'
1241+
VIEWER2 = 'user:phred@example.com'
1242+
BINDINGS = [
1243+
{'role': STORAGE_OWNER_ROLE, 'members': [OWNER1, OWNER2]},
1244+
{'role': STORAGE_EDITOR_ROLE, 'members': [EDITOR1, EDITOR2]},
1245+
{'role': STORAGE_VIEWER_ROLE, 'members': [VIEWER1, VIEWER2]},
1246+
]
1247+
RETURNED = {
1248+
'etag': ETAG,
1249+
'version': VERSION,
1250+
'bindings': BINDINGS,
1251+
}
1252+
after = ({'status': OK}, RETURNED)
1253+
policy = Policy()
1254+
for binding in BINDINGS:
1255+
policy[binding['role']] = binding['members']
1256+
1257+
connection = _Connection(after)
1258+
client = _Client(connection)
1259+
bucket = _Bucket(client=client)
1260+
blob = self._make_one(BLOB_NAME, bucket=bucket)
1261+
1262+
returned = blob.set_iam_policy(policy)
1263+
1264+
self.assertEqual(returned.etag, ETAG)
1265+
self.assertEqual(returned.version, VERSION)
1266+
self.assertEqual(dict(returned), dict(policy))
1267+
1268+
kw = connection._requested
1269+
self.assertEqual(len(kw), 1)
1270+
self.assertEqual(kw[0]['method'], 'PUT')
1271+
self.assertEqual(kw[0]['path'], '%s/iam' % (PATH,))
1272+
sent = kw[0]['data']
1273+
self.assertEqual(sent['resourceId'], PATH)
1274+
self.assertEqual(len(sent['bindings']), len(BINDINGS))
1275+
key = operator.itemgetter('role')
1276+
for found, expected in zip(
1277+
sorted(sent['bindings'], key=key),
1278+
sorted(BINDINGS, key=key)):
1279+
self.assertEqual(found['role'], expected['role'])
1280+
self.assertEqual(
1281+
sorted(found['members']), sorted(expected['members']))
1282+
1283+
def test_test_iam_permissions(self):
1284+
from six.moves.http_client import OK
1285+
from google.cloud.storage.iam import STORAGE_OBJECTS_LIST
1286+
from google.cloud.storage.iam import STORAGE_BUCKETS_GET
1287+
from google.cloud.storage.iam import STORAGE_BUCKETS_UPDATE
1288+
1289+
BLOB_NAME = 'blob-name'
1290+
PATH = '/b/name/o/%s' % (BLOB_NAME,)
1291+
PERMISSIONS = [
1292+
STORAGE_OBJECTS_LIST,
1293+
STORAGE_BUCKETS_GET,
1294+
STORAGE_BUCKETS_UPDATE,
1295+
]
1296+
ALLOWED = PERMISSIONS[1:]
1297+
RETURNED = {'permissions': ALLOWED}
1298+
after = ({'status': OK}, RETURNED)
1299+
connection = _Connection(after)
1300+
client = _Client(connection)
1301+
bucket = _Bucket(client=client)
1302+
blob = self._make_one(BLOB_NAME, bucket=bucket)
1303+
1304+
allowed = blob.test_iam_permissions(PERMISSIONS)
1305+
1306+
self.assertEqual(allowed, ALLOWED)
1307+
1308+
kw = connection._requested
1309+
self.assertEqual(len(kw), 1)
1310+
self.assertEqual(kw[0]['method'], 'GET')
1311+
self.assertEqual(kw[0]['path'], '%s/iam/testPermissions' % (PATH,))
1312+
self.assertEqual(kw[0]['query_params'], {'permissions': PERMISSIONS})
1313+
11761314
def test_make_public(self):
11771315
from six.moves.http_client import OK
11781316
from google.cloud.storage.acl import _ACLEntity

0 commit comments

Comments
 (0)