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