From ce6ab3dcbdef64875fb9137611517e1f8b270631 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Fri, 17 Aug 2018 17:16:38 -0700 Subject: [PATCH 1/5] WorkInProgress: Get auth tests running locally. --- pydata_google_auth/auth.py | 108 +++++++++++++++++++------------ pydata_google_auth/exceptions.py | 10 +-- tests/system/test_auth.py | 46 +++++++------ 3 files changed, 91 insertions(+), 73 deletions(-) diff --git a/pydata_google_auth/auth.py b/pydata_google_auth/auth.py index 916d777..017ac7b 100644 --- a/pydata_google_auth/auth.py +++ b/pydata_google_auth/auth.py @@ -5,7 +5,12 @@ import os import os.path -import pydata_google_auth.exceptions + +from google_auth_oauthlib import flow +import oauthlib.oauth2.rfc6749.errors +import google.auth.transport.requests + +from pydata_google_auth import exceptions logger = logging.getLogger(__name__) @@ -17,33 +22,13 @@ def default( client_secret, credentials_dirname, credentials_filename, + reauth=False, project_id=None, auth_local_webserver=False, try_credentials=None): if try_credentials is None: - def try_credentials(credentials, project_id): - return credentials, project_id - - return get_credentials( - scopes, - client_id, - client_secret, - credentials_dirname, - credentials_filename, - project_id=project_id, - auth_local_webserver=auth_local_webserver, - try_credentials=try_credentials) - + try_credentials = _try_credentials -def get_credentials( - scopes, - client_id, - client_secret, - credentials_dirname, - credentials_filename, - try_credentials, - project_id=None, reauth=False, - auth_local_webserver=False): # Try to retrieve Application Default Credentials credentials, default_project = get_application_default_credentials( scopes, project_id=project_id, try_credentials=try_credentials) @@ -61,6 +46,10 @@ def get_credentials( reauth=reauth, auth_local_webserver=auth_local_webserver, try_credentials=try_credentials) + + if not credentials: + raise exceptions.PydataCredentialsError( + 'Could not get any valid credentials.') return credentials, project_id @@ -90,14 +79,26 @@ def get_application_default_credentials( try: credentials, default_project = google.auth.default(scopes=scopes) - except (DefaultCredentialsError, IOError): + except (DefaultCredentialsError, IOError) as exc: + logger.debug('Error getting default credentials: {}'.format( + str(exc))) return None, None + # Only use the default project from the environment if no project is + # manually specified. + if project_id is None: + project_id = default_project + # Even though we now have credentials, check that the credentials can be - # used with BigQuery. For example, we could be running on a GCE instance - # that does not allow the BigQuery scopes. - billing_project = project_id or default_project - return try_credentials(credentials, billing_project) + # used with the API. For example, we could be running on a GCE instance + # that does not allow the required scopes. + credentials_error = try_credentials(credentials, project_id) + if credentials_error: + logger.debug('Error using default credentials: {}'.format( + str(credentials_error))) + return None, None + + return credentials, project_id def get_user_account_credentials( @@ -106,7 +107,10 @@ def get_user_account_credentials( client_secret, credentials_dirname, credentials_filename, - project_id=None, reauth=False, auth_local_webserver=False, + try_credentials=None, + project_id=None, + reauth=False, + auth_local_webserver=False, credentials_path=None): """Gets user account credentials. @@ -122,8 +126,8 @@ def get_user_account_credentials( GoogleCredentials : credentials Credentials for the user with BigQuery access. """ - from google_auth_oauthlib.flow import InstalledAppFlow - from oauthlib.oauth2.rfc6749.errors import OAuth2Error + if try_credentials is None: + try_credentials = _try_credentials # Use the default credentials location under ~/.config and the # equivalent directory on windows if the user has not specified a @@ -141,6 +145,7 @@ def get_user_account_credentials( os.rename(credentials_filename, credentials_path) credentials = load_user_account_credentials( + try_credentials, project_id=project_id, credentials_path=credentials_path) client_config = { @@ -154,7 +159,7 @@ def get_user_account_credentials( } if credentials is None or reauth: - app_flow = InstalledAppFlow.from_client_config( + app_flow = flow.InstalledAppFlow.from_client_config( client_config, scopes=scopes) try: @@ -162,9 +167,16 @@ def get_user_account_credentials( credentials = app_flow.run_local_server() else: credentials = app_flow.run_console() - except OAuth2Error as ex: - raise pydata_google_auth.exceptions.AccessDenied( - "Unable to get valid credentials: {0}".format(ex)) + except oauthlib.oauth2.rfc6749.errors.OAuth2Error as exc: + raise exceptions.PydataCredentialsError( + "Unable to get valid credentials: {0}".format(exc)) + + # Don't save the credentials if they can't be used with the API. + credentials_error = try_credentials(credentials, project_id) + if credentials_error: + logger.debug('Error using user credentials {}: {}'.format( + str(credentials_error))) + return None save_user_account_credentials(credentials, credentials_path) @@ -198,7 +210,9 @@ def load_user_account_credentials( try: with open(credentials_path) as credentials_file: credentials_json = json.load(credentials_file) - except (IOError, ValueError): + except (IOError, ValueError) as exc: + logger.debug('Error loading credentials from {}: {}'.format( + credentials_path, str(exc))) return None credentials = Credentials( @@ -210,11 +224,13 @@ def load_user_account_credentials( client_secret=credentials_json.get('client_secret'), scopes=credentials_json.get('scopes')) - # Refresh the token before trying to use it. - request = google.auth.transport.requests.Request() - credentials.refresh(request) + credentials_error = try_credentials(credentials, project_id) + if credentials_error: + logger.debug('Error using credentials loaded from {}: {}'.format( + credentials_path, str(credentials_error))) + return None - return try_credentials(credentials, project_id) + return credentials, project_id def get_default_credentials_path(credentials_dirname, credentials_filename): @@ -261,3 +277,15 @@ def save_user_account_credentials(credentials, credentials_path): json.dump(credentials_json, credentials_file) except IOError: logger.warning('Unable to save credentials.') + + +def _try_credentials(credentials, project_id): + # Refresh the token before trying to use it. + if not credentials.valid: + request = google.auth.transport.requests.Request() + credentials.refresh(request) + + if not credentials.valid: + return ValueError('credentials are invalid after refreshing') + + return None diff --git a/pydata_google_auth/exceptions.py b/pydata_google_auth/exceptions.py index a8b6aca..f2b0361 100644 --- a/pydata_google_auth/exceptions.py +++ b/pydata_google_auth/exceptions.py @@ -1,14 +1,6 @@ - -class AccessDenied(ValueError): +class PydataCredentialsError(ValueError): """ Raised when invalid credentials are provided, or tokens have expired. """ pass - - -class InvalidPrivateKeyFormat(ValueError): - """ - Raised when provided private key has invalid format. - """ - pass diff --git a/tests/system/test_auth.py b/tests/system/test_auth.py index 853b661..6020b29 100644 --- a/tests/system/test_auth.py +++ b/tests/system/test_auth.py @@ -1,51 +1,44 @@ """System tests for fetching Google BigQuery credentials.""" +import google.auth try: import mock except ImportError: # pragma: NO COVER from unittest import mock import pytest +import requests from pydata_google_auth import auth -def _try_credentials(credentials, project_id): - from google.cloud import bigquery - import google.api_core.exceptions - - if not credentials or not project_id: - return credentials, project_id - - try: - client = bigquery.Client(project=project_id, credentials=credentials) - # Check if the application has rights to the BigQuery project - client.query('SELECT 1').result() - return credentials, project_id - except google.api_core.exceptions.GoogleAPIError: - return None, None +TEST_CLIENT_ID = ( + '262006177488-3425ks60hkk80fssi9vpohv88g6q1iqd.apps.googleusercontent.com' +) +TEST_CLIENT_SECRET = 'JSF-iczmzEgbTR-XK-2xaWAc' +TEST_SCOPES = ['https://www.googleapis.com/auth/userinfo.email'] def _check_if_can_get_correct_default_credentials(): # Checks if "Application Default Credentials" can be fetched # from the environment the tests are running in. # See https://github.com/pandas-dev/pandas/issues/13577 - - import google.auth from google.auth.exceptions import DefaultCredentialsError - import pydata_google_auth.auth try: - credentials, project = google.auth.default( - scopes=['https://www.googleapis.com/auth/bigquery']) + credentials, project = google.auth.default(TEST_SCOPES) except (DefaultCredentialsError, IOError): return False - return auth._try_credentials(project, credentials) is not None + return _try_credentials(credentials, project) is None -def test_should_be_able_to_get_valid_credentials(project_id, private_key_path): - credentials, _ = auth.get_credentials( - project_id=project_id, private_key=private_key_path) +def test_should_be_able_to_get_valid_credentials(): + credentials, _ = auth.default( + TEST_SCOPES, + TEST_CLIENT_ID, + TEST_CLIENT_SECRET, + 'pydata_google_auth', + 'test_credentials.dat') assert credentials.valid @@ -76,7 +69,12 @@ def test_get_application_default_credentials_returns_credentials(): def test_get_user_account_credentials_bad_file_returns_credentials(): from google.auth.credentials import Credentials with mock.patch('__main__.open', side_effect=IOError()): - credentials = auth.get_user_account_credentials() + credentials = auth.get_user_account_credentials( + TEST_SCOPES, + TEST_CLIENT_ID, + TEST_CLIENT_SECRET, + 'pydata_google_auth', + 'test_credentials.dat') assert isinstance(credentials, Credentials) From 17b0211721515d8fd6966a78778de3719b65f565 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Fri, 7 Sep 2018 11:13:39 -0700 Subject: [PATCH 2/5] Remove try_credentials logic. --- pydata_google_auth/auth.py | 159 ++++++++++--------------------- pydata_google_auth/exceptions.py | 2 +- tests/system/test_auth.py | 14 --- tests/unit/test_auth.py | 2 +- 4 files changed, 51 insertions(+), 126 deletions(-) diff --git a/pydata_google_auth/auth.py b/pydata_google_auth/auth.py index 017ac7b..4d6723f 100644 --- a/pydata_google_auth/auth.py +++ b/pydata_google_auth/auth.py @@ -1,11 +1,13 @@ -"""Private module for fetching Google BigQuery credentials.""" +"""Private module for fetching Google API credentials.""" import json import logging import os import os.path - +import google.auth +import google.auth.exceptions +import google.oauth2.credentials from google_auth_oauthlib import flow import oauthlib.oauth2.rfc6749.errors import google.auth.transport.requests @@ -15,46 +17,45 @@ logger = logging.getLogger(__name__) +CLIENT_ID = ( + '262006177488-3425ks60hkk80fssi9vpohv88g6q1iqd' + '.apps.googleusercontent.com' +) +CLIENT_SECRET = 'JSF-iczmzEgbTR-XK-2xaWAc' +CREDENTIALS_DIRNAME = 'pydata' +CREDENTIALS_FILENAME = 'google_credentials.json' + def default( scopes, - client_id, - client_secret, - credentials_dirname, - credentials_filename, + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + credentials_dirname=CREDENTIALS_DIRNAME, + credentials_filename=CREDENTIALS_FILENAME, reauth=False, - project_id=None, - auth_local_webserver=False, - try_credentials=None): - if try_credentials is None: - try_credentials = _try_credentials - + auth_local_webserver=False): # Try to retrieve Application Default Credentials - credentials, default_project = get_application_default_credentials( - scopes, project_id=project_id, try_credentials=try_credentials) + credentials, default_project = get_application_default_credentials(scopes) if credentials: return credentials, default_project credentials = get_user_account_credentials( scopes, - client_id, - client_secret, - credentials_dirname, - credentials_filename, - project_id=project_id, + client_id=client_id, + client_secret=client_secret, + credentials_dirname=credentials_dirname, + credentials_filename=credentials_filename, reauth=reauth, - auth_local_webserver=auth_local_webserver, - try_credentials=try_credentials) + auth_local_webserver=auth_local_webserver) if not credentials: - raise exceptions.PydataCredentialsError( + raise exceptions.PyDataCredentialsError( 'Could not get any valid credentials.') - return credentials, project_id + return credentials, None -def get_application_default_credentials( - scopes, try_credentials, project_id=None): +def get_application_default_credentials(scopes): """ This method tries to retrieve the "default application credentials". This could be useful for running code on Google Cloud Platform. @@ -74,44 +75,22 @@ def get_application_default_credentials( from the environment. Or, the retrieved credentials do not have access to the project (project_id) on BigQuery. """ - import google.auth - from google.auth.exceptions import DefaultCredentialsError try: - credentials, default_project = google.auth.default(scopes=scopes) - except (DefaultCredentialsError, IOError) as exc: - logger.debug('Error getting default credentials: {}'.format( - str(exc))) + return google.auth.default(scopes=scopes) + except (google.auth.exceptions.DefaultCredentialsError, IOError) as exc: + logger.debug('Error getting default credentials: {}'.format(str(exc))) return None, None - # Only use the default project from the environment if no project is - # manually specified. - if project_id is None: - project_id = default_project - - # Even though we now have credentials, check that the credentials can be - # used with the API. For example, we could be running on a GCE instance - # that does not allow the required scopes. - credentials_error = try_credentials(credentials, project_id) - if credentials_error: - logger.debug('Error using default credentials: {}'.format( - str(credentials_error))) - return None, None - - return credentials, project_id - def get_user_account_credentials( scopes, - client_id, - client_secret, - credentials_dirname, - credentials_filename, - try_credentials=None, - project_id=None, + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + credentials_dirname=CREDENTIALS_DIRNAME, + credentials_filename=CREDENTIALS_FILENAME, reauth=False, - auth_local_webserver=False, - credentials_path=None): + auth_local_webserver=False): """Gets user account credentials. This method authenticates using user credentials, either loading saved @@ -126,27 +105,16 @@ def get_user_account_credentials( GoogleCredentials : credentials Credentials for the user with BigQuery access. """ - if try_credentials is None: - try_credentials = _try_credentials - # Use the default credentials location under ~/.config and the # equivalent directory on windows if the user has not specified a # credentials path. - if not credentials_path: - credentials_path = get_default_credentials_path( - credentials_dirname, - credentials_filename) - - # Previously, pandas-gbq saved user account credentials in the - # current working directory. If the bigquery_credentials.dat file - # exists in the current working directory, move the credentials to - # the new default location. - if os.path.isfile('bigquery_credentials.dat'): - os.rename(credentials_filename, credentials_path) - - credentials = load_user_account_credentials( - try_credentials, - project_id=project_id, credentials_path=credentials_path) + credentials_path = get_default_credentials_path( + credentials_dirname, + credentials_filename) + + credentials = None + if not reauth: + credentials = load_user_account_credentials(credentials_path) client_config = { 'installed': { @@ -158,7 +126,7 @@ def get_user_account_credentials( } } - if credentials is None or reauth: + if credentials is None: app_flow = flow.InstalledAppFlow.from_client_config( client_config, scopes=scopes) @@ -168,23 +136,15 @@ def get_user_account_credentials( else: credentials = app_flow.run_console() except oauthlib.oauth2.rfc6749.errors.OAuth2Error as exc: - raise exceptions.PydataCredentialsError( + raise exceptions.PyDataCredentialsError( "Unable to get valid credentials: {0}".format(exc)) - # Don't save the credentials if they can't be used with the API. - credentials_error = try_credentials(credentials, project_id) - if credentials_error: - logger.debug('Error using user credentials {}: {}'.format( - str(credentials_error))) - return None - save_user_account_credentials(credentials, credentials_path) return credentials -def load_user_account_credentials( - try_credentials, project_id=None, credentials_path=None): +def load_user_account_credentials(credentials_path): """ Loads user account credentials from a local file. @@ -204,9 +164,6 @@ def load_user_account_credentials( credentials do not have access to the project (project_id) on BigQuery. """ - import google.auth.transport.requests - from google.oauth2.credentials import Credentials - try: with open(credentials_path) as credentials_file: credentials_json = json.load(credentials_file) @@ -215,7 +172,7 @@ def load_user_account_credentials( credentials_path, str(exc))) return None - credentials = Credentials( + credentials = google.oauth2.credentials.Credentials( token=credentials_json.get('access_token'), refresh_token=credentials_json.get('refresh_token'), id_token=credentials_json.get('id_token'), @@ -224,24 +181,18 @@ def load_user_account_credentials( client_secret=credentials_json.get('client_secret'), scopes=credentials_json.get('scopes')) - credentials_error = try_credentials(credentials, project_id) - if credentials_error: - logger.debug('Error using credentials loaded from {}: {}'.format( - credentials_path, str(credentials_error))) - return None - - return credentials, project_id + return credentials def get_default_credentials_path(credentials_dirname, credentials_filename): """ - Gets the default path to the BigQuery credentials + Gets the default path to the Google user credentials .. versionadded 0.3.0 Returns ------- - Path to the BigQuery credentials + Path to the Google user credentials """ if os.name == 'nt': config_path = os.environ['APPDATA'] @@ -250,7 +201,7 @@ def get_default_credentials_path(credentials_dirname, credentials_filename): config_path = os.path.join(config_path, credentials_dirname) - # Create a pandas_gbq directory in an application-specific hidden + # Create a pydata directory in an application-specific hidden # user folder on the operating system. if not os.path.exists(config_path): os.makedirs(config_path) @@ -277,15 +228,3 @@ def save_user_account_credentials(credentials, credentials_path): json.dump(credentials_json, credentials_file) except IOError: logger.warning('Unable to save credentials.') - - -def _try_credentials(credentials, project_id): - # Refresh the token before trying to use it. - if not credentials.valid: - request = google.auth.transport.requests.Request() - credentials.refresh(request) - - if not credentials.valid: - return ValueError('credentials are invalid after refreshing') - - return None diff --git a/pydata_google_auth/exceptions.py b/pydata_google_auth/exceptions.py index f2b0361..0795dce 100644 --- a/pydata_google_auth/exceptions.py +++ b/pydata_google_auth/exceptions.py @@ -1,5 +1,5 @@ -class PydataCredentialsError(ValueError): +class PyDataCredentialsError(ValueError): """ Raised when invalid credentials are provided, or tokens have expired. """ diff --git a/tests/system/test_auth.py b/tests/system/test_auth.py index 6020b29..e9099bd 100644 --- a/tests/system/test_auth.py +++ b/tests/system/test_auth.py @@ -18,20 +18,6 @@ TEST_SCOPES = ['https://www.googleapis.com/auth/userinfo.email'] -def _check_if_can_get_correct_default_credentials(): - # Checks if "Application Default Credentials" can be fetched - # from the environment the tests are running in. - # See https://github.com/pandas-dev/pandas/issues/13577 - from google.auth.exceptions import DefaultCredentialsError - - try: - credentials, project = google.auth.default(TEST_SCOPES) - except (DefaultCredentialsError, IOError): - return False - - return _try_credentials(credentials, project) is None - - def test_should_be_able_to_get_valid_credentials(): credentials, _ = auth.default( TEST_SCOPES, diff --git a/tests/unit/test_auth.py b/tests/unit/test_auth.py index 6da3f35..02b8bfc 100644 --- a/tests/unit/test_auth.py +++ b/tests/unit/test_auth.py @@ -8,7 +8,7 @@ except ImportError: # pragma: NO COVER from unittest import mock -from pandas_gbq import auth +from pydata_google_auth import auth def test_get_credentials_private_key_contents(monkeypatch): From 6ff14db462692ae78403873f6eb6be6da6688eae Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Fri, 7 Sep 2018 12:35:56 -0700 Subject: [PATCH 3/5] Add system tests for user credentials. --- pydata_google_auth/__init__.py | 23 +++++++- pydata_google_auth/auth.py | 49 ++++++++++------- tests/system/conftest.py | 10 ---- tests/system/test_auth.py | 96 +++++++++++++--------------------- 4 files changed, 87 insertions(+), 91 deletions(-) delete mode 100644 tests/system/conftest.py diff --git a/pydata_google_auth/__init__.py b/pydata_google_auth/__init__.py index a58579b..23033a8 100644 --- a/pydata_google_auth/__init__.py +++ b/pydata_google_auth/__init__.py @@ -1,7 +1,26 @@ -from .auth import default # noqa +from .auth import default +from .auth import get_user_credentials from ._version import get_versions + versions = get_versions() __version__ = versions.get('closest-tag', versions['version']) __git_revision__ = versions['full-revisionid'] -del get_versions, versions + +"""Google BigQuery API wrapper. +The main concepts with this API are: +- :class:`~google.cloud.bigquery.client.Client` manages connections to the + BigQuery API. Use the client methods to run jobs (such as a + :class:`~google.cloud.bigquery.job.QueryJob` via + :meth:`~google.cloud.bigquery.client.Client.query`) and manage resources. +- :class:`~google.cloud.bigquery.dataset.Dataset` represents a + collection of tables. +- :class:`~google.cloud.bigquery.table.Table` represents a single "relation". +""" + +__all__ = [ + '__version__', + '__git_revision__', + 'default', + 'get_user_credentials', +] diff --git a/pydata_google_auth/auth.py b/pydata_google_auth/auth.py index 4d6723f..b48116d 100644 --- a/pydata_google_auth/auth.py +++ b/pydata_google_auth/auth.py @@ -23,7 +23,7 @@ ) CLIENT_SECRET = 'JSF-iczmzEgbTR-XK-2xaWAc' CREDENTIALS_DIRNAME = 'pydata' -CREDENTIALS_FILENAME = 'google_credentials.json' +CREDENTIALS_FILENAME = 'pydata_google_credentials.json' def default( @@ -37,10 +37,10 @@ def default( # Try to retrieve Application Default Credentials credentials, default_project = get_application_default_credentials(scopes) - if credentials: + if credentials and credentials.valid: return credentials, default_project - credentials = get_user_account_credentials( + credentials = get_user_credentials( scopes, client_id=client_id, client_secret=client_secret, @@ -49,9 +49,10 @@ def default( reauth=reauth, auth_local_webserver=auth_local_webserver) - if not credentials: + if not credentials or not credentials.valid: raise exceptions.PyDataCredentialsError( 'Could not get any valid credentials.') + return credentials, None @@ -77,13 +78,19 @@ def get_application_default_credentials(scopes): """ try: - return google.auth.default(scopes=scopes) + credentials, project = google.auth.default(scopes=scopes) except (google.auth.exceptions.DefaultCredentialsError, IOError) as exc: logger.debug('Error getting default credentials: {}'.format(str(exc))) return None, None + if credentials and not credentials.valid: + request = google.auth.transport.requests.Request() + credentials.refresh(request) + + return credentials, project + -def get_user_account_credentials( +def get_user_credentials( scopes, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, @@ -114,7 +121,7 @@ def get_user_account_credentials( credentials = None if not reauth: - credentials = load_user_account_credentials(credentials_path) + credentials = load_user_credentials_from_file(credentials_path) client_config = { 'installed': { @@ -141,10 +148,25 @@ def get_user_account_credentials( save_user_account_credentials(credentials, credentials_path) + if credentials and not credentials.valid: + request = google.auth.transport.requests.Request() + credentials.refresh(request) + return credentials -def load_user_account_credentials(credentials_path): +def load_user_credentials_from_info(credentials_json): + return google.oauth2.credentials.Credentials( + token=credentials_json.get('access_token'), + refresh_token=credentials_json.get('refresh_token'), + id_token=credentials_json.get('id_token'), + token_uri=credentials_json.get('token_uri'), + client_id=credentials_json.get('client_id'), + client_secret=credentials_json.get('client_secret'), + scopes=credentials_json.get('scopes')) + + +def load_user_credentials_from_file(credentials_path): """ Loads user account credentials from a local file. @@ -172,16 +194,7 @@ def load_user_account_credentials(credentials_path): credentials_path, str(exc))) return None - credentials = google.oauth2.credentials.Credentials( - token=credentials_json.get('access_token'), - refresh_token=credentials_json.get('refresh_token'), - id_token=credentials_json.get('id_token'), - token_uri=credentials_json.get('token_uri'), - client_id=credentials_json.get('client_id'), - client_secret=credentials_json.get('client_secret'), - scopes=credentials_json.get('scopes')) - - return credentials + return load_user_credentials_from_info(credentials_json) def get_default_credentials_path(credentials_dirname, credentials_filename): diff --git a/tests/system/conftest.py b/tests/system/conftest.py deleted file mode 100644 index 8fad7c0..0000000 --- a/tests/system/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Shared pytest fixtures for system tests.""" - -import os - -import pytest - - -@pytest.fixture(scope='session') -def project_id(): - return os.environ.get('GOOGLE_CLOUD_PROJECT') # noqa diff --git a/tests/system/test_auth.py b/tests/system/test_auth.py index e9099bd..4a99a8b 100644 --- a/tests/system/test_auth.py +++ b/tests/system/test_auth.py @@ -1,83 +1,57 @@ -"""System tests for fetching Google BigQuery credentials.""" +"""System tests for fetching Google API credentials.""" import google.auth try: import mock except ImportError: # pragma: NO COVER from unittest import mock + +from google.auth.exceptions import DefaultCredentialsError import pytest import requests -from pydata_google_auth import auth - -TEST_CLIENT_ID = ( - '262006177488-3425ks60hkk80fssi9vpohv88g6q1iqd.apps.googleusercontent.com' -) -TEST_CLIENT_SECRET = 'JSF-iczmzEgbTR-XK-2xaWAc' -TEST_SCOPES = ['https://www.googleapis.com/auth/userinfo.email'] +TEST_SCOPES = ['https://www.googleapis.com/auth/cloud-platform'] -def test_should_be_able_to_get_valid_credentials(): - credentials, _ = auth.default( - TEST_SCOPES, - TEST_CLIENT_ID, - TEST_CLIENT_SECRET, - 'pydata_google_auth', - 'test_credentials.dat') +def test_default_gets_valid_credentials(): + import pydata_google_auth + credentials, _ = pydata_google_auth.default( + TEST_SCOPES, auth_local_webserver=True) assert credentials.valid + assert credentials.has_scopes(TEST_SCOPES) -def test_get_application_default_credentials_does_not_throw_error(): - if _check_if_can_get_correct_default_credentials(): - # Can get real credentials, so mock it out to fail. - from google.auth.exceptions import DefaultCredentialsError - with mock.patch('google.auth.default', - side_effect=DefaultCredentialsError()): - credentials, _ = auth.get_application_default_credentials() - else: - credentials, _ = auth.get_application_default_credentials() - assert credentials is None - - -def test_get_application_default_credentials_returns_credentials(): - if not _check_if_can_get_correct_default_credentials(): - pytest.skip("Cannot get default_credentials " - "from the environment!") - from google.auth.credentials import Credentials - credentials, default_project = auth.get_application_default_credentials() +def test_default_gets_user_credentials(): + import pydata_google_auth + # Mock google.auth.default to fail, forcing user credentials. + with mock.patch('google.auth.default', + side_effect=DefaultCredentialsError()): + credentials, _ = pydata_google_auth.default( + TEST_SCOPES, auth_local_webserver=True) - assert isinstance(credentials, Credentials) - assert default_project is not None + assert credentials.valid + assert credentials.has_scopes(TEST_SCOPES) -@pytest.mark.local_auth -def test_get_user_account_credentials_bad_file_returns_credentials(): - from google.auth.credentials import Credentials - with mock.patch('__main__.open', side_effect=IOError()): - credentials = auth.get_user_account_credentials( - TEST_SCOPES, - TEST_CLIENT_ID, - TEST_CLIENT_SECRET, - 'pydata_google_auth', - 'test_credentials.dat') - assert isinstance(credentials, Credentials) +def test_get_user_credentials_gets_valid_credentials(): + import pydata_google_auth + credentials = pydata_google_auth.get_user_credentials( + TEST_SCOPES, auth_local_webserver=True) + assert credentials.valid + assert credentials.has_scopes(TEST_SCOPES) -@pytest.mark.local_auth -def test_get_user_account_credentials_returns_credentials(project_id): - from google.auth.credentials import Credentials - credentials = auth.get_user_account_credentials( - project_id=project_id, - auth_local_webserver=True) - assert isinstance(credentials, Credentials) +def test_get_user_credentials_from_file_gets_valid_credentials(): + import pydata_google_auth + import pydata_google_auth.auth + # Mock load_user_credentials_from_file to fail, forcing fresh credentials. + with mock.patch( + 'pydata_google_auth.auth.load_user_credentials_from_file', + return_value=None): + credentials = pydata_google_auth.get_user_credentials( + TEST_SCOPES, auth_local_webserver=True) -@pytest.mark.local_auth -def test_get_user_account_credentials_reauth_returns_credentials(project_id): - from google.auth.credentials import Credentials - credentials = auth.get_user_account_credentials( - project_id=project_id, - auth_local_webserver=True, - reauth=True) - assert isinstance(credentials, Credentials) + assert credentials.valid + assert credentials.has_scopes(TEST_SCOPES) From f26e49645da881ca4a57b65abcc9f134bc31d454 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Fri, 7 Sep 2018 12:56:24 -0700 Subject: [PATCH 4/5] Add unit tests. --- tests/unit/test_auth.py | 72 +++++++---------------------------------- 1 file changed, 12 insertions(+), 60 deletions(-) diff --git a/tests/unit/test_auth.py b/tests/unit/test_auth.py index 02b8bfc..2c6340e 100644 --- a/tests/unit/test_auth.py +++ b/tests/unit/test_auth.py @@ -4,64 +4,19 @@ import os.path try: - import mock -except ImportError: # pragma: NO COVER from unittest import mock +except ImportError: # pragma: NO COVER + import mock -from pydata_google_auth import auth - - -def test_get_credentials_private_key_contents(monkeypatch): - from google.oauth2 import service_account - - @classmethod - def from_service_account_info(cls, key_info): - mock_credentials = mock.create_autospec(cls) - mock_credentials.with_scopes.return_value = mock_credentials - mock_credentials.refresh.return_value = mock_credentials - return mock_credentials - - monkeypatch.setattr( - service_account.Credentials, - 'from_service_account_info', - from_service_account_info) - private_key = json.dumps({ - 'private_key': 'some_key', - 'client_email': 'service-account@example.com', - 'project_id': 'private-key-project' - }) - credentials, project = auth.get_credentials(private_key=private_key) - - assert credentials is not None - assert project == 'private-key-project' - - -def test_get_credentials_private_key_path(monkeypatch): - from google.oauth2 import service_account - - @classmethod - def from_service_account_info(cls, key_info): - mock_credentials = mock.create_autospec(cls) - mock_credentials.with_scopes.return_value = mock_credentials - mock_credentials.refresh.return_value = mock_credentials - return mock_credentials +import google.auth +import google.auth.credentials - monkeypatch.setattr( - service_account.Credentials, - 'from_service_account_info', - from_service_account_info) - private_key = os.path.join( - os.path.dirname(__file__), '..', 'data', 'dummy_key.json') - credentials, project = auth.get_credentials(private_key=private_key) - assert credentials is not None - assert project is None +TEST_SCOPES = ['https://www.googleapis.com/auth/cloud-platform'] -def test_get_credentials_default_credentials(monkeypatch): - import google.auth - import google.auth.credentials - import google.cloud.bigquery +def test_default_returns_google_auth_credentials(monkeypatch): + from pydata_google_auth import auth def mock_default_credentials(scopes=None, request=None): return ( @@ -70,17 +25,14 @@ def mock_default_credentials(scopes=None, request=None): ) monkeypatch.setattr(google.auth, 'default', mock_default_credentials) - mock_client = mock.create_autospec(google.cloud.bigquery.Client) - monkeypatch.setattr(google.cloud.bigquery, 'Client', mock_client) - credentials, project = auth.get_credentials() + credentials, project = auth.default(TEST_SCOPES) assert project == 'default-project' assert credentials is not None -def test_get_credentials_load_user_no_default(monkeypatch): - import google.auth - import google.auth.credentials +def test_default_loads_user_credentials(monkeypatch): + from pydata_google_auth import auth def mock_default_credentials(scopes=None, request=None): return (None, None) @@ -94,9 +46,9 @@ def mock_load_credentials(project_id=None, credentials_path=None): monkeypatch.setattr( auth, - 'load_user_account_credentials', + 'load_user_credentials_from_file', mock_load_credentials) - credentials, project = auth.get_credentials() + credentials, project = auth.default(TEST_SCOPES) assert project is None assert credentials is mock_user_credentials From 72a17195a524085396c1107eb944814395f74405 Mon Sep 17 00:00:00 2001 From: Tim Swast Date: Fri, 7 Sep 2018 13:42:44 -0700 Subject: [PATCH 5/5] Update docstring. --- pydata_google_auth/__init__.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pydata_google_auth/__init__.py b/pydata_google_auth/__init__.py index 23033a8..f2914d4 100644 --- a/pydata_google_auth/__init__.py +++ b/pydata_google_auth/__init__.py @@ -7,15 +7,9 @@ __version__ = versions.get('closest-tag', versions['version']) __git_revision__ = versions['full-revisionid'] -"""Google BigQuery API wrapper. -The main concepts with this API are: -- :class:`~google.cloud.bigquery.client.Client` manages connections to the - BigQuery API. Use the client methods to run jobs (such as a - :class:`~google.cloud.bigquery.job.QueryJob` via - :meth:`~google.cloud.bigquery.client.Client.query`) and manage resources. -- :class:`~google.cloud.bigquery.dataset.Dataset` represents a - collection of tables. -- :class:`~google.cloud.bigquery.table.Table` represents a single "relation". +"""pydata-google-auth + +This package provides helpers for fetching Google API credentials. """ __all__ = [