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

Storage: add support for V4 signed URLs #7460

Merged
merged 37 commits into from
Apr 17, 2019
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1d2d7f9
Rename '_signing.generate_signed_url' -> 'generate_signed_url_v2'.
tseaver Feb 26, 2019
cca6312
Add '_signing.generate_signed_url_v4'.
tseaver Feb 26, 2019
6a6566b
Expose V4 signing via 'Blob.generate_signed_url'.
tseaver Feb 26, 2019
9a25e9a
Uppercase signature query param name.
tseaver Mar 11, 2019
7ea200a
Set headers from 'content_{type,md5}' params.
tseaver Mar 11, 2019
98b7e45
Pass user-supplied query parameters through for V4 signing.
tseaver Mar 11, 2019
78d7de5
Plumb 'headers'/'query_parameters' args through to 'Blob.generate_sig…
tseaver Mar 11, 2019
188d0a3
Fix non-ascii blob name tests under Python 2.
tseaver Mar 12, 2019
31dade9
Blacken.
tseaver Mar 12, 2019
5c114d6
Lint.
tseaver Mar 12, 2019
01e2cd9
Document semantics of 'headers' passed when generating signed URLs.
tseaver Mar 12, 2019
9113cdd
Don't use subresource names in query string tests.
tseaver Mar 12, 2019
ba065a1
Fix header/qp handling for V2 signing.
tseaver Mar 14, 2019
c39ac66
Rename helper to indicate V2-only.
tseaver Mar 14, 2019
8b94be4
Fix typo causing signing tests to be skipped.
tseaver Mar 29, 2019
943e273
Prepare signed URL systests for testing both V2 and V4.
tseaver Mar 29, 2019
0c9c812
Add 'max_age' argument to 'Blob.generate_signed_url'.
tseaver Mar 31, 2019
a78605d
Add 'Host' header to signed headers, if not passed.
tseaver Mar 31, 2019
5a6d44c
Add support for system tests using 'expiration' and 'max_age'.
tseaver Mar 31, 2019
9833d68
Add partial V4 conformance test suite and bash to fit.
tseaver Apr 1, 2019
c860d51
Add support for bucket-level conformance tests.
tseaver Apr 1, 2019
87aa6b6
Add 'Simple headers' conformance test and fix.
tseaver Apr 1, 2019
e08ad30
Add conformance tests for collapsed header values and fix.
tseaver Apr 1, 2019
db05933
Lint.
tseaver Apr 1, 2019
dada493
Add V4 systests paralleling existing V2 tests.
tseaver Apr 1, 2019
de892c6
Add 'Bucket.generate_signed_url' method.
tseaver Apr 1, 2019
eae12c5
Add systests for listing bucket blobs (V2/V4).
tseaver Apr 1, 2019
dc7bd12
Add warning for users signing URLs without passing an explicit version.
tseaver Apr 1, 2019
c52e2db
Clean up V2 signing using 'canonicalize' better.
tseaver Apr 1, 2019
2c5759e
Tidy up V4 signing using 'get_canonical_headers'.
tseaver Apr 1, 2019
7617b4f
Handle 'RESUMABLE' method alias for V4 signing.
tseaver Apr 1, 2019
d33c68e
Add round-trip systest for signed resumable uploads.
tseaver Apr 1, 2019
c37e77a
Remove 'max_age' argument for signing.
tseaver Apr 17, 2019
2d04844
Remove warning when no version is passed to 'generate_signed_url'.
tseaver Apr 17, 2019
ccccaed
Remove warning message template.
tseaver Apr 17, 2019
3a583a4
Remove no-version warning for 'Bucket.generate_signed_url', too.
tseaver Apr 17, 2019
8f2e13f
Un-skip systests blocked on back-end case-flattening bug (now fixed).
tseaver Apr 17, 2019
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
418 changes: 388 additions & 30 deletions storage/google/cloud/storage/_signing.py

Large diffs are not rendered by default.

73 changes: 62 additions & 11 deletions storage/google/cloud/storage/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
from google.api_core.iam import Policy
from google.cloud.storage._helpers import _PropertyMixin
from google.cloud.storage._helpers import _scalar_property
from google.cloud.storage._signing import generate_signed_url
from google.cloud.storage._signing import generate_signed_url_v2
from google.cloud.storage._signing import generate_signed_url_v4
from google.cloud.storage.acl import ACL
from google.cloud.storage.acl import ObjectACL

