Skip to content

Commit

Permalink
Switch core to google-auth.
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Wayne Parrott committed Nov 11, 2016
1 parent 1157488 commit 2581686
Show file tree
Hide file tree
Showing 11 changed files with 95 additions and 753 deletions.
149 changes: 3 additions & 146 deletions core/google/cloud/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,18 @@

import calendar
import datetime
import json
import os
import re
import socket
from threading import local as Local

import google.auth
from google.protobuf import timestamp_pb2
try:
from google.appengine.api import app_identity
except ImportError:
app_identity = None
try:
import grpc
except ImportError: # pragma: NO COVER
grpc = None
import six
from six.moves import http_client
from six.moves import configparser

from google.cloud.environment_vars import PROJECT
from google.cloud.environment_vars import CREDENTIALS


_NOW = datetime.datetime.utcnow # To be replaced by tests.
Expand Down Expand Up @@ -168,134 +159,13 @@ def _ensure_tuple_or_list(arg_name, tuple_or_list):
return list(tuple_or_list)


def _app_engine_id():
"""Gets the App Engine application ID if it can be inferred.
:rtype: str or ``NoneType``
:returns: App Engine application ID if running in App Engine,
else ``None``.
"""
if app_identity is None:
return None

return app_identity.get_application_id()


def _file_project_id():
"""Gets the project ID from the credentials file if one is available.
:rtype: str or ``NoneType``
:returns: Project ID from JSON credentials file if value exists,
else ``None``.
"""
credentials_file_path = os.getenv(CREDENTIALS)
if credentials_file_path:
with open(credentials_file_path, 'rb') as credentials_file:
credentials_json = credentials_file.read()
credentials = json.loads(credentials_json.decode('utf-8'))
return credentials.get('project_id')


def _get_nix_config_path():
"""Get the ``gcloud`` CLI config path on *nix systems.
:rtype: str
:returns: The filename on a *nix system containing the CLI
config file.
"""
return os.path.join(_USER_ROOT, '.config', _GCLOUD_CONFIG_FILE)


def _get_windows_config_path():
"""Get the ``gcloud`` CLI config path on Windows systems.
:rtype: str
:returns: The filename on a Windows system containing the CLI
config file.
"""
appdata_dir = os.getenv('APPDATA', '')
return os.path.join(appdata_dir, _GCLOUD_CONFIG_FILE)


def _default_service_project_id():
"""Retrieves the project ID from the gcloud command line tool.
This assumes the ``.config`` directory is stored
- in ~/.config on *nix systems
- in the %APPDATA% directory on Windows systems
Additionally, the ${HOME} / "~" directory may not be present on Google
App Engine, so this may be conditionally ignored.
Files that cannot be opened with configparser are silently ignored; this is
designed so that you can specify a list of potential configuration file
locations.
:rtype: str or ``NoneType``
:returns: Project-ID from default configuration file else ``None``
"""
search_paths = []
if _USER_ROOT is not None:
search_paths.append(_get_nix_config_path())

if os.name == 'nt':
search_paths.append(_get_windows_config_path())

config = configparser.RawConfigParser()
config.read(search_paths)

if config.has_section(_GCLOUD_CONFIG_SECTION):
try:
return config.get(_GCLOUD_CONFIG_SECTION, _GCLOUD_CONFIG_KEY)
except configparser.NoOptionError:
return None


def _compute_engine_id():
"""Gets the Compute Engine project ID if it can be inferred.
Uses 169.254.169.254 for the metadata server to avoid request
latency from DNS lookup.
See https://cloud.google.com/compute/docs/metadata#metadataserver
for information about this IP address. (This IP is also used for
Amazon EC2 instances, so the metadata flavor is crucial.)
See https://github.com/google/oauth2client/issues/93 for context about
DNS latency.
:rtype: str or ``NoneType``
:returns: Compute Engine project ID if the metadata service is available,
else ``None``.
"""
host = '169.254.169.254'
uri_path = '/computeMetadata/v1/project/project-id'
headers = {'Metadata-Flavor': 'Google'}
connection = http_client.HTTPConnection(host, timeout=0.1)

