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_EXTERNAL_ACCOUNT_TYPE = "external_account"
38_VALID_TYPES = (_AUTHORIZED_USER_TYPE, _SERVICE_ACCOUNT_TYPE, _EXTERNAL_ACCOUNT_TYPE)
39
40# Help message when no credentials can be found.
41_HELP_MESSAGE = """\
42Could not automatically determine credentials. Please set {env} or \
43explicitly create credentials and re-run the application. For more \
44information, please see \
45https://cloud.google.com/docs/authentication/getting-started
46""".format(
47    env=environment_vars.CREDENTIALS
48).strip()
49
50# Warning when using Cloud SDK user credentials
51_CLOUD_SDK_CREDENTIALS_WARNING = """\
52Your application has authenticated using end user credentials from Google \
53Cloud SDK without a quota project. You might receive a "quota exceeded" \
54or "API not enabled" error. We recommend you rerun \
55`gcloud auth application-default login` and make sure a quota project is \
56added. Or you can use service accounts instead. For more information \
57about service accounts, see https://cloud.google.com/docs/authentication/"""
58
59# The subject token type used for AWS external_account credentials.
60_AWS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request"
61
62
63def _warn_about_problematic_credentials(credentials):
64    """Determines if the credentials are problematic.
65
66    Credentials from the Cloud SDK that are associated with Cloud SDK's project
67    are problematic because they may not have APIs enabled and have limited
68    quota. If this is the case, warn about it.
69    """
70    from google.auth import _cloud_sdk
71
72    if credentials.client_id == _cloud_sdk.CLOUD_SDK_CLIENT_ID:
73        warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)
74
75
76def load_credentials_from_file(
77    filename, scopes=None, default_scopes=None, quota_project_id=None, request=None
78):
79    """Loads Google credentials from a file.
80
81    The credentials file must be a service account key, stored authorized
82    user credentials or external account credentials.
83
84    Args:
85        filename (str): The full path to the credentials file.
86        scopes (Optional[Sequence[str]]): The list of scopes for the credentials. If
87            specified, the credentials will automatically be scoped if
88            necessary
89        default_scopes (Optional[Sequence[str]]): Default scopes passed by a
90            Google client library. Use 'scopes' for user-defined scopes.
91        quota_project_id (Optional[str]):  The project ID used for
92            quota and billing.
93        request (Optional[google.auth.transport.Request]): An object used to make
94            HTTP requests. This is used to determine the associated project ID
95            for a workload identity pool resource (external account credentials).
96            If not specified, then it will use a
97            google.auth.transport.requests.Request client to make requests.
98
99    Returns:
100        Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded
101            credentials and the project ID. Authorized user credentials do not
102            have the project ID information. External account credentials project
103            IDs may not always be determined.
104
105    Raises:
106        google.auth.exceptions.DefaultCredentialsError: if the file is in the
107            wrong format or is missing.
108    """
109    if not os.path.exists(filename):
110        raise exceptions.DefaultCredentialsError(
111            "File {} was not found.".format(filename)
112        )
113
114    with io.open(filename, "r") as file_obj:
115        try:
116            info = json.load(file_obj)
117        except ValueError as caught_exc:
118            new_exc = exceptions.DefaultCredentialsError(
119                "File {} is not a valid json file.".format(filename), caught_exc
120            )
121            six.raise_from(new_exc, caught_exc)
122
123    # The type key should indicate that the file is either a service account
124    # credentials file or an authorized user credentials file.
125    credential_type = info.get("type")
126
127    if credential_type == _AUTHORIZED_USER_TYPE:
128        from google.oauth2 import credentials
129
130        try:
131            credentials = credentials.Credentials.from_authorized_user_info(
132                info, scopes=scopes
133            )
134        except ValueError as caught_exc:
135            msg = "Failed to load authorized user credentials from {}".format(filename)
136            new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
137            six.raise_from(new_exc, caught_exc)
138        if quota_project_id:
139            credentials = credentials.with_quota_project(quota_project_id)
140        if not credentials.quota_project_id:
141            _warn_about_problematic_credentials(credentials)
142        return credentials, None
143
144    elif credential_type == _SERVICE_ACCOUNT_TYPE:
145        from google.oauth2 import service_account
146
147        try:
148            credentials = service_account.Credentials.from_service_account_info(
149                info, scopes=scopes, default_scopes=default_scopes
150            )
151        except ValueError as caught_exc:
152            msg = "Failed to load service account credentials from {}".format(filename)
153            new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
154            six.raise_from(new_exc, caught_exc)
155        if quota_project_id:
156            credentials = credentials.with_quota_project(quota_project_id)
157        return credentials, info.get("project_id")
158
159    elif credential_type == _EXTERNAL_ACCOUNT_TYPE:
160        credentials, project_id = _get_external_account_credentials(
161            info,
162            filename,
163            scopes=scopes,
164            default_scopes=default_scopes,
165            request=request,
166        )
167        if quota_project_id:
168            credentials = credentials.with_quota_project(quota_project_id)
169        return credentials, project_id
170
171    else:
172        raise exceptions.DefaultCredentialsError(
173            "The file {file} does not have a valid type. "
174            "Type is {type}, expected one of {valid_types}.".format(
175                file=filename, type=credential_type, valid_types=_VALID_TYPES
176            )
177        )
178
179
180def _get_gcloud_sdk_credentials(quota_project_id=None):
181    """Gets the credentials and project ID from the Cloud SDK."""
182    from google.auth import _cloud_sdk
183
184    _LOGGER.debug("Checking Cloud SDK credentials as part of auth process...")
185
186    # Check if application default credentials exist.
187    credentials_filename = _cloud_sdk.get_application_default_credentials_path()
188
189    if not os.path.isfile(credentials_filename):
190        _LOGGER.debug("Cloud SDK credentials not found on disk; not using them")
191        return None, None
192
193    credentials, project_id = load_credentials_from_file(
194        credentials_filename, quota_project_id=quota_project_id
195    )
196
197    if not project_id:
198        project_id = _cloud_sdk.get_project_id()
199
200    return credentials, project_id
201
202
203def _get_explicit_environ_credentials(quota_project_id=None):
204    """Gets credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
205    variable."""
206    from google.auth import _cloud_sdk
207
208    cloud_sdk_adc_path = _cloud_sdk.get_application_default_credentials_path()
209    explicit_file = os.environ.get(environment_vars.CREDENTIALS)
210
211    _LOGGER.debug(
212        "Checking %s for explicit credentials as part of auth process...", explicit_file
213    )
214
215    if explicit_file is not None and explicit_file == cloud_sdk_adc_path:
216        # Cloud sdk flow calls gcloud to fetch project id, so if the explicit
217        # file path is cloud sdk credentials path, then we should fall back
218        # to cloud sdk flow, otherwise project id cannot be obtained.
219        _LOGGER.debug(
220            "Explicit credentials path %s is the same as Cloud SDK credentials path, fall back to Cloud SDK credentials flow...",
221            explicit_file,
222        )
223        return _get_gcloud_sdk_credentials(quota_project_id=quota_project_id)
224
225    if explicit_file is not None:
226        credentials, project_id = load_credentials_from_file(
227            os.environ[environment_vars.CREDENTIALS], quota_project_id=quota_project_id
228        )
229
230        return credentials, project_id
231
232    else:
233        return None, None
234
235
236def _get_gae_credentials():
237    """Gets Google App Engine App Identity credentials and project ID."""
238    # If not GAE gen1, prefer the metadata service even if the GAE APIs are
239    # available as per https://google.aip.dev/auth/4115.
240    if os.environ.get(environment_vars.LEGACY_APPENGINE_RUNTIME) != "python27":
241        return None, None
242
243    # While this library is normally bundled with app_engine, there are
244    # some cases where it's not available, so we tolerate ImportError.
245    try:
246        _LOGGER.debug("Checking for App Engine runtime as part of auth process...")
247        import google.auth.app_engine as app_engine
248    except ImportError:
249        _LOGGER.warning("Import of App Engine auth library failed.")
250        return None, None
251
252    try:
253        credentials = app_engine.Credentials()
254        project_id = app_engine.get_project_id()
255        return credentials, project_id
256    except EnvironmentError:
257        _LOGGER.debug(
258            "No App Engine library was found so cannot authentication via App Engine Identity Credentials."
259        )
260        return None, None
261
262
263def _get_gce_credentials(request=None):
264    """Gets credentials and project ID from the GCE Metadata Service."""
265    # Ping requires a transport, but we want application default credentials
266    # to require no arguments. So, we'll use the _http_client transport which
267    # uses http.client. This is only acceptable because the metadata server
268    # doesn't do SSL and never requires proxies.
269
270    # While this library is normally bundled with compute_engine, there are
271    # some cases where it's not available, so we tolerate ImportError.
272    try:
273        from google.auth import compute_engine
274        from google.auth.compute_engine import _metadata
275    except ImportError:
276        _LOGGER.warning("Import of Compute Engine auth library failed.")
277        return None, None
278
279    if request is None:
280        request = google.auth.transport._http_client.Request()
281
282    if _metadata.ping(request=request):
283        # Get the project ID.
284        try:
285            project_id = _metadata.get_project_id(request=request)
286        except exceptions.TransportError:
287            project_id = None
288
289        return compute_engine.Credentials(), project_id
290    else:
291        _LOGGER.warning(
292            "Authentication failed using Compute Engine authentication due to unavailable metadata server."
293        )
294        return None, None
295
296
297def _get_external_account_credentials(
298    info, filename, scopes=None, default_scopes=None, request=None
299):
300    """Loads external account Credentials from the parsed external account info.
301
302    The credentials information must correspond to a supported external account
303    credentials.
304
305    Args:
306        info (Mapping[str, str]): The external account info in Google format.
307        filename (str): The full path to the credentials file.
308        scopes (Optional[Sequence[str]]): The list of scopes for the credentials. If
309            specified, the credentials will automatically be scoped if
310            necessary.
311        default_scopes (Optional[Sequence[str]]): Default scopes passed by a
312            Google client library. Use 'scopes' for user-defined scopes.
313        request (Optional[google.auth.transport.Request]): An object used to make
314            HTTP requests. This is used to determine the associated project ID
315            for a workload identity pool resource (external account credentials).
316            If not specified, then it will use a
317            google.auth.transport.requests.Request client to make requests.
318
319    Returns:
320        Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded
321            credentials and the project ID. External account credentials project
322            IDs may not always be determined.
323
324    Raises:
325        google.auth.exceptions.DefaultCredentialsError: if the info dictionary
326            is in the wrong format or is missing required information.
327    """
328    # There are currently 2 types of external_account credentials.
329    if info.get("subject_token_type") == _AWS_SUBJECT_TOKEN_TYPE:
330        # Check if configuration corresponds to an AWS credentials.
331        from google.auth import aws
332
333        credentials = aws.Credentials.from_info(
334            info, scopes=scopes, default_scopes=default_scopes
335        )
336    else:
337        try:
338            # Check if configuration corresponds to an Identity Pool credentials.
339            from google.auth import identity_pool
340
341            credentials = identity_pool.Credentials.from_info(
342                info, scopes=scopes, default_scopes=default_scopes
343            )
344        except ValueError:
345            # If the configuration is invalid or does not correspond to any
346            # supported external_account credentials, raise an error.
347            raise exceptions.DefaultCredentialsError(
348                "Failed to load external account credentials from {}".format(filename)
349            )
350    if request is None:
351        request = google.auth.transport.requests.Request()
352
353    return credentials, credentials.get_project_id(request=request)
354
355
356def default(scopes=None, request=None, quota_project_id=None, default_scopes=None):
357    """Gets the default credentials for the current environment.
358
359    `Application Default Credentials`_ provides an easy way to obtain
360    credentials to call Google APIs for server-to-server or local applications.
361    This function acquires credentials from the environment in the following
362    order:
363
364    1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
365       to the path of a valid service account JSON private key file, then it is
366       loaded and returned. The project ID returned is the project ID defined
367       in the service account file if available (some older files do not
368       contain project ID information).
369
370       If the environment variable is set to the path of a valid external
371       account JSON configuration file (workload identity federation), then the
372       configuration file is used to determine and retrieve the external
373       credentials from the current environment (AWS, Azure, etc).
374       These will then be exchanged for Google access tokens via the Google STS
375       endpoint.
376       The project ID returned in this case is the one corresponding to the
377       underlying workload identity pool resource if determinable.
378    2. If the `Google Cloud SDK`_ is installed and has application default
379       credentials set they are loaded and returned.
380
381       To enable application default credentials with the Cloud SDK run::
382
383            gcloud auth application-default login
384
385       If the Cloud SDK has an active project, the project ID is returned. The
386       active project can be set using::
387
388            gcloud config set project
389
390    3. If the application is running in the `App Engine standard environment`_
391       (first generation) then the credentials and project ID from the
392       `App Identity Service`_ are used.
393    4. If the application is running in `Compute Engine`_ or `Cloud Run`_ or
394       the `App Engine flexible environment`_ or the `App Engine standard
395       environment`_ (second generation) then the credentials and project ID
396       are obtained from the `Metadata Service`_.
397    5. If no credentials are found,
398       :class:`~google.auth.exceptions.DefaultCredentialsError` will be raised.
399
400    .. _Application Default Credentials: https://developers.google.com\
401            /identity/protocols/application-default-credentials
402    .. _Google Cloud SDK: https://cloud.google.com/sdk
403    .. _App Engine standard environment: https://cloud.google.com/appengine
404    .. _App Identity Service: https://cloud.google.com/appengine/docs/python\
405            /appidentity/
406    .. _Compute Engine: https://cloud.google.com/compute
407    .. _App Engine flexible environment: https://cloud.google.com\
408            /appengine/flexible
409    .. _Metadata Service: https://cloud.google.com/compute/docs\
410            /storing-retrieving-metadata
411    .. _Cloud Run: https://cloud.google.com/run
412
413    Example::
414
415        import google.auth
416
417        credentials, project_id = google.auth.default()
418
419    Args:
420        scopes (Sequence[str]): The list of scopes for the credentials. If
421            specified, the credentials will automatically be scoped if
422            necessary.
423        request (Optional[google.auth.transport.Request]): An object used to make
424            HTTP requests. This is used to either detect whether the application
425            is running on Compute Engine or to determine the associated project
426            ID for a workload identity pool resource (external account
427            credentials). If not specified, then it will either use the standard
428            library http client to make requests for Compute Engine credentials
429            or a google.auth.transport.requests.Request client for external
430            account credentials.
431        quota_project_id (Optional[str]): The project ID used for
432            quota and billing.
433        default_scopes (Optional[Sequence[str]]): Default scopes passed by a
434            Google client library. Use 'scopes' for user-defined scopes.
435    Returns:
436        Tuple[~google.auth.credentials.Credentials, Optional[str]]:
437            the current environment's credentials and project ID. Project ID
438            may be None, which indicates that the Project ID could not be
439            ascertained from the environment.
440
441    Raises:
442        ~google.auth.exceptions.DefaultCredentialsError:
443            If no credentials were found, or if the credentials found were
444            invalid.
445    """
446    from google.auth.credentials import with_scopes_if_required
447
448    explicit_project_id = os.environ.get(
449        environment_vars.PROJECT, os.environ.get(environment_vars.LEGACY_PROJECT)
450    )
451
452    checkers = (
453        # Avoid passing scopes here to prevent passing scopes to user credentials.
454        # with_scopes_if_required() below will ensure scopes/default scopes are
455        # safely set on the returned credentials since requires_scopes will
456        # guard against setting scopes on user credentials.
457        lambda: _get_explicit_environ_credentials(quota_project_id=quota_project_id),
458        lambda: _get_gcloud_sdk_credentials(quota_project_id=quota_project_id),
459        _get_gae_credentials,
460        lambda: _get_gce_credentials(request),
461    )
462
463    for checker in checkers:
464        credentials, project_id = checker()
465        if credentials is not None:
466            credentials = with_scopes_if_required(
467                credentials, scopes, default_scopes=default_scopes
468            )
469
470            # For external account credentials, scopes are required to determine
471            # the project ID. Try to get the project ID again if not yet
472            # determined.
473            if not project_id and callable(
474                getattr(credentials, "get_project_id", None)
475            ):
476                if request is None:
477                    request = google.auth.transport.requests.Request()
478                project_id = credentials.get_project_id(request=request)
479
480            if quota_project_id:
481                credentials = credentials.with_quota_project(quota_project_id)
482
483            effective_project_id = explicit_project_id or project_id
484            if not effective_project_id:
485                _LOGGER.warning(
486                    "No project ID could be determined. Consider running "
487                    "`gcloud config set project` or setting the %s "
488                    "environment variable",
489                    environment_vars.PROJECT,
490                )
491            return credentials, effective_project_id
492
493    raise exceptions.DefaultCredentialsError(_HELP_MESSAGE)
494