235 lines
8.5 KiB
Python
235 lines
8.5 KiB
Python
|
# Copyright 2014 Google Inc. All rights reserved.
|
||
|
#
|
||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
# you may not use this file except in compliance with the License.
|
||
|
# You may obtain a copy of the License at
|
||
|
#
|
||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||
|
#
|
||
|
# Unless required by applicable law or agreed to in writing, software
|
||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
# See the License for the specific language governing permissions and
|
||
|
# limitations under the License.
|
||
|
|
||
|
"""A simple wrapper around the OAuth2 credentials library."""
|
||
|
|
||
|
import base64
|
||
|
import datetime
|
||
|
import six
|
||
|
from six.moves.urllib.parse import urlencode
|
||
|
|
||
|
from oauth2client import client
|
||
|
|
||
|
from gcloud._helpers import UTC
|
||
|
from gcloud._helpers import _NOW
|
||
|
from gcloud._helpers import _microseconds_from_datetime
|
||
|
|
||
|
|
||
|
def get_credentials():
|
||
|
"""Gets credentials implicitly from the current environment.
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
You should not need to use this function directly. Instead, use a
|
||
|
helper method which uses this method under the hood.
|
||
|
|
||
|
Checks environment in order of precedence:
|
||
|
|
||
|
* Google App Engine (production and testing)
|
||
|
* Environment variable :envvar:`GOOGLE_APPLICATION_CREDENTIALS` pointing to
|
||
|
a file with stored credentials information.
|
||
|
* Stored "well known" file associated with ``gcloud`` command line tool.
|
||
|
* Google Compute Engine production environment.
|
||
|
|
||
|
The file referred to in :envvar:`GOOGLE_APPLICATION_CREDENTIALS` is
|
||
|
expected to contain information about credentials that are ready to use.
|
||
|
This means either service account information or user account information
|
||
|
with a ready-to-use refresh token:
|
||
|
|
||
|
.. code:: json
|
||
|
|
||
|
{
|
||
|
'type': 'authorized_user',
|
||
|
'client_id': '...',
|
||
|
'client_secret': '...',
|
||
|
'refresh_token': '...'
|
||
|
}
|
||
|
|
||
|
or
|
||
|
|
||
|
.. code:: json
|
||
|
|
||
|
{
|
||
|
'type': 'service_account',
|
||
|
'client_id': '...',
|
||
|
'client_email': '...',
|
||
|
'private_key_id': '...',
|
||
|
'private_key': '...'
|
||
|
}
|
||
|
|
||
|
The second of these is simply a JSON key downloaded from the Google APIs
|
||
|
console. The first is a close cousin of the "client secrets" JSON file
|
||
|
used by :mod:`oauth2client.clientsecrets` but differs in formatting.
|
||
|
|
||
|
:rtype: :class:`oauth2client.client.GoogleCredentials`,
|
||
|
:class:`oauth2client.contrib.appengine.AppAssertionCredentials`,
|
||
|
:class:`oauth2client.contrib.gce.AppAssertionCredentials`,
|
||
|
:class:`oauth2client.service_account.ServiceAccountCredentials`
|
||
|
:returns: A new credentials instance corresponding to the implicit
|
||
|
environment.
|
||
|
"""
|
||
|
return client.GoogleCredentials.get_application_default()
|
||
|
|
||
|
|
||
|
def _get_signed_query_params(credentials, expiration, string_to_sign):
|
||
|
"""Gets query parameters for creating a signed URL.
|
||
|
|
||
|
:type credentials: :class:`oauth2client.client.AssertionCredentials`
|
||
|
:param credentials: The credentials used to create a private key
|
||
|
for signing text.
|
||
|
|
||
|
:type expiration: int or long
|
||
|
:param expiration: When the signed URL should expire.
|
||
|
|
||
|
:type string_to_sign: string
|
||
|
:param string_to_sign: The string to be signed by the credentials.
|
||
|
|
||
|
:rtype: dict
|
||
|
:returns: Query parameters matching the signing credentials with a
|
||
|
signed payload.
|
||
|
"""
|
||
|
_, signature_bytes = credentials.sign_blob(string_to_sign)
|
||
|
signature = base64.b64encode(signature_bytes)
|
||
|
service_account_name = credentials.service_account_email
|
||
|
return {
|
||
|
'GoogleAccessId': service_account_name,
|
||
|
'Expires': str(expiration),
|
||
|
'Signature': signature,
|
||
|
}
|
||
|
|
||
|
|
||
|
def _get_expiration_seconds(expiration):
|
||
|
"""Convert 'expiration' to a number of seconds in the future.
|
||
|
|
||
|
:type expiration: int, long, datetime.datetime, datetime.timedelta
|
||
|
:param expiration: When the signed URL should expire.
|
||
|
|
||
|
:rtype: int
|
||
|
:returns: a timestamp as an absolute number of seconds.
|
||
|
"""
|
||
|
# If it's a timedelta, add it to `now` in UTC.
|
||
|
if isinstance(expiration, datetime.timedelta):
|
||
|
now = _NOW().replace(tzinfo=UTC)
|
||
|
expiration = now + expiration
|
||
|
|
||
|
# If it's a datetime, convert to a timestamp.
|
||
|
if isinstance(expiration, datetime.datetime):
|
||
|
micros = _microseconds_from_datetime(expiration)
|
||
|
expiration = micros // 10**6
|
||
|
|
||
|
if not isinstance(expiration, six.integer_types):
|
||
|
raise TypeError('Expected an integer timestamp, datetime, or '
|
||
|
'timedelta. Got %s' % type(expiration))
|
||
|
return expiration
|
||
|
|
||
|
|
||
|
def generate_signed_url(credentials, resource, expiration,
|
||
|
api_access_endpoint='',
|
||
|
method='GET', content_md5=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::
|
||
|
|
||
|
Assumes ``credentials`` implements a ``sign_blob()`` method that takes
|
||
|
bytes to sign and returns a pair of the key ID (unused here) and the
|
||
|
signed bytes (this is abstract in the base class
|
||
|
:class:`oauth2client.client.AssertionCredentials`). Also assumes
|
||
|
``credentials`` has a ``service_account_email`` property which
|
||
|
identifies the credentials.
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
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
|
||
|
sign text.
|
||
|
|
||
|
:type resource: string
|
||
|
:param resource: A pointer to a specific resource
|
||
|
(typically, ``/bucket-name/path/to/blob.txt``).
|
||
|
|
||
|
:type expiration: :class:`int`, :class:`long`, :class:`datetime.datetime`,
|
||
|
:class:`datetime.timedelta`
|
||
|
:param expiration: When the signed URL should expire.
|
||
|
|
||
|
:type api_access_endpoint: str
|
||
|
:param api_access_endpoint: Optional URI base. Defaults to empty string.
|
||
|
|
||
|
:type method: str
|
||
|
:param method: The HTTP verb that will be used when requesting the URL.
|
||
|
Defaults to ``'GET'``.
|
||
|
|
||
|
: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 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
|
||
|
until expiration.
|
||
|
"""
|
||
|
expiration = _get_expiration_seconds(expiration)
|
||
|
|
||
|
# Generate the string to sign.
|
||
|
string_to_sign = '\n'.join([
|
||
|
method,
|
||
|
content_md5 or '',
|
||
|
content_type or '',
|
||
|
str(expiration),
|
||
|
resource])
|
||
|
|
||
|
# Set the right query parameters.
|
||
|
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(
|
||
|
endpoint=api_access_endpoint, resource=resource,
|
||
|
querystring=urlencode(query_params))
|