try:
connection.request('GET', uri_path, headers=headers)
response = connection.getresponse()
if response.status == 200:
return response.read()
except socket.error: # socket.timeout or socket.error(64, 'Host is down')
pass
finally:
connection.close()


def _get_production_project():
"""Gets the production project if it can be inferred."""
return os.getenv(PROJECT)


def _determine_default_project(project=None):
"""Determine default project ID explicitly or implicitly as fall-back.
In implicit case, supports three environments. In order of precedence, the
implicit environments are:
* GOOGLE_CLOUD_PROJECT environment variable
* GOOGLE_CLOUD_PROJECT and GCLOUD_PROJECT environment variable
* GOOGLE_APPLICATION_CREDENTIALS JSON file
* Get default service project from
``$ gcloud beta auth application-default login``
Expand All @@ -309,20 +179,7 @@ def _determine_default_project(project=None):
:returns: Default project if it can be determined.
"""
if project is None:
project = _get_production_project()

if project is None:
project = _file_project_id()

if project is None:
project = _default_service_project_id()

if project is None:
project = _app_engine_id()

if project is None:
project = _compute_engine_id()

_, project = google.auth.default()
return project


Expand Down
33 changes: 7 additions & 26 deletions core/google/cloud/_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import six
from six.moves.urllib.parse import urlencode

import google.auth.credentials
import google_auth_httplib2
import httplib2

from google.cloud.exceptions import make_exception
Expand Down Expand Up @@ -77,7 +79,7 @@ class Connection(object):

