Repository: libcloud Updated Branches: refs/heads/trunk 69d27cd5b -> f30b3e37f
[google storage] Google Storage permissions support Closes #860 Signed-off-by: Eric Johnson <[email protected]> Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/f30b3e37 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/f30b3e37 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/f30b3e37 Branch: refs/heads/trunk Commit: f30b3e37f61b5c31c1e5db6dbe47067dc3b40aad Parents: 69d27cd Author: Scott Crunkleton <[email protected]> Authored: Thu Sep 29 11:30:08 2016 -0700 Committer: Eric Johnson <[email protected]> Committed: Fri Sep 30 06:27:08 2016 +0000 ---------------------------------------------------------------------- CHANGES.rst | 7 + libcloud/storage/drivers/google_storage.py | 255 +++++++++++++- .../fixtures/google_storage/get_container.json | 13 + .../fixtures/google_storage/get_object.json | 18 + .../google_storage/list_container_acl.json | 74 ++++ .../google_storage/list_object_acl.json | 86 +++++ libcloud/test/storage/test_google_storage.py | 353 +++++++++++++++++-- 7 files changed, 765 insertions(+), 41 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/f30b3e37/CHANGES.rst ---------------------------------------------------------------------- diff --git a/CHANGES.rst b/CHANGES.rst index 781d22d..9fb2618 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -51,6 +51,13 @@ Load Balancer (GITHUB-848) [Anton Kozyrev] +Storage +~~~~~~~ + +- Added storage permissions for Google Cloud Storage + (GITHUB-860) + [Scott Crunkleton] + Changes in Apache Libcloud 1.2.1 -------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/f30b3e37/libcloud/storage/drivers/google_storage.py ---------------------------------------------------------------------- diff --git a/libcloud/storage/drivers/google_storage.py b/libcloud/storage/drivers/google_storage.py index 580a29c..91e117e 100644 --- a/libcloud/storage/drivers/google_storage.py +++ b/libcloud/storage/drivers/google_storage.py @@ -14,16 +14,20 @@ # limitations under the License. import copy +import json import email.utils from libcloud.common.base import ConnectionUserAndKey from libcloud.common.google import GoogleAuthType from libcloud.common.google import GoogleOAuth2Credential +from libcloud.common.google import GoogleResponse +from libcloud.common.types import ProviderError from libcloud.storage.drivers.s3 import BaseS3Connection from libcloud.storage.drivers.s3 import BaseS3StorageDriver from libcloud.storage.drivers.s3 import S3RawResponse from libcloud.storage.drivers.s3 import S3Response +from libcloud.utils.py3 import httplib # Docs are a lie. Actual namespace returned is different that the one listed # in the docs. @@ -32,6 +36,21 @@ API_VERSION = '2006-03-01' NAMESPACE = 'http://doc.s3.amazonaws.com/%s' % (API_VERSION) +class ContainerPermissions(object): + values = ['NONE', 'READER', 'WRITER', 'OWNER'] + NONE = 0 + READER = 1 + WRITER = 2 + OWNER = 3 + + +class ObjectPermissions(object): + values = ['NONE', 'READER', 'OWNER'] + NONE = 0 + READER = 1 + OWNER = 2 + + class GoogleStorageConnection(ConnectionUserAndKey): """ Represents a single connection to the Google storage API endpoint. @@ -45,7 +64,7 @@ class GoogleStorageConnection(ConnectionUserAndKey): rawResponseCls = S3RawResponse PROJECT_ID_HEADER = 'x-goog-project-id' - def __init__(self, user_id, key, secure, auth_type=None, + def __init__(self, user_id, key, secure=True, auth_type=None, credential_file=None, **kwargs): self.auth_type = auth_type or GoogleAuthType.guess_type(user_id) if GoogleAuthType.is_oauth2(self.auth_type): @@ -65,7 +84,7 @@ class GoogleStorageConnection(ConnectionUserAndKey): return headers def get_project(self): - return getattr(self.driver, 'project') + return getattr(self.driver, 'project', None) def pre_connect_hook(self, params, headers): if self.auth_type == GoogleAuthType.GCS_S3: @@ -102,6 +121,28 @@ class GoogleStorageConnection(ConnectionUserAndKey): vendor_prefix=GoogleStorageDriver.http_vendor_prefix) +class GCSResponse(GoogleResponse): + pass + + +class GoogleStorageJSONConnection(GoogleStorageConnection): + """ + Represents a single connection to the Google storage JSON API endpoint. + + This can either authenticate via the Google OAuth2 methods or via + the S3 HMAC interoperability method. + """ + host = 'www.googleapis.com' + responseCls = GCSResponse + rawResponseCls = None + + def add_default_headers(self, headers): + headers = super(GoogleStorageJSONConnection, self).add_default_headers( + headers) + headers['Content-Type'] = 'application/json' + return headers + + class GoogleStorageDriver(BaseS3StorageDriver): """ Driver for Google Cloud Storage. @@ -121,7 +162,7 @@ class GoogleStorageDriver(BaseS3StorageDriver): From GCE instance:: - driver = GoogleStorageDriver(key=foo , secret=bar, ...) + driver = GoogleStorageDriver(key=foo, secret=bar, ...) Can also authenticate via Google Cloud Storage's S3 HMAC interoperability API. S3 user keys are 20 alphanumeric characters, starting with GOOG. @@ -131,9 +172,10 @@ class GoogleStorageDriver(BaseS3StorageDriver): driver = GoogleStorageDriver(key='GOOG0123456789ABCXYZ', secret=key_secret) """ - name = 'Google Storage' - website = 'http://cloud.google.com/' + name = 'Google Cloud Storage' + website = 'http://cloud.google.com/storage' connectionCls = GoogleStorageConnection + jsonConnectionCls = GoogleStorageJSONConnection hash_type = 'md5' namespace = NAMESPACE supports_chunked_encoding = False @@ -141,5 +183,206 @@ class GoogleStorageDriver(BaseS3StorageDriver): http_vendor_prefix = 'x-goog' def __init__(self, key, secret=None, project=None, **kwargs): - self.project = project super(GoogleStorageDriver, self).__init__(key, secret, **kwargs) + self.project = project + + self.json_connection = GoogleStorageJSONConnection( + key, secret, **kwargs) + + def _get_container_permissions(self, container_name): + """ + Return the container permissions for the current authenticated user. + + :param container_name: The container name. + :param container_name: ``str`` + + :return: The permissions on the container. + :rtype: ``int`` from ContainerPermissions + """ + # Try OWNER permissions first: try listing the bucket ACL. + # FORBIDDEN -> exists, but not an OWNER. + # NOT_FOUND -> bucket DNE, return NONE. + try: + self.json_connection.request( + '/storage/v1/b/%s/acl' % container_name) + return ContainerPermissions.OWNER + except ProviderError as e: + if e.http_code == httplib.FORBIDDEN: + pass + elif e.http_code == httplib.NOT_FOUND: + return ContainerPermissions.NONE + else: + raise + + # Try WRITER permissions with a noop request: try delete with an + # impossible precondition. Authorization is checked before file + # existence or preconditions. So, if we get a NOT_FOUND or a + # PRECONDITION_FAILED, then we must be authorized. + try: + self.json_connection.request( + '/storage/v1/b/%s/o/writecheck' % container_name, + headers={'x-goog-if-generation-match': '0'}, method='DELETE') + except ProviderError as e: + if e.http_code in [httplib.NOT_FOUND, httplib.PRECONDITION_FAILED]: + return ContainerPermissions.WRITER + elif e.http_code != httplib.FORBIDDEN: + raise + + # Last, try READER permissions: try getting container metadata. + try: + self.json_connection.request('/storage/v1/b/%s' % container_name) + return ContainerPermissions.READER + except ProviderError as e: + if e.http_code not in [httplib.FORBIDDEN, httplib.NOT_FOUND]: + raise + + return ContainerPermissions.NONE + + def _get_user(self): + """Gets this drivers' authenticated user, if any.""" + oauth2_creds = getattr(self.connection, 'oauth2_credential') + if oauth2_creds: + return oauth2_creds.user_id + else: + return None + + def _get_object_permissions(self, container_name, object_name): + """ + Return the object permissions for the current authenticated user. + If the object does not exist, or no object_name is given, return the + default object permissions. + + :param container_name: The container name. + :type container_name: ``str`` + + :param object_name: The object name. + :type object_name: ``str`` + + :return: The permissions on the object or default object permissions. + :rtype: ``int`` from ObjectPermissions + """ + # Try OWNER permissions first: try listing the object ACL. + try: + self.json_connection.request( + '/storage/v1/b/%s/o/%s/acl' % (container_name, object_name)) + return ObjectPermissions.OWNER + except ProviderError as e: + if e.http_code not in [httplib.FORBIDDEN, httplib.NOT_FOUND]: + raise + + # Try READER permissions: try getting the object. + try: + self.json_connection.request( + '/storage/v1/b/%s/o/%s' % (container_name, object_name)) + return ObjectPermissions.READER + except ProviderError as e: + if e.http_code not in [httplib.FORBIDDEN, httplib.NOT_FOUND]: + raise + + return ObjectPermissions.NONE + + def ex_delete_permissions(self, container_name, object_name=None, + entity=None): + """ + Delete permissions for an ACL entity on a container or object. + + :param container_name: The container name. + :type container_name: ``str`` + + :param object_name: The object name. Optional. Not providing an object + will delete a container permission. + :type object_name: ``str`` + + :param entity: The entity to whose permission will be deleted. + Optional. If not provided, the role will be applied to the + authenticated user, if using an OAuth2 authentication scheme. + :type entity: ``str`` or ``None`` + """ + if not entity: + user_id = self._get_user() + if not user_id: + raise ValueError( + 'Must provide an entity. Driver is not using an ' + 'authenticated user.') + else: + entity = 'user-%s' % user_id + + if object_name: + url = ('/storage/v1/b/%s/o/%s/acl/%s' % + (container_name, object_name, entity)) + else: + url = '/storage/v1/b/%s/acl/%s' % (container_name, entity) + + self.json_connection.request(url, method='DELETE') + + def ex_get_permissions(self, container_name, object_name=None): + """ + Return the permissions for the currently authenticated user. + + :param container_name: The container name. + :type container_name: ``str`` + + :param object_name: The object name. Optional. Not providing an object + will return only container permissions. + :type object_name: ``str`` or ``None`` + + :return: A tuple of container and object permissions. + :rtype: ``tuple`` of (``int``, ``int`` or ``None``) from + ContainerPermissions and ObjectPermissions, respectively. + """ + obj_perms = self._get_object_permissions( + container_name, object_name) if object_name else None + return self._get_container_permissions(container_name), obj_perms + + def ex_set_permissions(self, container_name, object_name=None, + entity=None, role=None): + """ + Set the permissions for an ACL entity on a container or an object. + + :param container_name: The container name. + :type container_name: ``str`` + + :param object_name: The object name. Optional. Not providing an object + will apply the acl to the container. + :type object_name: ``str`` + + :param entity: The entity to which apply the role. Optional. If not + provided, the role will be applied to the authenticated user, if + using an OAuth2 authentication scheme. + :type entity: ``str`` + + :param role: The permission/role to set on the entity. + :type role: ``int`` from ContainerPermissions or ObjectPermissions + or ``str``. + + :raises ValueError: If no entity was given, but was required. Or if + the role isn't valid for the bucket or object. + """ + if isinstance(role, int): + perms = ObjectPermissions if object_name else ContainerPermissions + try: + role = perms.values[role] + except IndexError: + raise ValueError( + '%s is not a valid role level for container=%s object=%s' % + (role, container_name, object_name)) + elif not isinstance(role, str): + raise ValueError('%s is not a valid permission.' % role) + + if not entity: + user_id = self._get_user() + if not user_id: + raise ValueError( + 'Must provide an entity. Driver is not using an ' + 'authenticated user.') + else: + entity = 'user-%s' % user_id + + if object_name: + url = '/storage/v1/b/%s/o/%s/acl' % (container_name, object_name) + else: + url = '/storage/v1/b/%s/acl' % container_name + + self.json_connection.request( + url, method='POST', + data=json.dumps({'role': role, 'entity': entity})) http://git-wip-us.apache.org/repos/asf/libcloud/blob/f30b3e37/libcloud/test/storage/fixtures/google_storage/get_container.json ---------------------------------------------------------------------- diff --git a/libcloud/test/storage/fixtures/google_storage/get_container.json b/libcloud/test/storage/fixtures/google_storage/get_container.json new file mode 100644 index 0000000..9eb85b1 --- /dev/null +++ b/libcloud/test/storage/fixtures/google_storage/get_container.json @@ -0,0 +1,13 @@ +{ + "kind": "storage#bucket", + "id": "test-bucket", + "selfLink": "https://www.googleapis.com/storage/v1/b/test-bucket", + "projectNumber": "123", + "name": "test-bucket", + "timeCreated": "2015-12-17T21:38:31.413Z", + "updated": "2015-12-17T21:38:31.413Z", + "metageneration": "123", + "location": "US", + "storageClass": "STANDARD", + "etag": "etag" +} http://git-wip-us.apache.org/repos/asf/libcloud/blob/f30b3e37/libcloud/test/storage/fixtures/google_storage/get_object.json ---------------------------------------------------------------------- diff --git a/libcloud/test/storage/fixtures/google_storage/get_object.json b/libcloud/test/storage/fixtures/google_storage/get_object.json new file mode 100644 index 0000000..324228d --- /dev/null +++ b/libcloud/test/storage/fixtures/google_storage/get_object.json @@ -0,0 +1,18 @@ +{ + "kind": "storage#object", + "id": "test-bucket/test-object/12345", + "selfLink": "https://www.googleapis.com/storage/v1/b/test-bucket/o/test-object ", + "name": "test-object", + "bucket": "test-bucket", + "generation": "12345", + "metageneration": "67890", + "contentType": "text/plain", + "timeCreated": "2016-09-14T01:19:33.850Z", + "updated": "2016-09-14T01:19:33.850Z", + "storageClass": "STANDARD", + "size": "123", + "md5Hash": "wVenkDHhxA+FkxgpvF/FUg==", + "mediaLink": "https://www.googleapis.com/download/storage/v1/b/test-bucket/o/test-object?generation=12345&alt=media", + "crc32": "+x0GyA==", + "etag": "etag" +} http://git-wip-us.apache.org/repos/asf/libcloud/blob/f30b3e37/libcloud/test/storage/fixtures/google_storage/list_container_acl.json ---------------------------------------------------------------------- diff --git a/libcloud/test/storage/fixtures/google_storage/list_container_acl.json b/libcloud/test/storage/fixtures/google_storage/list_container_acl.json new file mode 100644 index 0000000..97f57be --- /dev/null +++ b/libcloud/test/storage/fixtures/google_storage/list_container_acl.json @@ -0,0 +1,74 @@ +{ + "kind": "storage#bucketAccessControls", + "items": [ + { + "kind": "storage#bucketAccessControl", + "id": "test-bucket/project-owners-123", + "selfLink": "https://www.googleapis.com/storage/v1/b/test-bucket/acl/project-owners-123 ", + "bucket": "test-bucket", + "entity": "project-owners-123", + "role": "OWNER", + "projectTeam": { + "projectNumber": "123", + "team": "owners" + }, + "etag": "etag" + }, + { + "kind": "storage#bucketAccessControl", + "id": "test-bucket/project-editors-123", + "selfLink": "https://www.googleapis.com/storage/v1/b/test-bucket/acl/project-editors-123 ", + "bucket": "test-bucket", + "entity": "project-editors-123", + "role": "WRITER", + "projectTeam": { + "projectNumber": "123", + "team": "editors" + }, + "etag": "etag" + }, + { + "kind": "storage#bucketAccessControl", + "id": "test-bucket/project-viewers-123", + "selfLink": "https://www.googleapis.com/storage/v1/b/test-bucket/acl/project-viewers-123 ", + "bucket": "test-bucket", + "entity": "project-viewers-123", + "role": "READER", + "projectTeam": { + "projectNumber": "123", + "team": "viewers" + }, + "etag": "etag" + }, + { + "kind": "storage#bucketAccessControl", + "id": "test-bucket/[email protected]", + "selfLink": "https://www.googleapis.com/storage/v1/b/test-bucket/acl/[email protected] ", + "bucket": "test-bucket", + "entity": "[email protected]", + "role": "OWNER", + "email": "[email protected]", + "etag": "etag" + }, + { + "kind": "storage#bucketAccessControl", + "id": "test-bucket/domain-test.com", + "selfLink": "https://www.googleapis.com/storage/v1/b/test-bucket/acl/domain-test.com ", + "bucket": "test-bucket", + "entity": "domain-test.com", + "role": "OWNER", + "domain": "test.com", + "etag": "etag" + }, + { + "kind": "storage#bucketAccessControl", + "id": "test-bucket/[email protected]", + "selfLink": "https://www.googleapis.com/storage/v1/b/test-bucket/acl/[email protected] ", + "bucket": "test-bucket", + "entity": "[email protected]", + "role": "OWNER", + "email": "[email protected]", + "etag": "etag" + } + ] +} http://git-wip-us.apache.org/repos/asf/libcloud/blob/f30b3e37/libcloud/test/storage/fixtures/google_storage/list_object_acl.json ---------------------------------------------------------------------- diff --git a/libcloud/test/storage/fixtures/google_storage/list_object_acl.json b/libcloud/test/storage/fixtures/google_storage/list_object_acl.json new file mode 100644 index 0000000..d620139 --- /dev/null +++ b/libcloud/test/storage/fixtures/google_storage/list_object_acl.json @@ -0,0 +1,86 @@ +{ + "kind": "storage#objectAccessControls", + "items": [ + { + "kind": "storage#objectAccessControl", + "id": "test-bucket/test-object/12345/project-owners-123", + "selfLink": "https://www.googleapis.com/storage/v1/b/test-bucket/o/test-object/acl/project-owners-123 ", + "bucket": "test-bucket", + "object": "test-object", + "generation": "12345", + "entity": "project-owners-123", + "role": "OWNER", + "projectTeam": { + "projectNumber": "123", + "team": "owners" + }, + "etag": "etag" + }, + { + "kind": "storage#objectAccessControl", + "id": "test-bucket/test-object/12345/project-editors-123", + "selfLink": "https://www.googleapis.com/storage/v1/b/test-bucket/o/test-object/acl/project-editors-123 ", + "bucket": "test-bucket", + "object": "test-object", + "generation": "12345", + "entity": "project-editors-123", + "role": "OWNER", + "projectTeam": { + "projectNumber": "123", + "team": "editors" + }, + "etag": "etag" + }, + { + "kind": "storage#objectAccessControl", + "id": "test-bucket/test-object/12345/project-viewers-123", + "selfLink": "https://www.googleapis.com/storage/v1/b/test-bucket/o/test-object/acl/project-viewers-123 ", + "bucket": "test-bucket", + "object": "test-object", + "generation": "12345", + "entity": "project-viewers-123", + "role": "READER", + "projectTeam": { + "projectNumber": "123", + "team": "viewers" + }, + "etag": "etag" + }, + { + "kind": "storage#objectAccessControl", + "id": "test-bucket/test-object/12345/[email protected]", + "selfLink": "https://www.googleapis.com/storage/v1/b/test-bucket/o/test-object/acl/[email protected] ", + "bucket": "test-bucket", + "object": "test-object", + "generation": "12345", + "entity": "[email protected]", + "role": "OWNER", + "email": "[email protected]", + "etag": "etag" + }, + { + "kind": "storage#objectAccessControl", + "id": "test-bucket/test-object/12345/domain-test.com", + "selfLink": "https://www.googleapis.com/storage/v1/b/test-bucket/o/test-object/acl/domain-test.com ", + "bucket": "test-bucket", + "object": "test-object", + "generation": "12345", + "entity": "domain-test.com", + "role": "OWNER", + "domain": "test.com", + "etag": "etag" + }, + { + "kind": "storage#objectAccessControl", + "id": "test-bucket/test-object/12345/[email protected]", + "selfLink": "https://www.googleapis.com/storage/v1/b/test-bucket/o/test-object/acl/[email protected] ", + "bucket": "test-bucket", + "object": "test-object", + "generation": "12345", + "entity": "[email protected]", + "role": "OWNER", + "email": "[email protected]", + "etag": "etag" + } + ] +} http://git-wip-us.apache.org/repos/asf/libcloud/blob/f30b3e37/libcloud/test/storage/test_google_storage.py ---------------------------------------------------------------------- diff --git a/libcloud/test/storage/test_google_storage.py b/libcloud/test/storage/test_google_storage.py index 2668c50..a2ba5fe 100644 --- a/libcloud/test/storage/test_google_storage.py +++ b/libcloud/test/storage/test_google_storage.py @@ -14,7 +14,9 @@ # limitations under the License. import copy +import json import mock +import re import sys import unittest @@ -22,6 +24,7 @@ import email.utils from libcloud.common.google import GoogleAuthType from libcloud.storage.drivers import google_storage +from libcloud.test import StorageMockHttp from libcloud.test.common.test_google import GoogleTestCase from libcloud.test.file_fixtures import StorageFileFixtures from libcloud.test.secrets import STORAGE_GOOGLE_STORAGE_PARAMS @@ -29,11 +32,27 @@ from libcloud.test.storage.test_s3 import S3Tests, S3MockHttp from libcloud.utils.py3 import httplib CONN_CLS = google_storage.GoogleStorageConnection -STORAGE_CLS = google_storage.GoogleStorageDriver +JSON_CONN_CLS = google_storage.GoogleStorageJSONConnection TODAY = email.utils.formatdate(usegmt=True) +def _error_helper(code, headers): + message = httplib.responses[code] + body = { + 'error': { + 'errors': [ + { + 'code': code, + 'message': message, + 'reason': message, + }, + ], + }, + } + return code, json.dumps(body), headers, httplib.responses[code] + + class GoogleStorageMockHttp(S3MockHttp): fixtures = StorageFileFixtures('google_storage') @@ -49,12 +68,115 @@ class GoogleStorageMockHttp(S3MockHttp): 'last-modified': 'Thu, 13 Sep 2012 07:13:22 GMT' } - return ( - httplib.OK, - body, - headers, - httplib.responses[httplib.OK] - ) + return httplib.OK, body, headers, httplib.responses[httplib.OK] + + +class GoogleStorageJSONMockHttp(StorageMockHttp): + """ + Extracts bucket and object out of requests and routes to methods of the + forms (bucket, object, entity, and type are sanitized values + {'-', '.', '/' are replaced with '_'}): + + _<bucket>[_<type>] + _<bucket>_acl[_entity][_<type>] + _<bucket>_defaultObjectAcl[_<entity>][_<type>] + _<bucket>_<object>[_<type>] + _<bucket>_<object>_acl[_<entity>][_<type>] + + Ugly example: + /storage/v1/b/test-bucket/o/test-object/acl/test-entity + with type='FOO' yields + _test_bucket_test_object_acl_test_entity_FOO + """ + fixtures = StorageFileFixtures('google_storage') + base_headers = {} + + # Path regex captures bucket, object, defaultObjectAcl, and acl values. + path_rgx = re.compile( + r'/storage/[^/]+/b/([^/]+)' + r'(?:/(defaultObjectAcl(?:/[^/]+)?$)|' + r'(?:/o/(.+?))?(?:/(acl(?:/[^/]+)?))?$)') + + # Permissions to use when handling requests. + bucket_perms = google_storage.ContainerPermissions.NONE + object_perms = google_storage.ObjectPermissions.NONE + + _FORBIDDEN = _error_helper(httplib.FORBIDDEN, base_headers) + _NOT_FOUND = _error_helper(httplib.NOT_FOUND, base_headers) + _PRECONDITION_FAILED = _error_helper( + httplib.PRECONDITION_FAILED, base_headers) + + def _get_method_name(self, type, use_param, qs, path): + match = self.path_rgx.match(path) + if not match: + raise ValueError('%s is not a valid path.' % path) + + joined_groups = '_'.join([g for g in match.groups() if g]) + if type: + meth_name = '_%s_%s' % (joined_groups, type) + else: + meth_name = '_%s' % joined_groups + # Return sanitized method name. + return meth_name.replace('/', '_').replace('.', '_').replace('-', '_') + + def _response_helper(self, fixture): + body = self.fixtures.load(fixture) + return httplib.OK, body, {}, httplib.responses[httplib.OK] + + #################### + # Request handlers # + #################### + def _test_bucket(self, method, url, body, headers): + """Bucket request.""" + if method != 'GET': + raise NotImplementedError('%s is not implemented.' % method) + + if self.bucket_perms < google_storage.ContainerPermissions.READER: + return self._FORBIDDEN + else: + return self._response_helper('get_container.json') + + def _test_bucket_acl(self, method, url, body, headers): + """Bucket list ACL request.""" + if method != 'GET': + raise NotImplementedError('%s is not implemented.' % method) + + if self.bucket_perms < google_storage.ContainerPermissions.OWNER: + return self._FORBIDDEN + else: + return self._response_helper('list_container_acl.json') + + def _test_bucket_test_object(self, method, url, body, headers): + """Object request.""" + if method != 'GET': + raise NotImplementedError('%s is not implemented.' % method) + + if self.object_perms < google_storage.ObjectPermissions.READER: + return self._FORBIDDEN + else: + return self._response_helper('get_object.json') + + def _test_bucket_test_object_acl(self, method, url, body, headers): + """Object list ACL request.""" + if method != 'GET': + raise NotImplementedError('%s is not implemented.' % method) + + if self.object_perms < google_storage.ObjectPermissions.OWNER: + return self._FORBIDDEN + else: + return self._response_helper('list_object_acl.json') + + def _test_bucket_writecheck(self, method, url, body, headers): + gen_match = headers.get('x-goog-if-generation-match') + if method != 'DELETE' or gen_match != '0': + msg = ('Improper write check delete strategy. method: %s, ' + 'headers: %s' % (method, headers)) + raise ValueError(msg) + + if self.bucket_perms < google_storage.ContainerPermissions.WRITER: + return self._FORBIDDEN + else: + return self._PRECONDITION_FAILED class GoogleStorageConnectionTest(GoogleTestCase): @@ -66,23 +188,25 @@ class GoogleStorageConnectionTest(GoogleTestCase): project = 'foo-project' # Modify headers when there is no project. - conn = CONN_CLS('foo_user', 'bar_key', secure=True, - auth_type=GoogleAuthType.GCS_S3) - conn.get_project = lambda: None + conn = CONN_CLS( + 'foo_user', 'bar_key', secure=True, + auth_type=GoogleAuthType.GCS_S3) + conn.get_project = mock.Mock(return_value=None) headers = dict(starting_headers) headers['Date'] = TODAY - self.assertEqual(conn.add_default_headers(dict(starting_headers)), - headers) + self.assertEqual( + conn.add_default_headers(dict(starting_headers)), headers) # Modify headers when there is a project. - conn = CONN_CLS('foo_user', 'bar_key', secure=True, - auth_type=GoogleAuthType.GCS_S3) - conn.get_project = lambda: project + conn = CONN_CLS( + 'foo_user', 'bar_key', secure=True, + auth_type=GoogleAuthType.GCS_S3) + conn.get_project = mock.Mock(return_value=project) headers = dict(starting_headers) headers['Date'] = TODAY headers[CONN_CLS.PROJECT_ID_HEADER] = project - self.assertEqual(conn.add_default_headers(dict(starting_headers)), - headers) + self.assertEqual( + conn.add_default_headers(dict(starting_headers)), headers) @mock.patch('libcloud.storage.drivers.s3.' 'BaseS3Connection.get_auth_signature') @@ -105,20 +229,17 @@ class GoogleStorageConnectionTest(GoogleTestCase): 'other': 'lower this!' } - conn = CONN_CLS('foo_user', 'bar_key', secure=True, - auth_type=GoogleAuthType.GCS_S3) + conn = CONN_CLS( + 'foo_user', 'bar_key', secure=True, + auth_type=GoogleAuthType.GCS_S3) conn.method = 'GET' conn.action = '/path' result = conn._get_s3_auth_signature(starting_params, starting_headers) self.assertNotEqual(starting_headers, modified_headers) self.assertEqual(result, 'mock signature!') mock_s3_auth_sig_method.assert_called_once_with( - method='GET', - headers=modified_headers, - params=starting_params, - expires=None, - secret_key='bar_key', - path='/path', + method='GET', headers=modified_headers, params=starting_params, + expires=None, secret_key='bar_key', path='/path', vendor_prefix='x-goog' ) @@ -131,15 +252,15 @@ class GoogleStorageConnectionTest(GoogleTestCase): starting_params = {'starting': 'params'} starting_headers = {'starting': 'headers'} - conn = CONN_CLS('foo_user', 'bar_key', secure=True, - auth_type=GoogleAuthType.GCE) + conn = CONN_CLS( + 'foo_user', 'bar_key', secure=True, auth_type=GoogleAuthType.GCE) conn._get_s3_auth_signature = mock.Mock() conn.oauth2_credential = mock.Mock() conn.oauth2_credential.access_token = 'Access_Token!' expected_headers = dict(starting_headers) expected_headers['Authorization'] = 'Bearer Access_Token!' - result = conn.pre_connect_hook(dict(starting_params), - dict(starting_headers)) + result = conn.pre_connect_hook( + dict(starting_params), dict(starting_headers)) self.assertEqual(result, (starting_params, expected_headers)) def test_pre_connect_hook_hmac(self): @@ -155,8 +276,9 @@ class GoogleStorageConnectionTest(GoogleTestCase): fake_hmac_method.headers_passed = copy.deepcopy(headers) return 'fake signature!' - conn = CONN_CLS('foo_user', 'bar_key', secure=True, - auth_type=GoogleAuthType.GCS_S3) + conn = CONN_CLS( + 'foo_user', 'bar_key', secure=True, + auth_type=GoogleAuthType.GCS_S3) conn._get_s3_auth_signature = fake_hmac_method conn.action = 'GET' conn.method = '/foo' @@ -165,8 +287,8 @@ class GoogleStorageConnectionTest(GoogleTestCase): '%s %s:%s' % (google_storage.SIGNATURE_IDENTIFIER, 'foo_user', 'fake signature!') ) - result = conn.pre_connect_hook(dict(starting_params), - dict(starting_headers)) + result = conn.pre_connect_hook( + dict(starting_params), dict(starting_headers)) self.assertEqual(result, (dict(starting_params), expected_headers)) self.assertEqual(fake_hmac_method.params_passed, starting_params) self.assertEqual(fake_hmac_method.headers_passed, starting_headers) @@ -174,10 +296,14 @@ class GoogleStorageConnectionTest(GoogleTestCase): class GoogleStorageTests(S3Tests, GoogleTestCase): - driver_type = STORAGE_CLS + driver_type = google_storage.GoogleStorageDriver driver_args = STORAGE_GOOGLE_STORAGE_PARAMS mock_response_klass = GoogleStorageMockHttp - driver = google_storage.GoogleStorageDriver + + def setUp(self): + super(GoogleStorageTests, self).setUp() + self.driver_type.jsonConnectionCls.conn_classes = ( + None, GoogleStorageJSONMockHttp) def test_billing_not_enabled(self): # TODO @@ -187,6 +313,163 @@ class GoogleStorageTests(S3Tests, GoogleTestCase): # Not supported on Google Storage pass + def test_delete_permissions(self): + mock_request = mock.Mock() + self.driver.json_connection.request = mock_request + + # Test deleting object permissions. + self.driver.ex_delete_permissions( + 'bucket', 'object', entity='user-foo') + url = '/storage/v1/b/bucket/o/object/acl/user-foo' + mock_request.assert_called_once_with(url, method='DELETE') + + # Test deleting bucket permissions. + mock_request.reset_mock() + self.driver.ex_delete_permissions('bucket', entity='user-foo') + url = '/storage/v1/b/bucket/acl/user-foo' + mock_request.assert_called_once_with(url, method='DELETE') + + def test_delete_permissions_no_entity(self): + mock_request = mock.Mock() + mock_get_user = mock.Mock(return_value=None) + self.driver._get_user = mock_get_user + self.driver.json_connection.request = mock_request + + # Test deleting permissions on an object with no entity. + self.assertRaises( + ValueError, self.driver.ex_delete_permissions, 'bucket', 'object') + + # Test deleting permissions on an bucket with no entity. + self.assertRaises( + ValueError, self.driver.ex_delete_permissions, 'bucket') + + mock_request.assert_not_called() + + # Test deleting permissions on an object with a default entity. + mock_get_user.return_value = '[email protected]' + self.driver.ex_delete_permissions('bucket', 'object') + url = '/storage/v1/b/bucket/o/object/acl/[email protected]' + mock_request.assert_called_once_with(url, method='DELETE') + + # Test deleting permissions on an bucket with a default entity. + mock_request.reset_mock() + mock_get_user.return_value = '[email protected]' + self.driver.ex_delete_permissions('bucket') + url = '/storage/v1/b/bucket/acl/[email protected]' + mock_request.assert_called_once_with(url, method='DELETE') + + def test_get_permissions(self): + def test_permission_config(bucket_perms, object_perms): + GoogleStorageJSONMockHttp.bucket_perms = bucket_perms + GoogleStorageJSONMockHttp.object_perms = object_perms + + perms = self.driver.ex_get_permissions( + 'test-bucket', 'test-object') + self.assertEqual(perms, (bucket_perms, object_perms)) + + bucket_levels = range(len(google_storage.ContainerPermissions.values)) + object_levels = range(len(google_storage.ObjectPermissions.values)) + for bucket_perms in bucket_levels: + for object_perms in object_levels: + test_permission_config(bucket_perms, object_perms) + + def test_set_permissions(self): + mock_request = mock.Mock() + self.driver.json_connection.request = mock_request + + # Test setting object permissions. + self.driver.ex_set_permissions( + 'bucket', 'object', entity='user-foo', role='OWNER') + url = '/storage/v1/b/bucket/o/object/acl' + mock_request.assert_called_once_with( + url, method='POST', + data=json.dumps({'role': 'OWNER', 'entity': 'user-foo'})) + + # Test setting object permissions with an ObjectPermissions value. + mock_request.reset_mock() + self.driver.ex_set_permissions( + 'bucket', 'object', entity='user-foo', + role=google_storage.ObjectPermissions.OWNER) + url = '/storage/v1/b/bucket/o/object/acl' + mock_request.assert_called_once_with( + url, method='POST', + data=json.dumps({'role': 'OWNER', 'entity': 'user-foo'})) + + # Test setting bucket permissions. + mock_request.reset_mock() + self.driver.ex_set_permissions( + 'bucket', entity='user-foo', role='OWNER') + url = '/storage/v1/b/bucket/acl' + mock_request.assert_called_once_with( + url, method='POST', + data=json.dumps({'role': 'OWNER', 'entity': 'user-foo'})) + + # Test setting bucket permissions with a ContainerPermissions value. + mock_request.reset_mock() + self.driver.ex_set_permissions( + 'bucket', entity='user-foo', + role=google_storage.ContainerPermissions.OWNER) + url = '/storage/v1/b/bucket/acl' + mock_request.assert_called_once_with( + url, method='POST', + data=json.dumps({'role': 'OWNER', 'entity': 'user-foo'})) + + def test_set_permissions_bad_roles(self): + mock_request = mock.Mock() + self.driver.json_connection.request = mock_request + + # Test forgetting a role. + self.assertRaises( + ValueError, self.driver.ex_set_permissions, 'bucket', 'object') + self.assertRaises( + ValueError, self.driver.ex_set_permissions, 'bucket') + mock_request.assert_not_called() + + # Test container permissions on an object. + self.assertRaises( + ValueError, self.driver.ex_set_permissions, 'bucket', 'object', + role=google_storage.ContainerPermissions.OWNER) + mock_request.assert_not_called() + + # Test object permissions on a container. + self.assertRaises( + ValueError, self.driver.ex_set_permissions, 'bucket', + role=google_storage.ObjectPermissions.OWNER) + mock_request.assert_not_called() + + def test_set_permissions_no_entity(self): + mock_request = mock.Mock() + mock_get_user = mock.Mock(return_value=None) + self.driver._get_user = mock_get_user + self.driver.json_connection.request = mock_request + + # Test for setting object permissions with no entity. + self.assertRaises( + ValueError, self.driver.ex_set_permissions, 'bucket', 'object', + role='OWNER') + + # Test for setting bucket permissions with no entity. + self.assertRaises( + ValueError, self.driver.ex_set_permissions, 'bucket', role='OWNER') + + mock_request.assert_not_called() + + # Test for setting object permissions with a default entity. + mock_get_user.return_value = '[email protected]' + self.driver.ex_set_permissions('bucket', 'object', role='OWNER') + url = '/storage/v1/b/bucket/o/object/acl' + mock_request.assert_called_once_with( + url, method='POST', + data=json.dumps({'role': 'OWNER', 'entity': '[email protected]'})) + + # Test for setting bucket permissions with a default entity. + mock_request.reset_mock() + mock_get_user.return_value = '[email protected]' + self.driver.ex_set_permissions('bucket', role='OWNER') + url = '/storage/v1/b/bucket/acl' + mock_request.assert_called_once_with( + url, method='POST', + data=json.dumps({'role': 'OWNER', 'entity': '[email protected]'})) if __name__ == '__main__': sys.exit(unittest.main())
