From f444fca19fff4a6309d1d21b01e3116bd27cde16 Mon Sep 17 00:00:00 2001 From: Danny Hermes Date: Wed, 20 Jan 2016 22:46:28 -0800 Subject: [PATCH] Adding optional arguments to generate signed URI method. Allows customization of the experience for end users of the signed URI. --- gcloud/credentials.py | 53 ++++++++++++++++++++++++++++--------- gcloud/storage/blob.py | 52 +++++++++++++++++++++++++++--------- gcloud/storage/test_blob.py | 9 +++++++ gcloud/test_credentials.py | 36 ++++++++++++++++++++----- 4 files changed, 118 insertions(+), 32 deletions(-) diff --git a/gcloud/credentials.py b/gcloud/credentials.py index 42c7cd0addae..9874042ba47a 100644 --- a/gcloud/credentials.py +++ b/gcloud/credentials.py @@ -294,15 +294,22 @@ def _get_expiration_seconds(expiration): def generate_signed_url(credentials, resource, expiration, api_access_endpoint='', method='GET', content_md5=None, - content_type=None): + content_type=None, response_type=None, + response_disposition=None, generation=None): """Generate signed URL to provide query-string auth'n to a resource. .. note:: - If you are on Google Compute Engine, you can't generate a signed URL. - Follow https://github.com/GoogleCloudPlatform/gcloud-python/issues/922 - for updates on this. If you'd like to be able to generate a signed URL - from GCE, you can use a standard service account from a JSON file - rather than a GCE service account. + + If you are on Google Compute Engine, you can't generate a signed URL. + Follow `Issue 922`_ for updates on this. If you'd like to be able to + generate a signed URL from GCE, you can use a standard service account + from a JSON file rather than a GCE service account. + + See headers `reference`_ for more details on optional arguments. + + .. _Issue 922: https://github.com/GoogleCloudPlatform/\ + gcloud-python/issues/922 + .. _reference: https://cloud.google.com/storage/docs/reference-headers :type credentials: :class:`oauth2client.appengine.AppAssertionCredentials` :param credentials: Credentials object with an associated private key to @@ -316,19 +323,33 @@ def generate_signed_url(credentials, resource, expiration, :class:`datetime.timedelta` :param expiration: When the signed URL should expire. - :type api_access_endpoint: string + :type api_access_endpoint: str :param api_access_endpoint: Optional URI base. Defaults to empty string. - :type method: string + :type method: str :param method: The HTTP verb that will be used when requesting the URL. + Defaults to ``'GET'``. - :type content_md5: string - :param content_md5: The MD5 hash of the object referenced by + :type content_md5: str + :param content_md5: (Optional) The MD5 hash of the object referenced by ``resource``. - :type content_type: string - :param content_type: The content type of the object referenced by - ``resource``. + :type content_type: str + :param content_type: (Optional) The content type of the object referenced + by ``resource``. + + :type response_type: str + :param response_type: (Optional) Content type of responses to requests for + the signed URL. Used to over-ride the content type of + the underlying resource. + + :type response_disposition: str + :param response_disposition: (Optional) Content disposition of responses to + requests for the signed URL. + + :type generation: str + :param generation: (Optional) A value that indicates which generation of + the resource to fetch. :rtype: string :returns: A signed URL you can use to access the resource @@ -348,6 +369,12 @@ def generate_signed_url(credentials, resource, expiration, query_params = _get_signed_query_params(credentials, expiration, string_to_sign) + if response_type is not None: + query_params['response-content-type'] = response_type + if response_disposition is not None: + query_params['response-content-disposition'] = response_disposition + if generation is not None: + query_params['generation'] = generation # Return the built URL. return '{endpoint}{resource}?{querystring}'.format( diff --git a/gcloud/storage/blob.py b/gcloud/storage/blob.py index 8a6df5c6ce8d..2947cef50761 100644 --- a/gcloud/storage/blob.py +++ b/gcloud/storage/blob.py @@ -150,16 +150,20 @@ def public_url(self): quoted_name=quote(self.name, safe='')) def generate_signed_url(self, expiration, method='GET', - client=None, credentials=None): + client=None, credentials=None, + response_type=None, response_disposition=None, + generation=None): """Generates a signed URL for this blob. .. note:: - If you are on Google Compute Engine, you can't generate a signed URL. - Follow - https://github.com/GoogleCloudPlatform/gcloud-python/issues/922 - for updates on this. If you'd like to be able to generate a signed - URL from GCE, you can use a standard service account from a JSON - file rather than a GCE service account. + + If you are on Google Compute Engine, you can't generate a signed + URL. Follow `Issue 922`_ for updates on this. If you'd like to + be able to generate a signed URL from GCE, you can use a standard + service account from a JSON file rather than a GCE service account. + + .. _Issue 922: https://github.com/GoogleCloudPlatform/\ + gcloud-python/issues/922 If you have a blob that you want to allow access to for a set amount of time, you can use this method to generate a URL that @@ -172,18 +176,37 @@ def generate_signed_url(self, expiration, method='GET', :type expiration: int, long, datetime.datetime, datetime.timedelta :param expiration: When the signed URL should expire. - :type method: string + :type method: str :param method: The HTTP verb that will be used when requesting the URL. :type client: :class:`gcloud.storage.client.Client` or ``NoneType`` - :param client: Optional. The client to use. If not passed, falls back + :param client: (Optional) The client to use. If not passed, falls back to the ``client`` stored on the blob's bucket. :type credentials: :class:`oauth2client.client.OAuth2Credentials` or :class:`NoneType` - :param credentials: The OAuth2 credentials to use to sign the URL. - - :rtype: string + :param credentials: (Optional) The OAuth2 credentials to use to sign + the URL. Defaults to the credentials stored on the + client used. + + :type response_type: str + :param response_type: (Optional) Content type of responses to requests + for the signed URL. Used to over-ride the content + type of the underlying blob/object. + + :type response_disposition: str + :param response_disposition: (Optional) Content disposition of + responses to requests for the signed URL. + For example, to enable the signed URL + to initiate a file of ``blog.png``, use + the value + ``'attachment; filename=blob.png'``. + + :type generation: str + :param generation: (Optional) A value that indicates which generation + of the resource to fetch. + + :rtype: str :returns: A signed URL you can use to access the resource until expiration. """ @@ -198,7 +221,10 @@ def generate_signed_url(self, expiration, method='GET', return generate_signed_url( credentials, resource=resource, api_access_endpoint=_API_ACCESS_ENDPOINT, - expiration=expiration, method=method) + expiration=expiration, method=method, + response_type=response_type, + response_disposition=response_disposition, + generation=generation) def exists(self, client=None): """Determines whether or not this blob exists. diff --git a/gcloud/storage/test_blob.py b/gcloud/storage/test_blob.py index e4760607a767..2b958e6d4308 100644 --- a/gcloud/storage/test_blob.py +++ b/gcloud/storage/test_blob.py @@ -146,6 +146,9 @@ def _basic_generate_signed_url_helper(self, credentials=None): 'expiration': EXPIRATION, 'method': 'GET', 'resource': PATH, + 'response_type': None, + 'response_disposition': None, + 'generation': None, } self.assertEqual(SIGNER._signed, [(EXPECTED_ARGS, EXPECTED_KWARGS)]) @@ -180,6 +183,9 @@ def test_generate_signed_url_w_slash_in_name(self): 'expiration': EXPIRATION, 'method': 'GET', 'resource': '/name/parent%2Fchild', + 'response_type': None, + 'response_disposition': None, + 'generation': None, } self.assertEqual(SIGNER._signed, [(EXPECTED_ARGS, EXPECTED_KWARGS)]) @@ -208,6 +214,9 @@ def test_generate_signed_url_w_method_arg(self): 'expiration': EXPIRATION, 'method': 'POST', 'resource': PATH, + 'response_type': None, + 'response_disposition': None, + 'generation': None, } self.assertEqual(SIGNER._signed, [(EXPECTED_ARGS, EXPECTED_KWARGS)]) diff --git a/gcloud/test_credentials.py b/gcloud/test_credentials.py index 3d68555ce8ce..77558b53328d 100644 --- a/gcloud/test_credentials.py +++ b/gcloud/test_credentials.py @@ -187,7 +187,8 @@ def _callFUT(self, *args, **kwargs): from gcloud.credentials import generate_signed_url return generate_signed_url(*args, **kwargs) - def test_w_expiration_int(self): + def _generate_helper(self, response_type=None, response_disposition=None, + generation=None): import base64 from six.moves.urllib.parse import parse_qs from six.moves.urllib.parse import urlsplit @@ -209,21 +210,44 @@ def _get_signed_query_params(*args): with _Monkey(MUT, _get_signed_query_params=_get_signed_query_params): url = self._callFUT(CREDENTIALS, RESOURCE, 1000, - api_access_endpoint=ENDPOINT) + api_access_endpoint=ENDPOINT, + response_type=response_type, + response_disposition=response_disposition, + generation=generation) scheme, netloc, path, qs, frag = urlsplit(url) self.assertEqual(scheme, 'http') self.assertEqual(netloc, 'api.example.com') self.assertEqual(path, RESOURCE) params = parse_qs(qs) - self.assertEqual(len(params), 3) # In Py3k, parse_qs gives us text values: - self.assertEqual(params['Signature'], [SIGNED.decode('ascii')]) - self.assertEqual(params['Expires'], ['1000']) - self.assertEqual(params['GoogleAccessId'], + self.assertEqual(params.pop('Signature'), [SIGNED.decode('ascii')]) + self.assertEqual(params.pop('Expires'), ['1000']) + self.assertEqual(params.pop('GoogleAccessId'), [_Credentials.service_account_name]) + if response_type is not None: + self.assertEqual(params.pop('response-content-type'), + [response_type]) + if response_disposition is not None: + self.assertEqual(params.pop('response-content-disposition'), + [response_disposition]) + if generation is not None: + self.assertEqual(params.pop('generation'), [generation]) + # Make sure we have checked them all. + self.assertEqual(len(params), 0) self.assertEqual(frag, '') + def test_w_expiration_int(self): + self._generate_helper() + + def test_w_custom_fields(self): + response_type = 'text/plain' + response_disposition = 'attachment; filename=blob.png' + generation = '123' + self._generate_helper(response_type=response_type, + response_disposition=response_disposition, + generation=generation) + class Test__get_signature_bytes(unittest2.TestCase):