def __init__(self, credentials=None, http=None):
self._http = http
self._credentials = self._create_scoped_credentials(
self._credentials = google.auth.credentials.with_scopes_if_required(
credentials, self.SCOPE)

@property
Expand All @@ -98,34 +100,13 @@ def http(self):
:returns: A Http object used to transport data.
"""
if self._http is None:
self._http = httplib2.Http()
if self._credentials:
self._http = self._credentials.authorize(self._http)
self._http = google_auth_httplib2.AuthorizedHttp(
self._credentials)
else:
self._http = httplib2.Http()
return self._http

@staticmethod
def _create_scoped_credentials(credentials, scope):
"""Create a scoped set of credentials if it is required.
:type credentials: :class:`oauth2client.client.OAuth2Credentials` or
:class:`NoneType`
:param credentials: The OAuth2 Credentials to add a scope to.
:type scope: list of URLs
:param scope: the effective service auth scopes for the connection.
:rtype: :class:`oauth2client.client.OAuth2Credentials` or
:class:`NoneType`
:returns: A new credentials object that has a scope added (if needed).
"""
if credentials:
try:
if credentials.create_scoped_required():
credentials = credentials.create_scoped(scope)
except AttributeError:
pass
return credentials


class JSONConnection(Connection):
"""A connection to a Google JSON-based API.
Expand Down
39 changes: 2 additions & 37 deletions core/google/cloud/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

"""Base classes for client used to interact with Google Cloud APIs."""

from oauth2client.service_account import ServiceAccountCredentials
from google.oauth2 import service_account
import six

from google.cloud._helpers import _determine_default_project
Expand Down Expand Up @@ -55,46 +55,11 @@ def from_service_account_json(cls, json_credentials_path, *args, **kwargs):
"""
if 'credentials' in kwargs:
raise TypeError('credentials must not be in keyword arguments')
credentials = ServiceAccountCredentials.from_json_keyfile_name(
credentials = service_account.Credentials.from_service_account_file(
json_credentials_path)
kwargs['credentials'] = credentials
return cls(*args, **kwargs)

@classmethod
def from_service_account_p12(cls, client_email, private_key_path,
*args, **kwargs):
"""Factory to retrieve P12 credentials while creating client.
.. note::
Unless you have an explicit reason to use a PKCS12 key for your
service account, we recommend using a JSON key.
:type client_email: str
:param client_email: The e-mail attached to the service account.
:type private_key_path: str
:param private_key_path: The path to a private key file (this file was
given to you when you created the service
account). This file must be in P12 format.
:type args: tuple
:param args: Remaining positional arguments to pass to constructor.
:type kwargs: dict
:param kwargs: Remaining keyword arguments to pass to constructor.
:rtype: :class:`google.cloud.client.Client`
:returns: The client created with the retrieved P12 credentials.
:raises: :class:`TypeError` if there is a conflict with the kwargs
and the credentials created by the factory.
"""
if 'credentials' in kwargs:
raise TypeError('credentials must not be in keyword arguments')
credentials = ServiceAccountCredentials.from_p12_keyfile(
client_email, private_key_path)
kwargs['credentials'] = credentials
return cls(*args, **kwargs)


class Client(_ClientFactoryMixin):
"""Client to bundle configuration needed for API requests.
Expand Down
10 changes: 6 additions & 4 deletions core/google/cloud/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
import six
from six.moves.urllib.parse import urlencode

from oauth2client import client
import google.auth
import google.auth.credentials

from google.cloud._helpers import UTC
from google.cloud._helpers import _NOW
Expand Down Expand Up @@ -84,7 +85,8 @@ def get_credentials():
:returns: A new credentials instance corresponding to the implicit
environment.
"""
return client.GoogleCredentials.get_application_default()
credentials, _ = google.auth.default()
return credentials


def _get_signed_query_params(credentials, expiration, string_to_sign):
Expand All @@ -106,15 +108,15 @@ def _get_signed_query_params(credentials, expiration, string_to_sign):
:returns: Query parameters matching the signing credentials with a
signed payload.
"""
if not hasattr(credentials, 'sign_blob'):
if not isinstance(credentials, google.auth.credentials.Signing):
auth_uri = ('http://google-cloud-python.readthedocs.io/en/latest/'
'google-cloud-auth.html#setting-up-a-service-account')
raise AttributeError('you need a private key to sign credentials.'
'the credentials you are currently using %s '
'just contains a token. see %s for more '
'details.' % (type(credentials), auth_uri))

_, signature_bytes = credentials.sign_blob(string_to_sign)
signature_bytes = credentials.sign_bytes(string_to_sign)
signature = base64.b64encode(signature_bytes)
service_account_name = credentials.service_account_email
return {
Expand Down
6 changes: 0 additions & 6 deletions core/google/cloud/environment_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@
and tests.
"""

PROJECT = 'GOOGLE_CLOUD_PROJECT'
"""Environment variable defining default project."""

GCD_DATASET = 'DATASTORE_DATASET'
"""Environment variable defining default dataset ID under GCD."""

Expand All @@ -33,9 +30,6 @@
BIGTABLE_EMULATOR = 'BIGTABLE_EMULATOR_HOST'
"""Environment variable defining host for Bigtable emulator."""

CREDENTIALS = 'GOOGLE_APPLICATION_CREDENTIALS'
"""Environment variable defining location of Google credentials."""

DISABLE_GRPC = 'GOOGLE_CLOUD_DISABLE_GRPC'
"""Environment variable acting as flag to disable gRPC.
Expand Down
2 changes: 2 additions & 0 deletions core/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
'googleapis-common-protos >= 1.3.4',
'oauth2client >= 3.0.0, < 4.0.0dev',
'protobuf >= 3.0.0',
'google-auth >= 0.3.0, < 2.0.0dev',
'google-auth-httplib2',
'six',
]

Expand Down
1 change: 1 addition & 0 deletions core/tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ envlist =

[testing]
deps =
mock
pytest
covercmd =
py.test --quiet \
Expand Down
Loading

0 comments on commit 2581686

Please sign in to comment.