Implement application default credentials (#32)
diff --git a/docs/reference/google.auth.environment_vars.rst b/docs/reference/google.auth.environment_vars.rst new file mode 100644 index 0000000..fe34849 --- /dev/null +++ b/docs/reference/google.auth.environment_vars.rst
@@ -0,0 +1,7 @@ +google.auth.environment_vars module +=================================== + +.. automodule:: google.auth.environment_vars + :members: + :inherited-members: + :show-inheritance:
diff --git a/docs/reference/google.auth.rst b/docs/reference/google.auth.rst index e26862f..6769e1c 100644 --- a/docs/reference/google.auth.rst +++ b/docs/reference/google.auth.rst
@@ -21,6 +21,7 @@ google.auth.credentials google.auth.crypt + google.auth.environment_vars google.auth.exceptions google.auth.jwt
diff --git a/google/auth/__init__.py b/google/auth/__init__.py index 339b3d6..65e1395 100644 --- a/google/auth/__init__.py +++ b/google/auth/__init__.py
@@ -16,6 +16,13 @@ import logging +from google.auth._default import default + + +__all__ = [ + 'default', +] + # Set default logging handler to avoid "No handler found" warnings. logging.getLogger(__name__).addHandler(logging.NullHandler())
diff --git a/google/auth/_cloud_sdk.py b/google/auth/_cloud_sdk.py new file mode 100644 index 0000000..f48f5a6 --- /dev/null +++ b/google/auth/_cloud_sdk.py
@@ -0,0 +1,135 @@ +# Copyright 2015 Google Inc. +# +# 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. + +"""Helpers for reading the Google Cloud SDK's configuration.""" + +import os + +import six +from six.moves import configparser + +from google.auth import environment_vars +import google.oauth2.credentials + +# The Google OAuth 2.0 token endpoint. Used for authorized user credentials. +_GOOGLE_OAUTH2_TOKEN_ENDPOINT = 'https://accounts.google.com/o/oauth2/token' + +# The ~/.config subdirectory containing gcloud credentials. +_CONFIG_DIRECTORY = 'gcloud' +# Windows systems store config at %APPDATA%\gcloud +_WINDOWS_CONFIG_ROOT_ENV_VAR = 'APPDATA' +# The name of the file in the Cloud SDK config that contains default +# credentials. +_CREDENTIALS_FILENAME = 'application_default_credentials.json' +# The name of the file in the Cloud SDK config that contains the +# active configuration. +_ACTIVE_CONFIG_FILENAME = os.path.join( + 'configurations', 'config_default') +# The config section and key for the project ID in the cloud SDK config. +_PROJECT_CONFIG_SECTION = 'core' +_PROJECT_CONFIG_KEY = 'project' + + +def get_config_path(): + """Returns the absolute path the the Cloud SDK's configuration directory. + + Returns: + str: The Cloud SDK config path. + """ + # If the path is explicitly set, return that. + try: + return os.environ[environment_vars.CLOUD_SDK_CONFIG_DIR] + except KeyError: + pass + + # Non-windows systems store this at ~/.config/gcloud + if os.name != 'nt': + return os.path.join( + os.path.expanduser('~'), '.config', _CONFIG_DIRECTORY) + # Windows systems store config at %APPDATA%\gcloud + else: + try: + return os.path.join( + os.environ[_WINDOWS_CONFIG_ROOT_ENV_VAR], + _CONFIG_DIRECTORY) + except KeyError: + # This should never happen unless someone is really + # messing with things, but we'll cover the case anyway. + drive = os.environ.get('SystemDrive', 'C:') + return os.path.join( + drive, '\\', _CONFIG_DIRECTORY) + + +def get_application_default_credentials_path(): + """Gets the path to the application default credentials file. + + The path may or may not exist. + + Returns: + str: The full path to application default credentials. + """ + config_path = get_config_path() + return os.path.join(config_path, _CREDENTIALS_FILENAME) + + +def get_project_id(): + """Gets the project ID from the Cloud SDK's configuration. + + Returns: + Optional[str]: The project ID. + """ + config_path = get_config_path() + config_file = os.path.join(config_path, _ACTIVE_CONFIG_FILENAME) + + if not os.path.isfile(config_file): + return None + + config = configparser.RawConfigParser() + + try: + config.read(config_file) + except configparser.Error: + return None + + if config.has_section(_PROJECT_CONFIG_SECTION): + return config.get( + _PROJECT_CONFIG_SECTION, _PROJECT_CONFIG_KEY) + + +def load_authorized_user_credentials(info): + """Loads an authorized user credential. + + Args: + info (Mapping[str, str]): The loaded file's data. + + Returns: + google.oauth2.credentials.Credentials: The constructed credentials. + + Raises: + ValueError: if the info is in the wrong format or missing data. + """ + keys_needed = set(('refresh_token', 'client_id', 'client_secret')) + missing = keys_needed.difference(six.iterkeys(info)) + + if missing: + raise ValueError( + 'Authorized user info was not in the expected format, missing ' + 'fields {}.'.format(', '.join(missing))) + + return google.oauth2.credentials.Credentials( + None, # No access token, must be refreshed. + refresh_token=info['refresh_token'], + token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, + client_id=info['client_id'], + client_secret=info['client_secret'])
diff --git a/google/auth/_default.py b/google/auth/_default.py new file mode 100644 index 0000000..3f6993a --- /dev/null +++ b/google/auth/_default.py
@@ -0,0 +1,264 @@ +# Copyright 2015 Google Inc. +# +# 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. + +"""Application default credentials. + +Implements application default credentials and project ID detection. +""" + +import io +import json +import logging +import os + +from google.auth import _cloud_sdk +from google.auth import compute_engine +from google.auth import environment_vars +from google.auth import exceptions +from google.auth.compute_engine import _metadata +import google.auth.transport._http_client +from google.oauth2 import service_account +import google.oauth2.credentials + +_LOGGER = logging.getLogger(__name__) + +# Valid types accepted for file-based credentials. +_AUTHORIZED_USER_TYPE = 'authorized_user' +_SERVICE_ACCOUNT_TYPE = 'service_account' +_VALID_TYPES = (_AUTHORIZED_USER_TYPE, _SERVICE_ACCOUNT_TYPE) + +# Help message when no credentials can be found. +_HELP_MESSAGE = """ +Could not automatically determine credentials. Please set {env} or +explicitly create credential and re-run the application. For more +information, please see +https://developers.google.com/accounts/docs/application-default-credentials. +""".format(env=environment_vars.CREDENTIALS).strip() + + +def _load_credentials_from_file(filename): + """Loads credentials from a file. + + The credentials file must be a service account key or stored authorized + user credentials. + + Args: + filename (str): The full path to the credentials file. + + Returns: + Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded + credentials and the project ID. Authorized user credentials do not + have the project ID information. + + Raises: + google.auth.exceptions.DefaultCredentialsError: if the file is in the + wrong format. + """ + with io.open(filename, 'r') as file_obj: + try: + info = json.load(file_obj) + except ValueError as exc: + raise exceptions.DefaultCredentialsError( + 'File {} is not a valid json file.'.format(filename), exc) + + # The type key should indicate that the file is either a service account + # credentials file or an authorized user credentials file. + credential_type = info.get('type') + + if credential_type == _AUTHORIZED_USER_TYPE: + try: + credentials = _cloud_sdk.load_authorized_user_credentials(info) + except ValueError as exc: + raise exceptions.DefaultCredentialsError( + 'Failed to load authorized user credentials from {}'.format( + filename), exc) + # Authorized user credentials do not contain the project ID. + return credentials, None + + elif credential_type == _SERVICE_ACCOUNT_TYPE: + try: + credentials = ( + service_account.Credentials.from_service_account_info(info)) + except ValueError as exc: + raise exceptions.DefaultCredentialsError( + 'Failed to load service account credentials from {}'.format( + filename), exc) + return credentials, info.get('project_id') + + else: + raise exceptions.DefaultCredentialsError( + 'The file {file} does not have a valid type. ' + 'Type is {type}, expected one of {valid_types}.'.format( + file=filename, type=credential_type, valid_types=_VALID_TYPES)) + + +def _get_gcloud_sdk_credentials(): + """Gets the credentials and project ID from the Cloud SDK.""" + # Check if application default credentials exist. + credentials_filename = ( + _cloud_sdk.get_application_default_credentials_path()) + + if not os.path.isfile(credentials_filename): + return None, None + + credentials, project_id = _load_credentials_from_file( + credentials_filename) + + if not project_id: + project_id = _cloud_sdk.get_project_id() + + if not project_id: + _LOGGER.warning( + 'No project ID could be determined from the Cloud SDK ' + 'configuration. Consider running `gcloud config set project` or ' + 'setting the %s environment variable', environment_vars.PROJECT) + + return credentials, project_id + + +def _get_explicit_environ_credentials(): + """Gets credentials from the GOOGLE_APPLICATION_CREDENTIALS environment + variable.""" + explicit_file = os.environ.get(environment_vars.CREDENTIALS) + + if explicit_file is not None: + credentials, project_id = _load_credentials_from_file( + os.environ[environment_vars.CREDENTIALS]) + + if not project_id: + _LOGGER.warning( + 'No project ID could be determined from the credentials at %s ' + 'Consider setting the %s environment variable', + environment_vars.CREDENTIALS, environment_vars.PROJECT) + + return credentials, project_id + + else: + return None, None + + +def _get_gae_credentials(): + """Gets Google App Engine App Identity credentials and project ID.""" + return None, None + + +def _get_gce_credentials(request=None): + """Gets credentials and project ID from the GCE Metadata Service.""" + # Ping requires a transport, but we want application default credentials + # to require no arguments. So, we'll use the _http_client transport which + # uses http.client. This is only acceptable because the metadata server + # doesn't do SSL and never requires proxies. + + if request is None: + request = google.auth.transport._http_client.Request() + + if _metadata.ping(request=request): + # Get the project ID. + try: + project_id = _metadata.get(request, 'project/project-id') + except exceptions.TransportError: + _LOGGER.warning( + 'No project ID could be determined from the Compute Engine ' + 'metadata service. Consider setting the %s environment ' + 'variable.', environment_vars.PROJECT) + project_id = None + + return compute_engine.Credentials(), project_id + else: + return None, None + + +def default(request=None): + """Gets the default credentials for the current environment. + + `Application Default Credentials`_ provides an easy way to obtain + credentials to call Google APIs for server-to-server or local applications. + This function acquires credentials from the environment in the following + order: + + 1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set + to the path of a valid service account JSON private key file, then it is + loaded and returned. The project ID returned is the project ID defined + in the service account file if available (some older files do not + contain project ID information). + 2. If the `Google Cloud SDK`_ is installed and has application default + credentials set they are loaded and returned. + + To enable application default credentials with the Cloud SDK run:: + + gcloud auth application-default login + + If the Cloud SDK has an active project, the project ID is returned. The + active project can be set using:: + + gcloud config set project + + 3. If the application is running in the `App Engine standard environment`_ + then the credentials and project ID from the `App Identity Service`_ + are used. + 4. If the application is running in `Compute Engine`_ or the + `App Engine flexible environment`_ then the credentials and project ID + are obtained from the `Metadata Service`_. + 5. If no credentials are found, + :class:`~google.auth.exceptions.DefaultCredentialsError` will be raised. + + .. _Application Default Credentials: https://developers.google.com\ + /identity/protocols/application-default-credentials + .. _Google Cloud SDK: https://cloud.google.com/sdk + .. _App Engine standard environment: https://cloud.google.com/appengine + .. _App Identity Service: https://cloud.google.com/appengine/docs/python\ + /appidentity/ + .. _Compute Engine: https://cloud.google.com/compute + .. _App Engine flexible environment: https://cloud.google.com\ + /appengine/flexible + .. _Metadata Service: https://cloud.google.com/compute/docs\ + /storing-retrieving-metadata + + Example:: + + import google.auth + + credentials, project_id = google.auth.default() + + Args: + request (google.auth.transport.Request): An object used to make + HTTP requests. This is used to detect whether the application + is running on Compute Engine. If not specified, then it will + use the standard library http client to make requests. + + Returns: + Tuple[~google.auth.credentials.Credentials, Optional[str]]: + the current environment's credentials and project ID. Project ID + may be None, which indicates that the Project ID could not be + ascertained from the environment. + + Raises: + ~google.auth.exceptions.DefaultCredentialsError: + If no credentials were found, or if the credentials found were + invalid. + """ + explicit_project_id = os.environ.get(environment_vars.PROJECT) + + checkers = ( + _get_explicit_environ_credentials, + _get_gcloud_sdk_credentials, + _get_gae_credentials, + lambda: _get_gce_credentials(request)) + + for checker in checkers: + credentials, project_id = checker() + if credentials is not None: + return credentials, explicit_project_id or project_id + + raise exceptions.DefaultCredentialsError(_HELP_MESSAGE)
diff --git a/google/auth/environment_vars.py b/google/auth/environment_vars.py new file mode 100644 index 0000000..9785c34 --- /dev/null +++ b/google/auth/environment_vars.py
@@ -0,0 +1,32 @@ +# Copyright 2016 Google Inc. +# +# 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. + +"""Environment variables used by :mod:`google.auth`.""" + + +PROJECT = 'GOOGLE_CLOUD_PROJECT' +"""Environment variable defining default project. + +This used by :func:`google.auth.default` to explicitly set a project ID. This +environment variable is also used by the Google Cloud Python Library. +""" + +CREDENTIALS = 'GOOGLE_APPLICATION_CREDENTIALS' +"""Environment variable defining the location of Google application default +credentials.""" + +# The environment variable name which can replace ~/.config if set. +CLOUD_SDK_CONFIG_DIR = 'CLOUDSDK_CONFIG' +"""Environment variable defines the location of Google Cloud SDK's config +files."""
diff --git a/tests/data/authorized_user.json b/tests/data/authorized_user.json new file mode 100644 index 0000000..4787ace --- /dev/null +++ b/tests/data/authorized_user.json
@@ -0,0 +1,6 @@ +{ + "client_id": "123", + "client_secret": "secret", + "refresh_token": "alabalaportocala", + "type": "authorized_user" +}
diff --git a/tests/data/cloud_sdk.cfg b/tests/data/cloud_sdk.cfg new file mode 100644 index 0000000..089aac5 --- /dev/null +++ b/tests/data/cloud_sdk.cfg
@@ -0,0 +1,2 @@ +[core] +project = example-project
diff --git a/tests/test__cloud_sdk.py b/tests/test__cloud_sdk.py new file mode 100644 index 0000000..35ee426 --- /dev/null +++ b/tests/test__cloud_sdk.py
@@ -0,0 +1,146 @@ +# Copyright 2016 Google Inc. +# +# 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. + +import json +import os + +import mock +import pytest + +from google.auth import _cloud_sdk +from google.auth import environment_vars +import google.oauth2.credentials + + +DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') +AUTHORIZED_USER_FILE = os.path.join(DATA_DIR, 'authorized_user.json') + +with open(AUTHORIZED_USER_FILE) as fh: + AUTHORIZED_USER_FILE_DATA = json.load(fh) + +SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, 'service_account.json') + +with open(SERVICE_ACCOUNT_FILE) as fh: + SERVICE_ACCOUNT_FILE_DATA = json.load(fh) + +with open(os.path.join(DATA_DIR, 'cloud_sdk.cfg')) as fh: + CLOUD_SDK_CONFIG_DATA = fh.read() + +CONFIG_PATH_PATCH = mock.patch('google.auth._cloud_sdk.get_config_path') + + [email protected] +def config_file(tmpdir): + config_dir = tmpdir.join( + '.config', _cloud_sdk._CONFIG_DIRECTORY) + config_file = config_dir.join( + _cloud_sdk._ACTIVE_CONFIG_FILENAME) + + with CONFIG_PATH_PATCH as mock_get_config_dir: + mock_get_config_dir.return_value = str(config_dir) + yield config_file + + +def test_get_project_id(config_file): + config_file.write(CLOUD_SDK_CONFIG_DATA, ensure=True) + project_id = _cloud_sdk.get_project_id() + assert project_id == 'example-project' + + +def test_get_project_id_non_existent(config_file): + project_id = _cloud_sdk.get_project_id() + assert project_id is None + + +def test_get_project_id_bad_file(config_file): + config_file.write('<<<badconfig', ensure=True) + project_id = _cloud_sdk.get_project_id() + assert project_id is None + + +def test_get_project_id_no_section(config_file): + config_file.write('[section]', ensure=True) + project_id = _cloud_sdk.get_project_id() + assert project_id is None + + +@CONFIG_PATH_PATCH +def test_get_application_default_credentials_path(mock_get_config_dir): + config_path = 'config_path' + mock_get_config_dir.return_value = config_path + credentials_path = _cloud_sdk.get_application_default_credentials_path() + assert credentials_path == os.path.join( + config_path, _cloud_sdk._CREDENTIALS_FILENAME) + + +def test_get_config_path_env_var(monkeypatch): + config_path_sentinel = 'config_path' + monkeypatch.setenv( + environment_vars.CLOUD_SDK_CONFIG_DIR, config_path_sentinel) + config_path = _cloud_sdk.get_config_path() + assert config_path == config_path_sentinel + + [email protected]('os.path.expanduser') +def test_get_config_path_unix(mock_expanduser): + mock_expanduser.side_effect = lambda path: path + + config_path = _cloud_sdk.get_config_path() + + assert os.path.split(config_path) == ( + '~/.config', _cloud_sdk._CONFIG_DIRECTORY) + + [email protected]('os.name', new='nt') +def test_get_config_path_windows(monkeypatch): + appdata = 'appdata' + monkeypatch.setenv(_cloud_sdk._WINDOWS_CONFIG_ROOT_ENV_VAR, appdata) + + config_path = _cloud_sdk.get_config_path() + + assert os.path.split(config_path) == ( + appdata, _cloud_sdk._CONFIG_DIRECTORY) + + [email protected]('os.name', new='nt') +def test_get_config_path_no_appdata(monkeypatch): + monkeypatch.delenv(_cloud_sdk._WINDOWS_CONFIG_ROOT_ENV_VAR, raising=False) + monkeypatch.setenv('SystemDrive', 'G:') + + config_path = _cloud_sdk.get_config_path() + + assert os.path.split(config_path) == ( + 'G:/\\', _cloud_sdk._CONFIG_DIRECTORY) + + +def test_load_authorized_user_credentials(): + credentials = _cloud_sdk.load_authorized_user_credentials( + AUTHORIZED_USER_FILE_DATA) + + assert isinstance(credentials, google.oauth2.credentials.Credentials) + + assert credentials.token is None + assert (credentials._refresh_token == + AUTHORIZED_USER_FILE_DATA['refresh_token']) + assert credentials._client_id == AUTHORIZED_USER_FILE_DATA['client_id'] + assert (credentials._client_secret == + AUTHORIZED_USER_FILE_DATA['client_secret']) + assert credentials._token_uri == _cloud_sdk._GOOGLE_OAUTH2_TOKEN_ENDPOINT + + +def test_load_authorized_user_credentials_bad_format(): + with pytest.raises(ValueError) as excinfo: + _cloud_sdk.load_authorized_user_credentials({}) + + assert excinfo.match(r'missing fields')
diff --git a/tests/test__default.py b/tests/test__default.py new file mode 100644 index 0000000..137fdcd --- /dev/null +++ b/tests/test__default.py
@@ -0,0 +1,263 @@ +# Copyright 2016 Google Inc. +# +# 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. + +import json +import os + +import mock +import pytest + +from google.auth import _default +from google.auth import compute_engine +from google.auth import environment_vars +from google.auth import exceptions +from google.oauth2 import service_account +import google.oauth2.credentials + + +DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') +AUTHORIZED_USER_FILE = os.path.join(DATA_DIR, 'authorized_user.json') + +with open(AUTHORIZED_USER_FILE) as fh: + AUTHORIZED_USER_FILE_DATA = json.load(fh) + +SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, 'service_account.json') + +with open(SERVICE_ACCOUNT_FILE) as fh: + SERVICE_ACCOUNT_FILE_DATA = json.load(fh) + +with open(os.path.join(DATA_DIR, 'cloud_sdk.cfg')) as fh: + CLOUD_SDK_CONFIG_DATA = fh.read() + +LOAD_FILE_PATCH = mock.patch( + 'google.auth._default._load_credentials_from_file', return_value=( + mock.sentinel.credentials, mock.sentinel.project_id)) + + +def test__load_credentials_from_file_invalid_json(tmpdir): + jsonfile = tmpdir.join('invalid.json') + jsonfile.write('{') + + with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: + _default._load_credentials_from_file(str(jsonfile)) + + assert excinfo.match(r'not a valid json file') + + +def test__load_credentials_from_file_invalid_type(tmpdir): + jsonfile = tmpdir.join('invalid.json') + jsonfile.write(json.dumps({'type': 'not-a-real-type'})) + + with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: + _default._load_credentials_from_file(str(jsonfile)) + + assert excinfo.match(r'does not have a valid type') + + +def test__load_credentials_from_file_authorized_user(): + credentials, project_id = _default._load_credentials_from_file( + AUTHORIZED_USER_FILE) + assert isinstance(credentials, google.oauth2.credentials.Credentials) + assert project_id is None + + +def test__load_credentials_from_file_authorized_user_bad_format(tmpdir): + filename = tmpdir.join('authorized_user_bad.json') + filename.write(json.dumps({'type': 'authorized_user'})) + + with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: + _default._load_credentials_from_file(str(filename)) + + assert excinfo.match(r'Failed to load authorized user') + assert excinfo.match(r'missing fields') + + +def test__load_credentials_from_file_service_account(): + credentials, project_id = _default._load_credentials_from_file( + SERVICE_ACCOUNT_FILE) + assert isinstance(credentials, service_account.Credentials) + assert project_id == SERVICE_ACCOUNT_FILE_DATA['project_id'] + + +def test__load_credentials_from_file_service_account_bad_format(tmpdir): + filename = tmpdir.join('serivce_account_bad.json') + filename.write(json.dumps({'type': 'service_account'})) + + with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: + _default._load_credentials_from_file(str(filename)) + + assert excinfo.match(r'Failed to load service account') + assert excinfo.match(r'missing fields') + + [email protected](os.environ, {}, clear=True) +def test__get_explicit_environ_credentials_no_env(): + assert _default._get_explicit_environ_credentials() == (None, None) + + +@LOAD_FILE_PATCH +def test__get_explicit_environ_credentials(mock_load, monkeypatch): + monkeypatch.setenv(environment_vars.CREDENTIALS, 'filename') + + credentials, project_id = _default._get_explicit_environ_credentials() + + assert credentials is mock.sentinel.credentials + assert project_id is mock.sentinel.project_id + mock_load.assert_called_with('filename') + + +@LOAD_FILE_PATCH +def test__get_explicit_environ_credentials_no_project_id( + mock_load, monkeypatch): + mock_load.return_value = (mock.sentinel.credentials, None) + monkeypatch.setenv(environment_vars.CREDENTIALS, 'filename') + + credentials, project_id = _default._get_explicit_environ_credentials() + + assert credentials is mock.sentinel.credentials + assert project_id is None + + +@LOAD_FILE_PATCH [email protected]('google.auth._cloud_sdk.get_application_default_credentials_path') +def test__get_gcloud_sdk_credentials( + mock_get_adc_path, mock_load): + mock_get_adc_path.return_value = SERVICE_ACCOUNT_FILE + + credentials, project_id = _default._get_gcloud_sdk_credentials() + + assert credentials is mock.sentinel.credentials + assert project_id is mock.sentinel.project_id + mock_load.assert_called_with(SERVICE_ACCOUNT_FILE) + + [email protected]('google.auth._cloud_sdk.get_application_default_credentials_path') +def test__get_gcloud_sdk_credentials_non_existent(mock_get_adc_path, tmpdir): + non_existent = tmpdir.join('non-existent') + mock_get_adc_path.return_value = str(non_existent) + + credentials, project_id = _default._get_gcloud_sdk_credentials() + + assert credentials is None + assert project_id is None + + [email protected]( + 'google.auth._cloud_sdk.get_project_id', + return_value=mock.sentinel.project_id) [email protected]('os.path.isfile', return_value=True) +@LOAD_FILE_PATCH +def test__get_gcloud_sdk_credentials_project_id( + mock_load, unused_mock_isfile, mock_get_project_id): + # Don't return a project ID from load file, make the function check + # the Cloud SDK project. + mock_load.return_value = (mock.sentinel.credentials, None) + + credentials, project_id = _default._get_gcloud_sdk_credentials() + + assert credentials == mock.sentinel.credentials + assert project_id == mock.sentinel.project_id + assert mock_get_project_id.called + + [email protected]( + 'google.auth._cloud_sdk.get_project_id', + return_value=None) [email protected]('os.path.isfile', return_value=True) +@LOAD_FILE_PATCH +def test__get_gcloud_sdk_credentials_no_project_id( + mock_load, unused_mock_isfile, mock_get_project_id): + # Don't return a project ID from load file, make the function check + # the Cloud SDK project. + mock_load.return_value = (mock.sentinel.credentials, None) + + credentials, project_id = _default._get_gcloud_sdk_credentials() + + assert credentials == mock.sentinel.credentials + assert project_id is None + + +def test__get_gae_credentials(): + assert _default._get_gae_credentials() == (None, None) + + [email protected]( + 'google.auth.compute_engine._metadata.ping', return_value=True) [email protected]( + 'google.auth.compute_engine._metadata.get', return_value='example-project') +def test__get_gce_credentials(get_mock, ping_mock): + credentials, project_id = _default._get_gce_credentials() + + assert isinstance(credentials, compute_engine.Credentials) + assert project_id == 'example-project' + + [email protected]('google.auth.compute_engine._metadata.ping', return_value=False) +def test__get_gce_credentials_no_ping(ping_mock): + credentials, project_id = _default._get_gce_credentials() + + assert credentials is None + assert project_id is None + + [email protected]( + 'google.auth.compute_engine._metadata.ping', return_value=True) [email protected]( + 'google.auth.compute_engine._metadata.get', + side_effect=exceptions.TransportError()) +def test__get_gce_credentials_no_project_id(get_mock, ping_mock): + credentials, project_id = _default._get_gce_credentials() + + assert isinstance(credentials, compute_engine.Credentials) + assert project_id is None + + [email protected]('google.auth.compute_engine._metadata.ping', return_value=False) +def test__get_gce_credentials_explicit_request(ping_mock): + _default._get_gce_credentials(mock.sentinel.request) + ping_mock.assert_called_with(request=mock.sentinel.request) + + [email protected]( + 'google.auth._default._get_explicit_environ_credentials', + return_value=(mock.sentinel.credentials, mock.sentinel.project_id)) +def test_default_early_out(get_mock): + assert _default.default() == ( + mock.sentinel.credentials, mock.sentinel.project_id) + + [email protected]( + 'google.auth._default._get_explicit_environ_credentials', + return_value=(mock.sentinel.credentials, mock.sentinel.project_id)) +def test_default_explict_project_id(get_mock, monkeypatch): + monkeypatch.setenv(environment_vars.PROJECT, 'explicit-env') + assert _default.default() == ( + mock.sentinel.credentials, 'explicit-env') + + [email protected]( + 'google.auth._default._get_explicit_environ_credentials', + return_value=(None, None)) [email protected]( + 'google.auth._default._get_gcloud_sdk_credentials', + return_value=(None, None)) [email protected]( + 'google.auth._default._get_gae_credentials', + return_value=(None, None)) [email protected]( + 'google.auth._default._get_gce_credentials', + return_value=(None, None)) +def test_default_fail(unused_gce, unused_gae, unused_sdk, unused_explicit): + with pytest.raises(exceptions.DefaultCredentialsError): + assert _default.default()