1# Copyright 2015 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Application default credentials.
16
17Implements application default credentials and project ID detection.
18"""
19
20import io
21import json
22import logging
23import os
24import warnings
25
26import six
27
28from google.auth import environment_vars
29from google.auth import exceptions
30import google.auth.transport._http_client
31
32_LOGGER = logging.getLogger(__name__)
33
34# Valid types accepted for file-based credentials.
35_AUTHORIZED_USER_TYPE = 'authorized_user'
36_SERVICE_ACCOUNT_TYPE = 'service_account'
37_VALID_TYPES = (_AUTHORIZED_USER_TYPE, _SERVICE_ACCOUNT_TYPE)
38
39# Help message when no credentials can be found.
40_HELP_MESSAGE = """\
41Could not automatically determine credentials. Please set {env} or \
42explicitly create credentials and re-run the application. For more \
43information, please see \
44https://cloud.google.com/docs/authentication/getting-started
45""".format(env=environment_vars.CREDENTIALS).strip()
46
47# Warning when using Cloud SDK user credentials
48_CLOUD_SDK_CREDENTIALS_WARNING = """\
49Your application has authenticated using end user credentials from Google \
50Cloud SDK. We recommend that most server applications use service accounts \
51instead. If your application continues to use end user credentials from Cloud \
52SDK, you might receive a "quota exceeded" or "API not enabled" error. For \
53more information about service accounts, see \
54https://cloud.google.com/docs/authentication/"""
55
56
57def _warn_about_problematic_credentials(credentials):
58    """Determines if the credentials are problematic.
59
60    Credentials from the Cloud SDK that are associated with Cloud SDK's project
61    are problematic because they may not have APIs enabled and have limited
62    quota. If this is the case, warn about it.
63    """
64    from google.auth import _cloud_sdk
65    if credentials.client_id == _cloud_sdk.CLOUD_SDK_CLIENT_ID:
66        warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)
67
68
69def _load_credentials_from_file(filename):
70    """Loads credentials from a file.
71
72    The credentials file must be a service account key or stored authorized
73    user credentials.
74
75    Args:
76        filename (str): The full path to the credentials file.
77
78    Returns:
79        Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded
80            credentials and the project ID. Authorized user credentials do not
81            have the project ID information.
82
83    Raises:
84        google.auth.exceptions.DefaultCredentialsError: if the file is in the
85            wrong format or is missing.
86    """
87    if not os.path.exists(filename):
88        raise exceptions.DefaultCredentialsError(
89            'File {} was not found.'.format(filename))
90
91    with io.open(filename, 'r') as file_obj:
92        try:
93            info = json.load(file_obj)
94        except ValueError as caught_exc:
95            new_exc = exceptions.DefaultCredentialsError(
96                'File {} is not a valid json file.'.format(filename),
97                caught_exc)
98            six.raise_from(new_exc, caught_exc)
99
100    # The type key should indicate that the file is either a service account
101    # credentials file or an authorized user credentials file.
102    credential_type = info.get('type')
103
104    if credential_type == _AUTHORIZED_USER_TYPE:
105        from google.auth import _cloud_sdk
106
107        try:
108            credentials = _cloud_sdk.load_authorized_user_credentials(info)
109        except ValueError as caught_exc:
110            msg = 'Failed to load authorized user credentials from {}'.format(
111                filename)
112            new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
113            six.raise_from(new_exc, caught_exc)
114        # Authorized user credentials do not contain the project ID.
115        _warn_about_problematic_credentials(credentials)
116        return credentials, None
117
118    elif credential_type == _SERVICE_ACCOUNT_TYPE:
119        from google.oauth2 import service_account
120
121        try:
122            credentials = (
123                service_account.Credentials.from_service_account_info(info))
124        except ValueError as caught_exc:
125            msg = 'Failed to load service account credentials from {}'.format(
126                filename)
127            new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
128            six.raise_from(new_exc, caught_exc)
129        return credentials, info.get('project_id')
130
131    else:
132        raise exceptions.DefaultCredentialsError(
133            'The file {file} does not have a valid type. '
134            'Type is {type}, expected one of {valid_types}.'.format(
135                file=filename, type=credential_type, valid_types=_VALID_TYPES))
136
137
138def _get_gcloud_sdk_credentials():
139    """Gets the credentials and project ID from the Cloud SDK."""
140    from google.auth import _cloud_sdk
141
142    # Check if application default credentials exist.
143    credentials_filename = (
144        _cloud_sdk.get_application_default_credentials_path())
145
146    if not os.path.isfile(credentials_filename):
147        return None, None
148
149    credentials, project_id = _load_credentials_from_file(
150        credentials_filename)
151
152    if not project_id:
153        project_id = _cloud_sdk.get_project_id()
154
155    return credentials, project_id
156
157
158def _get_explicit_environ_credentials():
159    """Gets credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
160    variable."""
161    explicit_file = os.environ.get(environment_vars.CREDENTIALS)
162
163    if explicit_file is not None:
164        credentials, project_id = _load_credentials_from_file(
165            os.environ[environment_vars.CREDENTIALS])
166
167        return credentials, project_id
168
169    else:
170        return None, None
171
172
173def _get_gae_credentials():
174    """Gets Google App Engine App Identity credentials and project ID."""
175    # While this library is normally bundled with app_engine, there are
176    # some cases where it's not available, so we tolerate ImportError.
177    try:
178        import google.auth.app_engine as app_engine
179    except ImportError:
180        return None, None
181
182    try:
183        credentials = app_engine.Credentials()
184        project_id = app_engine.get_project_id()
185        return credentials, project_id
186    except EnvironmentError:
187        return None, None
188
189
190def _get_gce_credentials(request=None):
191    """Gets credentials and project ID from the GCE Metadata Service."""
192    # Ping requires a transport, but we want application default credentials
193    # to require no arguments. So, we'll use the _http_client transport which
194    # uses http.client. This is only acceptable because the metadata server
195    # doesn't do SSL and never requires proxies.
196
197    # While this library is normally bundled with compute_engine, there are
198    # some cases where it's not available, so we tolerate ImportError.
199    try:
200        from google.auth import compute_engine
201        from google.auth.compute_engine import _metadata
202    except ImportError:
203        return None, None
204
205    if request is None:
206        request = google.auth.transport._http_client.Request()
207
208    if _metadata.ping(request=request):
209        # Get the project ID.
210        try:
211            project_id = _metadata.get_project_id(request=request)
212        except exceptions.TransportError:
213            project_id = None
214
215        return compute_engine.Credentials(), project_id
216    else:
217        return None, None
218
219
220def default(scopes=None, request=None):
221    """Gets the default credentials for the current environment.
222
223    `Application Default Credentials`_ provides an easy way to obtain
224    credentials to call Google APIs for server-to-server or local applications.
225    This function acquires credentials from the environment in the following
226    order:
227
228    1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
229       to the path of a valid service account JSON private key file, then it is
230       loaded and returned. The project ID returned is the project ID defined
231       in the service account file if available (some older files do not
232       contain project ID information).
233    2. If the `Google Cloud SDK`_ is installed and has application default
234       credentials set they are loaded and returned.
235
236       To enable application default credentials with the Cloud SDK run::
237
238            gcloud auth application-default login
239
240       If the Cloud SDK has an active project, the project ID is returned. The
241       active project can be set using::
242
243            gcloud config set project
244
245    3. If the application is running in the `App Engine standard environment`_
246       then the credentials and project ID from the `App Identity Service`_
247       are used.
248    4. If the application is running in `Compute Engine`_ or the
249       `App Engine flexible environment`_ then the credentials and project ID
250       are obtained from the `Metadata Service`_.
251    5. If no credentials are found,
252       :class:`~google.auth.exceptions.DefaultCredentialsError` will be raised.
253
254    .. _Application Default Credentials: https://developers.google.com\
255            /identity/protocols/application-default-credentials
256    .. _Google Cloud SDK: https://cloud.google.com/sdk
257    .. _App Engine standard environment: https://cloud.google.com/appengine
258    .. _App Identity Service: https://cloud.google.com/appengine/docs/python\
259            /appidentity/
260    .. _Compute Engine: https://cloud.google.com/compute
261    .. _App Engine flexible environment: https://cloud.google.com\
262            /appengine/flexible
263    .. _Metadata Service: https://cloud.google.com/compute/docs\
264            /storing-retrieving-metadata
265
266    Example::
267
268        import google.auth
269
270        credentials, project_id = google.auth.default()
271
272    Args:
273        scopes (Sequence[str]): The list of scopes for the credentials. If
274            specified, the credentials will automatically be scoped if
275            necessary.
276        request (google.auth.transport.Request): An object used to make
277            HTTP requests. This is used to detect whether the application
278            is running on Compute Engine. If not specified, then it will
279            use the standard library http client to make requests.
280
281    Returns:
282        Tuple[~google.auth.credentials.Credentials, Optional[str]]:
283            the current environment's credentials and project ID. Project ID
284            may be None, which indicates that the Project ID could not be
285            ascertained from the environment.
286
287    Raises:
288        ~google.auth.exceptions.DefaultCredentialsError:
289            If no credentials were found, or if the credentials found were
290            invalid.
291    """
292    from google.auth.credentials import with_scopes_if_required
293
294    explicit_project_id = os.environ.get(
295        environment_vars.PROJECT,
296        os.environ.get(environment_vars.LEGACY_PROJECT))
297
298    checkers = (
299        _get_explicit_environ_credentials,
300        _get_gcloud_sdk_credentials,
301        _get_gae_credentials,
302        lambda: _get_gce_credentials(request))
303
304    for checker in checkers:
305        credentials, project_id = checker()
306        if credentials is not None:
307            credentials = with_scopes_if_required(credentials, scopes)
308            effective_project_id = explicit_project_id or project_id
309            if not effective_project_id:
310                _LOGGER.warning(
311                    'No project ID could be determined. Consider running '
312                    '`gcloud config set project` or setting the %s '
313                    'environment variable',
314                    environment_vars.PROJECT)
315            return credentials, effective_project_id
316
317    raise exceptions.DefaultCredentialsError(_HELP_MESSAGE)
318