Expand Down Expand Up @@ -96,6 +97,12 @@
_READ_LESS_THAN_SIZE = (
"Size {:d} was specified but the file-like object only had " "{:d} bytes remaining."
)
_SIGNED_URL_V2_DEFAULT_MESSAGE = (
tseaver marked this conversation as resolved.
Show resolved Hide resolved
"You have generated a signed URL using the default v2 signing "
"implementation. In the future, this will default to v4. "
"You may experience breaking changes if you use longer than 7 day "
"expiration times with v4. To opt-in to the behavior specify version='v2'."
)

_DEFAULT_CHUNKSIZE = 104857600 # 1024 * 1024 B * 100 = 100 MB
_MAX_MULTIPART_SIZE = 8388608 # 8 MB
Expand Down Expand Up @@ -302,14 +309,19 @@ def public_url(self):

def generate_signed_url(
self,
expiration,
expiration=None,
api_access_endpoint=_API_ACCESS_ENDPOINT,
method="GET",
content_md5=None,
content_type=None,
generation=None,
response_disposition=None,
response_type=None,
generation=None,
headers=None,
query_parameters=None,
client=None,
credentials=None,
version=None,
):
"""Generates a signed URL for this blob.

Expand All @@ -332,20 +344,23 @@ def generate_signed_url(
accessible blobs, but don't want to require users to explicitly
log in.

:type expiration: int, long, datetime.datetime, datetime.timedelta
:param expiration: When the signed URL should expire.
:type expiration: Union[Integer, datetime.datetime, datetime.timedelta]
:param expiration: Point in time when the signed URL should expire.

:type api_access_endpoint: str
:param api_access_endpoint: Optional URI base.

:type method: str
:param method: The HTTP verb that will be used when requesting the URL.

:type content_md5: str
:param content_md5: (Optional) The MD5 hash of the object referenced by
``resource``.

:type content_type: str
:param content_type: (Optional) The content type of the object
referenced by ``resource``.

:type generation: str
:param generation: (Optional) A value that indicates which generation
of the resource to fetch.

:type response_disposition: str
:param response_disposition: (Optional) Content disposition of
responses to requests for the signed URL.
Expand All @@ -359,6 +374,24 @@ def generate_signed_url(
for the signed URL. Used to over-ride the content
type of the underlying blob/object.

:type generation: str
:param generation: (Optional) A value that indicates which generation
of the resource to fetch.

:type headers: dict
:param headers:
(Optional) Additional HTTP headers to be included as part of the
signed URLs. See:
https://cloud.google.com/storage/docs/xml-api/reference-headers
Requests using the signed URL *must* pass the specified header
(name and value) with each request for the URL.

:type query_parameters: dict
:param query_parameters:
(Optional) Additional query paramtersto be included as part of the
signed URLs. See:
https://cloud.google.com/storage/docs/xml-api/reference-headers#query

:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
:param client: (Optional) The client to use. If not passed, falls back
Expand All @@ -371,6 +404,11 @@ def generate_signed_url(
the URL. Defaults to the credentials stored on the
client used.

:type version: str
:param version: (Optional) The version of signed credential to create.
Must be one of 'v2' | 'v4'.

:raises: :exc:`ValueError` when version is invalid.
:raises: :exc:`TypeError` when expiration is not a valid type.
:raises: :exc:`AttributeError` if credentials is not an instance
of :class:`google.auth.credentials.Signing`.
Expand All @@ -379,6 +417,11 @@ def generate_signed_url(
:returns: A signed URL you can use to access the resource
until expiration.
"""
if version is None:
version = "v2"
elif version not in ("v2", "v4"):
raise ValueError("'version' must be either 'v2' or 'v4'")

