Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding optional arguments to generate signed URI method. #1409

Merged
merged 1 commit into from
Jan 28, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 40 additions & 13 deletions gcloud/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.


:type credentials: :class:`oauth2client.appengine.AppAssertionCredentials`
:param credentials: Credentials object with an associated private key to
Expand All @@ -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
Expand All @@ -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(
Expand Down
52 changes: 39 additions & 13 deletions gcloud/storage/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
"""
Expand All @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions gcloud/storage/test_blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)])

Expand Down Expand Up @@ -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)])

Expand Down Expand Up @@ -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)])

Expand Down
36 changes: 30 additions & 6 deletions gcloud/test_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):

Expand Down