resource = "/{bucket_name}/{quoted_name}".format(
bucket_name=self.bucket.name, quoted_name=quote(self.name.encode("utf-8"))
)
Expand All @@ -387,16 +430,24 @@ def generate_signed_url(
client = self._require_client(client)
credentials = client._credentials

return generate_signed_url(
if version == "v2":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tseaver could you add a warning as log output that V2 will change to V4 in the future:

You have generated a signed URL using the default v2 signing implementation. In the future, this will default to v4. You may experience breaking changes if you use longer than 7 day expiration times with v4. To opt-in to the behavior specify version="v2".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

helper = generate_signed_url_v2
else:
helper = generate_signed_url_v4

return helper(
credentials,
resource=resource,
api_access_endpoint=_API_ACCESS_ENDPOINT,
expiration=expiration,
api_access_endpoint=api_access_endpoint,
method=method.upper(),
content_md5=content_md5,
content_type=content_type,
response_type=response_type,
response_disposition=response_disposition,
generation=generation,
headers=headers,
query_parameters=query_parameters,
)

def exists(self, client=None):
Expand Down
111 changes: 111 additions & 0 deletions storage/google/cloud/storage/bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@
from google.cloud.storage._helpers import _PropertyMixin
from google.cloud.storage._helpers import _scalar_property
from google.cloud.storage._helpers import _validate_name
from google.cloud.storage._signing import generate_signed_url_v2
from google.cloud.storage._signing import generate_signed_url_v4
from google.cloud.storage.acl import BucketACL
from google.cloud.storage.acl import DefaultObjectACL
from google.cloud.storage.blob import _SIGNED_URL_V2_DEFAULT_MESSAGE
from google.cloud.storage.blob import Blob
from google.cloud.storage.notification import BucketNotification
from google.cloud.storage.notification import NONE_PAYLOAD_FORMAT
Expand All @@ -45,6 +48,7 @@
"valid before the bucket is created. Instead, pass the location "
"to `Bucket.create`."
)
_API_ACCESS_ENDPOINT = "https://storage.googleapis.com"


def _blobs_page_start(iterator, page, response):
Expand Down Expand Up @@ -1969,3 +1973,110 @@ def lock_retention_policy(self, client=None):
method="POST", path=path, query_params=query_params, _target_object=self
)
self._set_properties(api_response)

def generate_signed_url(
self,
expiration=None,
api_access_endpoint=_API_ACCESS_ENDPOINT,
method="GET",
headers=None,
query_parameters=None,
client=None,
credentials=None,
version=None,
):
"""Generates a signed URL for this bucket.

.. note::

If you are on Google Compute Engine, you can't generate a signed
URL using GCE service account. Follow `Issue 50`_ 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 50: https://github.com/GoogleCloudPlatform/\
google-auth-library-python/issues/50

If you have a bucket that you want to allow access to for a set
amount of time, you can use this method to generate a URL that
is only valid within a certain time period.

This is particularly useful if you don't want publicly
accessible buckets, but don't want to require users to explicitly
log in.

:type expiration: Union[Integer, datetime.datetime, datetime.timedelta]
:param expiration: Point in time when the signed URL should expire.

:type api_access_endpoint: str
:param api_access_endpoint: Optional URI base.

:type method: str
:param method: The HTTP verb that will be used when requesting the URL.

:type headers: dict
:param headers:
(Optional) Additional HTTP headers to be included as part of the
signed URLs. See:
https://cloud.google.com/storage/docs/xml-api/reference-headers
Requests using the signed URL *must* pass the specified header
(name and value) with each request for the URL.

:type query_parameters: dict
:param query_parameters:
(Optional) Additional query paramtersto be included as part of the
signed URLs. See:
https://cloud.google.com/storage/docs/xml-api/reference-headers#query

:type client: :class:`~google.cloud.storage.client.Client` or
``NoneType``
: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: (Optional) The OAuth2 credentials to use to sign
the URL. Defaults to the credentials stored on the
client used.

:type version: str
:param version: (Optional) The version of signed credential to create.
Must be one of 'v2' | 'v4'.

:raises: :exc:`ValueError` when version is invalid.
:raises: :exc:`TypeError` when expiration is not a valid type.
:raises: :exc:`AttributeError` if credentials is not an instance
of :class:`google.auth.credentials.Signing`.

:rtype: str
:returns: A signed URL you can use to access the resource
until expiration.
"""
if version is None:
version = "v2"
warnings.warn(DeprecationWarning(_SIGNED_URL_V2_DEFAULT_MESSAGE))
tseaver marked this conversation as resolved.
Show resolved Hide resolved
elif version not in ("v2", "v4"):
raise ValueError("'version' must be either 'v2' or 'v4'")

resource = "/{bucket_name}".format(bucket_name=self.name)

if credentials is None:
client = self._require_client(client)
credentials = client._credentials

if version == "v2":
helper = generate_signed_url_v2
else:
helper = generate_signed_url_v4

return helper(
credentials,
resource=resource,
expiration=expiration,
api_access_endpoint=api_access_endpoint,
method=method.upper(),
headers=headers,
query_parameters=query_parameters,
)
Loading