Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- lib/python3.10/site-packages/cachetools/__pycache__/__init__.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/cachetools/__pycache__/_decorators.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/cachetools/__pycache__/func.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/cachetools/__pycache__/keys.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/comm-0.2.3.dist-info/licenses/LICENSE +29 -0
- lib/python3.10/site-packages/filelock/__pycache__/__init__.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/filelock/__pycache__/_api.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/filelock/__pycache__/_error.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/filelock/__pycache__/_soft.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/filelock/__pycache__/_unix.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/filelock/__pycache__/_util.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/filelock/__pycache__/_windows.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/filelock/__pycache__/asyncio.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/filelock/__pycache__/version.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/google/auth/__init__.py +53 -0
- lib/python3.10/site-packages/google/auth/_cloud_sdk.py +153 -0
- lib/python3.10/site-packages/google/auth/_credentials_async.py +171 -0
- lib/python3.10/site-packages/google/auth/_credentials_base.py +75 -0
- lib/python3.10/site-packages/google/auth/_default.py +685 -0
- lib/python3.10/site-packages/google/auth/_default_async.py +282 -0
- lib/python3.10/site-packages/google/auth/_exponential_backoff.py +164 -0
- lib/python3.10/site-packages/google/auth/_helpers.py +513 -0
- lib/python3.10/site-packages/google/auth/_jwt_async.py +164 -0
- lib/python3.10/site-packages/google/auth/_oauth2client.py +167 -0
- lib/python3.10/site-packages/google/auth/_refresh_worker.py +109 -0
- lib/python3.10/site-packages/google/auth/_service_account_info.py +80 -0
- lib/python3.10/site-packages/google/auth/aio/__init__.py +25 -0
- lib/python3.10/site-packages/google/auth/aio/__pycache__/__init__.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/google/auth/aio/__pycache__/_helpers.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/google/auth/aio/__pycache__/credentials.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/google/auth/aio/_helpers.py +62 -0
- lib/python3.10/site-packages/google/auth/aio/credentials.py +143 -0
- lib/python3.10/site-packages/google/auth/aio/transport/__init__.py +144 -0
- lib/python3.10/site-packages/google/auth/aio/transport/__pycache__/__init__.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/google/auth/aio/transport/__pycache__/aiohttp.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/google/auth/aio/transport/__pycache__/sessions.cpython-310.pyc +0 -0
- lib/python3.10/site-packages/google/auth/aio/transport/aiohttp.py +190 -0
- lib/python3.10/site-packages/google/auth/aio/transport/sessions.py +268 -0
- lib/python3.10/site-packages/google/auth/api_key.py +76 -0
- lib/python3.10/site-packages/google/auth/app_engine.py +180 -0
- lib/python3.10/site-packages/google/auth/aws.py +861 -0
- lib/python3.10/site-packages/google/auth/compute_engine/_metadata.py +379 -0
- lib/python3.10/site-packages/google/auth/compute_engine/credentials.py +497 -0
- lib/python3.10/site-packages/google/auth/credentials.py +522 -0
- lib/python3.10/site-packages/google/auth/downscoped.py +512 -0
- lib/python3.10/site-packages/google/auth/environment_vars.py +84 -0
- lib/python3.10/site-packages/google/auth/exceptions.py +108 -0
- lib/python3.10/site-packages/google/auth/external_account.py +628 -0
- lib/python3.10/site-packages/google/auth/external_account_authorized_user.py +380 -0
- lib/python3.10/site-packages/google/auth/iam.py +136 -0
lib/python3.10/site-packages/cachetools/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (23.1 kB). View file
|
|
|
lib/python3.10/site-packages/cachetools/__pycache__/_decorators.cpython-310.pyc
ADDED
|
Binary file (4.83 kB). View file
|
|
|
lib/python3.10/site-packages/cachetools/__pycache__/func.cpython-310.pyc
ADDED
|
Binary file (3.92 kB). View file
|
|
|
lib/python3.10/site-packages/cachetools/__pycache__/keys.cpython-310.pyc
ADDED
|
Binary file (2.47 kB). View file
|
|
|
lib/python3.10/site-packages/comm-0.2.3.dist-info/licenses/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BSD 3-Clause License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2022, Jupyter
|
| 4 |
+
All rights reserved.
|
| 5 |
+
|
| 6 |
+
Redistribution and use in source and binary forms, with or without
|
| 7 |
+
modification, are permitted provided that the following conditions are met:
|
| 8 |
+
|
| 9 |
+
1. Redistributions of source code must retain the above copyright notice, this
|
| 10 |
+
list of conditions and the following disclaimer.
|
| 11 |
+
|
| 12 |
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
| 13 |
+
this list of conditions and the following disclaimer in the documentation
|
| 14 |
+
and/or other materials provided with the distribution.
|
| 15 |
+
|
| 16 |
+
3. Neither the name of the copyright holder nor the names of its
|
| 17 |
+
contributors may be used to endorse or promote products derived from
|
| 18 |
+
this software without specific prior written permission.
|
| 19 |
+
|
| 20 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
| 21 |
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
| 22 |
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
| 23 |
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
| 24 |
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
| 25 |
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
| 26 |
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
| 27 |
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
| 28 |
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
| 29 |
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
lib/python3.10/site-packages/filelock/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (1.4 kB). View file
|
|
|
lib/python3.10/site-packages/filelock/__pycache__/_api.cpython-310.pyc
ADDED
|
Binary file (12.9 kB). View file
|
|
|
lib/python3.10/site-packages/filelock/__pycache__/_error.cpython-310.pyc
ADDED
|
Binary file (1.48 kB). View file
|
|
|
lib/python3.10/site-packages/filelock/__pycache__/_soft.cpython-310.pyc
ADDED
|
Binary file (1.59 kB). View file
|
|
|
lib/python3.10/site-packages/filelock/__pycache__/_unix.cpython-310.pyc
ADDED
|
Binary file (2.22 kB). View file
|
|
|
lib/python3.10/site-packages/filelock/__pycache__/_util.cpython-310.pyc
ADDED
|
Binary file (1.53 kB). View file
|
|
|
lib/python3.10/site-packages/filelock/__pycache__/_windows.cpython-310.pyc
ADDED
|
Binary file (2.1 kB). View file
|
|
|
lib/python3.10/site-packages/filelock/__pycache__/asyncio.cpython-310.pyc
ADDED
|
Binary file (11.9 kB). View file
|
|
|
lib/python3.10/site-packages/filelock/__pycache__/version.cpython-310.pyc
ADDED
|
Binary file (575 Bytes). View file
|
|
|
lib/python3.10/site-packages/google/auth/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 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 |
+
"""Google Auth Library for Python."""
|
| 16 |
+
|
| 17 |
+
import logging
|
| 18 |
+
import sys
|
| 19 |
+
import warnings
|
| 20 |
+
|
| 21 |
+
from google.auth import version as google_auth_version
|
| 22 |
+
from google.auth._default import (
|
| 23 |
+
default,
|
| 24 |
+
load_credentials_from_dict,
|
| 25 |
+
load_credentials_from_file,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
__version__ = google_auth_version.__version__
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
__all__ = ["default", "load_credentials_from_file", "load_credentials_from_dict"]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class Python37DeprecationWarning(DeprecationWarning): # pragma: NO COVER
|
| 36 |
+
"""
|
| 37 |
+
Deprecation warning raised when Python 3.7 runtime is detected.
|
| 38 |
+
Python 3.7 support will be dropped after January 1, 2024.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
pass
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# Checks if the current runtime is Python 3.7.
|
| 45 |
+
if sys.version_info.major == 3 and sys.version_info.minor == 7: # pragma: NO COVER
|
| 46 |
+
message = (
|
| 47 |
+
"After January 1, 2024, new releases of this library will drop support "
|
| 48 |
+
"for Python 3.7."
|
| 49 |
+
)
|
| 50 |
+
warnings.warn(message, Python37DeprecationWarning)
|
| 51 |
+
|
| 52 |
+
# Set default logging handler to avoid "No handler found" warnings.
|
| 53 |
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
lib/python3.10/site-packages/google/auth/_cloud_sdk.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"""Helpers for reading the Google Cloud SDK's configuration."""
|
| 16 |
+
|
| 17 |
+
import os
|
| 18 |
+
import subprocess
|
| 19 |
+
|
| 20 |
+
from google.auth import _helpers
|
| 21 |
+
from google.auth import environment_vars
|
| 22 |
+
from google.auth import exceptions
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# The ~/.config subdirectory containing gcloud credentials.
|
| 26 |
+
_CONFIG_DIRECTORY = "gcloud"
|
| 27 |
+
# Windows systems store config at %APPDATA%\gcloud
|
| 28 |
+
_WINDOWS_CONFIG_ROOT_ENV_VAR = "APPDATA"
|
| 29 |
+
# The name of the file in the Cloud SDK config that contains default
|
| 30 |
+
# credentials.
|
| 31 |
+
_CREDENTIALS_FILENAME = "application_default_credentials.json"
|
| 32 |
+
# The name of the Cloud SDK shell script
|
| 33 |
+
_CLOUD_SDK_POSIX_COMMAND = "gcloud"
|
| 34 |
+
_CLOUD_SDK_WINDOWS_COMMAND = "gcloud.cmd"
|
| 35 |
+
# The command to get the Cloud SDK configuration
|
| 36 |
+
_CLOUD_SDK_CONFIG_GET_PROJECT_COMMAND = ("config", "get", "project")
|
| 37 |
+
# The command to get google user access token
|
| 38 |
+
_CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND = ("auth", "print-access-token")
|
| 39 |
+
# Cloud SDK's application-default client ID
|
| 40 |
+
CLOUD_SDK_CLIENT_ID = (
|
| 41 |
+
"764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com"
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def get_config_path():
|
| 46 |
+
"""Returns the absolute path the the Cloud SDK's configuration directory.
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
str: The Cloud SDK config path.
|
| 50 |
+
"""
|
| 51 |
+
# If the path is explicitly set, return that.
|
| 52 |
+
try:
|
| 53 |
+
return os.environ[environment_vars.CLOUD_SDK_CONFIG_DIR]
|
| 54 |
+
except KeyError:
|
| 55 |
+
pass
|
| 56 |
+
|
| 57 |
+
# Non-windows systems store this at ~/.config/gcloud
|
| 58 |
+
if os.name != "nt":
|
| 59 |
+
return os.path.join(os.path.expanduser("~"), ".config", _CONFIG_DIRECTORY)
|
| 60 |
+
# Windows systems store config at %APPDATA%\gcloud
|
| 61 |
+
else:
|
| 62 |
+
try:
|
| 63 |
+
return os.path.join(
|
| 64 |
+
os.environ[_WINDOWS_CONFIG_ROOT_ENV_VAR], _CONFIG_DIRECTORY
|
| 65 |
+
)
|
| 66 |
+
except KeyError:
|
| 67 |
+
# This should never happen unless someone is really
|
| 68 |
+
# messing with things, but we'll cover the case anyway.
|
| 69 |
+
drive = os.environ.get("SystemDrive", "C:")
|
| 70 |
+
return os.path.join(drive, "\\", _CONFIG_DIRECTORY)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def get_application_default_credentials_path():
|
| 74 |
+
"""Gets the path to the application default credentials file.
|
| 75 |
+
|
| 76 |
+
The path may or may not exist.
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
str: The full path to application default credentials.
|
| 80 |
+
"""
|
| 81 |
+
config_path = get_config_path()
|
| 82 |
+
return os.path.join(config_path, _CREDENTIALS_FILENAME)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _run_subprocess_ignore_stderr(command):
|
| 86 |
+
""" Return subprocess.check_output with the given command and ignores stderr."""
|
| 87 |
+
with open(os.devnull, "w") as devnull:
|
| 88 |
+
output = subprocess.check_output(command, stderr=devnull)
|
| 89 |
+
return output
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def get_project_id():
|
| 93 |
+
"""Gets the project ID from the Cloud SDK.
|
| 94 |
+
|
| 95 |
+
Returns:
|
| 96 |
+
Optional[str]: The project ID.
|
| 97 |
+
"""
|
| 98 |
+
if os.name == "nt":
|
| 99 |
+
command = _CLOUD_SDK_WINDOWS_COMMAND
|
| 100 |
+
else:
|
| 101 |
+
command = _CLOUD_SDK_POSIX_COMMAND
|
| 102 |
+
|
| 103 |
+
try:
|
| 104 |
+
# Ignore the stderr coming from gcloud, so it won't be mixed into the output.
|
| 105 |
+
# https://github.com/googleapis/google-auth-library-python/issues/673
|
| 106 |
+
project = _run_subprocess_ignore_stderr(
|
| 107 |
+
(command,) + _CLOUD_SDK_CONFIG_GET_PROJECT_COMMAND
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
# Turn bytes into a string and remove "\n"
|
| 111 |
+
project = _helpers.from_bytes(project).strip()
|
| 112 |
+
return project if project else None
|
| 113 |
+
except (subprocess.CalledProcessError, OSError, IOError):
|
| 114 |
+
return None
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def get_auth_access_token(account=None):
|
| 118 |
+
"""Load user access token with the ``gcloud auth print-access-token`` command.
|
| 119 |
+
|
| 120 |
+
Args:
|
| 121 |
+
account (Optional[str]): Account to get the access token for. If not
|
| 122 |
+
specified, the current active account will be used.
|
| 123 |
+
|
| 124 |
+
Returns:
|
| 125 |
+
str: The user access token.
|
| 126 |
+
|
| 127 |
+
Raises:
|
| 128 |
+
google.auth.exceptions.UserAccessTokenError: if failed to get access
|
| 129 |
+
token from gcloud.
|
| 130 |
+
"""
|
| 131 |
+
if os.name == "nt":
|
| 132 |
+
command = _CLOUD_SDK_WINDOWS_COMMAND
|
| 133 |
+
else:
|
| 134 |
+
command = _CLOUD_SDK_POSIX_COMMAND
|
| 135 |
+
|
| 136 |
+
try:
|
| 137 |
+
if account:
|
| 138 |
+
command = (
|
| 139 |
+
(command,)
|
| 140 |
+
+ _CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND
|
| 141 |
+
+ ("--account=" + account,)
|
| 142 |
+
)
|
| 143 |
+
else:
|
| 144 |
+
command = (command,) + _CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND
|
| 145 |
+
|
| 146 |
+
access_token = subprocess.check_output(command, stderr=subprocess.STDOUT)
|
| 147 |
+
# remove the trailing "\n"
|
| 148 |
+
return access_token.decode("utf-8").strip()
|
| 149 |
+
except (subprocess.CalledProcessError, OSError, IOError) as caught_exc:
|
| 150 |
+
new_exc = exceptions.UserAccessTokenError(
|
| 151 |
+
"Failed to obtain access token", caught_exc
|
| 152 |
+
)
|
| 153 |
+
raise new_exc from caught_exc
|
lib/python3.10/site-packages/google/auth/_credentials_async.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020 Google LLC
|
| 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 |
+
|
| 16 |
+
"""Interfaces for credentials."""
|
| 17 |
+
|
| 18 |
+
import abc
|
| 19 |
+
import inspect
|
| 20 |
+
|
| 21 |
+
from google.auth import credentials
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class Credentials(credentials.Credentials, metaclass=abc.ABCMeta):
|
| 25 |
+
"""Async inherited credentials class from google.auth.credentials.
|
| 26 |
+
The added functionality is the before_request call which requires
|
| 27 |
+
async/await syntax.
|
| 28 |
+
All credentials have a :attr:`token` that is used for authentication and
|
| 29 |
+
may also optionally set an :attr:`expiry` to indicate when the token will
|
| 30 |
+
no longer be valid.
|
| 31 |
+
|
| 32 |
+
Most credentials will be :attr:`invalid` until :meth:`refresh` is called.
|
| 33 |
+
Credentials can do this automatically before the first HTTP request in
|
| 34 |
+
:meth:`before_request`.
|
| 35 |
+
|
| 36 |
+
Although the token and expiration will change as the credentials are
|
| 37 |
+
:meth:`refreshed <refresh>` and used, credentials should be considered
|
| 38 |
+
immutable. Various credentials will accept configuration such as private
|
| 39 |
+
keys, scopes, and other options. These options are not changeable after
|
| 40 |
+
construction. Some classes will provide mechanisms to copy the credentials
|
| 41 |
+
with modifications such as :meth:`ScopedCredentials.with_scopes`.
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
async def before_request(self, request, method, url, headers):
|
| 45 |
+
"""Performs credential-specific before request logic.
|
| 46 |
+
|
| 47 |
+
Refreshes the credentials if necessary, then calls :meth:`apply` to
|
| 48 |
+
apply the token to the authentication header.
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
request (google.auth.transport.Request): The object used to make
|
| 52 |
+
HTTP requests.
|
| 53 |
+
method (str): The request's HTTP method or the RPC method being
|
| 54 |
+
invoked.
|
| 55 |
+
url (str): The request's URI or the RPC service's URI.
|
| 56 |
+
headers (Mapping): The request's headers.
|
| 57 |
+
"""
|
| 58 |
+
# pylint: disable=unused-argument
|
| 59 |
+
# (Subclasses may use these arguments to ascertain information about
|
| 60 |
+
# the http request.)
|
| 61 |
+
|
| 62 |
+
if not self.valid:
|
| 63 |
+
if inspect.iscoroutinefunction(self.refresh):
|
| 64 |
+
await self.refresh(request)
|
| 65 |
+
else:
|
| 66 |
+
self.refresh(request)
|
| 67 |
+
self.apply(headers)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class CredentialsWithQuotaProject(credentials.CredentialsWithQuotaProject):
|
| 71 |
+
"""Abstract base for credentials supporting ``with_quota_project`` factory"""
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class AnonymousCredentials(credentials.AnonymousCredentials, Credentials):
|
| 75 |
+
"""Credentials that do not provide any authentication information.
|
| 76 |
+
|
| 77 |
+
These are useful in the case of services that support anonymous access or
|
| 78 |
+
local service emulators that do not use credentials. This class inherits
|
| 79 |
+
from the sync anonymous credentials file, but is kept if async credentials
|
| 80 |
+
is initialized and we would like anonymous credentials.
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class ReadOnlyScoped(credentials.ReadOnlyScoped, metaclass=abc.ABCMeta):
|
| 85 |
+
"""Interface for credentials whose scopes can be queried.
|
| 86 |
+
|
| 87 |
+
OAuth 2.0-based credentials allow limiting access using scopes as described
|
| 88 |
+
in `RFC6749 Section 3.3`_.
|
| 89 |
+
If a credential class implements this interface then the credentials either
|
| 90 |
+
use scopes in their implementation.
|
| 91 |
+
|
| 92 |
+
Some credentials require scopes in order to obtain a token. You can check
|
| 93 |
+
if scoping is necessary with :attr:`requires_scopes`::
|
| 94 |
+
|
| 95 |
+
if credentials.requires_scopes:
|
| 96 |
+
# Scoping is required.
|
| 97 |
+
credentials = _credentials_async.with_scopes(scopes=['one', 'two'])
|
| 98 |
+
|
| 99 |
+
Credentials that require scopes must either be constructed with scopes::
|
| 100 |
+
|
| 101 |
+
credentials = SomeScopedCredentials(scopes=['one', 'two'])
|
| 102 |
+
|
| 103 |
+
Or must copy an existing instance using :meth:`with_scopes`::
|
| 104 |
+
|
| 105 |
+
scoped_credentials = _credentials_async.with_scopes(scopes=['one', 'two'])
|
| 106 |
+
|
| 107 |
+
Some credentials have scopes but do not allow or require scopes to be set,
|
| 108 |
+
these credentials can be used as-is.
|
| 109 |
+
|
| 110 |
+
.. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
|
| 111 |
+
"""
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
class Scoped(credentials.Scoped):
|
| 115 |
+
"""Interface for credentials whose scopes can be replaced while copying.
|
| 116 |
+
|
| 117 |
+
OAuth 2.0-based credentials allow limiting access using scopes as described
|
| 118 |
+
in `RFC6749 Section 3.3`_.
|
| 119 |
+
If a credential class implements this interface then the credentials either
|
| 120 |
+
use scopes in their implementation.
|
| 121 |
+
|
| 122 |
+
Some credentials require scopes in order to obtain a token. You can check
|
| 123 |
+
if scoping is necessary with :attr:`requires_scopes`::
|
| 124 |
+
|
| 125 |
+
if credentials.requires_scopes:
|
| 126 |
+
# Scoping is required.
|
| 127 |
+
credentials = _credentials_async.create_scoped(['one', 'two'])
|
| 128 |
+
|
| 129 |
+
Credentials that require scopes must either be constructed with scopes::
|
| 130 |
+
|
| 131 |
+
credentials = SomeScopedCredentials(scopes=['one', 'two'])
|
| 132 |
+
|
| 133 |
+
Or must copy an existing instance using :meth:`with_scopes`::
|
| 134 |
+
|
| 135 |
+
scoped_credentials = credentials.with_scopes(scopes=['one', 'two'])
|
| 136 |
+
|
| 137 |
+
Some credentials have scopes but do not allow or require scopes to be set,
|
| 138 |
+
these credentials can be used as-is.
|
| 139 |
+
|
| 140 |
+
.. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
|
| 141 |
+
"""
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def with_scopes_if_required(credentials, scopes):
|
| 145 |
+
"""Creates a copy of the credentials with scopes if scoping is required.
|
| 146 |
+
|
| 147 |
+
This helper function is useful when you do not know (or care to know) the
|
| 148 |
+
specific type of credentials you are using (such as when you use
|
| 149 |
+
:func:`google.auth.default`). This function will call
|
| 150 |
+
:meth:`Scoped.with_scopes` if the credentials are scoped credentials and if
|
| 151 |
+
the credentials require scoping. Otherwise, it will return the credentials
|
| 152 |
+
as-is.
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
credentials (google.auth.credentials.Credentials): The credentials to
|
| 156 |
+
scope if necessary.
|
| 157 |
+
scopes (Sequence[str]): The list of scopes to use.
|
| 158 |
+
|
| 159 |
+
Returns:
|
| 160 |
+
google.auth._credentials_async.Credentials: Either a new set of scoped
|
| 161 |
+
credentials, or the passed in credentials instance if no scoping
|
| 162 |
+
was required.
|
| 163 |
+
"""
|
| 164 |
+
if isinstance(credentials, Scoped) and credentials.requires_scopes:
|
| 165 |
+
return credentials.with_scopes(scopes)
|
| 166 |
+
else:
|
| 167 |
+
return credentials
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
class Signing(credentials.Signing, metaclass=abc.ABCMeta):
|
| 171 |
+
"""Interface for credentials that can cryptographically sign messages."""
|
lib/python3.10/site-packages/google/auth/_credentials_base.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 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 |
+
|
| 16 |
+
"""Interface for base credentials."""
|
| 17 |
+
|
| 18 |
+
import abc
|
| 19 |
+
|
| 20 |
+
from google.auth import _helpers
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class _BaseCredentials(metaclass=abc.ABCMeta):
|
| 24 |
+
"""Base class for all credentials.
|
| 25 |
+
|
| 26 |
+
All credentials have a :attr:`token` that is used for authentication and
|
| 27 |
+
may also optionally set an :attr:`expiry` to indicate when the token will
|
| 28 |
+
no longer be valid.
|
| 29 |
+
|
| 30 |
+
Most credentials will be :attr:`invalid` until :meth:`refresh` is called.
|
| 31 |
+
Credentials can do this automatically before the first HTTP request in
|
| 32 |
+
:meth:`before_request`.
|
| 33 |
+
|
| 34 |
+
Although the token and expiration will change as the credentials are
|
| 35 |
+
:meth:`refreshed <refresh>` and used, credentials should be considered
|
| 36 |
+
immutable. Various credentials will accept configuration such as private
|
| 37 |
+
keys, scopes, and other options. These options are not changeable after
|
| 38 |
+
construction. Some classes will provide mechanisms to copy the credentials
|
| 39 |
+
with modifications such as :meth:`ScopedCredentials.with_scopes`.
|
| 40 |
+
|
| 41 |
+
Attributes:
|
| 42 |
+
token (Optional[str]): The bearer token that can be used in HTTP headers to make
|
| 43 |
+
authenticated requests.
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def __init__(self):
|
| 47 |
+
self.token = None
|
| 48 |
+
|
| 49 |
+
@abc.abstractmethod
|
| 50 |
+
def refresh(self, request):
|
| 51 |
+
"""Refreshes the access token.
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
request (google.auth.transport.Request): The object used to make
|
| 55 |
+
HTTP requests.
|
| 56 |
+
|
| 57 |
+
Raises:
|
| 58 |
+
google.auth.exceptions.RefreshError: If the credentials could
|
| 59 |
+
not be refreshed.
|
| 60 |
+
"""
|
| 61 |
+
# pylint: disable=missing-raises-doc
|
| 62 |
+
# (pylint doesn't recognize that this is abstract)
|
| 63 |
+
raise NotImplementedError("Refresh must be implemented")
|
| 64 |
+
|
| 65 |
+
def _apply(self, headers, token=None):
|
| 66 |
+
"""Apply the token to the authentication header.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
headers (Mapping): The HTTP request headers.
|
| 70 |
+
token (Optional[str]): If specified, overrides the current access
|
| 71 |
+
token.
|
| 72 |
+
"""
|
| 73 |
+
headers["authorization"] = "Bearer {}".format(
|
| 74 |
+
_helpers.from_bytes(token or self.token)
|
| 75 |
+
)
|
lib/python3.10/site-packages/google/auth/_default.py
ADDED
|
@@ -0,0 +1,685 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 17 |
+
Implements application default credentials and project ID detection.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import io
|
| 21 |
+
import json
|
| 22 |
+
import logging
|
| 23 |
+
import os
|
| 24 |
+
import warnings
|
| 25 |
+
|
| 26 |
+
from google.auth import environment_vars
|
| 27 |
+
from google.auth import exceptions
|
| 28 |
+
import google.auth.transport._http_client
|
| 29 |
+
|
| 30 |
+
_LOGGER = logging.getLogger(__name__)
|
| 31 |
+
|
| 32 |
+
# Valid types accepted for file-based credentials.
|
| 33 |
+
_AUTHORIZED_USER_TYPE = "authorized_user"
|
| 34 |
+
_SERVICE_ACCOUNT_TYPE = "service_account"
|
| 35 |
+
_EXTERNAL_ACCOUNT_TYPE = "external_account"
|
| 36 |
+
_EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE = "external_account_authorized_user"
|
| 37 |
+
_IMPERSONATED_SERVICE_ACCOUNT_TYPE = "impersonated_service_account"
|
| 38 |
+
_GDCH_SERVICE_ACCOUNT_TYPE = "gdch_service_account"
|
| 39 |
+
_VALID_TYPES = (
|
| 40 |
+
_AUTHORIZED_USER_TYPE,
|
| 41 |
+
_SERVICE_ACCOUNT_TYPE,
|
| 42 |
+
_EXTERNAL_ACCOUNT_TYPE,
|
| 43 |
+
_EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE,
|
| 44 |
+
_IMPERSONATED_SERVICE_ACCOUNT_TYPE,
|
| 45 |
+
_GDCH_SERVICE_ACCOUNT_TYPE,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
# Help message when no credentials can be found.
|
| 49 |
+
_CLOUD_SDK_MISSING_CREDENTIALS = """\
|
| 50 |
+
Your default credentials were not found. To set up Application Default Credentials, \
|
| 51 |
+
see https://cloud.google.com/docs/authentication/external/set-up-adc for more information.\
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
# Warning when using Cloud SDK user credentials
|
| 55 |
+
_CLOUD_SDK_CREDENTIALS_WARNING = """\
|
| 56 |
+
Your application has authenticated using end user credentials from Google \
|
| 57 |
+
Cloud SDK without a quota project. You might receive a "quota exceeded" \
|
| 58 |
+
or "API not enabled" error. See the following page for troubleshooting: \
|
| 59 |
+
https://cloud.google.com/docs/authentication/adc-troubleshooting/user-creds. \
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
# The subject token type used for AWS external_account credentials.
|
| 63 |
+
_AWS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request"
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _warn_about_problematic_credentials(credentials):
|
| 67 |
+
"""Determines if the credentials are problematic.
|
| 68 |
+
|
| 69 |
+
Credentials from the Cloud SDK that are associated with Cloud SDK's project
|
| 70 |
+
are problematic because they may not have APIs enabled and have limited
|
| 71 |
+
quota. If this is the case, warn about it.
|
| 72 |
+
"""
|
| 73 |
+
from google.auth import _cloud_sdk
|
| 74 |
+
|
| 75 |
+
if credentials.client_id == _cloud_sdk.CLOUD_SDK_CLIENT_ID:
|
| 76 |
+
warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def load_credentials_from_file(
|
| 80 |
+
filename, scopes=None, default_scopes=None, quota_project_id=None, request=None
|
| 81 |
+
):
|
| 82 |
+
"""Loads Google credentials from a file.
|
| 83 |
+
|
| 84 |
+
The credentials file must be a service account key, stored authorized
|
| 85 |
+
user credentials, external account credentials, or impersonated service
|
| 86 |
+
account credentials.
|
| 87 |
+
|
| 88 |
+
.. warning::
|
| 89 |
+
Important: If you accept a credential configuration (credential JSON/File/Stream)
|
| 90 |
+
from an external source for authentication to Google Cloud Platform, you must
|
| 91 |
+
validate it before providing it to any Google API or client library. Providing an
|
| 92 |
+
unvalidated credential configuration to Google APIs or libraries can compromise
|
| 93 |
+
the security of your systems and data. For more information, refer to
|
| 94 |
+
`Validate credential configurations from external sources`_.
|
| 95 |
+
|
| 96 |
+
.. _Validate credential configurations from external sources:
|
| 97 |
+
https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
filename (str): The full path to the credentials file.
|
| 101 |
+
scopes (Optional[Sequence[str]]): The list of scopes for the credentials. If
|
| 102 |
+
specified, the credentials will automatically be scoped if
|
| 103 |
+
necessary
|
| 104 |
+
default_scopes (Optional[Sequence[str]]): Default scopes passed by a
|
| 105 |
+
Google client library. Use 'scopes' for user-defined scopes.
|
| 106 |
+
quota_project_id (Optional[str]): The project ID used for
|
| 107 |
+
quota and billing.
|
| 108 |
+
request (Optional[google.auth.transport.Request]): An object used to make
|
| 109 |
+
HTTP requests. This is used to determine the associated project ID
|
| 110 |
+
for a workload identity pool resource (external account credentials).
|
| 111 |
+
If not specified, then it will use a
|
| 112 |
+
google.auth.transport.requests.Request client to make requests.
|
| 113 |
+
|
| 114 |
+
Returns:
|
| 115 |
+
Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded
|
| 116 |
+
credentials and the project ID. Authorized user credentials do not
|
| 117 |
+
have the project ID information. External account credentials project
|
| 118 |
+
IDs may not always be determined.
|
| 119 |
+
|
| 120 |
+
Raises:
|
| 121 |
+
google.auth.exceptions.DefaultCredentialsError: if the file is in the
|
| 122 |
+
wrong format or is missing.
|
| 123 |
+
"""
|
| 124 |
+
if not os.path.exists(filename):
|
| 125 |
+
raise exceptions.DefaultCredentialsError(
|
| 126 |
+
"File {} was not found.".format(filename)
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
with io.open(filename, "r") as file_obj:
|
| 130 |
+
try:
|
| 131 |
+
info = json.load(file_obj)
|
| 132 |
+
except ValueError as caught_exc:
|
| 133 |
+
new_exc = exceptions.DefaultCredentialsError(
|
| 134 |
+
"File {} is not a valid json file.".format(filename), caught_exc
|
| 135 |
+
)
|
| 136 |
+
raise new_exc from caught_exc
|
| 137 |
+
return _load_credentials_from_info(
|
| 138 |
+
filename, info, scopes, default_scopes, quota_project_id, request
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def load_credentials_from_dict(
|
| 143 |
+
info, scopes=None, default_scopes=None, quota_project_id=None, request=None
|
| 144 |
+
):
|
| 145 |
+
"""Loads Google credentials from a dict.
|
| 146 |
+
|
| 147 |
+
The credentials file must be a service account key, stored authorized
|
| 148 |
+
user credentials, external account credentials, or impersonated service
|
| 149 |
+
account credentials.
|
| 150 |
+
|
| 151 |
+
.. warning::
|
| 152 |
+
Important: If you accept a credential configuration (credential JSON/File/Stream)
|
| 153 |
+
from an external source for authentication to Google Cloud Platform, you must
|
| 154 |
+
validate it before providing it to any Google API or client library. Providing an
|
| 155 |
+
unvalidated credential configuration to Google APIs or libraries can compromise
|
| 156 |
+
the security of your systems and data. For more information, refer to
|
| 157 |
+
`Validate credential configurations from external sources`_.
|
| 158 |
+
|
| 159 |
+
.. _Validate credential configurations from external sources:
|
| 160 |
+
https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
|
| 161 |
+
|
| 162 |
+
Args:
|
| 163 |
+
info (Dict[str, Any]): A dict object containing the credentials
|
| 164 |
+
scopes (Optional[Sequence[str]]): The list of scopes for the credentials. If
|
| 165 |
+
specified, the credentials will automatically be scoped if
|
| 166 |
+
necessary
|
| 167 |
+
default_scopes (Optional[Sequence[str]]): Default scopes passed by a
|
| 168 |
+
Google client library. Use 'scopes' for user-defined scopes.
|
| 169 |
+
quota_project_id (Optional[str]): The project ID used for
|
| 170 |
+
quota and billing.
|
| 171 |
+
request (Optional[google.auth.transport.Request]): An object used to make
|
| 172 |
+
HTTP requests. This is used to determine the associated project ID
|
| 173 |
+
for a workload identity pool resource (external account credentials).
|
| 174 |
+
If not specified, then it will use a
|
| 175 |
+
google.auth.transport.requests.Request client to make requests.
|
| 176 |
+
|
| 177 |
+
Returns:
|
| 178 |
+
Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded
|
| 179 |
+
credentials and the project ID. Authorized user credentials do not
|
| 180 |
+
have the project ID information. External account credentials project
|
| 181 |
+
IDs may not always be determined.
|
| 182 |
+
|
| 183 |
+
Raises:
|
| 184 |
+
google.auth.exceptions.DefaultCredentialsError: if the file is in the
|
| 185 |
+
wrong format or is missing.
|
| 186 |
+
"""
|
| 187 |
+
if not isinstance(info, dict):
|
| 188 |
+
raise exceptions.DefaultCredentialsError(
|
| 189 |
+
"info object was of type {} but dict type was expected.".format(type(info))
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
return _load_credentials_from_info(
|
| 193 |
+
"dict object", info, scopes, default_scopes, quota_project_id, request
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def _load_credentials_from_info(
|
| 198 |
+
filename, info, scopes, default_scopes, quota_project_id, request
|
| 199 |
+
):
|
| 200 |
+
from google.auth.credentials import CredentialsWithQuotaProject
|
| 201 |
+
|
| 202 |
+
credential_type = info.get("type")
|
| 203 |
+
|
| 204 |
+
if credential_type == _AUTHORIZED_USER_TYPE:
|
| 205 |
+
credentials, project_id = _get_authorized_user_credentials(
|
| 206 |
+
filename, info, scopes
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
elif credential_type == _SERVICE_ACCOUNT_TYPE:
|
| 210 |
+
credentials, project_id = _get_service_account_credentials(
|
| 211 |
+
filename, info, scopes, default_scopes
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
elif credential_type == _EXTERNAL_ACCOUNT_TYPE:
|
| 215 |
+
credentials, project_id = _get_external_account_credentials(
|
| 216 |
+
info,
|
| 217 |
+
filename,
|
| 218 |
+
scopes=scopes,
|
| 219 |
+
default_scopes=default_scopes,
|
| 220 |
+
request=request,
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
elif credential_type == _EXTERNAL_ACCOUNT_AUTHORIZED_USER_TYPE:
|
| 224 |
+
credentials, project_id = _get_external_account_authorized_user_credentials(
|
| 225 |
+
filename, info, request
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
elif credential_type == _IMPERSONATED_SERVICE_ACCOUNT_TYPE:
|
| 229 |
+
credentials, project_id = _get_impersonated_service_account_credentials(
|
| 230 |
+
filename, info, scopes
|
| 231 |
+
)
|
| 232 |
+
elif credential_type == _GDCH_SERVICE_ACCOUNT_TYPE:
|
| 233 |
+
credentials, project_id = _get_gdch_service_account_credentials(filename, info)
|
| 234 |
+
else:
|
| 235 |
+
raise exceptions.DefaultCredentialsError(
|
| 236 |
+
"The file {file} does not have a valid type. "
|
| 237 |
+
"Type is {type}, expected one of {valid_types}.".format(
|
| 238 |
+
file=filename, type=credential_type, valid_types=_VALID_TYPES
|
| 239 |
+
)
|
| 240 |
+
)
|
| 241 |
+
if isinstance(credentials, CredentialsWithQuotaProject):
|
| 242 |
+
credentials = _apply_quota_project_id(credentials, quota_project_id)
|
| 243 |
+
return credentials, project_id
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def _get_gcloud_sdk_credentials(quota_project_id=None):
|
| 247 |
+
"""Gets the credentials and project ID from the Cloud SDK."""
|
| 248 |
+
from google.auth import _cloud_sdk
|
| 249 |
+
|
| 250 |
+
_LOGGER.debug("Checking Cloud SDK credentials as part of auth process...")
|
| 251 |
+
|
| 252 |
+
# Check if application default credentials exist.
|
| 253 |
+
credentials_filename = _cloud_sdk.get_application_default_credentials_path()
|
| 254 |
+
|
| 255 |
+
if not os.path.isfile(credentials_filename):
|
| 256 |
+
_LOGGER.debug("Cloud SDK credentials not found on disk; not using them")
|
| 257 |
+
return None, None
|
| 258 |
+
|
| 259 |
+
credentials, project_id = load_credentials_from_file(
|
| 260 |
+
credentials_filename, quota_project_id=quota_project_id
|
| 261 |
+
)
|
| 262 |
+
credentials._cred_file_path = credentials_filename
|
| 263 |
+
|
| 264 |
+
if not project_id:
|
| 265 |
+
project_id = _cloud_sdk.get_project_id()
|
| 266 |
+
|
| 267 |
+
return credentials, project_id
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def _get_explicit_environ_credentials(quota_project_id=None):
|
| 271 |
+
"""Gets credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
|
| 272 |
+
variable."""
|
| 273 |
+
from google.auth import _cloud_sdk
|
| 274 |
+
|
| 275 |
+
cloud_sdk_adc_path = _cloud_sdk.get_application_default_credentials_path()
|
| 276 |
+
explicit_file = os.environ.get(environment_vars.CREDENTIALS)
|
| 277 |
+
|
| 278 |
+
_LOGGER.debug(
|
| 279 |
+
"Checking %s for explicit credentials as part of auth process...", explicit_file
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
if explicit_file is not None and explicit_file == cloud_sdk_adc_path:
|
| 283 |
+
# Cloud sdk flow calls gcloud to fetch project id, so if the explicit
|
| 284 |
+
# file path is cloud sdk credentials path, then we should fall back
|
| 285 |
+
# to cloud sdk flow, otherwise project id cannot be obtained.
|
| 286 |
+
_LOGGER.debug(
|
| 287 |
+
"Explicit credentials path %s is the same as Cloud SDK credentials path, fall back to Cloud SDK credentials flow...",
|
| 288 |
+
explicit_file,
|
| 289 |
+
)
|
| 290 |
+
return _get_gcloud_sdk_credentials(quota_project_id=quota_project_id)
|
| 291 |
+
|
| 292 |
+
if explicit_file is not None:
|
| 293 |
+
credentials, project_id = load_credentials_from_file(
|
| 294 |
+
os.environ[environment_vars.CREDENTIALS], quota_project_id=quota_project_id
|
| 295 |
+
)
|
| 296 |
+
credentials._cred_file_path = f"{explicit_file} file via the GOOGLE_APPLICATION_CREDENTIALS environment variable"
|
| 297 |
+
|
| 298 |
+
return credentials, project_id
|
| 299 |
+
|
| 300 |
+
else:
|
| 301 |
+
return None, None
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def _get_gae_credentials():
|
| 305 |
+
"""Gets Google App Engine App Identity credentials and project ID."""
|
| 306 |
+
# If not GAE gen1, prefer the metadata service even if the GAE APIs are
|
| 307 |
+
# available as per https://google.aip.dev/auth/4115.
|
| 308 |
+
if os.environ.get(environment_vars.LEGACY_APPENGINE_RUNTIME) != "python27":
|
| 309 |
+
return None, None
|
| 310 |
+
|
| 311 |
+
# While this library is normally bundled with app_engine, there are
|
| 312 |
+
# some cases where it's not available, so we tolerate ImportError.
|
| 313 |
+
try:
|
| 314 |
+
_LOGGER.debug("Checking for App Engine runtime as part of auth process...")
|
| 315 |
+
import google.auth.app_engine as app_engine
|
| 316 |
+
except ImportError:
|
| 317 |
+
_LOGGER.warning("Import of App Engine auth library failed.")
|
| 318 |
+
return None, None
|
| 319 |
+
|
| 320 |
+
try:
|
| 321 |
+
credentials = app_engine.Credentials()
|
| 322 |
+
project_id = app_engine.get_project_id()
|
| 323 |
+
return credentials, project_id
|
| 324 |
+
except EnvironmentError:
|
| 325 |
+
_LOGGER.debug(
|
| 326 |
+
"No App Engine library was found so cannot authentication via App Engine Identity Credentials."
|
| 327 |
+
)
|
| 328 |
+
return None, None
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
def _get_gce_credentials(request=None, quota_project_id=None):
|
| 332 |
+
"""Gets credentials and project ID from the GCE Metadata Service."""
|
| 333 |
+
# Ping requires a transport, but we want application default credentials
|
| 334 |
+
# to require no arguments. So, we'll use the _http_client transport which
|
| 335 |
+
# uses http.client. This is only acceptable because the metadata server
|
| 336 |
+
# doesn't do SSL and never requires proxies.
|
| 337 |
+
|
| 338 |
+
# While this library is normally bundled with compute_engine, there are
|
| 339 |
+
# some cases where it's not available, so we tolerate ImportError.
|
| 340 |
+
try:
|
| 341 |
+
from google.auth import compute_engine
|
| 342 |
+
from google.auth.compute_engine import _metadata
|
| 343 |
+
except ImportError:
|
| 344 |
+
_LOGGER.warning("Import of Compute Engine auth library failed.")
|
| 345 |
+
return None, None
|
| 346 |
+
|
| 347 |
+
if request is None:
|
| 348 |
+
request = google.auth.transport._http_client.Request()
|
| 349 |
+
|
| 350 |
+
if _metadata.is_on_gce(request=request):
|
| 351 |
+
# Get the project ID.
|
| 352 |
+
try:
|
| 353 |
+
project_id = _metadata.get_project_id(request=request)
|
| 354 |
+
except exceptions.TransportError:
|
| 355 |
+
project_id = None
|
| 356 |
+
|
| 357 |
+
cred = compute_engine.Credentials()
|
| 358 |
+
cred = _apply_quota_project_id(cred, quota_project_id)
|
| 359 |
+
|
| 360 |
+
return cred, project_id
|
| 361 |
+
else:
|
| 362 |
+
_LOGGER.warning(
|
| 363 |
+
"Authentication failed using Compute Engine authentication due to unavailable metadata server."
|
| 364 |
+
)
|
| 365 |
+
return None, None
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def _get_external_account_credentials(
|
| 369 |
+
info, filename, scopes=None, default_scopes=None, request=None
|
| 370 |
+
):
|
| 371 |
+
"""Loads external account Credentials from the parsed external account info.
|
| 372 |
+
|
| 373 |
+
The credentials information must correspond to a supported external account
|
| 374 |
+
credentials.
|
| 375 |
+
|
| 376 |
+
Args:
|
| 377 |
+
info (Mapping[str, str]): The external account info in Google format.
|
| 378 |
+
filename (str): The full path to the credentials file.
|
| 379 |
+
scopes (Optional[Sequence[str]]): The list of scopes for the credentials. If
|
| 380 |
+
specified, the credentials will automatically be scoped if
|
| 381 |
+
necessary.
|
| 382 |
+
default_scopes (Optional[Sequence[str]]): Default scopes passed by a
|
| 383 |
+
Google client library. Use 'scopes' for user-defined scopes.
|
| 384 |
+
request (Optional[google.auth.transport.Request]): An object used to make
|
| 385 |
+
HTTP requests. This is used to determine the associated project ID
|
| 386 |
+
for a workload identity pool resource (external account credentials).
|
| 387 |
+
If not specified, then it will use a
|
| 388 |
+
google.auth.transport.requests.Request client to make requests.
|
| 389 |
+
|
| 390 |
+
Returns:
|
| 391 |
+
Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded
|
| 392 |
+
credentials and the project ID. External account credentials project
|
| 393 |
+
IDs may not always be determined.
|
| 394 |
+
|
| 395 |
+
Raises:
|
| 396 |
+
google.auth.exceptions.DefaultCredentialsError: if the info dictionary
|
| 397 |
+
is in the wrong format or is missing required information.
|
| 398 |
+
"""
|
| 399 |
+
# There are currently 3 types of external_account credentials.
|
| 400 |
+
if info.get("subject_token_type") == _AWS_SUBJECT_TOKEN_TYPE:
|
| 401 |
+
# Check if configuration corresponds to an AWS credentials.
|
| 402 |
+
from google.auth import aws
|
| 403 |
+
|
| 404 |
+
credentials = aws.Credentials.from_info(
|
| 405 |
+
info, scopes=scopes, default_scopes=default_scopes
|
| 406 |
+
)
|
| 407 |
+
elif (
|
| 408 |
+
info.get("credential_source") is not None
|
| 409 |
+
and info.get("credential_source").get("executable") is not None
|
| 410 |
+
):
|
| 411 |
+
from google.auth import pluggable
|
| 412 |
+
|
| 413 |
+
credentials = pluggable.Credentials.from_info(
|
| 414 |
+
info, scopes=scopes, default_scopes=default_scopes
|
| 415 |
+
)
|
| 416 |
+
else:
|
| 417 |
+
try:
|
| 418 |
+
# Check if configuration corresponds to an Identity Pool credentials.
|
| 419 |
+
from google.auth import identity_pool
|
| 420 |
+
|
| 421 |
+
credentials = identity_pool.Credentials.from_info(
|
| 422 |
+
info, scopes=scopes, default_scopes=default_scopes
|
| 423 |
+
)
|
| 424 |
+
except ValueError:
|
| 425 |
+
# If the configuration is invalid or does not correspond to any
|
| 426 |
+
# supported external_account credentials, raise an error.
|
| 427 |
+
raise exceptions.DefaultCredentialsError(
|
| 428 |
+
"Failed to load external account credentials from {}".format(filename)
|
| 429 |
+
)
|
| 430 |
+
if request is None:
|
| 431 |
+
import google.auth.transport.requests
|
| 432 |
+
|
| 433 |
+
request = google.auth.transport.requests.Request()
|
| 434 |
+
|
| 435 |
+
return credentials, credentials.get_project_id(request=request)
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
def _get_external_account_authorized_user_credentials(
|
| 439 |
+
filename, info, scopes=None, default_scopes=None, request=None
|
| 440 |
+
):
|
| 441 |
+
try:
|
| 442 |
+
from google.auth import external_account_authorized_user
|
| 443 |
+
|
| 444 |
+
credentials = external_account_authorized_user.Credentials.from_info(info)
|
| 445 |
+
except ValueError:
|
| 446 |
+
raise exceptions.DefaultCredentialsError(
|
| 447 |
+
"Failed to load external account authorized user credentials from {}".format(
|
| 448 |
+
filename
|
| 449 |
+
)
|
| 450 |
+
)
|
| 451 |
+
|
| 452 |
+
return credentials, None
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
def _get_authorized_user_credentials(filename, info, scopes=None):
|
| 456 |
+
from google.oauth2 import credentials
|
| 457 |
+
|
| 458 |
+
try:
|
| 459 |
+
credentials = credentials.Credentials.from_authorized_user_info(
|
| 460 |
+
info, scopes=scopes
|
| 461 |
+
)
|
| 462 |
+
except ValueError as caught_exc:
|
| 463 |
+
msg = "Failed to load authorized user credentials from {}".format(filename)
|
| 464 |
+
new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
|
| 465 |
+
raise new_exc from caught_exc
|
| 466 |
+
return credentials, None
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
def _get_service_account_credentials(filename, info, scopes=None, default_scopes=None):
|
| 470 |
+
from google.oauth2 import service_account
|
| 471 |
+
|
| 472 |
+
try:
|
| 473 |
+
credentials = service_account.Credentials.from_service_account_info(
|
| 474 |
+
info, scopes=scopes, default_scopes=default_scopes
|
| 475 |
+
)
|
| 476 |
+
except ValueError as caught_exc:
|
| 477 |
+
msg = "Failed to load service account credentials from {}".format(filename)
|
| 478 |
+
new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
|
| 479 |
+
raise new_exc from caught_exc
|
| 480 |
+
return credentials, info.get("project_id")
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
def _get_impersonated_service_account_credentials(filename, info, scopes):
|
| 484 |
+
from google.auth import impersonated_credentials
|
| 485 |
+
|
| 486 |
+
try:
|
| 487 |
+
credentials = impersonated_credentials.Credentials.from_impersonated_service_account_info(
|
| 488 |
+
info, scopes=scopes
|
| 489 |
+
)
|
| 490 |
+
except ValueError as caught_exc:
|
| 491 |
+
msg = "Failed to load impersonated service account credentials from {}".format(
|
| 492 |
+
filename
|
| 493 |
+
)
|
| 494 |
+
new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
|
| 495 |
+
raise new_exc from caught_exc
|
| 496 |
+
return credentials, None
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
def _get_gdch_service_account_credentials(filename, info):
|
| 500 |
+
from google.oauth2 import gdch_credentials
|
| 501 |
+
|
| 502 |
+
try:
|
| 503 |
+
credentials = gdch_credentials.ServiceAccountCredentials.from_service_account_info(
|
| 504 |
+
info
|
| 505 |
+
)
|
| 506 |
+
except ValueError as caught_exc:
|
| 507 |
+
msg = "Failed to load GDCH service account credentials from {}".format(filename)
|
| 508 |
+
new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
|
| 509 |
+
raise new_exc from caught_exc
|
| 510 |
+
return credentials, info.get("project")
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
def get_api_key_credentials(key):
|
| 514 |
+
"""Return credentials with the given API key."""
|
| 515 |
+
from google.auth import api_key
|
| 516 |
+
|
| 517 |
+
return api_key.Credentials(key)
|
| 518 |
+
|
| 519 |
+
|
| 520 |
+
def _apply_quota_project_id(credentials, quota_project_id):
|
| 521 |
+
if quota_project_id:
|
| 522 |
+
credentials = credentials.with_quota_project(quota_project_id)
|
| 523 |
+
else:
|
| 524 |
+
credentials = credentials.with_quota_project_from_environment()
|
| 525 |
+
|
| 526 |
+
from google.oauth2 import credentials as authorized_user_credentials
|
| 527 |
+
|
| 528 |
+
if isinstance(credentials, authorized_user_credentials.Credentials) and (
|
| 529 |
+
not credentials.quota_project_id
|
| 530 |
+
):
|
| 531 |
+
_warn_about_problematic_credentials(credentials)
|
| 532 |
+
return credentials
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
def default(scopes=None, request=None, quota_project_id=None, default_scopes=None):
|
| 536 |
+
"""Gets the default credentials for the current environment.
|
| 537 |
+
|
| 538 |
+
`Application Default Credentials`_ provides an easy way to obtain
|
| 539 |
+
credentials to call Google APIs for server-to-server or local applications.
|
| 540 |
+
This function acquires credentials from the environment in the following
|
| 541 |
+
order:
|
| 542 |
+
|
| 543 |
+
1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
|
| 544 |
+
to the path of a valid service account JSON private key file, then it is
|
| 545 |
+
loaded and returned. The project ID returned is the project ID defined
|
| 546 |
+
in the service account file if available (some older files do not
|
| 547 |
+
contain project ID information).
|
| 548 |
+
|
| 549 |
+
If the environment variable is set to the path of a valid external
|
| 550 |
+
account JSON configuration file (workload identity federation), then the
|
| 551 |
+
configuration file is used to determine and retrieve the external
|
| 552 |
+
credentials from the current environment (AWS, Azure, etc).
|
| 553 |
+
These will then be exchanged for Google access tokens via the Google STS
|
| 554 |
+
endpoint.
|
| 555 |
+
The project ID returned in this case is the one corresponding to the
|
| 556 |
+
underlying workload identity pool resource if determinable.
|
| 557 |
+
|
| 558 |
+
If the environment variable is set to the path of a valid GDCH service
|
| 559 |
+
account JSON file (`Google Distributed Cloud Hosted`_), then a GDCH
|
| 560 |
+
credential will be returned. The project ID returned is the project
|
| 561 |
+
specified in the JSON file.
|
| 562 |
+
2. If the `Google Cloud SDK`_ is installed and has application default
|
| 563 |
+
credentials set they are loaded and returned.
|
| 564 |
+
|
| 565 |
+
To enable application default credentials with the Cloud SDK run::
|
| 566 |
+
|
| 567 |
+
gcloud auth application-default login
|
| 568 |
+
|
| 569 |
+
If the Cloud SDK has an active project, the project ID is returned. The
|
| 570 |
+
active project can be set using::
|
| 571 |
+
|
| 572 |
+
gcloud config set project
|
| 573 |
+
|
| 574 |
+
3. If the application is running in the `App Engine standard environment`_
|
| 575 |
+
(first generation) then the credentials and project ID from the
|
| 576 |
+
`App Identity Service`_ are used.
|
| 577 |
+
4. If the application is running in `Compute Engine`_ or `Cloud Run`_ or
|
| 578 |
+
the `App Engine flexible environment`_ or the `App Engine standard
|
| 579 |
+
environment`_ (second generation) then the credentials and project ID
|
| 580 |
+
are obtained from the `Metadata Service`_.
|
| 581 |
+
5. If no credentials are found,
|
| 582 |
+
:class:`~google.auth.exceptions.DefaultCredentialsError` will be raised.
|
| 583 |
+
|
| 584 |
+
.. _Application Default Credentials: https://developers.google.com\
|
| 585 |
+
/identity/protocols/application-default-credentials
|
| 586 |
+
.. _Google Cloud SDK: https://cloud.google.com/sdk
|
| 587 |
+
.. _App Engine standard environment: https://cloud.google.com/appengine
|
| 588 |
+
.. _App Identity Service: https://cloud.google.com/appengine/docs/python\
|
| 589 |
+
/appidentity/
|
| 590 |
+
.. _Compute Engine: https://cloud.google.com/compute
|
| 591 |
+
.. _App Engine flexible environment: https://cloud.google.com\
|
| 592 |
+
/appengine/flexible
|
| 593 |
+
.. _Metadata Service: https://cloud.google.com/compute/docs\
|
| 594 |
+
/storing-retrieving-metadata
|
| 595 |
+
.. _Cloud Run: https://cloud.google.com/run
|
| 596 |
+
.. _Google Distributed Cloud Hosted: https://cloud.google.com/blog/topics\
|
| 597 |
+
/hybrid-cloud/announcing-google-distributed-cloud-edge-and-hosted
|
| 598 |
+
|
| 599 |
+
Example::
|
| 600 |
+
|
| 601 |
+
import google.auth
|
| 602 |
+
|
| 603 |
+
credentials, project_id = google.auth.default()
|
| 604 |
+
|
| 605 |
+
Args:
|
| 606 |
+
scopes (Sequence[str]): The list of scopes for the credentials. If
|
| 607 |
+
specified, the credentials will automatically be scoped if
|
| 608 |
+
necessary.
|
| 609 |
+
request (Optional[google.auth.transport.Request]): An object used to make
|
| 610 |
+
HTTP requests. This is used to either detect whether the application
|
| 611 |
+
is running on Compute Engine or to determine the associated project
|
| 612 |
+
ID for a workload identity pool resource (external account
|
| 613 |
+
credentials). If not specified, then it will either use the standard
|
| 614 |
+
library http client to make requests for Compute Engine credentials
|
| 615 |
+
or a google.auth.transport.requests.Request client for external
|
| 616 |
+
account credentials.
|
| 617 |
+
quota_project_id (Optional[str]): The project ID used for
|
| 618 |
+
quota and billing.
|
| 619 |
+
default_scopes (Optional[Sequence[str]]): Default scopes passed by a
|
| 620 |
+
Google client library. Use 'scopes' for user-defined scopes.
|
| 621 |
+
Returns:
|
| 622 |
+
Tuple[~google.auth.credentials.Credentials, Optional[str]]:
|
| 623 |
+
the current environment's credentials and project ID. Project ID
|
| 624 |
+
may be None, which indicates that the Project ID could not be
|
| 625 |
+
ascertained from the environment.
|
| 626 |
+
|
| 627 |
+
Raises:
|
| 628 |
+
~google.auth.exceptions.DefaultCredentialsError:
|
| 629 |
+
If no credentials were found, or if the credentials found were
|
| 630 |
+
invalid.
|
| 631 |
+
"""
|
| 632 |
+
from google.auth.credentials import with_scopes_if_required
|
| 633 |
+
from google.auth.credentials import CredentialsWithQuotaProject
|
| 634 |
+
|
| 635 |
+
explicit_project_id = os.environ.get(
|
| 636 |
+
environment_vars.PROJECT, os.environ.get(environment_vars.LEGACY_PROJECT)
|
| 637 |
+
)
|
| 638 |
+
|
| 639 |
+
checkers = (
|
| 640 |
+
# Avoid passing scopes here to prevent passing scopes to user credentials.
|
| 641 |
+
# with_scopes_if_required() below will ensure scopes/default scopes are
|
| 642 |
+
# safely set on the returned credentials since requires_scopes will
|
| 643 |
+
# guard against setting scopes on user credentials.
|
| 644 |
+
lambda: _get_explicit_environ_credentials(quota_project_id=quota_project_id),
|
| 645 |
+
lambda: _get_gcloud_sdk_credentials(quota_project_id=quota_project_id),
|
| 646 |
+
_get_gae_credentials,
|
| 647 |
+
lambda: _get_gce_credentials(request, quota_project_id=quota_project_id),
|
| 648 |
+
)
|
| 649 |
+
|
| 650 |
+
for checker in checkers:
|
| 651 |
+
credentials, project_id = checker()
|
| 652 |
+
if credentials is not None:
|
| 653 |
+
credentials = with_scopes_if_required(
|
| 654 |
+
credentials, scopes, default_scopes=default_scopes
|
| 655 |
+
)
|
| 656 |
+
|
| 657 |
+
effective_project_id = explicit_project_id or project_id
|
| 658 |
+
|
| 659 |
+
# For external account credentials, scopes are required to determine
|
| 660 |
+
# the project ID. Try to get the project ID again if not yet
|
| 661 |
+
# determined.
|
| 662 |
+
if not effective_project_id and callable(
|
| 663 |
+
getattr(credentials, "get_project_id", None)
|
| 664 |
+
):
|
| 665 |
+
if request is None:
|
| 666 |
+
import google.auth.transport.requests
|
| 667 |
+
|
| 668 |
+
request = google.auth.transport.requests.Request()
|
| 669 |
+
effective_project_id = credentials.get_project_id(request=request)
|
| 670 |
+
|
| 671 |
+
if quota_project_id and isinstance(
|
| 672 |
+
credentials, CredentialsWithQuotaProject
|
| 673 |
+
):
|
| 674 |
+
credentials = credentials.with_quota_project(quota_project_id)
|
| 675 |
+
|
| 676 |
+
if not effective_project_id:
|
| 677 |
+
_LOGGER.warning(
|
| 678 |
+
"No project ID could be determined. Consider running "
|
| 679 |
+
"`gcloud config set project` or setting the %s "
|
| 680 |
+
"environment variable",
|
| 681 |
+
environment_vars.PROJECT,
|
| 682 |
+
)
|
| 683 |
+
return credentials, effective_project_id
|
| 684 |
+
|
| 685 |
+
raise exceptions.DefaultCredentialsError(_CLOUD_SDK_MISSING_CREDENTIALS)
|
lib/python3.10/site-packages/google/auth/_default_async.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020 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 |
+
|
| 17 |
+
Implements application default credentials and project ID detection.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import io
|
| 21 |
+
import json
|
| 22 |
+
import os
|
| 23 |
+
|
| 24 |
+
from google.auth import _default
|
| 25 |
+
from google.auth import environment_vars
|
| 26 |
+
from google.auth import exceptions
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def load_credentials_from_file(filename, scopes=None, quota_project_id=None):
|
| 30 |
+
"""Loads Google credentials from a file.
|
| 31 |
+
|
| 32 |
+
The credentials file must be a service account key or stored authorized
|
| 33 |
+
user credentials.
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
filename (str): The full path to the credentials file.
|
| 37 |
+
scopes (Optional[Sequence[str]]): The list of scopes for the credentials. If
|
| 38 |
+
specified, the credentials will automatically be scoped if
|
| 39 |
+
necessary
|
| 40 |
+
quota_project_id (Optional[str]): The project ID used for
|
| 41 |
+
quota and billing.
|
| 42 |
+
|
| 43 |
+
Returns:
|
| 44 |
+
Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded
|
| 45 |
+
credentials and the project ID. Authorized user credentials do not
|
| 46 |
+
have the project ID information.
|
| 47 |
+
|
| 48 |
+
Raises:
|
| 49 |
+
google.auth.exceptions.DefaultCredentialsError: if the file is in the
|
| 50 |
+
wrong format or is missing.
|
| 51 |
+
"""
|
| 52 |
+
if not os.path.exists(filename):
|
| 53 |
+
raise exceptions.DefaultCredentialsError(
|
| 54 |
+
"File {} was not found.".format(filename)
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
with io.open(filename, "r") as file_obj:
|
| 58 |
+
try:
|
| 59 |
+
info = json.load(file_obj)
|
| 60 |
+
except ValueError as caught_exc:
|
| 61 |
+
new_exc = exceptions.DefaultCredentialsError(
|
| 62 |
+
"File {} is not a valid json file.".format(filename), caught_exc
|
| 63 |
+
)
|
| 64 |
+
raise new_exc from caught_exc
|
| 65 |
+
|
| 66 |
+
# The type key should indicate that the file is either a service account
|
| 67 |
+
# credentials file or an authorized user credentials file.
|
| 68 |
+
credential_type = info.get("type")
|
| 69 |
+
|
| 70 |
+
if credential_type == _default._AUTHORIZED_USER_TYPE:
|
| 71 |
+
from google.oauth2 import _credentials_async as credentials
|
| 72 |
+
|
| 73 |
+
try:
|
| 74 |
+
credentials = credentials.Credentials.from_authorized_user_info(
|
| 75 |
+
info, scopes=scopes
|
| 76 |
+
)
|
| 77 |
+
except ValueError as caught_exc:
|
| 78 |
+
msg = "Failed to load authorized user credentials from {}".format(filename)
|
| 79 |
+
new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
|
| 80 |
+
raise new_exc from caught_exc
|
| 81 |
+
if quota_project_id:
|
| 82 |
+
credentials = credentials.with_quota_project(quota_project_id)
|
| 83 |
+
if not credentials.quota_project_id:
|
| 84 |
+
_default._warn_about_problematic_credentials(credentials)
|
| 85 |
+
return credentials, None
|
| 86 |
+
|
| 87 |
+
elif credential_type == _default._SERVICE_ACCOUNT_TYPE:
|
| 88 |
+
from google.oauth2 import _service_account_async as service_account
|
| 89 |
+
|
| 90 |
+
try:
|
| 91 |
+
credentials = service_account.Credentials.from_service_account_info(
|
| 92 |
+
info, scopes=scopes
|
| 93 |
+
).with_quota_project(quota_project_id)
|
| 94 |
+
except ValueError as caught_exc:
|
| 95 |
+
msg = "Failed to load service account credentials from {}".format(filename)
|
| 96 |
+
new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
|
| 97 |
+
raise new_exc from caught_exc
|
| 98 |
+
return credentials, info.get("project_id")
|
| 99 |
+
|
| 100 |
+
else:
|
| 101 |
+
raise exceptions.DefaultCredentialsError(
|
| 102 |
+
"The file {file} does not have a valid type. "
|
| 103 |
+
"Type is {type}, expected one of {valid_types}.".format(
|
| 104 |
+
file=filename, type=credential_type, valid_types=_default._VALID_TYPES
|
| 105 |
+
)
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _get_gcloud_sdk_credentials(quota_project_id=None):
|
| 110 |
+
"""Gets the credentials and project ID from the Cloud SDK."""
|
| 111 |
+
from google.auth import _cloud_sdk
|
| 112 |
+
|
| 113 |
+
# Check if application default credentials exist.
|
| 114 |
+
credentials_filename = _cloud_sdk.get_application_default_credentials_path()
|
| 115 |
+
|
| 116 |
+
if not os.path.isfile(credentials_filename):
|
| 117 |
+
return None, None
|
| 118 |
+
|
| 119 |
+
credentials, project_id = load_credentials_from_file(
|
| 120 |
+
credentials_filename, quota_project_id=quota_project_id
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
if not project_id:
|
| 124 |
+
project_id = _cloud_sdk.get_project_id()
|
| 125 |
+
|
| 126 |
+
return credentials, project_id
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _get_explicit_environ_credentials(quota_project_id=None):
|
| 130 |
+
"""Gets credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
|
| 131 |
+
variable."""
|
| 132 |
+
from google.auth import _cloud_sdk
|
| 133 |
+
|
| 134 |
+
cloud_sdk_adc_path = _cloud_sdk.get_application_default_credentials_path()
|
| 135 |
+
explicit_file = os.environ.get(environment_vars.CREDENTIALS)
|
| 136 |
+
|
| 137 |
+
if explicit_file is not None and explicit_file == cloud_sdk_adc_path:
|
| 138 |
+
# Cloud sdk flow calls gcloud to fetch project id, so if the explicit
|
| 139 |
+
# file path is cloud sdk credentials path, then we should fall back
|
| 140 |
+
# to cloud sdk flow, otherwise project id cannot be obtained.
|
| 141 |
+
return _get_gcloud_sdk_credentials(quota_project_id=quota_project_id)
|
| 142 |
+
|
| 143 |
+
if explicit_file is not None:
|
| 144 |
+
credentials, project_id = load_credentials_from_file(
|
| 145 |
+
os.environ[environment_vars.CREDENTIALS], quota_project_id=quota_project_id
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
return credentials, project_id
|
| 149 |
+
|
| 150 |
+
else:
|
| 151 |
+
return None, None
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def _get_gae_credentials():
|
| 155 |
+
"""Gets Google App Engine App Identity credentials and project ID."""
|
| 156 |
+
# While this library is normally bundled with app_engine, there are
|
| 157 |
+
# some cases where it's not available, so we tolerate ImportError.
|
| 158 |
+
|
| 159 |
+
return _default._get_gae_credentials()
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _get_gce_credentials(request=None):
|
| 163 |
+
"""Gets credentials and project ID from the GCE Metadata Service."""
|
| 164 |
+
# Ping requires a transport, but we want application default credentials
|
| 165 |
+
# to require no arguments. So, we'll use the _http_client transport which
|
| 166 |
+
# uses http.client. This is only acceptable because the metadata server
|
| 167 |
+
# doesn't do SSL and never requires proxies.
|
| 168 |
+
|
| 169 |
+
# While this library is normally bundled with compute_engine, there are
|
| 170 |
+
# some cases where it's not available, so we tolerate ImportError.
|
| 171 |
+
|
| 172 |
+
return _default._get_gce_credentials(request)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def default_async(scopes=None, request=None, quota_project_id=None):
|
| 176 |
+
"""Gets the default credentials for the current environment.
|
| 177 |
+
|
| 178 |
+
`Application Default Credentials`_ provides an easy way to obtain
|
| 179 |
+
credentials to call Google APIs for server-to-server or local applications.
|
| 180 |
+
This function acquires credentials from the environment in the following
|
| 181 |
+
order:
|
| 182 |
+
|
| 183 |
+
1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
|
| 184 |
+
to the path of a valid service account JSON private key file, then it is
|
| 185 |
+
loaded and returned. The project ID returned is the project ID defined
|
| 186 |
+
in the service account file if available (some older files do not
|
| 187 |
+
contain project ID information).
|
| 188 |
+
2. If the `Google Cloud SDK`_ is installed and has application default
|
| 189 |
+
credentials set they are loaded and returned.
|
| 190 |
+
|
| 191 |
+
To enable application default credentials with the Cloud SDK run::
|
| 192 |
+
|
| 193 |
+
gcloud auth application-default login
|
| 194 |
+
|
| 195 |
+
If the Cloud SDK has an active project, the project ID is returned. The
|
| 196 |
+
active project can be set using::
|
| 197 |
+
|
| 198 |
+
gcloud config set project
|
| 199 |
+
|
| 200 |
+
3. If the application is running in the `App Engine standard environment`_
|
| 201 |
+
(first generation) then the credentials and project ID from the
|
| 202 |
+
`App Identity Service`_ are used.
|
| 203 |
+
4. If the application is running in `Compute Engine`_ or `Cloud Run`_ or
|
| 204 |
+
the `App Engine flexible environment`_ or the `App Engine standard
|
| 205 |
+
environment`_ (second generation) then the credentials and project ID
|
| 206 |
+
are obtained from the `Metadata Service`_.
|
| 207 |
+
5. If no credentials are found,
|
| 208 |
+
:class:`~google.auth.exceptions.DefaultCredentialsError` will be raised.
|
| 209 |
+
|
| 210 |
+
.. _Application Default Credentials: https://developers.google.com\
|
| 211 |
+
/identity/protocols/application-default-credentials
|
| 212 |
+
.. _Google Cloud SDK: https://cloud.google.com/sdk
|
| 213 |
+
.. _App Engine standard environment: https://cloud.google.com/appengine
|
| 214 |
+
.. _App Identity Service: https://cloud.google.com/appengine/docs/python\
|
| 215 |
+
/appidentity/
|
| 216 |
+
.. _Compute Engine: https://cloud.google.com/compute
|
| 217 |
+
.. _App Engine flexible environment: https://cloud.google.com\
|
| 218 |
+
/appengine/flexible
|
| 219 |
+
.. _Metadata Service: https://cloud.google.com/compute/docs\
|
| 220 |
+
/storing-retrieving-metadata
|
| 221 |
+
.. _Cloud Run: https://cloud.google.com/run
|
| 222 |
+
|
| 223 |
+
Example::
|
| 224 |
+
|
| 225 |
+
import google.auth
|
| 226 |
+
|
| 227 |
+
credentials, project_id = google.auth.default()
|
| 228 |
+
|
| 229 |
+
Args:
|
| 230 |
+
scopes (Sequence[str]): The list of scopes for the credentials. If
|
| 231 |
+
specified, the credentials will automatically be scoped if
|
| 232 |
+
necessary.
|
| 233 |
+
request (google.auth.transport.Request): An object used to make
|
| 234 |
+
HTTP requests. This is used to detect whether the application
|
| 235 |
+
is running on Compute Engine. If not specified, then it will
|
| 236 |
+
use the standard library http client to make requests.
|
| 237 |
+
quota_project_id (Optional[str]): The project ID used for
|
| 238 |
+
quota and billing.
|
| 239 |
+
Returns:
|
| 240 |
+
Tuple[~google.auth.credentials.Credentials, Optional[str]]:
|
| 241 |
+
the current environment's credentials and project ID. Project ID
|
| 242 |
+
may be None, which indicates that the Project ID could not be
|
| 243 |
+
ascertained from the environment.
|
| 244 |
+
|
| 245 |
+
Raises:
|
| 246 |
+
~google.auth.exceptions.DefaultCredentialsError:
|
| 247 |
+
If no credentials were found, or if the credentials found were
|
| 248 |
+
invalid.
|
| 249 |
+
"""
|
| 250 |
+
from google.auth._credentials_async import with_scopes_if_required
|
| 251 |
+
from google.auth.credentials import CredentialsWithQuotaProject
|
| 252 |
+
|
| 253 |
+
explicit_project_id = os.environ.get(
|
| 254 |
+
environment_vars.PROJECT, os.environ.get(environment_vars.LEGACY_PROJECT)
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
checkers = (
|
| 258 |
+
lambda: _get_explicit_environ_credentials(quota_project_id=quota_project_id),
|
| 259 |
+
lambda: _get_gcloud_sdk_credentials(quota_project_id=quota_project_id),
|
| 260 |
+
_get_gae_credentials,
|
| 261 |
+
lambda: _get_gce_credentials(request),
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
for checker in checkers:
|
| 265 |
+
credentials, project_id = checker()
|
| 266 |
+
if credentials is not None:
|
| 267 |
+
credentials = with_scopes_if_required(credentials, scopes)
|
| 268 |
+
if quota_project_id and isinstance(
|
| 269 |
+
credentials, CredentialsWithQuotaProject
|
| 270 |
+
):
|
| 271 |
+
credentials = credentials.with_quota_project(quota_project_id)
|
| 272 |
+
effective_project_id = explicit_project_id or project_id
|
| 273 |
+
if not effective_project_id:
|
| 274 |
+
_default._LOGGER.warning(
|
| 275 |
+
"No project ID could be determined. Consider running "
|
| 276 |
+
"`gcloud config set project` or setting the %s "
|
| 277 |
+
"environment variable",
|
| 278 |
+
environment_vars.PROJECT,
|
| 279 |
+
)
|
| 280 |
+
return credentials, effective_project_id
|
| 281 |
+
|
| 282 |
+
raise exceptions.DefaultCredentialsError(_default._CLOUD_SDK_MISSING_CREDENTIALS)
|
lib/python3.10/site-packages/google/auth/_exponential_backoff.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2022 Google LLC
|
| 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 |
+
import asyncio
|
| 16 |
+
import random
|
| 17 |
+
import time
|
| 18 |
+
|
| 19 |
+
from google.auth import exceptions
|
| 20 |
+
|
| 21 |
+
# The default amount of retry attempts
|
| 22 |
+
_DEFAULT_RETRY_TOTAL_ATTEMPTS = 3
|
| 23 |
+
|
| 24 |
+
# The default initial backoff period (1.0 second).
|
| 25 |
+
_DEFAULT_INITIAL_INTERVAL_SECONDS = 1.0
|
| 26 |
+
|
| 27 |
+
# The default randomization factor (0.1 which results in a random period ranging
|
| 28 |
+
# between 10% below and 10% above the retry interval).
|
| 29 |
+
_DEFAULT_RANDOMIZATION_FACTOR = 0.1
|
| 30 |
+
|
| 31 |
+
# The default multiplier value (2 which is 100% increase per back off).
|
| 32 |
+
_DEFAULT_MULTIPLIER = 2.0
|
| 33 |
+
|
| 34 |
+
"""Exponential Backoff Utility
|
| 35 |
+
|
| 36 |
+
This is a private module that implements the exponential back off algorithm.
|
| 37 |
+
It can be used as a utility for code that needs to retry on failure, for example
|
| 38 |
+
an HTTP request.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class _BaseExponentialBackoff:
|
| 43 |
+
"""An exponential backoff iterator base class.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
total_attempts Optional[int]:
|
| 47 |
+
The maximum amount of retries that should happen.
|
| 48 |
+
The default value is 3 attempts.
|
| 49 |
+
initial_wait_seconds Optional[int]:
|
| 50 |
+
The amount of time to sleep in the first backoff. This parameter
|
| 51 |
+
should be in seconds.
|
| 52 |
+
The default value is 1 second.
|
| 53 |
+
randomization_factor Optional[float]:
|
| 54 |
+
The amount of jitter that should be in each backoff. For example,
|
| 55 |
+
a value of 0.1 will introduce a jitter range of 10% to the
|
| 56 |
+
current backoff period.
|
| 57 |
+
The default value is 0.1.
|
| 58 |
+
multiplier Optional[float]:
|
| 59 |
+
The backoff multipler. This adjusts how much each backoff will
|
| 60 |
+
increase. For example a value of 2.0 leads to a 200% backoff
|
| 61 |
+
on each attempt. If the initial_wait is 1.0 it would look like
|
| 62 |
+
this sequence [1.0, 2.0, 4.0, 8.0].
|
| 63 |
+
The default value is 2.0.
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
def __init__(
|
| 67 |
+
self,
|
| 68 |
+
total_attempts=_DEFAULT_RETRY_TOTAL_ATTEMPTS,
|
| 69 |
+
initial_wait_seconds=_DEFAULT_INITIAL_INTERVAL_SECONDS,
|
| 70 |
+
randomization_factor=_DEFAULT_RANDOMIZATION_FACTOR,
|
| 71 |
+
multiplier=_DEFAULT_MULTIPLIER,
|
| 72 |
+
):
|
| 73 |
+
if total_attempts < 1:
|
| 74 |
+
raise exceptions.InvalidValue(
|
| 75 |
+
f"total_attempts must be greater than or equal to 1 but was {total_attempts}"
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
self._total_attempts = total_attempts
|
| 79 |
+
self._initial_wait_seconds = initial_wait_seconds
|
| 80 |
+
|
| 81 |
+
self._current_wait_in_seconds = self._initial_wait_seconds
|
| 82 |
+
|
| 83 |
+
self._randomization_factor = randomization_factor
|
| 84 |
+
self._multiplier = multiplier
|
| 85 |
+
self._backoff_count = 0
|
| 86 |
+
|
| 87 |
+
@property
|
| 88 |
+
def total_attempts(self):
|
| 89 |
+
"""The total amount of backoff attempts that will be made."""
|
| 90 |
+
return self._total_attempts
|
| 91 |
+
|
| 92 |
+
@property
|
| 93 |
+
def backoff_count(self):
|
| 94 |
+
"""The current amount of backoff attempts that have been made."""
|
| 95 |
+
return self._backoff_count
|
| 96 |
+
|
| 97 |
+
def _reset(self):
|
| 98 |
+
self._backoff_count = 0
|
| 99 |
+
self._current_wait_in_seconds = self._initial_wait_seconds
|
| 100 |
+
|
| 101 |
+
def _calculate_jitter(self):
|
| 102 |
+
jitter_variance = self._current_wait_in_seconds * self._randomization_factor
|
| 103 |
+
jitter = random.uniform(
|
| 104 |
+
self._current_wait_in_seconds - jitter_variance,
|
| 105 |
+
self._current_wait_in_seconds + jitter_variance,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
return jitter
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class ExponentialBackoff(_BaseExponentialBackoff):
|
| 112 |
+
"""An exponential backoff iterator. This can be used in a for loop to
|
| 113 |
+
perform requests with exponential backoff.
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
def __init__(self, *args, **kwargs):
|
| 117 |
+
super(ExponentialBackoff, self).__init__(*args, **kwargs)
|
| 118 |
+
|
| 119 |
+
def __iter__(self):
|
| 120 |
+
self._reset()
|
| 121 |
+
return self
|
| 122 |
+
|
| 123 |
+
def __next__(self):
|
| 124 |
+
if self._backoff_count >= self._total_attempts:
|
| 125 |
+
raise StopIteration
|
| 126 |
+
self._backoff_count += 1
|
| 127 |
+
|
| 128 |
+
if self._backoff_count <= 1:
|
| 129 |
+
return self._backoff_count
|
| 130 |
+
|
| 131 |
+
jitter = self._calculate_jitter()
|
| 132 |
+
|
| 133 |
+
time.sleep(jitter)
|
| 134 |
+
|
| 135 |
+
self._current_wait_in_seconds *= self._multiplier
|
| 136 |
+
return self._backoff_count
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
class AsyncExponentialBackoff(_BaseExponentialBackoff):
|
| 140 |
+
"""An async exponential backoff iterator. This can be used in a for loop to
|
| 141 |
+
perform async requests with exponential backoff.
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
def __init__(self, *args, **kwargs):
|
| 145 |
+
super(AsyncExponentialBackoff, self).__init__(*args, **kwargs)
|
| 146 |
+
|
| 147 |
+
def __aiter__(self):
|
| 148 |
+
self._reset()
|
| 149 |
+
return self
|
| 150 |
+
|
| 151 |
+
async def __anext__(self):
|
| 152 |
+
if self._backoff_count >= self._total_attempts:
|
| 153 |
+
raise StopAsyncIteration
|
| 154 |
+
self._backoff_count += 1
|
| 155 |
+
|
| 156 |
+
if self._backoff_count <= 1:
|
| 157 |
+
return self._backoff_count
|
| 158 |
+
|
| 159 |
+
jitter = self._calculate_jitter()
|
| 160 |
+
|
| 161 |
+
await asyncio.sleep(jitter)
|
| 162 |
+
|
| 163 |
+
self._current_wait_in_seconds *= self._multiplier
|
| 164 |
+
return self._backoff_count
|
lib/python3.10/site-packages/google/auth/_helpers.py
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"""Helper functions for commonly used utilities."""
|
| 16 |
+
|
| 17 |
+
import base64
|
| 18 |
+
import calendar
|
| 19 |
+
import datetime
|
| 20 |
+
from email.message import Message
|
| 21 |
+
import hashlib
|
| 22 |
+
import json
|
| 23 |
+
import logging
|
| 24 |
+
import sys
|
| 25 |
+
from typing import Any, Dict, Mapping, Optional, Union
|
| 26 |
+
import urllib
|
| 27 |
+
|
| 28 |
+
from google.auth import exceptions
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# _BASE_LOGGER_NAME is the base logger for all google-based loggers.
|
| 32 |
+
_BASE_LOGGER_NAME = "google"
|
| 33 |
+
|
| 34 |
+
# _LOGGING_INITIALIZED ensures that base logger is only configured once
|
| 35 |
+
# (unless already configured by the end-user).
|
| 36 |
+
_LOGGING_INITIALIZED = False
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# The smallest MDS cache used by this library stores tokens until 4 minutes from
|
| 40 |
+
# expiry.
|
| 41 |
+
REFRESH_THRESHOLD = datetime.timedelta(minutes=3, seconds=45)
|
| 42 |
+
|
| 43 |
+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1684): Audit and update the list below.
|
| 44 |
+
_SENSITIVE_FIELDS = {
|
| 45 |
+
"accessToken",
|
| 46 |
+
"access_token",
|
| 47 |
+
"id_token",
|
| 48 |
+
"client_id",
|
| 49 |
+
"refresh_token",
|
| 50 |
+
"client_secret",
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def copy_docstring(source_class):
|
| 55 |
+
"""Decorator that copies a method's docstring from another class.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
source_class (type): The class that has the documented method.
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
Callable: A decorator that will copy the docstring of the same
|
| 62 |
+
named method in the source class to the decorated method.
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
def decorator(method):
|
| 66 |
+
"""Decorator implementation.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
method (Callable): The method to copy the docstring to.
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
Callable: the same method passed in with an updated docstring.
|
| 73 |
+
|
| 74 |
+
Raises:
|
| 75 |
+
google.auth.exceptions.InvalidOperation: if the method already has a docstring.
|
| 76 |
+
"""
|
| 77 |
+
if method.__doc__:
|
| 78 |
+
raise exceptions.InvalidOperation("Method already has a docstring.")
|
| 79 |
+
|
| 80 |
+
source_method = getattr(source_class, method.__name__)
|
| 81 |
+
method.__doc__ = source_method.__doc__
|
| 82 |
+
|
| 83 |
+
return method
|
| 84 |
+
|
| 85 |
+
return decorator
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def parse_content_type(header_value):
|
| 89 |
+
"""Parse a 'content-type' header value to get just the plain media-type (without parameters).
|
| 90 |
+
|
| 91 |
+
This is done using the class Message from email.message as suggested in PEP 594
|
| 92 |
+
(because the cgi is now deprecated and will be removed in python 3.13,
|
| 93 |
+
see https://peps.python.org/pep-0594/#cgi).
|
| 94 |
+
|
| 95 |
+
Args:
|
| 96 |
+
header_value (str): The value of a 'content-type' header as a string.
|
| 97 |
+
|
| 98 |
+
Returns:
|
| 99 |
+
str: A string with just the lowercase media-type from the parsed 'content-type' header.
|
| 100 |
+
If the provided content-type is not parsable, returns 'text/plain',
|
| 101 |
+
the default value for textual files.
|
| 102 |
+
"""
|
| 103 |
+
m = Message()
|
| 104 |
+
m["content-type"] = header_value
|
| 105 |
+
return (
|
| 106 |
+
m.get_content_type()
|
| 107 |
+
) # Despite the name, actually returns just the media-type
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def utcnow():
|
| 111 |
+
"""Returns the current UTC datetime.
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
datetime: The current time in UTC.
|
| 115 |
+
"""
|
| 116 |
+
# We used datetime.utcnow() before, since it's deprecated from python 3.12,
|
| 117 |
+
# we are using datetime.now(timezone.utc) now. "utcnow()" is offset-native
|
| 118 |
+
# (no timezone info), but "now()" is offset-aware (with timezone info).
|
| 119 |
+
# This will cause datetime comparison problem. For backward compatibility,
|
| 120 |
+
# we need to remove the timezone info.
|
| 121 |
+
now = datetime.datetime.now(datetime.timezone.utc)
|
| 122 |
+
now = now.replace(tzinfo=None)
|
| 123 |
+
return now
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def datetime_to_secs(value):
|
| 127 |
+
"""Convert a datetime object to the number of seconds since the UNIX epoch.
|
| 128 |
+
|
| 129 |
+
Args:
|
| 130 |
+
value (datetime): The datetime to convert.
|
| 131 |
+
|
| 132 |
+
Returns:
|
| 133 |
+
int: The number of seconds since the UNIX epoch.
|
| 134 |
+
"""
|
| 135 |
+
return calendar.timegm(value.utctimetuple())
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def to_bytes(value, encoding="utf-8"):
|
| 139 |
+
"""Converts a string value to bytes, if necessary.
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
value (Union[str, bytes]): The value to be converted.
|
| 143 |
+
encoding (str): The encoding to use to convert unicode to bytes.
|
| 144 |
+
Defaults to "utf-8".
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
bytes: The original value converted to bytes (if unicode) or as
|
| 148 |
+
passed in if it started out as bytes.
|
| 149 |
+
|
| 150 |
+
Raises:
|
| 151 |
+
google.auth.exceptions.InvalidValue: If the value could not be converted to bytes.
|
| 152 |
+
"""
|
| 153 |
+
result = value.encode(encoding) if isinstance(value, str) else value
|
| 154 |
+
if isinstance(result, bytes):
|
| 155 |
+
return result
|
| 156 |
+
else:
|
| 157 |
+
raise exceptions.InvalidValue(
|
| 158 |
+
"{0!r} could not be converted to bytes".format(value)
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def from_bytes(value):
|
| 163 |
+
"""Converts bytes to a string value, if necessary.
|
| 164 |
+
|
| 165 |
+
Args:
|
| 166 |
+
value (Union[str, bytes]): The value to be converted.
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
str: The original value converted to unicode (if bytes) or as passed in
|
| 170 |
+
if it started out as unicode.
|
| 171 |
+
|
| 172 |
+
Raises:
|
| 173 |
+
google.auth.exceptions.InvalidValue: If the value could not be converted to unicode.
|
| 174 |
+
"""
|
| 175 |
+
result = value.decode("utf-8") if isinstance(value, bytes) else value
|
| 176 |
+
if isinstance(result, str):
|
| 177 |
+
return result
|
| 178 |
+
else:
|
| 179 |
+
raise exceptions.InvalidValue(
|
| 180 |
+
"{0!r} could not be converted to unicode".format(value)
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def update_query(url, params, remove=None):
|
| 185 |
+
"""Updates a URL's query parameters.
|
| 186 |
+
|
| 187 |
+
Replaces any current values if they are already present in the URL.
|
| 188 |
+
|
| 189 |
+
Args:
|
| 190 |
+
url (str): The URL to update.
|
| 191 |
+
params (Mapping[str, str]): A mapping of query parameter
|
| 192 |
+
keys to values.
|
| 193 |
+
remove (Sequence[str]): Parameters to remove from the query string.
|
| 194 |
+
|
| 195 |
+
Returns:
|
| 196 |
+
str: The URL with updated query parameters.
|
| 197 |
+
|
| 198 |
+
Examples:
|
| 199 |
+
|
| 200 |
+
>>> url = 'http://example.com?a=1'
|
| 201 |
+
>>> update_query(url, {'a': '2'})
|
| 202 |
+
http://example.com?a=2
|
| 203 |
+
>>> update_query(url, {'b': '3'})
|
| 204 |
+
http://example.com?a=1&b=3
|
| 205 |
+
>> update_query(url, {'b': '3'}, remove=['a'])
|
| 206 |
+
http://example.com?b=3
|
| 207 |
+
|
| 208 |
+
"""
|
| 209 |
+
if remove is None:
|
| 210 |
+
remove = []
|
| 211 |
+
|
| 212 |
+
# Split the URL into parts.
|
| 213 |
+
parts = urllib.parse.urlparse(url)
|
| 214 |
+
# Parse the query string.
|
| 215 |
+
query_params = urllib.parse.parse_qs(parts.query)
|
| 216 |
+
# Update the query parameters with the new parameters.
|
| 217 |
+
query_params.update(params)
|
| 218 |
+
# Remove any values specified in remove.
|
| 219 |
+
query_params = {
|
| 220 |
+
key: value for key, value in query_params.items() if key not in remove
|
| 221 |
+
}
|
| 222 |
+
# Re-encoded the query string.
|
| 223 |
+
new_query = urllib.parse.urlencode(query_params, doseq=True)
|
| 224 |
+
# Unsplit the url.
|
| 225 |
+
new_parts = parts._replace(query=new_query)
|
| 226 |
+
return urllib.parse.urlunparse(new_parts)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def scopes_to_string(scopes):
|
| 230 |
+
"""Converts scope value to a string suitable for sending to OAuth 2.0
|
| 231 |
+
authorization servers.
|
| 232 |
+
|
| 233 |
+
Args:
|
| 234 |
+
scopes (Sequence[str]): The sequence of scopes to convert.
|
| 235 |
+
|
| 236 |
+
Returns:
|
| 237 |
+
str: The scopes formatted as a single string.
|
| 238 |
+
"""
|
| 239 |
+
return " ".join(scopes)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def string_to_scopes(scopes):
|
| 243 |
+
"""Converts stringifed scopes value to a list.
|
| 244 |
+
|
| 245 |
+
Args:
|
| 246 |
+
scopes (Union[Sequence, str]): The string of space-separated scopes
|
| 247 |
+
to convert.
|
| 248 |
+
Returns:
|
| 249 |
+
Sequence(str): The separated scopes.
|
| 250 |
+
"""
|
| 251 |
+
if not scopes:
|
| 252 |
+
return []
|
| 253 |
+
|
| 254 |
+
return scopes.split(" ")
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def padded_urlsafe_b64decode(value):
|
| 258 |
+
"""Decodes base64 strings lacking padding characters.
|
| 259 |
+
|
| 260 |
+
Google infrastructure tends to omit the base64 padding characters.
|
| 261 |
+
|
| 262 |
+
Args:
|
| 263 |
+
value (Union[str, bytes]): The encoded value.
|
| 264 |
+
|
| 265 |
+
Returns:
|
| 266 |
+
bytes: The decoded value
|
| 267 |
+
"""
|
| 268 |
+
b64string = to_bytes(value)
|
| 269 |
+
padded = b64string + b"=" * (-len(b64string) % 4)
|
| 270 |
+
return base64.urlsafe_b64decode(padded)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def unpadded_urlsafe_b64encode(value):
|
| 274 |
+
"""Encodes base64 strings removing any padding characters.
|
| 275 |
+
|
| 276 |
+
`rfc 7515`_ defines Base64url to NOT include any padding
|
| 277 |
+
characters, but the stdlib doesn't do that by default.
|
| 278 |
+
|
| 279 |
+
_rfc7515: https://tools.ietf.org/html/rfc7515#page-6
|
| 280 |
+
|
| 281 |
+
Args:
|
| 282 |
+
value (Union[str|bytes]): The bytes-like value to encode
|
| 283 |
+
|
| 284 |
+
Returns:
|
| 285 |
+
Union[str|bytes]: The encoded value
|
| 286 |
+
"""
|
| 287 |
+
return base64.urlsafe_b64encode(value).rstrip(b"=")
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def is_python_3():
|
| 291 |
+
"""Check if the Python interpreter is Python 2 or 3.
|
| 292 |
+
|
| 293 |
+
Returns:
|
| 294 |
+
bool: True if the Python interpreter is Python 3 and False otherwise.
|
| 295 |
+
"""
|
| 296 |
+
return sys.version_info > (3, 0)
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def _hash_sensitive_info(data: Union[dict, list]) -> Union[dict, list, str]:
|
| 300 |
+
"""
|
| 301 |
+
Hashes sensitive information within a dictionary.
|
| 302 |
+
|
| 303 |
+
Args:
|
| 304 |
+
data: The dictionary containing data to be processed.
|
| 305 |
+
|
| 306 |
+
Returns:
|
| 307 |
+
A new dictionary with sensitive values replaced by their SHA512 hashes.
|
| 308 |
+
If the input is a list, returns a list with each element recursively processed.
|
| 309 |
+
If the input is neither a dict nor a list, returns the type of the input as a string.
|
| 310 |
+
|
| 311 |
+
"""
|
| 312 |
+
if isinstance(data, dict):
|
| 313 |
+
hashed_data: Dict[Any, Union[Optional[str], dict, list]] = {}
|
| 314 |
+
for key, value in data.items():
|
| 315 |
+
if key in _SENSITIVE_FIELDS and not isinstance(value, (dict, list)):
|
| 316 |
+
hashed_data[key] = _hash_value(value, key)
|
| 317 |
+
elif isinstance(value, (dict, list)):
|
| 318 |
+
hashed_data[key] = _hash_sensitive_info(value)
|
| 319 |
+
else:
|
| 320 |
+
hashed_data[key] = value
|
| 321 |
+
return hashed_data
|
| 322 |
+
elif isinstance(data, list):
|
| 323 |
+
hashed_list = []
|
| 324 |
+
for val in data:
|
| 325 |
+
hashed_list.append(_hash_sensitive_info(val))
|
| 326 |
+
return hashed_list
|
| 327 |
+
else:
|
| 328 |
+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1701):
|
| 329 |
+
# Investigate and hash sensitive info before logging when the data type is
|
| 330 |
+
# not a dict or a list.
|
| 331 |
+
return str(type(data))
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def _hash_value(value, field_name: str) -> Optional[str]:
|
| 335 |
+
"""Hashes a value and returns a formatted hash string."""
|
| 336 |
+
if value is None:
|
| 337 |
+
return None
|
| 338 |
+
encoded_value = str(value).encode("utf-8")
|
| 339 |
+
hash_object = hashlib.sha512()
|
| 340 |
+
hash_object.update(encoded_value)
|
| 341 |
+
hex_digest = hash_object.hexdigest()
|
| 342 |
+
return f"hashed_{field_name}-{hex_digest}"
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def _logger_configured(logger: logging.Logger) -> bool:
|
| 346 |
+
"""Determines whether `logger` has non-default configuration
|
| 347 |
+
|
| 348 |
+
Args:
|
| 349 |
+
logger: The logger to check.
|
| 350 |
+
|
| 351 |
+
Returns:
|
| 352 |
+
bool: Whether the logger has any non-default configuration.
|
| 353 |
+
"""
|
| 354 |
+
return (
|
| 355 |
+
logger.handlers != [] or logger.level != logging.NOTSET or not logger.propagate
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def is_logging_enabled(logger: logging.Logger) -> bool:
|
| 360 |
+
"""
|
| 361 |
+
Checks if debug logging is enabled for the given logger.
|
| 362 |
+
|
| 363 |
+
Args:
|
| 364 |
+
logger: The logging.Logger instance to check.
|
| 365 |
+
|
| 366 |
+
Returns:
|
| 367 |
+
True if debug logging is enabled, False otherwise.
|
| 368 |
+
"""
|
| 369 |
+
# NOTE: Log propagation to the root logger is disabled unless
|
| 370 |
+
# the base logger i.e. logging.getLogger("google") is
|
| 371 |
+
# explicitly configured by the end user. Ideally this
|
| 372 |
+
# needs to happen in the client layer (already does for GAPICs).
|
| 373 |
+
# However, this is implemented here to avoid logging
|
| 374 |
+
# (if a root logger is configured) when a version of google-auth
|
| 375 |
+
# which supports logging is used with:
|
| 376 |
+
# - an older version of a GAPIC which does not support logging.
|
| 377 |
+
# - Apiary client which does not support logging.
|
| 378 |
+
global _LOGGING_INITIALIZED
|
| 379 |
+
if not _LOGGING_INITIALIZED:
|
| 380 |
+
base_logger = logging.getLogger(_BASE_LOGGER_NAME)
|
| 381 |
+
if not _logger_configured(base_logger):
|
| 382 |
+
base_logger.propagate = False
|
| 383 |
+
_LOGGING_INITIALIZED = True
|
| 384 |
+
|
| 385 |
+
return logger.isEnabledFor(logging.DEBUG)
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def request_log(
|
| 389 |
+
logger: logging.Logger,
|
| 390 |
+
method: str,
|
| 391 |
+
url: str,
|
| 392 |
+
body: Optional[bytes],
|
| 393 |
+
headers: Optional[Mapping[str, str]],
|
| 394 |
+
) -> None:
|
| 395 |
+
"""
|
| 396 |
+
Logs an HTTP request at the DEBUG level if logging is enabled.
|
| 397 |
+
|
| 398 |
+
Args:
|
| 399 |
+
logger: The logging.Logger instance to use.
|
| 400 |
+
method: The HTTP method (e.g., "GET", "POST").
|
| 401 |
+
url: The URL of the request.
|
| 402 |
+
body: The request body (can be None).
|
| 403 |
+
headers: The request headers (can be None).
|
| 404 |
+
"""
|
| 405 |
+
if is_logging_enabled(logger):
|
| 406 |
+
content_type = (
|
| 407 |
+
headers["Content-Type"] if headers and "Content-Type" in headers else ""
|
| 408 |
+
)
|
| 409 |
+
json_body = _parse_request_body(body, content_type=content_type)
|
| 410 |
+
logged_body = _hash_sensitive_info(json_body)
|
| 411 |
+
logger.debug(
|
| 412 |
+
"Making request...",
|
| 413 |
+
extra={
|
| 414 |
+
"httpRequest": {
|
| 415 |
+
"method": method,
|
| 416 |
+
"url": url,
|
| 417 |
+
"body": logged_body,
|
| 418 |
+
"headers": headers,
|
| 419 |
+
}
|
| 420 |
+
},
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
def _parse_request_body(body: Optional[bytes], content_type: str = "") -> Any:
|
| 425 |
+
"""
|
| 426 |
+
Parses a request body, handling bytes and string types, and different content types.
|
| 427 |
+
|
| 428 |
+
Args:
|
| 429 |
+
body (Optional[bytes]): The request body.
|
| 430 |
+
content_type (str): The content type of the request body, e.g., "application/json",
|
| 431 |
+
"application/x-www-form-urlencoded", or "text/plain". If empty, attempts
|
| 432 |
+
to parse as JSON.
|
| 433 |
+
|
| 434 |
+
Returns:
|
| 435 |
+
Parsed body (dict, str, or None).
|
| 436 |
+
- JSON: Decodes if content_type is "application/json" or None (fallback).
|
| 437 |
+
- URL-encoded: Parses if content_type is "application/x-www-form-urlencoded".
|
| 438 |
+
- Plain text: Returns string if content_type is "text/plain".
|
| 439 |
+
- None: Returns if body is None, UTF-8 decode fails, or content_type is unknown.
|
| 440 |
+
"""
|
| 441 |
+
if body is None:
|
| 442 |
+
return None
|
| 443 |
+
try:
|
| 444 |
+
body_str = body.decode("utf-8")
|
| 445 |
+
except (UnicodeDecodeError, AttributeError):
|
| 446 |
+
return None
|
| 447 |
+
content_type = content_type.lower()
|
| 448 |
+
if not content_type or "application/json" in content_type:
|
| 449 |
+
try:
|
| 450 |
+
return json.loads(body_str)
|
| 451 |
+
except (json.JSONDecodeError, TypeError):
|
| 452 |
+
return body_str
|
| 453 |
+
if "application/x-www-form-urlencoded" in content_type:
|
| 454 |
+
parsed_query = urllib.parse.parse_qs(body_str)
|
| 455 |
+
result = {k: v[0] for k, v in parsed_query.items()}
|
| 456 |
+
return result
|
| 457 |
+
if "text/plain" in content_type:
|
| 458 |
+
return body_str
|
| 459 |
+
return None
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
def _parse_response(response: Any) -> Any:
|
| 463 |
+
"""
|
| 464 |
+
Parses a response, attempting to decode JSON.
|
| 465 |
+
|
| 466 |
+
Args:
|
| 467 |
+
response: The response object to parse. This can be any type, but
|
| 468 |
+
it is expected to have a `json()` method if it contains JSON.
|
| 469 |
+
|
| 470 |
+
Returns:
|
| 471 |
+
The parsed response. If the response contains valid JSON, the
|
| 472 |
+
decoded JSON object (e.g., a dictionary or list) is returned.
|
| 473 |
+
If the response does not have a `json()` method or if the JSON
|
| 474 |
+
decoding fails, None is returned.
|
| 475 |
+
"""
|
| 476 |
+
try:
|
| 477 |
+
json_response = response.json()
|
| 478 |
+
return json_response
|
| 479 |
+
except Exception:
|
| 480 |
+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1744):
|
| 481 |
+
# Parse and return response payload as json based on different content types.
|
| 482 |
+
return None
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
def _response_log_base(logger: logging.Logger, parsed_response: Any) -> None:
|
| 486 |
+
"""
|
| 487 |
+
Logs a parsed HTTP response at the DEBUG level.
|
| 488 |
+
|
| 489 |
+
This internal helper function takes a parsed response and logs it
|
| 490 |
+
using the provided logger. It also applies a hashing function to
|
| 491 |
+
potentially sensitive information before logging.
|
| 492 |
+
|
| 493 |
+
Args:
|
| 494 |
+
logger: The logging.Logger instance to use for logging.
|
| 495 |
+
parsed_response: The parsed HTTP response object (e.g., a dictionary,
|
| 496 |
+
list, or the original response if parsing failed).
|
| 497 |
+
"""
|
| 498 |
+
|
| 499 |
+
logged_response = _hash_sensitive_info(parsed_response)
|
| 500 |
+
logger.debug("Response received...", extra={"httpResponse": logged_response})
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
def response_log(logger: logging.Logger, response: Any) -> None:
|
| 504 |
+
"""
|
| 505 |
+
Logs an HTTP response at the DEBUG level if logging is enabled.
|
| 506 |
+
|
| 507 |
+
Args:
|
| 508 |
+
logger: The logging.Logger instance to use.
|
| 509 |
+
response: The HTTP response object to log.
|
| 510 |
+
"""
|
| 511 |
+
if is_logging_enabled(logger):
|
| 512 |
+
json_response = _parse_response(response)
|
| 513 |
+
_response_log_base(logger, json_response)
|
lib/python3.10/site-packages/google/auth/_jwt_async.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020 Google LLC
|
| 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 |
+
"""JSON Web Tokens
|
| 16 |
+
|
| 17 |
+
Provides support for creating (encoding) and verifying (decoding) JWTs,
|
| 18 |
+
especially JWTs generated and consumed by Google infrastructure.
|
| 19 |
+
|
| 20 |
+
See `rfc7519`_ for more details on JWTs.
|
| 21 |
+
|
| 22 |
+
To encode a JWT use :func:`encode`::
|
| 23 |
+
|
| 24 |
+
from google.auth import crypt
|
| 25 |
+
from google.auth import jwt_async
|
| 26 |
+
|
| 27 |
+
signer = crypt.Signer(private_key)
|
| 28 |
+
payload = {'some': 'payload'}
|
| 29 |
+
encoded = jwt_async.encode(signer, payload)
|
| 30 |
+
|
| 31 |
+
To decode a JWT and verify claims use :func:`decode`::
|
| 32 |
+
|
| 33 |
+
claims = jwt_async.decode(encoded, certs=public_certs)
|
| 34 |
+
|
| 35 |
+
You can also skip verification::
|
| 36 |
+
|
| 37 |
+
claims = jwt_async.decode(encoded, verify=False)
|
| 38 |
+
|
| 39 |
+
.. _rfc7519: https://tools.ietf.org/html/rfc7519
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
NOTE: This async support is experimental and marked internal. This surface may
|
| 43 |
+
change in minor releases.
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
from google.auth import _credentials_async
|
| 47 |
+
from google.auth import jwt
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def encode(signer, payload, header=None, key_id=None):
|
| 51 |
+
"""Make a signed JWT.
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
signer (google.auth.crypt.Signer): The signer used to sign the JWT.
|
| 55 |
+
payload (Mapping[str, str]): The JWT payload.
|
| 56 |
+
header (Mapping[str, str]): Additional JWT header payload.
|
| 57 |
+
key_id (str): The key id to add to the JWT header. If the
|
| 58 |
+
signer has a key id it will be used as the default. If this is
|
| 59 |
+
specified it will override the signer's key id.
|
| 60 |
+
|
| 61 |
+
Returns:
|
| 62 |
+
bytes: The encoded JWT.
|
| 63 |
+
"""
|
| 64 |
+
return jwt.encode(signer, payload, header, key_id)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def decode(token, certs=None, verify=True, audience=None):
|
| 68 |
+
"""Decode and verify a JWT.
|
| 69 |
+
|
| 70 |
+
Args:
|
| 71 |
+
token (str): The encoded JWT.
|
| 72 |
+
certs (Union[str, bytes, Mapping[str, Union[str, bytes]]]): The
|
| 73 |
+
certificate used to validate the JWT signature. If bytes or string,
|
| 74 |
+
it must the the public key certificate in PEM format. If a mapping,
|
| 75 |
+
it must be a mapping of key IDs to public key certificates in PEM
|
| 76 |
+
format. The mapping must contain the same key ID that's specified
|
| 77 |
+
in the token's header.
|
| 78 |
+
verify (bool): Whether to perform signature and claim validation.
|
| 79 |
+
Verification is done by default.
|
| 80 |
+
audience (str): The audience claim, 'aud', that this JWT should
|
| 81 |
+
contain. If None then the JWT's 'aud' parameter is not verified.
|
| 82 |
+
|
| 83 |
+
Returns:
|
| 84 |
+
Mapping[str, str]: The deserialized JSON payload in the JWT.
|
| 85 |
+
|
| 86 |
+
Raises:
|
| 87 |
+
ValueError: if any verification checks failed.
|
| 88 |
+
"""
|
| 89 |
+
|
| 90 |
+
return jwt.decode(token, certs, verify, audience)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class Credentials(
|
| 94 |
+
jwt.Credentials, _credentials_async.Signing, _credentials_async.Credentials
|
| 95 |
+
):
|
| 96 |
+
"""Credentials that use a JWT as the bearer token.
|
| 97 |
+
|
| 98 |
+
These credentials require an "audience" claim. This claim identifies the
|
| 99 |
+
intended recipient of the bearer token.
|
| 100 |
+
|
| 101 |
+
The constructor arguments determine the claims for the JWT that is
|
| 102 |
+
sent with requests. Usually, you'll construct these credentials with
|
| 103 |
+
one of the helper constructors as shown in the next section.
|
| 104 |
+
|
| 105 |
+
To create JWT credentials using a Google service account private key
|
| 106 |
+
JSON file::
|
| 107 |
+
|
| 108 |
+
audience = 'https://pubsub.googleapis.com/google.pubsub.v1.Publisher'
|
| 109 |
+
credentials = jwt_async.Credentials.from_service_account_file(
|
| 110 |
+
'service-account.json',
|
| 111 |
+
audience=audience)
|
| 112 |
+
|
| 113 |
+
If you already have the service account file loaded and parsed::
|
| 114 |
+
|
| 115 |
+
service_account_info = json.load(open('service_account.json'))
|
| 116 |
+
credentials = jwt_async.Credentials.from_service_account_info(
|
| 117 |
+
service_account_info,
|
| 118 |
+
audience=audience)
|
| 119 |
+
|
| 120 |
+
Both helper methods pass on arguments to the constructor, so you can
|
| 121 |
+
specify the JWT claims::
|
| 122 |
+
|
| 123 |
+
credentials = jwt_async.Credentials.from_service_account_file(
|
| 124 |
+
'service-account.json',
|
| 125 |
+
audience=audience,
|
| 126 |
+
additional_claims={'meta': 'data'})
|
| 127 |
+
|
| 128 |
+
You can also construct the credentials directly if you have a
|
| 129 |
+
:class:`~google.auth.crypt.Signer` instance::
|
| 130 |
+
|
| 131 |
+
credentials = jwt_async.Credentials(
|
| 132 |
+
signer,
|
| 133 |
+
issuer='your-issuer',
|
| 134 |
+
subject='your-subject',
|
| 135 |
+
audience=audience)
|
| 136 |
+
|
| 137 |
+
The claims are considered immutable. If you want to modify the claims,
|
| 138 |
+
you can easily create another instance using :meth:`with_claims`::
|
| 139 |
+
|
| 140 |
+
new_audience = (
|
| 141 |
+
'https://pubsub.googleapis.com/google.pubsub.v1.Subscriber')
|
| 142 |
+
new_credentials = credentials.with_claims(audience=new_audience)
|
| 143 |
+
"""
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
class OnDemandCredentials(
|
| 147 |
+
jwt.OnDemandCredentials, _credentials_async.Signing, _credentials_async.Credentials
|
| 148 |
+
):
|
| 149 |
+
"""On-demand JWT credentials.
|
| 150 |
+
|
| 151 |
+
Like :class:`Credentials`, this class uses a JWT as the bearer token for
|
| 152 |
+
authentication. However, this class does not require the audience at
|
| 153 |
+
construction time. Instead, it will generate a new token on-demand for
|
| 154 |
+
each request using the request URI as the audience. It caches tokens
|
| 155 |
+
so that multiple requests to the same URI do not incur the overhead
|
| 156 |
+
of generating a new token every time.
|
| 157 |
+
|
| 158 |
+
This behavior is especially useful for `gRPC`_ clients. A gRPC service may
|
| 159 |
+
have multiple audience and gRPC clients may not know all of the audiences
|
| 160 |
+
required for accessing a particular service. With these credentials,
|
| 161 |
+
no knowledge of the audiences is required ahead of time.
|
| 162 |
+
|
| 163 |
+
.. _grpc: http://www.grpc.io/
|
| 164 |
+
"""
|
lib/python3.10/site-packages/google/auth/_oauth2client.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 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 |
+
"""Helpers for transitioning from oauth2client to google-auth.
|
| 16 |
+
|
| 17 |
+
.. warning::
|
| 18 |
+
This module is private as it is intended to assist first-party downstream
|
| 19 |
+
clients with the transition from oauth2client to google-auth.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import absolute_import
|
| 23 |
+
|
| 24 |
+
from google.auth import _helpers
|
| 25 |
+
import google.auth.app_engine
|
| 26 |
+
import google.auth.compute_engine
|
| 27 |
+
import google.oauth2.credentials
|
| 28 |
+
import google.oauth2.service_account
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
import oauth2client.client # type: ignore
|
| 32 |
+
import oauth2client.contrib.gce # type: ignore
|
| 33 |
+
import oauth2client.service_account # type: ignore
|
| 34 |
+
except ImportError as caught_exc:
|
| 35 |
+
raise ImportError("oauth2client is not installed.") from caught_exc
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
import oauth2client.contrib.appengine # type: ignore
|
| 39 |
+
|
| 40 |
+
_HAS_APPENGINE = True
|
| 41 |
+
except ImportError:
|
| 42 |
+
_HAS_APPENGINE = False
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
_CONVERT_ERROR_TMPL = "Unable to convert {} to a google-auth credentials class."
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _convert_oauth2_credentials(credentials):
|
| 49 |
+
"""Converts to :class:`google.oauth2.credentials.Credentials`.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
credentials (Union[oauth2client.client.OAuth2Credentials,
|
| 53 |
+
oauth2client.client.GoogleCredentials]): The credentials to
|
| 54 |
+
convert.
|
| 55 |
+
|
| 56 |
+
Returns:
|
| 57 |
+
google.oauth2.credentials.Credentials: The converted credentials.
|
| 58 |
+
"""
|
| 59 |
+
new_credentials = google.oauth2.credentials.Credentials(
|
| 60 |
+
token=credentials.access_token,
|
| 61 |
+
refresh_token=credentials.refresh_token,
|
| 62 |
+
token_uri=credentials.token_uri,
|
| 63 |
+
client_id=credentials.client_id,
|
| 64 |
+
client_secret=credentials.client_secret,
|
| 65 |
+
scopes=credentials.scopes,
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
new_credentials._expires = credentials.token_expiry
|
| 69 |
+
|
| 70 |
+
return new_credentials
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _convert_service_account_credentials(credentials):
|
| 74 |
+
"""Converts to :class:`google.oauth2.service_account.Credentials`.
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
credentials (Union[
|
| 78 |
+
oauth2client.service_account.ServiceAccountCredentials,
|
| 79 |
+
oauth2client.service_account._JWTAccessCredentials]): The
|
| 80 |
+
credentials to convert.
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
google.oauth2.service_account.Credentials: The converted credentials.
|
| 84 |
+
"""
|
| 85 |
+
info = credentials.serialization_data.copy()
|
| 86 |
+
info["token_uri"] = credentials.token_uri
|
| 87 |
+
return google.oauth2.service_account.Credentials.from_service_account_info(info)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _convert_gce_app_assertion_credentials(credentials):
|
| 91 |
+
"""Converts to :class:`google.auth.compute_engine.Credentials`.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
credentials (oauth2client.contrib.gce.AppAssertionCredentials): The
|
| 95 |
+
credentials to convert.
|
| 96 |
+
|
| 97 |
+
Returns:
|
| 98 |
+
google.oauth2.service_account.Credentials: The converted credentials.
|
| 99 |
+
"""
|
| 100 |
+
return google.auth.compute_engine.Credentials(
|
| 101 |
+
service_account_email=credentials.service_account_email
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _convert_appengine_app_assertion_credentials(credentials):
|
| 106 |
+
"""Converts to :class:`google.auth.app_engine.Credentials`.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
credentials (oauth2client.contrib.app_engine.AppAssertionCredentials):
|
| 110 |
+
The credentials to convert.
|
| 111 |
+
|
| 112 |
+
Returns:
|
| 113 |
+
google.oauth2.service_account.Credentials: The converted credentials.
|
| 114 |
+
"""
|
| 115 |
+
# pylint: disable=invalid-name
|
| 116 |
+
return google.auth.app_engine.Credentials(
|
| 117 |
+
scopes=_helpers.string_to_scopes(credentials.scope),
|
| 118 |
+
service_account_id=credentials.service_account_id,
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
_CLASS_CONVERSION_MAP = {
|
| 123 |
+
oauth2client.client.OAuth2Credentials: _convert_oauth2_credentials,
|
| 124 |
+
oauth2client.client.GoogleCredentials: _convert_oauth2_credentials,
|
| 125 |
+
oauth2client.service_account.ServiceAccountCredentials: _convert_service_account_credentials,
|
| 126 |
+
oauth2client.service_account._JWTAccessCredentials: _convert_service_account_credentials,
|
| 127 |
+
oauth2client.contrib.gce.AppAssertionCredentials: _convert_gce_app_assertion_credentials,
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
if _HAS_APPENGINE:
|
| 131 |
+
_CLASS_CONVERSION_MAP[
|
| 132 |
+
oauth2client.contrib.appengine.AppAssertionCredentials
|
| 133 |
+
] = _convert_appengine_app_assertion_credentials
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def convert(credentials):
|
| 137 |
+
"""Convert oauth2client credentials to google-auth credentials.
|
| 138 |
+
|
| 139 |
+
This class converts:
|
| 140 |
+
|
| 141 |
+
- :class:`oauth2client.client.OAuth2Credentials` to
|
| 142 |
+
:class:`google.oauth2.credentials.Credentials`.
|
| 143 |
+
- :class:`oauth2client.client.GoogleCredentials` to
|
| 144 |
+
:class:`google.oauth2.credentials.Credentials`.
|
| 145 |
+
- :class:`oauth2client.service_account.ServiceAccountCredentials` to
|
| 146 |
+
:class:`google.oauth2.service_account.Credentials`.
|
| 147 |
+
- :class:`oauth2client.service_account._JWTAccessCredentials` to
|
| 148 |
+
:class:`google.oauth2.service_account.Credentials`.
|
| 149 |
+
- :class:`oauth2client.contrib.gce.AppAssertionCredentials` to
|
| 150 |
+
:class:`google.auth.compute_engine.Credentials`.
|
| 151 |
+
- :class:`oauth2client.contrib.appengine.AppAssertionCredentials` to
|
| 152 |
+
:class:`google.auth.app_engine.Credentials`.
|
| 153 |
+
|
| 154 |
+
Returns:
|
| 155 |
+
google.auth.credentials.Credentials: The converted credentials.
|
| 156 |
+
|
| 157 |
+
Raises:
|
| 158 |
+
ValueError: If the credentials could not be converted.
|
| 159 |
+
"""
|
| 160 |
+
|
| 161 |
+
credentials_class = type(credentials)
|
| 162 |
+
|
| 163 |
+
try:
|
| 164 |
+
return _CLASS_CONVERSION_MAP[credentials_class](credentials)
|
| 165 |
+
except KeyError as caught_exc:
|
| 166 |
+
new_exc = ValueError(_CONVERT_ERROR_TMPL.format(credentials_class))
|
| 167 |
+
raise new_exc from caught_exc
|
lib/python3.10/site-packages/google/auth/_refresh_worker.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2023 Google LLC
|
| 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 |
+
import copy
|
| 16 |
+
import logging
|
| 17 |
+
import threading
|
| 18 |
+
|
| 19 |
+
import google.auth.exceptions as e
|
| 20 |
+
|
| 21 |
+
_LOGGER = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class RefreshThreadManager:
|
| 25 |
+
"""
|
| 26 |
+
Organizes exactly one background job that refresh a token.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(self):
|
| 30 |
+
"""Initializes the manager."""
|
| 31 |
+
|
| 32 |
+
self._worker = None
|
| 33 |
+
self._lock = threading.Lock() # protects access to worker threads.
|
| 34 |
+
|
| 35 |
+
def start_refresh(self, cred, request):
|
| 36 |
+
"""Starts a refresh thread for the given credentials.
|
| 37 |
+
The credentials are refreshed using the request parameter.
|
| 38 |
+
request and cred MUST not be None
|
| 39 |
+
|
| 40 |
+
Returns True if a background refresh was kicked off. False otherwise.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
cred: A credentials object.
|
| 44 |
+
request: A request object.
|
| 45 |
+
Returns:
|
| 46 |
+
bool
|
| 47 |
+
"""
|
| 48 |
+
if cred is None or request is None:
|
| 49 |
+
raise e.InvalidValue(
|
| 50 |
+
"Unable to start refresh. cred and request must be valid and instantiated objects."
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
with self._lock:
|
| 54 |
+
if self._worker is not None and self._worker._error_info is not None:
|
| 55 |
+
return False
|
| 56 |
+
|
| 57 |
+
if self._worker is None or not self._worker.is_alive(): # pragma: NO COVER
|
| 58 |
+
self._worker = RefreshThread(cred=cred, request=copy.deepcopy(request))
|
| 59 |
+
self._worker.start()
|
| 60 |
+
return True
|
| 61 |
+
|
| 62 |
+
def clear_error(self):
|
| 63 |
+
"""
|
| 64 |
+
Removes any errors that were stored from previous background refreshes.
|
| 65 |
+
"""
|
| 66 |
+
with self._lock:
|
| 67 |
+
if self._worker:
|
| 68 |
+
self._worker._error_info = None
|
| 69 |
+
|
| 70 |
+
def __getstate__(self):
|
| 71 |
+
"""Pickle helper that serializes the _lock attribute."""
|
| 72 |
+
state = self.__dict__.copy()
|
| 73 |
+
state["_lock"] = None
|
| 74 |
+
return state
|
| 75 |
+
|
| 76 |
+
def __setstate__(self, state):
|
| 77 |
+
"""Pickle helper that deserializes the _lock attribute."""
|
| 78 |
+
state["_lock"] = threading.Lock()
|
| 79 |
+
self.__dict__.update(state)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class RefreshThread(threading.Thread):
|
| 83 |
+
"""
|
| 84 |
+
Thread that refreshes credentials.
|
| 85 |
+
"""
|
| 86 |
+
|
| 87 |
+
def __init__(self, cred, request, **kwargs):
|
| 88 |
+
"""Initializes the thread.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
cred: A Credential object to refresh.
|
| 92 |
+
request: A Request object used to perform a credential refresh.
|
| 93 |
+
**kwargs: Additional keyword arguments.
|
| 94 |
+
"""
|
| 95 |
+
|
| 96 |
+
super().__init__(**kwargs)
|
| 97 |
+
self._cred = cred
|
| 98 |
+
self._request = request
|
| 99 |
+
self._error_info = None
|
| 100 |
+
|
| 101 |
+
def run(self):
|
| 102 |
+
"""
|
| 103 |
+
Perform the credential refresh.
|
| 104 |
+
"""
|
| 105 |
+
try:
|
| 106 |
+
self._cred.refresh(self._request)
|
| 107 |
+
except Exception as err: # pragma: NO COVER
|
| 108 |
+
_LOGGER.error(f"Background refresh failed due to: {err}")
|
| 109 |
+
self._error_info = err
|
lib/python3.10/site-packages/google/auth/_service_account_info.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 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 |
+
"""Helper functions for loading data from a Google service account file."""
|
| 16 |
+
|
| 17 |
+
import io
|
| 18 |
+
import json
|
| 19 |
+
|
| 20 |
+
from google.auth import crypt
|
| 21 |
+
from google.auth import exceptions
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def from_dict(data, require=None, use_rsa_signer=True):
|
| 25 |
+
"""Validates a dictionary containing Google service account data.
|
| 26 |
+
|
| 27 |
+
Creates and returns a :class:`google.auth.crypt.Signer` instance from the
|
| 28 |
+
private key specified in the data.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
data (Mapping[str, str]): The service account data
|
| 32 |
+
require (Sequence[str]): List of keys required to be present in the
|
| 33 |
+
info.
|
| 34 |
+
use_rsa_signer (Optional[bool]): Whether to use RSA signer or EC signer.
|
| 35 |
+
We use RSA signer by default.
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
google.auth.crypt.Signer: A signer created from the private key in the
|
| 39 |
+
service account file.
|
| 40 |
+
|
| 41 |
+
Raises:
|
| 42 |
+
MalformedError: if the data was in the wrong format, or if one of the
|
| 43 |
+
required keys is missing.
|
| 44 |
+
"""
|
| 45 |
+
keys_needed = set(require if require is not None else [])
|
| 46 |
+
|
| 47 |
+
missing = keys_needed.difference(data.keys())
|
| 48 |
+
|
| 49 |
+
if missing:
|
| 50 |
+
raise exceptions.MalformedError(
|
| 51 |
+
"Service account info was not in the expected format, missing "
|
| 52 |
+
"fields {}.".format(", ".join(missing))
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
# Create a signer.
|
| 56 |
+
if use_rsa_signer:
|
| 57 |
+
signer = crypt.RSASigner.from_service_account_info(data)
|
| 58 |
+
else:
|
| 59 |
+
signer = crypt.ES256Signer.from_service_account_info(data)
|
| 60 |
+
|
| 61 |
+
return signer
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def from_filename(filename, require=None, use_rsa_signer=True):
|
| 65 |
+
"""Reads a Google service account JSON file and returns its parsed info.
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
filename (str): The path to the service account .json file.
|
| 69 |
+
require (Sequence[str]): List of keys required to be present in the
|
| 70 |
+
info.
|
| 71 |
+
use_rsa_signer (Optional[bool]): Whether to use RSA signer or EC signer.
|
| 72 |
+
We use RSA signer by default.
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
Tuple[ Mapping[str, str], google.auth.crypt.Signer ]: The verified
|
| 76 |
+
info and a signer instance.
|
| 77 |
+
"""
|
| 78 |
+
with io.open(filename, "r", encoding="utf-8") as json_file:
|
| 79 |
+
data = json.load(json_file)
|
| 80 |
+
return data, from_dict(data, require=require, use_rsa_signer=use_rsa_signer)
|
lib/python3.10/site-packages/google/auth/aio/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 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 |
+
"""Google Auth AIO Library for Python."""
|
| 16 |
+
|
| 17 |
+
import logging
|
| 18 |
+
|
| 19 |
+
from google.auth import version as google_auth_version
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
__version__ = google_auth_version.__version__
|
| 23 |
+
|
| 24 |
+
# Set default logging handler to avoid "No handler found" warnings.
|
| 25 |
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
lib/python3.10/site-packages/google/auth/aio/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (430 Bytes). View file
|
|
|
lib/python3.10/site-packages/google/auth/aio/__pycache__/_helpers.cpython-310.pyc
ADDED
|
Binary file (1.51 kB). View file
|
|
|
lib/python3.10/site-packages/google/auth/aio/__pycache__/credentials.cpython-310.pyc
ADDED
|
Binary file (5.7 kB). View file
|
|
|
lib/python3.10/site-packages/google/auth/aio/_helpers.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 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 |
+
"""Helper functions for commonly used utilities."""
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
import logging
|
| 19 |
+
from typing import Any
|
| 20 |
+
|
| 21 |
+
from google.auth import _helpers
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
async def _parse_response_async(response: Any) -> Any:
|
| 25 |
+
"""
|
| 26 |
+
Parses an async response, attempting to decode JSON.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
response: The response object to parse. This can be any type, but
|
| 30 |
+
it is expected to have a `json()` method if it contains JSON.
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
The parsed response. If the response contains valid JSON, the
|
| 34 |
+
decoded JSON object (e.g., a dictionary) is returned.
|
| 35 |
+
If the response does not have a `json()` method or if the JSON
|
| 36 |
+
decoding fails, None is returned.
|
| 37 |
+
"""
|
| 38 |
+
try:
|
| 39 |
+
json_response = await response.json()
|
| 40 |
+
return json_response
|
| 41 |
+
except Exception:
|
| 42 |
+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1745):
|
| 43 |
+
# Parse and return response payload as json based on different content types.
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
async def response_log_async(logger: logging.Logger, response: Any) -> None:
|
| 48 |
+
"""
|
| 49 |
+
Logs an Async HTTP response at the DEBUG level if logging is enabled.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
logger: The logging.Logger instance to use.
|
| 53 |
+
response: The HTTP response object to log.
|
| 54 |
+
"""
|
| 55 |
+
if _helpers.is_logging_enabled(logger):
|
| 56 |
+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1755):
|
| 57 |
+
# Parsing the response for async streaming logging results in
|
| 58 |
+
# the stream to be empty downstream. For now, we will not be logging
|
| 59 |
+
# the response for async responses until we investigate further.
|
| 60 |
+
# json_response = await _parse_response_async(response)
|
| 61 |
+
json_response = None
|
| 62 |
+
_helpers._response_log_base(logger, json_response)
|
lib/python3.10/site-packages/google/auth/aio/credentials.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 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 |
+
|
| 16 |
+
"""Interfaces for asynchronous credentials."""
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
from google.auth import _helpers
|
| 20 |
+
from google.auth import exceptions
|
| 21 |
+
from google.auth._credentials_base import _BaseCredentials
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class Credentials(_BaseCredentials):
|
| 25 |
+
"""Base class for all asynchronous credentials.
|
| 26 |
+
|
| 27 |
+
All credentials have a :attr:`token` that is used for authentication and
|
| 28 |
+
may also optionally set an :attr:`expiry` to indicate when the token will
|
| 29 |
+
no longer be valid.
|
| 30 |
+
|
| 31 |
+
Most credentials will be :attr:`invalid` until :meth:`refresh` is called.
|
| 32 |
+
Credentials can do this automatically before the first HTTP request in
|
| 33 |
+
:meth:`before_request`.
|
| 34 |
+
|
| 35 |
+
Although the token and expiration will change as the credentials are
|
| 36 |
+
:meth:`refreshed <refresh>` and used, credentials should be considered
|
| 37 |
+
immutable. Various credentials will accept configuration such as private
|
| 38 |
+
keys, scopes, and other options. These options are not changeable after
|
| 39 |
+
construction. Some classes will provide mechanisms to copy the credentials
|
| 40 |
+
with modifications such as :meth:`ScopedCredentials.with_scopes`.
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
def __init__(self):
|
| 44 |
+
super(Credentials, self).__init__()
|
| 45 |
+
|
| 46 |
+
async def apply(self, headers, token=None):
|
| 47 |
+
"""Apply the token to the authentication header.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
headers (Mapping): The HTTP request headers.
|
| 51 |
+
token (Optional[str]): If specified, overrides the current access
|
| 52 |
+
token.
|
| 53 |
+
"""
|
| 54 |
+
self._apply(headers, token=token)
|
| 55 |
+
|
| 56 |
+
async def refresh(self, request):
|
| 57 |
+
"""Refreshes the access token.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
request (google.auth.aio.transport.Request): The object used to make
|
| 61 |
+
HTTP requests.
|
| 62 |
+
|
| 63 |
+
Raises:
|
| 64 |
+
google.auth.exceptions.RefreshError: If the credentials could
|
| 65 |
+
not be refreshed.
|
| 66 |
+
"""
|
| 67 |
+
raise NotImplementedError("Refresh must be implemented")
|
| 68 |
+
|
| 69 |
+
async def before_request(self, request, method, url, headers):
|
| 70 |
+
"""Performs credential-specific before request logic.
|
| 71 |
+
|
| 72 |
+
Refreshes the credentials if necessary, then calls :meth:`apply` to
|
| 73 |
+
apply the token to the authentication header.
|
| 74 |
+
|
| 75 |
+
Args:
|
| 76 |
+
request (google.auth.aio.transport.Request): The object used to make
|
| 77 |
+
HTTP requests.
|
| 78 |
+
method (str): The request's HTTP method or the RPC method being
|
| 79 |
+
invoked.
|
| 80 |
+
url (str): The request's URI or the RPC service's URI.
|
| 81 |
+
headers (Mapping): The request's headers.
|
| 82 |
+
"""
|
| 83 |
+
await self.apply(headers)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class StaticCredentials(Credentials):
|
| 87 |
+
"""Asynchronous Credentials representing an immutable access token.
|
| 88 |
+
|
| 89 |
+
The credentials are considered immutable except the tokens which can be
|
| 90 |
+
configured in the constructor ::
|
| 91 |
+
|
| 92 |
+
credentials = StaticCredentials(token="token123")
|
| 93 |
+
|
| 94 |
+
StaticCredentials does not support :meth `refresh` and assumes that the configured
|
| 95 |
+
token is valid and not expired. StaticCredentials will never attempt to
|
| 96 |
+
refresh the token.
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
def __init__(self, token):
|
| 100 |
+
"""
|
| 101 |
+
Args:
|
| 102 |
+
token (str): The access token.
|
| 103 |
+
"""
|
| 104 |
+
super(StaticCredentials, self).__init__()
|
| 105 |
+
self.token = token
|
| 106 |
+
|
| 107 |
+
@_helpers.copy_docstring(Credentials)
|
| 108 |
+
async def refresh(self, request):
|
| 109 |
+
raise exceptions.InvalidOperation("Static credentials cannot be refreshed.")
|
| 110 |
+
|
| 111 |
+
# Note: before_request should never try to refresh access tokens.
|
| 112 |
+
# StaticCredentials intentionally does not support it.
|
| 113 |
+
@_helpers.copy_docstring(Credentials)
|
| 114 |
+
async def before_request(self, request, method, url, headers):
|
| 115 |
+
await self.apply(headers)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class AnonymousCredentials(Credentials):
|
| 119 |
+
"""Asynchronous Credentials that do not provide any authentication information.
|
| 120 |
+
|
| 121 |
+
These are useful in the case of services that support anonymous access or
|
| 122 |
+
local service emulators that do not use credentials.
|
| 123 |
+
"""
|
| 124 |
+
|
| 125 |
+
async def refresh(self, request):
|
| 126 |
+
"""Raises :class:``InvalidOperation``, anonymous credentials cannot be
|
| 127 |
+
refreshed."""
|
| 128 |
+
raise exceptions.InvalidOperation("Anonymous credentials cannot be refreshed.")
|
| 129 |
+
|
| 130 |
+
async def apply(self, headers, token=None):
|
| 131 |
+
"""Anonymous credentials do nothing to the request.
|
| 132 |
+
|
| 133 |
+
The optional ``token`` argument is not supported.
|
| 134 |
+
|
| 135 |
+
Raises:
|
| 136 |
+
google.auth.exceptions.InvalidValue: If a token was specified.
|
| 137 |
+
"""
|
| 138 |
+
if token is not None:
|
| 139 |
+
raise exceptions.InvalidValue("Anonymous credentials don't support tokens.")
|
| 140 |
+
|
| 141 |
+
async def before_request(self, request, method, url, headers):
|
| 142 |
+
"""Anonymous credentials do nothing to the request."""
|
| 143 |
+
pass
|
lib/python3.10/site-packages/google/auth/aio/transport/__init__.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 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 |
+
"""Transport - Asynchronous HTTP client library support.
|
| 16 |
+
|
| 17 |
+
:mod:`google.auth.aio` is designed to work with various asynchronous client libraries such
|
| 18 |
+
as aiohttp. In order to work across these libraries with different
|
| 19 |
+
interfaces some abstraction is needed.
|
| 20 |
+
|
| 21 |
+
This module provides two interfaces that are implemented by transport adapters
|
| 22 |
+
to support HTTP libraries. :class:`Request` defines the interface expected by
|
| 23 |
+
:mod:`google.auth` to make asynchronous requests. :class:`Response` defines the interface
|
| 24 |
+
for the return value of :class:`Request`.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
import abc
|
| 28 |
+
from typing import AsyncGenerator, Mapping, Optional
|
| 29 |
+
|
| 30 |
+
import google.auth.transport
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
_DEFAULT_TIMEOUT_SECONDS = 180
|
| 34 |
+
|
| 35 |
+
DEFAULT_RETRYABLE_STATUS_CODES = google.auth.transport.DEFAULT_RETRYABLE_STATUS_CODES
|
| 36 |
+
"""Sequence[int]: HTTP status codes indicating a request can be retried.
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
DEFAULT_MAX_RETRY_ATTEMPTS = 3
|
| 41 |
+
"""int: How many times to retry a request."""
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class Response(metaclass=abc.ABCMeta):
|
| 45 |
+
"""Asynchronous HTTP Response Interface."""
|
| 46 |
+
|
| 47 |
+
@property
|
| 48 |
+
@abc.abstractmethod
|
| 49 |
+
def status_code(self) -> int:
|
| 50 |
+
"""
|
| 51 |
+
The HTTP response status code.
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
int: The HTTP response status code.
|
| 55 |
+
|
| 56 |
+
"""
|
| 57 |
+
raise NotImplementedError("status_code must be implemented.")
|
| 58 |
+
|
| 59 |
+
@property
|
| 60 |
+
@abc.abstractmethod
|
| 61 |
+
def headers(self) -> Mapping[str, str]:
|
| 62 |
+
"""The HTTP response headers.
|
| 63 |
+
|
| 64 |
+
Returns:
|
| 65 |
+
Mapping[str, str]: The HTTP response headers.
|
| 66 |
+
"""
|
| 67 |
+
raise NotImplementedError("headers must be implemented.")
|
| 68 |
+
|
| 69 |
+
@abc.abstractmethod
|
| 70 |
+
async def content(self, chunk_size: int) -> AsyncGenerator[bytes, None]:
|
| 71 |
+
"""The raw response content.
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
chunk_size (int): The size of each chunk.
|
| 75 |
+
|
| 76 |
+
Yields:
|
| 77 |
+
AsyncGenerator[bytes, None]: An asynchronous generator yielding
|
| 78 |
+
response chunks as bytes.
|
| 79 |
+
"""
|
| 80 |
+
raise NotImplementedError("content must be implemented.")
|
| 81 |
+
|
| 82 |
+
@abc.abstractmethod
|
| 83 |
+
async def read(self) -> bytes:
|
| 84 |
+
"""Read the entire response content as bytes.
|
| 85 |
+
|
| 86 |
+
Returns:
|
| 87 |
+
bytes: The entire response content.
|
| 88 |
+
"""
|
| 89 |
+
raise NotImplementedError("read must be implemented.")
|
| 90 |
+
|
| 91 |
+
@abc.abstractmethod
|
| 92 |
+
async def close(self):
|
| 93 |
+
"""Close the response after it is fully consumed to resource."""
|
| 94 |
+
raise NotImplementedError("close must be implemented.")
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
class Request(metaclass=abc.ABCMeta):
|
| 98 |
+
"""Interface for a callable that makes HTTP requests.
|
| 99 |
+
|
| 100 |
+
Specific transport implementations should provide an implementation of
|
| 101 |
+
this that adapts their specific request / response API.
|
| 102 |
+
|
| 103 |
+
.. automethod:: __call__
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
@abc.abstractmethod
|
| 107 |
+
async def __call__(
|
| 108 |
+
self,
|
| 109 |
+
url: str,
|
| 110 |
+
method: str,
|
| 111 |
+
body: Optional[bytes],
|
| 112 |
+
headers: Optional[Mapping[str, str]],
|
| 113 |
+
timeout: float,
|
| 114 |
+
**kwargs
|
| 115 |
+
) -> Response:
|
| 116 |
+
"""Make an HTTP request.
|
| 117 |
+
|
| 118 |
+
Args:
|
| 119 |
+
url (str): The URI to be requested.
|
| 120 |
+
method (str): The HTTP method to use for the request. Defaults
|
| 121 |
+
to 'GET'.
|
| 122 |
+
body (Optional[bytes]): The payload / body in HTTP request.
|
| 123 |
+
headers (Mapping[str, str]): Request headers.
|
| 124 |
+
timeout (float): The number of seconds to wait for a
|
| 125 |
+
response from the server. If not specified or if None, the
|
| 126 |
+
transport-specific default timeout will be used.
|
| 127 |
+
kwargs: Additional arguments passed on to the transport's
|
| 128 |
+
request method.
|
| 129 |
+
|
| 130 |
+
Returns:
|
| 131 |
+
google.auth.aio.transport.Response: The HTTP response.
|
| 132 |
+
|
| 133 |
+
Raises:
|
| 134 |
+
google.auth.exceptions.TransportError: If any exception occurred.
|
| 135 |
+
"""
|
| 136 |
+
# pylint: disable=redundant-returns-doc, missing-raises-doc
|
| 137 |
+
# (pylint doesn't play well with abstract docstrings.)
|
| 138 |
+
raise NotImplementedError("__call__ must be implemented.")
|
| 139 |
+
|
| 140 |
+
async def close(self) -> None:
|
| 141 |
+
"""
|
| 142 |
+
Close the underlying session.
|
| 143 |
+
"""
|
| 144 |
+
raise NotImplementedError("close must be implemented.")
|
lib/python3.10/site-packages/google/auth/aio/transport/__pycache__/__init__.cpython-310.pyc
ADDED
|
Binary file (4.56 kB). View file
|
|
|
lib/python3.10/site-packages/google/auth/aio/transport/__pycache__/aiohttp.cpython-310.pyc
ADDED
|
Binary file (6.78 kB). View file
|
|
|
lib/python3.10/site-packages/google/auth/aio/transport/__pycache__/sessions.cpython-310.pyc
ADDED
|
Binary file (8.4 kB). View file
|
|
|
lib/python3.10/site-packages/google/auth/aio/transport/aiohttp.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 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 |
+
"""Transport adapter for Asynchronous HTTP Requests based on aiohttp.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import asyncio
|
| 19 |
+
import logging
|
| 20 |
+
from typing import AsyncGenerator, Mapping, Optional
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
import aiohttp # type: ignore
|
| 24 |
+
except ImportError as caught_exc: # pragma: NO COVER
|
| 25 |
+
raise ImportError(
|
| 26 |
+
"The aiohttp library is not installed from please install the aiohttp package to use the aiohttp transport."
|
| 27 |
+
) from caught_exc
|
| 28 |
+
|
| 29 |
+
from google.auth import _helpers
|
| 30 |
+
from google.auth import exceptions
|
| 31 |
+
from google.auth.aio import _helpers as _helpers_async
|
| 32 |
+
from google.auth.aio import transport
|
| 33 |
+
|
| 34 |
+
_LOGGER = logging.getLogger(__name__)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class Response(transport.Response):
|
| 38 |
+
"""
|
| 39 |
+
Represents an HTTP response and its data. It is returned by ``google.auth.aio.transport.sessions.AsyncAuthorizedSession``.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
response (aiohttp.ClientResponse): An instance of aiohttp.ClientResponse.
|
| 43 |
+
|
| 44 |
+
Attributes:
|
| 45 |
+
status_code (int): The HTTP status code of the response.
|
| 46 |
+
headers (Mapping[str, str]): The HTTP headers of the response.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def __init__(self, response: aiohttp.ClientResponse):
|
| 50 |
+
self._response = response
|
| 51 |
+
|
| 52 |
+
@property
|
| 53 |
+
@_helpers.copy_docstring(transport.Response)
|
| 54 |
+
def status_code(self) -> int:
|
| 55 |
+
return self._response.status
|
| 56 |
+
|
| 57 |
+
@property
|
| 58 |
+
@_helpers.copy_docstring(transport.Response)
|
| 59 |
+
def headers(self) -> Mapping[str, str]:
|
| 60 |
+
return {key: value for key, value in self._response.headers.items()}
|
| 61 |
+
|
| 62 |
+
@_helpers.copy_docstring(transport.Response)
|
| 63 |
+
async def content(self, chunk_size: int = 1024) -> AsyncGenerator[bytes, None]:
|
| 64 |
+
try:
|
| 65 |
+
async for chunk in self._response.content.iter_chunked(
|
| 66 |
+
chunk_size
|
| 67 |
+
): # pragma: no branch
|
| 68 |
+
yield chunk
|
| 69 |
+
except aiohttp.ClientPayloadError as exc:
|
| 70 |
+
raise exceptions.ResponseError(
|
| 71 |
+
"Failed to read from the payload stream."
|
| 72 |
+
) from exc
|
| 73 |
+
|
| 74 |
+
@_helpers.copy_docstring(transport.Response)
|
| 75 |
+
async def read(self) -> bytes:
|
| 76 |
+
try:
|
| 77 |
+
return await self._response.read()
|
| 78 |
+
except aiohttp.ClientResponseError as exc:
|
| 79 |
+
raise exceptions.ResponseError("Failed to read the response body.") from exc
|
| 80 |
+
|
| 81 |
+
@_helpers.copy_docstring(transport.Response)
|
| 82 |
+
async def close(self):
|
| 83 |
+
self._response.close()
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class Request(transport.Request):
|
| 87 |
+
"""Asynchronous Requests request adapter.
|
| 88 |
+
|
| 89 |
+
This class is used internally for making requests using aiohttp
|
| 90 |
+
in a consistent way. If you use :class:`google.auth.aio.transport.sessions.AsyncAuthorizedSession`
|
| 91 |
+
you do not need to construct or use this class directly.
|
| 92 |
+
|
| 93 |
+
This class can be useful if you want to configure a Request callable
|
| 94 |
+
with a custom ``aiohttp.ClientSession`` in :class:`AuthorizedSession` or if
|
| 95 |
+
you want to manually refresh a :class:`~google.auth.aio.credentials.Credentials` instance::
|
| 96 |
+
|
| 97 |
+
import aiohttp
|
| 98 |
+
import google.auth.aio.transport.aiohttp
|
| 99 |
+
|
| 100 |
+
# Default example:
|
| 101 |
+
request = google.auth.aio.transport.aiohttp.Request()
|
| 102 |
+
await credentials.refresh(request)
|
| 103 |
+
|
| 104 |
+
# Custom aiohttp Session Example:
|
| 105 |
+
session = session=aiohttp.ClientSession(auto_decompress=False)
|
| 106 |
+
request = google.auth.aio.transport.aiohttp.Request(session=session)
|
| 107 |
+
auth_sesion = google.auth.aio.transport.sessions.AsyncAuthorizedSession(auth_request=request)
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
session (aiohttp.ClientSession): An instance :class:`aiohttp.ClientSession` used
|
| 111 |
+
to make HTTP requests. If not specified, a session will be created.
|
| 112 |
+
|
| 113 |
+
.. automethod:: __call__
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
def __init__(self, session: aiohttp.ClientSession = None):
|
| 117 |
+
self._session = session
|
| 118 |
+
self._closed = False
|
| 119 |
+
|
| 120 |
+
async def __call__(
|
| 121 |
+
self,
|
| 122 |
+
url: str,
|
| 123 |
+
method: str = "GET",
|
| 124 |
+
body: Optional[bytes] = None,
|
| 125 |
+
headers: Optional[Mapping[str, str]] = None,
|
| 126 |
+
timeout: float = transport._DEFAULT_TIMEOUT_SECONDS,
|
| 127 |
+
**kwargs,
|
| 128 |
+
) -> transport.Response:
|
| 129 |
+
"""
|
| 130 |
+
Make an HTTP request using aiohttp.
|
| 131 |
+
|
| 132 |
+
Args:
|
| 133 |
+
url (str): The URL to be requested.
|
| 134 |
+
method (Optional[str]):
|
| 135 |
+
The HTTP method to use for the request. Defaults to 'GET'.
|
| 136 |
+
body (Optional[bytes]):
|
| 137 |
+
The payload or body in HTTP request.
|
| 138 |
+
headers (Optional[Mapping[str, str]]):
|
| 139 |
+
Request headers.
|
| 140 |
+
timeout (float): The number of seconds to wait for a
|
| 141 |
+
response from the server. If not specified or if None, the
|
| 142 |
+
requests default timeout will be used.
|
| 143 |
+
kwargs: Additional arguments passed through to the underlying
|
| 144 |
+
aiohttp :meth:`aiohttp.Session.request` method.
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
google.auth.aio.transport.Response: The HTTP response.
|
| 148 |
+
|
| 149 |
+
Raises:
|
| 150 |
+
- google.auth.exceptions.TransportError: If the request fails or if the session is closed.
|
| 151 |
+
- google.auth.exceptions.TimeoutError: If the request times out.
|
| 152 |
+
"""
|
| 153 |
+
|
| 154 |
+
try:
|
| 155 |
+
if self._closed:
|
| 156 |
+
raise exceptions.TransportError("session is closed.")
|
| 157 |
+
|
| 158 |
+
if not self._session:
|
| 159 |
+
self._session = aiohttp.ClientSession()
|
| 160 |
+
|
| 161 |
+
client_timeout = aiohttp.ClientTimeout(total=timeout)
|
| 162 |
+
_helpers.request_log(_LOGGER, method, url, body, headers)
|
| 163 |
+
response = await self._session.request(
|
| 164 |
+
method,
|
| 165 |
+
url,
|
| 166 |
+
data=body,
|
| 167 |
+
headers=headers,
|
| 168 |
+
timeout=client_timeout,
|
| 169 |
+
**kwargs,
|
| 170 |
+
)
|
| 171 |
+
await _helpers_async.response_log_async(_LOGGER, response)
|
| 172 |
+
return Response(response)
|
| 173 |
+
|
| 174 |
+
except aiohttp.ClientError as caught_exc:
|
| 175 |
+
client_exc = exceptions.TransportError(f"Failed to send request to {url}.")
|
| 176 |
+
raise client_exc from caught_exc
|
| 177 |
+
|
| 178 |
+
except asyncio.TimeoutError as caught_exc:
|
| 179 |
+
timeout_exc = exceptions.TimeoutError(
|
| 180 |
+
f"Request timed out after {timeout} seconds."
|
| 181 |
+
)
|
| 182 |
+
raise timeout_exc from caught_exc
|
| 183 |
+
|
| 184 |
+
async def close(self) -> None:
|
| 185 |
+
"""
|
| 186 |
+
Close the underlying aiohttp session to release the acquired resources.
|
| 187 |
+
"""
|
| 188 |
+
if not self._closed and self._session:
|
| 189 |
+
await self._session.close()
|
| 190 |
+
self._closed = True
|
lib/python3.10/site-packages/google/auth/aio/transport/sessions.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 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 |
+
import asyncio
|
| 16 |
+
from contextlib import asynccontextmanager
|
| 17 |
+
import functools
|
| 18 |
+
import time
|
| 19 |
+
from typing import Mapping, Optional
|
| 20 |
+
|
| 21 |
+
from google.auth import _exponential_backoff, exceptions
|
| 22 |
+
from google.auth.aio import transport
|
| 23 |
+
from google.auth.aio.credentials import Credentials
|
| 24 |
+
from google.auth.exceptions import TimeoutError
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
from google.auth.aio.transport.aiohttp import Request as AiohttpRequest
|
| 28 |
+
|
| 29 |
+
AIOHTTP_INSTALLED = True
|
| 30 |
+
except ImportError: # pragma: NO COVER
|
| 31 |
+
AIOHTTP_INSTALLED = False
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@asynccontextmanager
|
| 35 |
+
async def timeout_guard(timeout):
|
| 36 |
+
"""
|
| 37 |
+
timeout_guard is an asynchronous context manager to apply a timeout to an asynchronous block of code.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
timeout (float): The time in seconds before the context manager times out.
|
| 41 |
+
|
| 42 |
+
Raises:
|
| 43 |
+
google.auth.exceptions.TimeoutError: If the code within the context exceeds the provided timeout.
|
| 44 |
+
|
| 45 |
+
Usage:
|
| 46 |
+
async with timeout_guard(10) as with_timeout:
|
| 47 |
+
await with_timeout(async_function())
|
| 48 |
+
"""
|
| 49 |
+
start = time.monotonic()
|
| 50 |
+
total_timeout = timeout
|
| 51 |
+
|
| 52 |
+
def _remaining_time():
|
| 53 |
+
elapsed = time.monotonic() - start
|
| 54 |
+
remaining = total_timeout - elapsed
|
| 55 |
+
if remaining <= 0:
|
| 56 |
+
raise TimeoutError(
|
| 57 |
+
f"Context manager exceeded the configured timeout of {total_timeout}s."
|
| 58 |
+
)
|
| 59 |
+
return remaining
|
| 60 |
+
|
| 61 |
+
async def with_timeout(coro):
|
| 62 |
+
try:
|
| 63 |
+
remaining = _remaining_time()
|
| 64 |
+
response = await asyncio.wait_for(coro, remaining)
|
| 65 |
+
return response
|
| 66 |
+
except (asyncio.TimeoutError, TimeoutError) as e:
|
| 67 |
+
raise TimeoutError(
|
| 68 |
+
f"The operation {coro} exceeded the configured timeout of {total_timeout}s."
|
| 69 |
+
) from e
|
| 70 |
+
|
| 71 |
+
try:
|
| 72 |
+
yield with_timeout
|
| 73 |
+
|
| 74 |
+
finally:
|
| 75 |
+
_remaining_time()
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class AsyncAuthorizedSession:
|
| 79 |
+
"""This is an asynchronous implementation of :class:`google.auth.requests.AuthorizedSession` class.
|
| 80 |
+
We utilize an instance of a class that implements :class:`google.auth.aio.transport.Request` configured
|
| 81 |
+
by the caller or otherwise default to `google.auth.aio.transport.aiohttp.Request` if the external aiohttp
|
| 82 |
+
package is installed.
|
| 83 |
+
|
| 84 |
+
A Requests Session class with credentials.
|
| 85 |
+
|
| 86 |
+
This class is used to perform asynchronous requests to API endpoints that require
|
| 87 |
+
authorization::
|
| 88 |
+
|
| 89 |
+
import aiohttp
|
| 90 |
+
from google.auth.aio.transport import sessions
|
| 91 |
+
|
| 92 |
+
async with sessions.AsyncAuthorizedSession(credentials) as authed_session:
|
| 93 |
+
response = await authed_session.request(
|
| 94 |
+
'GET', 'https://www.googleapis.com/storage/v1/b')
|
| 95 |
+
|
| 96 |
+
The underlying :meth:`request` implementation handles adding the
|
| 97 |
+
credentials' headers to the request and refreshing credentials as needed.
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
credentials (google.auth.aio.credentials.Credentials):
|
| 101 |
+
The credentials to add to the request.
|
| 102 |
+
auth_request (Optional[google.auth.aio.transport.Request]):
|
| 103 |
+
An instance of a class that implements
|
| 104 |
+
:class:`~google.auth.aio.transport.Request` used to make requests
|
| 105 |
+
and refresh credentials. If not passed,
|
| 106 |
+
an instance of :class:`~google.auth.aio.transport.aiohttp.Request`
|
| 107 |
+
is created.
|
| 108 |
+
|
| 109 |
+
Raises:
|
| 110 |
+
- google.auth.exceptions.TransportError: If `auth_request` is `None`
|
| 111 |
+
and the external package `aiohttp` is not installed.
|
| 112 |
+
- google.auth.exceptions.InvalidType: If the provided credentials are
|
| 113 |
+
not of type `google.auth.aio.credentials.Credentials`.
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
def __init__(
|
| 117 |
+
self, credentials: Credentials, auth_request: Optional[transport.Request] = None
|
| 118 |
+
):
|
| 119 |
+
if not isinstance(credentials, Credentials):
|
| 120 |
+
raise exceptions.InvalidType(
|
| 121 |
+
f"The configured credentials of type {type(credentials)} are invalid and must be of type `google.auth.aio.credentials.Credentials`"
|
| 122 |
+
)
|
| 123 |
+
self._credentials = credentials
|
| 124 |
+
_auth_request = auth_request
|
| 125 |
+
if not _auth_request and AIOHTTP_INSTALLED:
|
| 126 |
+
_auth_request = AiohttpRequest()
|
| 127 |
+
if _auth_request is None:
|
| 128 |
+
raise exceptions.TransportError(
|
| 129 |
+
"`auth_request` must either be configured or the external package `aiohttp` must be installed to use the default value."
|
| 130 |
+
)
|
| 131 |
+
self._auth_request = _auth_request
|
| 132 |
+
|
| 133 |
+
async def request(
|
| 134 |
+
self,
|
| 135 |
+
method: str,
|
| 136 |
+
url: str,
|
| 137 |
+
data: Optional[bytes] = None,
|
| 138 |
+
headers: Optional[Mapping[str, str]] = None,
|
| 139 |
+
max_allowed_time: float = transport._DEFAULT_TIMEOUT_SECONDS,
|
| 140 |
+
timeout: float = transport._DEFAULT_TIMEOUT_SECONDS,
|
| 141 |
+
**kwargs,
|
| 142 |
+
) -> transport.Response:
|
| 143 |
+
"""
|
| 144 |
+
Args:
|
| 145 |
+
method (str): The http method used to make the request.
|
| 146 |
+
url (str): The URI to be requested.
|
| 147 |
+
data (Optional[bytes]): The payload or body in HTTP request.
|
| 148 |
+
headers (Optional[Mapping[str, str]]): Request headers.
|
| 149 |
+
timeout (float):
|
| 150 |
+
The amount of time in seconds to wait for the server response
|
| 151 |
+
with each individual request.
|
| 152 |
+
max_allowed_time (float):
|
| 153 |
+
If the method runs longer than this, a ``Timeout`` exception is
|
| 154 |
+
automatically raised. Unlike the ``timeout`` parameter, this
|
| 155 |
+
value applies to the total method execution time, even if
|
| 156 |
+
multiple requests are made under the hood.
|
| 157 |
+
|
| 158 |
+
Mind that it is not guaranteed that the timeout error is raised
|
| 159 |
+
at ``max_allowed_time``. It might take longer, for example, if
|
| 160 |
+
an underlying request takes a lot of time, but the request
|
| 161 |
+
itself does not timeout, e.g. if a large file is being
|
| 162 |
+
transmitted. The timout error will be raised after such
|
| 163 |
+
request completes.
|
| 164 |
+
|
| 165 |
+
Returns:
|
| 166 |
+
google.auth.aio.transport.Response: The HTTP response.
|
| 167 |
+
|
| 168 |
+
Raises:
|
| 169 |
+
google.auth.exceptions.TimeoutError: If the method does not complete within
|
| 170 |
+
the configured `max_allowed_time` or the request exceeds the configured
|
| 171 |
+
`timeout`.
|
| 172 |
+
"""
|
| 173 |
+
|
| 174 |
+
retries = _exponential_backoff.AsyncExponentialBackoff(
|
| 175 |
+
total_attempts=transport.DEFAULT_MAX_RETRY_ATTEMPTS
|
| 176 |
+
)
|
| 177 |
+
async with timeout_guard(max_allowed_time) as with_timeout:
|
| 178 |
+
await with_timeout(
|
| 179 |
+
# Note: before_request will attempt to refresh credentials if expired.
|
| 180 |
+
self._credentials.before_request(
|
| 181 |
+
self._auth_request, method, url, headers
|
| 182 |
+
)
|
| 183 |
+
)
|
| 184 |
+
# Workaround issue in python 3.9 related to code coverage by adding `# pragma: no branch`
|
| 185 |
+
# See https://github.com/googleapis/gapic-generator-python/pull/1174#issuecomment-1025132372
|
| 186 |
+
async for _ in retries: # pragma: no branch
|
| 187 |
+
response = await with_timeout(
|
| 188 |
+
self._auth_request(url, method, data, headers, timeout, **kwargs)
|
| 189 |
+
)
|
| 190 |
+
if response.status_code not in transport.DEFAULT_RETRYABLE_STATUS_CODES:
|
| 191 |
+
break
|
| 192 |
+
return response
|
| 193 |
+
|
| 194 |
+
@functools.wraps(request)
|
| 195 |
+
async def get(
|
| 196 |
+
self,
|
| 197 |
+
url: str,
|
| 198 |
+
data: Optional[bytes] = None,
|
| 199 |
+
headers: Optional[Mapping[str, str]] = None,
|
| 200 |
+
max_allowed_time: float = transport._DEFAULT_TIMEOUT_SECONDS,
|
| 201 |
+
timeout: float = transport._DEFAULT_TIMEOUT_SECONDS,
|
| 202 |
+
**kwargs,
|
| 203 |
+
) -> transport.Response:
|
| 204 |
+
return await self.request(
|
| 205 |
+
"GET", url, data, headers, max_allowed_time, timeout, **kwargs
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
@functools.wraps(request)
|
| 209 |
+
async def post(
|
| 210 |
+
self,
|
| 211 |
+
url: str,
|
| 212 |
+
data: Optional[bytes] = None,
|
| 213 |
+
headers: Optional[Mapping[str, str]] = None,
|
| 214 |
+
max_allowed_time: float = transport._DEFAULT_TIMEOUT_SECONDS,
|
| 215 |
+
timeout: float = transport._DEFAULT_TIMEOUT_SECONDS,
|
| 216 |
+
**kwargs,
|
| 217 |
+
) -> transport.Response:
|
| 218 |
+
return await self.request(
|
| 219 |
+
"POST", url, data, headers, max_allowed_time, timeout, **kwargs
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
@functools.wraps(request)
|
| 223 |
+
async def put(
|
| 224 |
+
self,
|
| 225 |
+
url: str,
|
| 226 |
+
data: Optional[bytes] = None,
|
| 227 |
+
headers: Optional[Mapping[str, str]] = None,
|
| 228 |
+
max_allowed_time: float = transport._DEFAULT_TIMEOUT_SECONDS,
|
| 229 |
+
timeout: float = transport._DEFAULT_TIMEOUT_SECONDS,
|
| 230 |
+
**kwargs,
|
| 231 |
+
) -> transport.Response:
|
| 232 |
+
return await self.request(
|
| 233 |
+
"PUT", url, data, headers, max_allowed_time, timeout, **kwargs
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
@functools.wraps(request)
|
| 237 |
+
async def patch(
|
| 238 |
+
self,
|
| 239 |
+
url: str,
|
| 240 |
+
data: Optional[bytes] = None,
|
| 241 |
+
headers: Optional[Mapping[str, str]] = None,
|
| 242 |
+
max_allowed_time: float = transport._DEFAULT_TIMEOUT_SECONDS,
|
| 243 |
+
timeout: float = transport._DEFAULT_TIMEOUT_SECONDS,
|
| 244 |
+
**kwargs,
|
| 245 |
+
) -> transport.Response:
|
| 246 |
+
return await self.request(
|
| 247 |
+
"PATCH", url, data, headers, max_allowed_time, timeout, **kwargs
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
@functools.wraps(request)
|
| 251 |
+
async def delete(
|
| 252 |
+
self,
|
| 253 |
+
url: str,
|
| 254 |
+
data: Optional[bytes] = None,
|
| 255 |
+
headers: Optional[Mapping[str, str]] = None,
|
| 256 |
+
max_allowed_time: float = transport._DEFAULT_TIMEOUT_SECONDS,
|
| 257 |
+
timeout: float = transport._DEFAULT_TIMEOUT_SECONDS,
|
| 258 |
+
**kwargs,
|
| 259 |
+
) -> transport.Response:
|
| 260 |
+
return await self.request(
|
| 261 |
+
"DELETE", url, data, headers, max_allowed_time, timeout, **kwargs
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
async def close(self) -> None:
|
| 265 |
+
"""
|
| 266 |
+
Close the underlying auth request session.
|
| 267 |
+
"""
|
| 268 |
+
await self._auth_request.close()
|
lib/python3.10/site-packages/google/auth/api_key.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2022 Google LLC
|
| 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 |
+
"""Google API key support.
|
| 16 |
+
This module provides authentication using the `API key`_.
|
| 17 |
+
.. _API key:
|
| 18 |
+
https://cloud.google.com/docs/authentication/api-keys/
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from google.auth import _helpers
|
| 22 |
+
from google.auth import credentials
|
| 23 |
+
from google.auth import exceptions
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class Credentials(credentials.Credentials):
|
| 27 |
+
"""API key credentials.
|
| 28 |
+
These credentials use API key to provide authorization to applications.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
def __init__(self, token):
|
| 32 |
+
"""
|
| 33 |
+
Args:
|
| 34 |
+
token (str): API key string
|
| 35 |
+
Raises:
|
| 36 |
+
ValueError: If the provided API key is not a non-empty string.
|
| 37 |
+
"""
|
| 38 |
+
super(Credentials, self).__init__()
|
| 39 |
+
if not token:
|
| 40 |
+
raise exceptions.InvalidValue("Token must be a non-empty API key string")
|
| 41 |
+
self.token = token
|
| 42 |
+
|
| 43 |
+
@property
|
| 44 |
+
def expired(self):
|
| 45 |
+
return False
|
| 46 |
+
|
| 47 |
+
@property
|
| 48 |
+
def valid(self):
|
| 49 |
+
return True
|
| 50 |
+
|
| 51 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 52 |
+
def refresh(self, request):
|
| 53 |
+
return
|
| 54 |
+
|
| 55 |
+
def apply(self, headers, token=None):
|
| 56 |
+
"""Apply the API key token to the x-goog-api-key header.
|
| 57 |
+
Args:
|
| 58 |
+
headers (Mapping): The HTTP request headers.
|
| 59 |
+
token (Optional[str]): If specified, overrides the current access
|
| 60 |
+
token.
|
| 61 |
+
"""
|
| 62 |
+
headers["x-goog-api-key"] = token or self.token
|
| 63 |
+
|
| 64 |
+
def before_request(self, request, method, url, headers):
|
| 65 |
+
"""Performs credential-specific before request logic.
|
| 66 |
+
Refreshes the credentials if necessary, then calls :meth:`apply` to
|
| 67 |
+
apply the token to the x-goog-api-key header.
|
| 68 |
+
Args:
|
| 69 |
+
request (google.auth.transport.Request): The object used to make
|
| 70 |
+
HTTP requests.
|
| 71 |
+
method (str): The request's HTTP method or the RPC method being
|
| 72 |
+
invoked.
|
| 73 |
+
url (str): The request's URI or the RPC service's URI.
|
| 74 |
+
headers (Mapping): The request's headers.
|
| 75 |
+
"""
|
| 76 |
+
self.apply(headers)
|
lib/python3.10/site-packages/google/auth/app_engine.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 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 |
+
"""Google App Engine standard environment support.
|
| 16 |
+
|
| 17 |
+
This module provides authentication and signing for applications running on App
|
| 18 |
+
Engine in the standard environment using the `App Identity API`_.
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
.. _App Identity API:
|
| 22 |
+
https://cloud.google.com/appengine/docs/python/appidentity/
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
import datetime
|
| 26 |
+
|
| 27 |
+
from google.auth import _helpers
|
| 28 |
+
from google.auth import credentials
|
| 29 |
+
from google.auth import crypt
|
| 30 |
+
from google.auth import exceptions
|
| 31 |
+
|
| 32 |
+
# pytype: disable=import-error
|
| 33 |
+
try:
|
| 34 |
+
from google.appengine.api import app_identity # type: ignore
|
| 35 |
+
except ImportError:
|
| 36 |
+
app_identity = None # type: ignore
|
| 37 |
+
# pytype: enable=import-error
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class Signer(crypt.Signer):
|
| 41 |
+
"""Signs messages using the App Engine App Identity service.
|
| 42 |
+
|
| 43 |
+
This can be used in place of :class:`google.auth.crypt.Signer` when
|
| 44 |
+
running in the App Engine standard environment.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
@property
|
| 48 |
+
def key_id(self):
|
| 49 |
+
"""Optional[str]: The key ID used to identify this private key.
|
| 50 |
+
|
| 51 |
+
.. warning::
|
| 52 |
+
This is always ``None``. The key ID used by App Engine can not
|
| 53 |
+
be reliably determined ahead of time.
|
| 54 |
+
"""
|
| 55 |
+
return None
|
| 56 |
+
|
| 57 |
+
@_helpers.copy_docstring(crypt.Signer)
|
| 58 |
+
def sign(self, message):
|
| 59 |
+
message = _helpers.to_bytes(message)
|
| 60 |
+
_, signature = app_identity.sign_blob(message)
|
| 61 |
+
return signature
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def get_project_id():
|
| 65 |
+
"""Gets the project ID for the current App Engine application.
|
| 66 |
+
|
| 67 |
+
Returns:
|
| 68 |
+
str: The project ID
|
| 69 |
+
|
| 70 |
+
Raises:
|
| 71 |
+
google.auth.exceptions.OSError: If the App Engine APIs are unavailable.
|
| 72 |
+
"""
|
| 73 |
+
# pylint: disable=missing-raises-doc
|
| 74 |
+
# Pylint rightfully thinks google.auth.exceptions.OSError is OSError, but doesn't
|
| 75 |
+
# realize it's a valid alias.
|
| 76 |
+
if app_identity is None:
|
| 77 |
+
raise exceptions.OSError("The App Engine APIs are not available.")
|
| 78 |
+
return app_identity.get_application_id()
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class Credentials(
|
| 82 |
+
credentials.Scoped, credentials.Signing, credentials.CredentialsWithQuotaProject
|
| 83 |
+
):
|
| 84 |
+
"""App Engine standard environment credentials.
|
| 85 |
+
|
| 86 |
+
These credentials use the App Engine App Identity API to obtain access
|
| 87 |
+
tokens.
|
| 88 |
+
"""
|
| 89 |
+
|
| 90 |
+
def __init__(
|
| 91 |
+
self,
|
| 92 |
+
scopes=None,
|
| 93 |
+
default_scopes=None,
|
| 94 |
+
service_account_id=None,
|
| 95 |
+
quota_project_id=None,
|
| 96 |
+
):
|
| 97 |
+
"""
|
| 98 |
+
Args:
|
| 99 |
+
scopes (Sequence[str]): Scopes to request from the App Identity
|
| 100 |
+
API.
|
| 101 |
+
default_scopes (Sequence[str]): Default scopes passed by a
|
| 102 |
+
Google client library. Use 'scopes' for user-defined scopes.
|
| 103 |
+
service_account_id (str): The service account ID passed into
|
| 104 |
+
:func:`google.appengine.api.app_identity.get_access_token`.
|
| 105 |
+
If not specified, the default application service account
|
| 106 |
+
ID will be used.
|
| 107 |
+
quota_project_id (Optional[str]): The project ID used for quota
|
| 108 |
+
and billing.
|
| 109 |
+
|
| 110 |
+
Raises:
|
| 111 |
+
google.auth.exceptions.OSError: If the App Engine APIs are unavailable.
|
| 112 |
+
"""
|
| 113 |
+
# pylint: disable=missing-raises-doc
|
| 114 |
+
# Pylint rightfully thinks google.auth.exceptions.OSError is OSError, but doesn't
|
| 115 |
+
# realize it's a valid alias.
|
| 116 |
+
if app_identity is None:
|
| 117 |
+
raise exceptions.OSError("The App Engine APIs are not available.")
|
| 118 |
+
|
| 119 |
+
super(Credentials, self).__init__()
|
| 120 |
+
self._scopes = scopes
|
| 121 |
+
self._default_scopes = default_scopes
|
| 122 |
+
self._service_account_id = service_account_id
|
| 123 |
+
self._signer = Signer()
|
| 124 |
+
self._quota_project_id = quota_project_id
|
| 125 |
+
|
| 126 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 127 |
+
def refresh(self, request):
|
| 128 |
+
scopes = self._scopes if self._scopes is not None else self._default_scopes
|
| 129 |
+
# pylint: disable=unused-argument
|
| 130 |
+
token, ttl = app_identity.get_access_token(scopes, self._service_account_id)
|
| 131 |
+
expiry = datetime.datetime.utcfromtimestamp(ttl)
|
| 132 |
+
|
| 133 |
+
self.token, self.expiry = token, expiry
|
| 134 |
+
|
| 135 |
+
@property
|
| 136 |
+
def service_account_email(self):
|
| 137 |
+
"""The service account email."""
|
| 138 |
+
if self._service_account_id is None:
|
| 139 |
+
self._service_account_id = app_identity.get_service_account_name()
|
| 140 |
+
return self._service_account_id
|
| 141 |
+
|
| 142 |
+
@property
|
| 143 |
+
def requires_scopes(self):
|
| 144 |
+
"""Checks if the credentials requires scopes.
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
bool: True if there are no scopes set otherwise False.
|
| 148 |
+
"""
|
| 149 |
+
return not self._scopes and not self._default_scopes
|
| 150 |
+
|
| 151 |
+
@_helpers.copy_docstring(credentials.Scoped)
|
| 152 |
+
def with_scopes(self, scopes, default_scopes=None):
|
| 153 |
+
return self.__class__(
|
| 154 |
+
scopes=scopes,
|
| 155 |
+
default_scopes=default_scopes,
|
| 156 |
+
service_account_id=self._service_account_id,
|
| 157 |
+
quota_project_id=self.quota_project_id,
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
|
| 161 |
+
def with_quota_project(self, quota_project_id):
|
| 162 |
+
return self.__class__(
|
| 163 |
+
scopes=self._scopes,
|
| 164 |
+
service_account_id=self._service_account_id,
|
| 165 |
+
quota_project_id=quota_project_id,
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
@_helpers.copy_docstring(credentials.Signing)
|
| 169 |
+
def sign_bytes(self, message):
|
| 170 |
+
return self._signer.sign(message)
|
| 171 |
+
|
| 172 |
+
@property # type: ignore
|
| 173 |
+
@_helpers.copy_docstring(credentials.Signing)
|
| 174 |
+
def signer_email(self):
|
| 175 |
+
return self.service_account_email
|
| 176 |
+
|
| 177 |
+
@property # type: ignore
|
| 178 |
+
@_helpers.copy_docstring(credentials.Signing)
|
| 179 |
+
def signer(self):
|
| 180 |
+
return self._signer
|
lib/python3.10/site-packages/google/auth/aws.py
ADDED
|
@@ -0,0 +1,861 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020 Google LLC
|
| 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 |
+
"""AWS Credentials and AWS Signature V4 Request Signer.
|
| 16 |
+
|
| 17 |
+
This module provides credentials to access Google Cloud resources from Amazon
|
| 18 |
+
Web Services (AWS) workloads. These credentials are recommended over the
|
| 19 |
+
use of service account credentials in AWS as they do not involve the management
|
| 20 |
+
of long-live service account private keys.
|
| 21 |
+
|
| 22 |
+
AWS Credentials are initialized using external_account arguments which are
|
| 23 |
+
typically loaded from the external credentials JSON file.
|
| 24 |
+
|
| 25 |
+
This module also provides a definition for an abstract AWS security credentials supplier.
|
| 26 |
+
This supplier can be implemented to return valid AWS security credentials and an AWS region
|
| 27 |
+
and used to create AWS credentials. The credentials will then call the
|
| 28 |
+
supplier instead of using pre-defined methods such as calling the EC2 metadata endpoints.
|
| 29 |
+
|
| 30 |
+
This module also provides a basic implementation of the
|
| 31 |
+
`AWS Signature Version 4`_ request signing algorithm.
|
| 32 |
+
|
| 33 |
+
AWS Credentials use serialized signed requests to the
|
| 34 |
+
`AWS STS GetCallerIdentity`_ API that can be exchanged for Google access tokens
|
| 35 |
+
via the GCP STS endpoint.
|
| 36 |
+
|
| 37 |
+
.. _AWS Signature Version 4: https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
|
| 38 |
+
.. _AWS STS GetCallerIdentity: https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
import abc
|
| 42 |
+
from dataclasses import dataclass
|
| 43 |
+
import hashlib
|
| 44 |
+
import hmac
|
| 45 |
+
import http.client as http_client
|
| 46 |
+
import json
|
| 47 |
+
import os
|
| 48 |
+
import posixpath
|
| 49 |
+
import re
|
| 50 |
+
from typing import Optional
|
| 51 |
+
import urllib
|
| 52 |
+
from urllib.parse import urljoin
|
| 53 |
+
|
| 54 |
+
from google.auth import _helpers
|
| 55 |
+
from google.auth import environment_vars
|
| 56 |
+
from google.auth import exceptions
|
| 57 |
+
from google.auth import external_account
|
| 58 |
+
|
| 59 |
+
# AWS Signature Version 4 signing algorithm identifier.
|
| 60 |
+
_AWS_ALGORITHM = "AWS4-HMAC-SHA256"
|
| 61 |
+
# The termination string for the AWS credential scope value as defined in
|
| 62 |
+
# https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
|
| 63 |
+
_AWS_REQUEST_TYPE = "aws4_request"
|
| 64 |
+
# The AWS authorization header name for the security session token if available.
|
| 65 |
+
_AWS_SECURITY_TOKEN_HEADER = "x-amz-security-token"
|
| 66 |
+
# The AWS authorization header name for the auto-generated date.
|
| 67 |
+
_AWS_DATE_HEADER = "x-amz-date"
|
| 68 |
+
# The default AWS regional credential verification URL.
|
| 69 |
+
_DEFAULT_AWS_REGIONAL_CREDENTIAL_VERIFICATION_URL = (
|
| 70 |
+
"https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
|
| 71 |
+
)
|
| 72 |
+
# IMDSV2 session token lifetime. This is set to a low value because the session token is used immediately.
|
| 73 |
+
_IMDSV2_SESSION_TOKEN_TTL_SECONDS = "300"
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class RequestSigner(object):
|
| 77 |
+
"""Implements an AWS request signer based on the AWS Signature Version 4 signing
|
| 78 |
+
process.
|
| 79 |
+
https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
def __init__(self, region_name):
|
| 83 |
+
"""Instantiates an AWS request signer used to compute authenticated signed
|
| 84 |
+
requests to AWS APIs based on the AWS Signature Version 4 signing process.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
region_name (str): The AWS region to use.
|
| 88 |
+
"""
|
| 89 |
+
|
| 90 |
+
self._region_name = region_name
|
| 91 |
+
|
| 92 |
+
def get_request_options(
|
| 93 |
+
self,
|
| 94 |
+
aws_security_credentials,
|
| 95 |
+
url,
|
| 96 |
+
method,
|
| 97 |
+
request_payload="",
|
| 98 |
+
additional_headers={},
|
| 99 |
+
):
|
| 100 |
+
"""Generates the signed request for the provided HTTP request for calling
|
| 101 |
+
an AWS API. This follows the steps described at:
|
| 102 |
+
https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
|
| 103 |
+
|
| 104 |
+
Args:
|
| 105 |
+
aws_security_credentials (AWSSecurityCredentials): The AWS security credentials.
|
| 106 |
+
url (str): The AWS service URL containing the canonical URI and
|
| 107 |
+
query string.
|
| 108 |
+
method (str): The HTTP method used to call this API.
|
| 109 |
+
request_payload (Optional[str]): The optional request payload if
|
| 110 |
+
available.
|
| 111 |
+
additional_headers (Optional[Mapping[str, str]]): The optional
|
| 112 |
+
additional headers needed for the requested AWS API.
|
| 113 |
+
|
| 114 |
+
Returns:
|
| 115 |
+
Mapping[str, str]: The AWS signed request dictionary object.
|
| 116 |
+
"""
|
| 117 |
+
|
| 118 |
+
additional_headers = additional_headers or {}
|
| 119 |
+
|
| 120 |
+
uri = urllib.parse.urlparse(url)
|
| 121 |
+
# Normalize the URL path. This is needed for the canonical_uri.
|
| 122 |
+
# os.path.normpath can't be used since it normalizes "/" paths
|
| 123 |
+
# to "\\" in Windows OS.
|
| 124 |
+
normalized_uri = urllib.parse.urlparse(
|
| 125 |
+
urljoin(url, posixpath.normpath(uri.path))
|
| 126 |
+
)
|
| 127 |
+
# Validate provided URL.
|
| 128 |
+
if not uri.hostname or uri.scheme != "https":
|
| 129 |
+
raise exceptions.InvalidResource("Invalid AWS service URL")
|
| 130 |
+
|
| 131 |
+
header_map = _generate_authentication_header_map(
|
| 132 |
+
host=uri.hostname,
|
| 133 |
+
canonical_uri=normalized_uri.path or "/",
|
| 134 |
+
canonical_querystring=_get_canonical_querystring(uri.query),
|
| 135 |
+
method=method,
|
| 136 |
+
region=self._region_name,
|
| 137 |
+
aws_security_credentials=aws_security_credentials,
|
| 138 |
+
request_payload=request_payload,
|
| 139 |
+
additional_headers=additional_headers,
|
| 140 |
+
)
|
| 141 |
+
headers = {
|
| 142 |
+
"Authorization": header_map.get("authorization_header"),
|
| 143 |
+
"host": uri.hostname,
|
| 144 |
+
}
|
| 145 |
+
# Add x-amz-date if available.
|
| 146 |
+
if "amz_date" in header_map:
|
| 147 |
+
headers[_AWS_DATE_HEADER] = header_map.get("amz_date")
|
| 148 |
+
# Append additional optional headers, eg. X-Amz-Target, Content-Type, etc.
|
| 149 |
+
for key in additional_headers:
|
| 150 |
+
headers[key] = additional_headers[key]
|
| 151 |
+
|
| 152 |
+
# Add session token if available.
|
| 153 |
+
if aws_security_credentials.session_token is not None:
|
| 154 |
+
headers[_AWS_SECURITY_TOKEN_HEADER] = aws_security_credentials.session_token
|
| 155 |
+
|
| 156 |
+
signed_request = {"url": url, "method": method, "headers": headers}
|
| 157 |
+
if request_payload:
|
| 158 |
+
signed_request["data"] = request_payload
|
| 159 |
+
return signed_request
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _get_canonical_querystring(query):
|
| 163 |
+
"""Generates the canonical query string given a raw query string.
|
| 164 |
+
Logic is based on
|
| 165 |
+
https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
| 166 |
+
|
| 167 |
+
Args:
|
| 168 |
+
query (str): The raw query string.
|
| 169 |
+
|
| 170 |
+
Returns:
|
| 171 |
+
str: The canonical query string.
|
| 172 |
+
"""
|
| 173 |
+
# Parse raw query string.
|
| 174 |
+
querystring = urllib.parse.parse_qs(query)
|
| 175 |
+
querystring_encoded_map = {}
|
| 176 |
+
for key in querystring:
|
| 177 |
+
quote_key = urllib.parse.quote(key, safe="-_.~")
|
| 178 |
+
# URI encode key.
|
| 179 |
+
querystring_encoded_map[quote_key] = []
|
| 180 |
+
for item in querystring[key]:
|
| 181 |
+
# For each key, URI encode all values for that key.
|
| 182 |
+
querystring_encoded_map[quote_key].append(
|
| 183 |
+
urllib.parse.quote(item, safe="-_.~")
|
| 184 |
+
)
|
| 185 |
+
# Sort values for each key.
|
| 186 |
+
querystring_encoded_map[quote_key].sort()
|
| 187 |
+
# Sort keys.
|
| 188 |
+
sorted_keys = list(querystring_encoded_map.keys())
|
| 189 |
+
sorted_keys.sort()
|
| 190 |
+
# Reconstruct the query string. Preserve keys with multiple values.
|
| 191 |
+
querystring_encoded_pairs = []
|
| 192 |
+
for key in sorted_keys:
|
| 193 |
+
for item in querystring_encoded_map[key]:
|
| 194 |
+
querystring_encoded_pairs.append("{}={}".format(key, item))
|
| 195 |
+
return "&".join(querystring_encoded_pairs)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def _sign(key, msg):
|
| 199 |
+
"""Creates the HMAC-SHA256 hash of the provided message using the provided
|
| 200 |
+
key.
|
| 201 |
+
|
| 202 |
+
Args:
|
| 203 |
+
key (str): The HMAC-SHA256 key to use.
|
| 204 |
+
msg (str): The message to hash.
|
| 205 |
+
|
| 206 |
+
Returns:
|
| 207 |
+
str: The computed hash bytes.
|
| 208 |
+
"""
|
| 209 |
+
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def _get_signing_key(key, date_stamp, region_name, service_name):
|
| 213 |
+
"""Calculates the signing key used to calculate the signature for
|
| 214 |
+
AWS Signature Version 4 based on:
|
| 215 |
+
https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
|
| 216 |
+
|
| 217 |
+
Args:
|
| 218 |
+
key (str): The AWS secret access key.
|
| 219 |
+
date_stamp (str): The '%Y%m%d' date format.
|
| 220 |
+
region_name (str): The AWS region.
|
| 221 |
+
service_name (str): The AWS service name, eg. sts.
|
| 222 |
+
|
| 223 |
+
Returns:
|
| 224 |
+
str: The signing key bytes.
|
| 225 |
+
"""
|
| 226 |
+
k_date = _sign(("AWS4" + key).encode("utf-8"), date_stamp)
|
| 227 |
+
k_region = _sign(k_date, region_name)
|
| 228 |
+
k_service = _sign(k_region, service_name)
|
| 229 |
+
k_signing = _sign(k_service, "aws4_request")
|
| 230 |
+
return k_signing
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def _generate_authentication_header_map(
|
| 234 |
+
host,
|
| 235 |
+
canonical_uri,
|
| 236 |
+
canonical_querystring,
|
| 237 |
+
method,
|
| 238 |
+
region,
|
| 239 |
+
aws_security_credentials,
|
| 240 |
+
request_payload="",
|
| 241 |
+
additional_headers={},
|
| 242 |
+
):
|
| 243 |
+
"""Generates the authentication header map needed for generating the AWS
|
| 244 |
+
Signature Version 4 signed request.
|
| 245 |
+
|
| 246 |
+
Args:
|
| 247 |
+
host (str): The AWS service URL hostname.
|
| 248 |
+
canonical_uri (str): The AWS service URL path name.
|
| 249 |
+
canonical_querystring (str): The AWS service URL query string.
|
| 250 |
+
method (str): The HTTP method used to call this API.
|
| 251 |
+
region (str): The AWS region.
|
| 252 |
+
aws_security_credentials (AWSSecurityCredentials): The AWS security credentials.
|
| 253 |
+
request_payload (Optional[str]): The optional request payload if
|
| 254 |
+
available.
|
| 255 |
+
additional_headers (Optional[Mapping[str, str]]): The optional
|
| 256 |
+
additional headers needed for the requested AWS API.
|
| 257 |
+
|
| 258 |
+
Returns:
|
| 259 |
+
Mapping[str, str]: The AWS authentication header dictionary object.
|
| 260 |
+
This contains the x-amz-date and authorization header information.
|
| 261 |
+
"""
|
| 262 |
+
# iam.amazonaws.com host => iam service.
|
| 263 |
+
# sts.us-east-2.amazonaws.com host => sts service.
|
| 264 |
+
service_name = host.split(".")[0]
|
| 265 |
+
|
| 266 |
+
current_time = _helpers.utcnow()
|
| 267 |
+
amz_date = current_time.strftime("%Y%m%dT%H%M%SZ")
|
| 268 |
+
date_stamp = current_time.strftime("%Y%m%d")
|
| 269 |
+
|
| 270 |
+
# Change all additional headers to be lower case.
|
| 271 |
+
full_headers = {}
|
| 272 |
+
for key in additional_headers:
|
| 273 |
+
full_headers[key.lower()] = additional_headers[key]
|
| 274 |
+
# Add AWS session token if available.
|
| 275 |
+
if aws_security_credentials.session_token is not None:
|
| 276 |
+
full_headers[
|
| 277 |
+
_AWS_SECURITY_TOKEN_HEADER
|
| 278 |
+
] = aws_security_credentials.session_token
|
| 279 |
+
|
| 280 |
+
# Required headers
|
| 281 |
+
full_headers["host"] = host
|
| 282 |
+
# Do not use generated x-amz-date if the date header is provided.
|
| 283 |
+
# Previously the date was not fixed with x-amz- and could be provided
|
| 284 |
+
# manually.
|
| 285 |
+
# https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req
|
| 286 |
+
if "date" not in full_headers:
|
| 287 |
+
full_headers[_AWS_DATE_HEADER] = amz_date
|
| 288 |
+
|
| 289 |
+
# Header keys need to be sorted alphabetically.
|
| 290 |
+
canonical_headers = ""
|
| 291 |
+
header_keys = list(full_headers.keys())
|
| 292 |
+
header_keys.sort()
|
| 293 |
+
for key in header_keys:
|
| 294 |
+
canonical_headers = "{}{}:{}\n".format(
|
| 295 |
+
canonical_headers, key, full_headers[key]
|
| 296 |
+
)
|
| 297 |
+
signed_headers = ";".join(header_keys)
|
| 298 |
+
|
| 299 |
+
payload_hash = hashlib.sha256((request_payload or "").encode("utf-8")).hexdigest()
|
| 300 |
+
|
| 301 |
+
# https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
| 302 |
+
canonical_request = "{}\n{}\n{}\n{}\n{}\n{}".format(
|
| 303 |
+
method,
|
| 304 |
+
canonical_uri,
|
| 305 |
+
canonical_querystring,
|
| 306 |
+
canonical_headers,
|
| 307 |
+
signed_headers,
|
| 308 |
+
payload_hash,
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
credential_scope = "{}/{}/{}/{}".format(
|
| 312 |
+
date_stamp, region, service_name, _AWS_REQUEST_TYPE
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
# https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
|
| 316 |
+
string_to_sign = "{}\n{}\n{}\n{}".format(
|
| 317 |
+
_AWS_ALGORITHM,
|
| 318 |
+
amz_date,
|
| 319 |
+
credential_scope,
|
| 320 |
+
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest(),
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
# https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
|
| 324 |
+
signing_key = _get_signing_key(
|
| 325 |
+
aws_security_credentials.secret_access_key, date_stamp, region, service_name
|
| 326 |
+
)
|
| 327 |
+
signature = hmac.new(
|
| 328 |
+
signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
|
| 329 |
+
).hexdigest()
|
| 330 |
+
|
| 331 |
+
# https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
|
| 332 |
+
authorization_header = "{} Credential={}/{}, SignedHeaders={}, Signature={}".format(
|
| 333 |
+
_AWS_ALGORITHM,
|
| 334 |
+
aws_security_credentials.access_key_id,
|
| 335 |
+
credential_scope,
|
| 336 |
+
signed_headers,
|
| 337 |
+
signature,
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
authentication_header = {"authorization_header": authorization_header}
|
| 341 |
+
# Do not use generated x-amz-date if the date header is provided.
|
| 342 |
+
if "date" not in full_headers:
|
| 343 |
+
authentication_header["amz_date"] = amz_date
|
| 344 |
+
return authentication_header
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
@dataclass
|
| 348 |
+
class AwsSecurityCredentials:
|
| 349 |
+
"""A class that models AWS security credentials with an optional session token.
|
| 350 |
+
|
| 351 |
+
Attributes:
|
| 352 |
+
access_key_id (str): The AWS security credentials access key id.
|
| 353 |
+
secret_access_key (str): The AWS security credentials secret access key.
|
| 354 |
+
session_token (Optional[str]): The optional AWS security credentials session token. This should be set when using temporary credentials.
|
| 355 |
+
"""
|
| 356 |
+
|
| 357 |
+
access_key_id: str
|
| 358 |
+
secret_access_key: str
|
| 359 |
+
session_token: Optional[str] = None
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
class AwsSecurityCredentialsSupplier(metaclass=abc.ABCMeta):
|
| 363 |
+
"""Base class for AWS security credential suppliers. This can be implemented with custom logic to retrieve
|
| 364 |
+
AWS security credentials to exchange for a Google Cloud access token. The AWS external account credential does
|
| 365 |
+
not cache the AWS security credentials, so caching logic should be added in the implementation.
|
| 366 |
+
"""
|
| 367 |
+
|
| 368 |
+
@abc.abstractmethod
|
| 369 |
+
def get_aws_security_credentials(self, context, request):
|
| 370 |
+
"""Returns the AWS security credentials for the requested context.
|
| 371 |
+
|
| 372 |
+
.. warning: This is not cached by the calling Google credential, so caching logic should be implemented in the supplier.
|
| 373 |
+
|
| 374 |
+
Args:
|
| 375 |
+
context (google.auth.externalaccount.SupplierContext): The context object
|
| 376 |
+
containing information about the requested audience and subject token type.
|
| 377 |
+
request (google.auth.transport.Request): The object used to make
|
| 378 |
+
HTTP requests.
|
| 379 |
+
|
| 380 |
+
Raises:
|
| 381 |
+
google.auth.exceptions.RefreshError: If an error is encountered during
|
| 382 |
+
security credential retrieval logic.
|
| 383 |
+
|
| 384 |
+
Returns:
|
| 385 |
+
AwsSecurityCredentials: The requested AWS security credentials.
|
| 386 |
+
"""
|
| 387 |
+
raise NotImplementedError("")
|
| 388 |
+
|
| 389 |
+
@abc.abstractmethod
|
| 390 |
+
def get_aws_region(self, context, request):
|
| 391 |
+
"""Returns the AWS region for the requested context.
|
| 392 |
+
|
| 393 |
+
Args:
|
| 394 |
+
context (google.auth.externalaccount.SupplierContext): The context object
|
| 395 |
+
containing information about the requested audience and subject token type.
|
| 396 |
+
request (google.auth.transport.Request): The object used to make
|
| 397 |
+
HTTP requests.
|
| 398 |
+
|
| 399 |
+
Raises:
|
| 400 |
+
google.auth.exceptions.RefreshError: If an error is encountered during
|
| 401 |
+
region retrieval logic.
|
| 402 |
+
|
| 403 |
+
Returns:
|
| 404 |
+
str: The AWS region.
|
| 405 |
+
"""
|
| 406 |
+
raise NotImplementedError("")
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
class _DefaultAwsSecurityCredentialsSupplier(AwsSecurityCredentialsSupplier):
|
| 410 |
+
"""Default implementation of AWS security credentials supplier. Supports retrieving
|
| 411 |
+
credentials and region via EC2 metadata endpoints and environment variables.
|
| 412 |
+
"""
|
| 413 |
+
|
| 414 |
+
def __init__(self, credential_source):
|
| 415 |
+
self._region_url = credential_source.get("region_url")
|
| 416 |
+
self._security_credentials_url = credential_source.get("url")
|
| 417 |
+
self._imdsv2_session_token_url = credential_source.get(
|
| 418 |
+
"imdsv2_session_token_url"
|
| 419 |
+
)
|
| 420 |
+
|
| 421 |
+
@_helpers.copy_docstring(AwsSecurityCredentialsSupplier)
|
| 422 |
+
def get_aws_security_credentials(self, context, request):
|
| 423 |
+
|
| 424 |
+
# Check environment variables for permanent credentials first.
|
| 425 |
+
# https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
|
| 426 |
+
env_aws_access_key_id = os.environ.get(environment_vars.AWS_ACCESS_KEY_ID)
|
| 427 |
+
env_aws_secret_access_key = os.environ.get(
|
| 428 |
+
environment_vars.AWS_SECRET_ACCESS_KEY
|
| 429 |
+
)
|
| 430 |
+
# This is normally not available for permanent credentials.
|
| 431 |
+
env_aws_session_token = os.environ.get(environment_vars.AWS_SESSION_TOKEN)
|
| 432 |
+
if env_aws_access_key_id and env_aws_secret_access_key:
|
| 433 |
+
return AwsSecurityCredentials(
|
| 434 |
+
env_aws_access_key_id, env_aws_secret_access_key, env_aws_session_token
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
imdsv2_session_token = self._get_imdsv2_session_token(request)
|
| 438 |
+
role_name = self._get_metadata_role_name(request, imdsv2_session_token)
|
| 439 |
+
|
| 440 |
+
# Get security credentials.
|
| 441 |
+
credentials = self._get_metadata_security_credentials(
|
| 442 |
+
request, role_name, imdsv2_session_token
|
| 443 |
+
)
|
| 444 |
+
|
| 445 |
+
return AwsSecurityCredentials(
|
| 446 |
+
credentials.get("AccessKeyId"),
|
| 447 |
+
credentials.get("SecretAccessKey"),
|
| 448 |
+
credentials.get("Token"),
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
@_helpers.copy_docstring(AwsSecurityCredentialsSupplier)
|
| 452 |
+
def get_aws_region(self, context, request):
|
| 453 |
+
# The AWS metadata server is not available in some AWS environments
|
| 454 |
+
# such as AWS lambda. Instead, it is available via environment
|
| 455 |
+
# variable.
|
| 456 |
+
env_aws_region = os.environ.get(environment_vars.AWS_REGION)
|
| 457 |
+
if env_aws_region is not None:
|
| 458 |
+
return env_aws_region
|
| 459 |
+
|
| 460 |
+
env_aws_region = os.environ.get(environment_vars.AWS_DEFAULT_REGION)
|
| 461 |
+
if env_aws_region is not None:
|
| 462 |
+
return env_aws_region
|
| 463 |
+
|
| 464 |
+
if not self._region_url:
|
| 465 |
+
raise exceptions.RefreshError("Unable to determine AWS region")
|
| 466 |
+
|
| 467 |
+
headers = None
|
| 468 |
+
imdsv2_session_token = self._get_imdsv2_session_token(request)
|
| 469 |
+
if imdsv2_session_token is not None:
|
| 470 |
+
headers = {"X-aws-ec2-metadata-token": imdsv2_session_token}
|
| 471 |
+
|
| 472 |
+
response = request(url=self._region_url, method="GET", headers=headers)
|
| 473 |
+
|
| 474 |
+
# Support both string and bytes type response.data.
|
| 475 |
+
response_body = (
|
| 476 |
+
response.data.decode("utf-8")
|
| 477 |
+
if hasattr(response.data, "decode")
|
| 478 |
+
else response.data
|
| 479 |
+
)
|
| 480 |
+
|
| 481 |
+
if response.status != http_client.OK:
|
| 482 |
+
raise exceptions.RefreshError(
|
| 483 |
+
"Unable to retrieve AWS region: {}".format(response_body)
|
| 484 |
+
)
|
| 485 |
+
|
| 486 |
+
# This endpoint will return the region in format: us-east-2b.
|
| 487 |
+
# Only the us-east-2 part should be used.
|
| 488 |
+
return response_body[:-1]
|
| 489 |
+
|
| 490 |
+
def _get_imdsv2_session_token(self, request):
|
| 491 |
+
if request is not None and self._imdsv2_session_token_url is not None:
|
| 492 |
+
headers = {
|
| 493 |
+
"X-aws-ec2-metadata-token-ttl-seconds": _IMDSV2_SESSION_TOKEN_TTL_SECONDS
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
imdsv2_session_token_response = request(
|
| 497 |
+
url=self._imdsv2_session_token_url, method="PUT", headers=headers
|
| 498 |
+
)
|
| 499 |
+
|
| 500 |
+
if imdsv2_session_token_response.status != http_client.OK:
|
| 501 |
+
raise exceptions.RefreshError(
|
| 502 |
+
"Unable to retrieve AWS Session Token: {}".format(
|
| 503 |
+
imdsv2_session_token_response.data
|
| 504 |
+
)
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
return imdsv2_session_token_response.data
|
| 508 |
+
else:
|
| 509 |
+
return None
|
| 510 |
+
|
| 511 |
+
def _get_metadata_security_credentials(
|
| 512 |
+
self, request, role_name, imdsv2_session_token
|
| 513 |
+
):
|
| 514 |
+
"""Retrieves the AWS security credentials required for signing AWS
|
| 515 |
+
requests from the AWS metadata server.
|
| 516 |
+
|
| 517 |
+
Args:
|
| 518 |
+
request (google.auth.transport.Request): A callable used to make
|
| 519 |
+
HTTP requests.
|
| 520 |
+
role_name (str): The AWS role name required by the AWS metadata
|
| 521 |
+
server security_credentials endpoint in order to return the
|
| 522 |
+
credentials.
|
| 523 |
+
imdsv2_session_token (str): The AWS IMDSv2 session token to be added as a
|
| 524 |
+
header in the requests to AWS metadata endpoint.
|
| 525 |
+
|
| 526 |
+
Returns:
|
| 527 |
+
Mapping[str, str]: The AWS metadata server security credentials
|
| 528 |
+
response.
|
| 529 |
+
|
| 530 |
+
Raises:
|
| 531 |
+
google.auth.exceptions.RefreshError: If an error occurs while
|
| 532 |
+
retrieving the AWS security credentials.
|
| 533 |
+
"""
|
| 534 |
+
headers = {"Content-Type": "application/json"}
|
| 535 |
+
if imdsv2_session_token is not None:
|
| 536 |
+
headers["X-aws-ec2-metadata-token"] = imdsv2_session_token
|
| 537 |
+
|
| 538 |
+
response = request(
|
| 539 |
+
url="{}/{}".format(self._security_credentials_url, role_name),
|
| 540 |
+
method="GET",
|
| 541 |
+
headers=headers,
|
| 542 |
+
)
|
| 543 |
+
|
| 544 |
+
# support both string and bytes type response.data
|
| 545 |
+
response_body = (
|
| 546 |
+
response.data.decode("utf-8")
|
| 547 |
+
if hasattr(response.data, "decode")
|
| 548 |
+
else response.data
|
| 549 |
+
)
|
| 550 |
+
|
| 551 |
+
if response.status != http_client.OK:
|
| 552 |
+
raise exceptions.RefreshError(
|
| 553 |
+
"Unable to retrieve AWS security credentials: {}".format(response_body)
|
| 554 |
+
)
|
| 555 |
+
|
| 556 |
+
credentials_response = json.loads(response_body)
|
| 557 |
+
|
| 558 |
+
return credentials_response
|
| 559 |
+
|
| 560 |
+
def _get_metadata_role_name(self, request, imdsv2_session_token):
|
| 561 |
+
"""Retrieves the AWS role currently attached to the current AWS
|
| 562 |
+
workload by querying the AWS metadata server. This is needed for the
|
| 563 |
+
AWS metadata server security credentials endpoint in order to retrieve
|
| 564 |
+
the AWS security credentials needed to sign requests to AWS APIs.
|
| 565 |
+
|
| 566 |
+
Args:
|
| 567 |
+
request (google.auth.transport.Request): A callable used to make
|
| 568 |
+
HTTP requests.
|
| 569 |
+
imdsv2_session_token (str): The AWS IMDSv2 session token to be added as a
|
| 570 |
+
header in the requests to AWS metadata endpoint.
|
| 571 |
+
|
| 572 |
+
Returns:
|
| 573 |
+
str: The AWS role name.
|
| 574 |
+
|
| 575 |
+
Raises:
|
| 576 |
+
google.auth.exceptions.RefreshError: If an error occurs while
|
| 577 |
+
retrieving the AWS role name.
|
| 578 |
+
"""
|
| 579 |
+
if self._security_credentials_url is None:
|
| 580 |
+
raise exceptions.RefreshError(
|
| 581 |
+
"Unable to determine the AWS metadata server security credentials endpoint"
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
headers = None
|
| 585 |
+
if imdsv2_session_token is not None:
|
| 586 |
+
headers = {"X-aws-ec2-metadata-token": imdsv2_session_token}
|
| 587 |
+
|
| 588 |
+
response = request(
|
| 589 |
+
url=self._security_credentials_url, method="GET", headers=headers
|
| 590 |
+
)
|
| 591 |
+
|
| 592 |
+
# support both string and bytes type response.data
|
| 593 |
+
response_body = (
|
| 594 |
+
response.data.decode("utf-8")
|
| 595 |
+
if hasattr(response.data, "decode")
|
| 596 |
+
else response.data
|
| 597 |
+
)
|
| 598 |
+
|
| 599 |
+
if response.status != http_client.OK:
|
| 600 |
+
raise exceptions.RefreshError(
|
| 601 |
+
"Unable to retrieve AWS role name {}".format(response_body)
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
+
return response_body
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
class Credentials(external_account.Credentials):
|
| 608 |
+
"""AWS external account credentials.
|
| 609 |
+
This is used to exchange serialized AWS signature v4 signed requests to
|
| 610 |
+
AWS STS GetCallerIdentity service for Google access tokens.
|
| 611 |
+
"""
|
| 612 |
+
|
| 613 |
+
def __init__(
|
| 614 |
+
self,
|
| 615 |
+
audience,
|
| 616 |
+
subject_token_type,
|
| 617 |
+
token_url=external_account._DEFAULT_TOKEN_URL,
|
| 618 |
+
credential_source=None,
|
| 619 |
+
aws_security_credentials_supplier=None,
|
| 620 |
+
*args,
|
| 621 |
+
**kwargs
|
| 622 |
+
):
|
| 623 |
+
"""Instantiates an AWS workload external account credentials object.
|
| 624 |
+
|
| 625 |
+
Args:
|
| 626 |
+
audience (str): The STS audience field.
|
| 627 |
+
subject_token_type (str): The subject token type based on the Oauth2.0 token exchange spec.
|
| 628 |
+
Expected values include::
|
| 629 |
+
|
| 630 |
+
“urn:ietf:params:aws:token-type:aws4_request”
|
| 631 |
+
|
| 632 |
+
token_url (Optional [str]): The STS endpoint URL. If not provided, will default to "https://sts.googleapis.com/v1/token".
|
| 633 |
+
credential_source (Optional [Mapping]): The credential source dictionary used
|
| 634 |
+
to provide instructions on how to retrieve external credential to be exchanged for Google access tokens.
|
| 635 |
+
Either a credential source or an AWS security credentials supplier must be provided.
|
| 636 |
+
|
| 637 |
+
Example credential_source for AWS credential::
|
| 638 |
+
|
| 639 |
+
{
|
| 640 |
+
"environment_id": "aws1",
|
| 641 |
+
"regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
|
| 642 |
+
"region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone",
|
| 643 |
+
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials",
|
| 644 |
+
imdsv2_session_token_url": "http://169.254.169.254/latest/api/token"
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
aws_security_credentials_supplier (Optional [AwsSecurityCredentialsSupplier]): Optional AWS security credentials supplier.
|
| 648 |
+
This will be called to supply valid AWS security credentails which will then
|
| 649 |
+
be exchanged for Google access tokens. Either an AWS security credentials supplier
|
| 650 |
+
or a credential source must be provided.
|
| 651 |
+
args (List): Optional positional arguments passed into the underlying :meth:`~external_account.Credentials.__init__` method.
|
| 652 |
+
kwargs (Mapping): Optional keyword arguments passed into the underlying :meth:`~external_account.Credentials.__init__` method.
|
| 653 |
+
|
| 654 |
+
Raises:
|
| 655 |
+
google.auth.exceptions.RefreshError: If an error is encountered during
|
| 656 |
+
access token retrieval logic.
|
| 657 |
+
ValueError: For invalid parameters.
|
| 658 |
+
|
| 659 |
+
.. note:: Typically one of the helper constructors
|
| 660 |
+
:meth:`from_file` or
|
| 661 |
+
:meth:`from_info` are used instead of calling the constructor directly.
|
| 662 |
+
"""
|
| 663 |
+
super(Credentials, self).__init__(
|
| 664 |
+
audience=audience,
|
| 665 |
+
subject_token_type=subject_token_type,
|
| 666 |
+
token_url=token_url,
|
| 667 |
+
credential_source=credential_source,
|
| 668 |
+
*args,
|
| 669 |
+
**kwargs
|
| 670 |
+
)
|
| 671 |
+
if credential_source is None and aws_security_credentials_supplier is None:
|
| 672 |
+
raise exceptions.InvalidValue(
|
| 673 |
+
"A valid credential source or AWS security credentials supplier must be provided."
|
| 674 |
+
)
|
| 675 |
+
if (
|
| 676 |
+
credential_source is not None
|
| 677 |
+
and aws_security_credentials_supplier is not None
|
| 678 |
+
):
|
| 679 |
+
raise exceptions.InvalidValue(
|
| 680 |
+
"AWS credential cannot have both a credential source and an AWS security credentials supplier."
|
| 681 |
+
)
|
| 682 |
+
|
| 683 |
+
if aws_security_credentials_supplier:
|
| 684 |
+
self._aws_security_credentials_supplier = aws_security_credentials_supplier
|
| 685 |
+
# The regional cred verification URL would normally be provided through the credential source. So set it to the default one here.
|
| 686 |
+
self._cred_verification_url = (
|
| 687 |
+
_DEFAULT_AWS_REGIONAL_CREDENTIAL_VERIFICATION_URL
|
| 688 |
+
)
|
| 689 |
+
else:
|
| 690 |
+
environment_id = credential_source.get("environment_id") or ""
|
| 691 |
+
self._aws_security_credentials_supplier = _DefaultAwsSecurityCredentialsSupplier(
|
| 692 |
+
credential_source
|
| 693 |
+
)
|
| 694 |
+
self._cred_verification_url = credential_source.get(
|
| 695 |
+
"regional_cred_verification_url"
|
| 696 |
+
)
|
| 697 |
+
|
| 698 |
+
# Get the environment ID, i.e. "aws1". Currently, only one version supported (1).
|
| 699 |
+
matches = re.match(r"^(aws)([\d]+)$", environment_id)
|
| 700 |
+
if matches:
|
| 701 |
+
env_id, env_version = matches.groups()
|
| 702 |
+
else:
|
| 703 |
+
env_id, env_version = (None, None)
|
| 704 |
+
|
| 705 |
+
if env_id != "aws" or self._cred_verification_url is None:
|
| 706 |
+
raise exceptions.InvalidResource(
|
| 707 |
+
"No valid AWS 'credential_source' provided"
|
| 708 |
+
)
|
| 709 |
+
elif env_version is None or int(env_version) != 1:
|
| 710 |
+
raise exceptions.InvalidValue(
|
| 711 |
+
"aws version '{}' is not supported in the current build.".format(
|
| 712 |
+
env_version
|
| 713 |
+
)
|
| 714 |
+
)
|
| 715 |
+
|
| 716 |
+
self._target_resource = audience
|
| 717 |
+
self._request_signer = None
|
| 718 |
+
|
| 719 |
+
def retrieve_subject_token(self, request):
|
| 720 |
+
"""Retrieves the subject token using the credential_source object.
|
| 721 |
+
The subject token is a serialized `AWS GetCallerIdentity signed request`_.
|
| 722 |
+
|
| 723 |
+
The logic is summarized as:
|
| 724 |
+
|
| 725 |
+
Retrieve the AWS region from the AWS_REGION or AWS_DEFAULT_REGION
|
| 726 |
+
environment variable or from the AWS metadata server availability-zone
|
| 727 |
+
if not found in the environment variable.
|
| 728 |
+
|
| 729 |
+
Check AWS credentials in environment variables. If not found, retrieve
|
| 730 |
+
from the AWS metadata server security-credentials endpoint.
|
| 731 |
+
|
| 732 |
+
When retrieving AWS credentials from the metadata server
|
| 733 |
+
security-credentials endpoint, the AWS role needs to be determined by
|
| 734 |
+
calling the security-credentials endpoint without any argument. Then the
|
| 735 |
+
credentials can be retrieved via: security-credentials/role_name
|
| 736 |
+
|
| 737 |
+
Generate the signed request to AWS STS GetCallerIdentity action.
|
| 738 |
+
|
| 739 |
+
Inject x-goog-cloud-target-resource into header and serialize the
|
| 740 |
+
signed request. This will be the subject-token to pass to GCP STS.
|
| 741 |
+
|
| 742 |
+
.. _AWS GetCallerIdentity signed request:
|
| 743 |
+
https://cloud.google.com/iam/docs/access-resources-aws#exchange-token
|
| 744 |
+
|
| 745 |
+
Args:
|
| 746 |
+
request (google.auth.transport.Request): A callable used to make
|
| 747 |
+
HTTP requests.
|
| 748 |
+
Returns:
|
| 749 |
+
str: The retrieved subject token.
|
| 750 |
+
"""
|
| 751 |
+
|
| 752 |
+
# Initialize the request signer if not yet initialized after determining
|
| 753 |
+
# the current AWS region.
|
| 754 |
+
if self._request_signer is None:
|
| 755 |
+
self._region = self._aws_security_credentials_supplier.get_aws_region(
|
| 756 |
+
self._supplier_context, request
|
| 757 |
+
)
|
| 758 |
+
self._request_signer = RequestSigner(self._region)
|
| 759 |
+
|
| 760 |
+
# Retrieve the AWS security credentials needed to generate the signed
|
| 761 |
+
# request.
|
| 762 |
+
aws_security_credentials = self._aws_security_credentials_supplier.get_aws_security_credentials(
|
| 763 |
+
self._supplier_context, request
|
| 764 |
+
)
|
| 765 |
+
# Generate the signed request to AWS STS GetCallerIdentity API.
|
| 766 |
+
# Use the required regional endpoint. Otherwise, the request will fail.
|
| 767 |
+
request_options = self._request_signer.get_request_options(
|
| 768 |
+
aws_security_credentials,
|
| 769 |
+
self._cred_verification_url.replace("{region}", self._region),
|
| 770 |
+
"POST",
|
| 771 |
+
)
|
| 772 |
+
# The GCP STS endpoint expects the headers to be formatted as:
|
| 773 |
+
# [
|
| 774 |
+
# {key: 'x-amz-date', value: '...'},
|
| 775 |
+
# {key: 'Authorization', value: '...'},
|
| 776 |
+
# ...
|
| 777 |
+
# ]
|
| 778 |
+
# And then serialized as:
|
| 779 |
+
# quote(json.dumps({
|
| 780 |
+
# url: '...',
|
| 781 |
+
# method: 'POST',
|
| 782 |
+
# headers: [{key: 'x-amz-date', value: '...'}, ...]
|
| 783 |
+
# }))
|
| 784 |
+
request_headers = request_options.get("headers")
|
| 785 |
+
# The full, canonical resource name of the workload identity pool
|
| 786 |
+
# provider, with or without the HTTPS prefix.
|
| 787 |
+
# Including this header as part of the signature is recommended to
|
| 788 |
+
# ensure data integrity.
|
| 789 |
+
request_headers["x-goog-cloud-target-resource"] = self._target_resource
|
| 790 |
+
|
| 791 |
+
# Serialize AWS signed request.
|
| 792 |
+
aws_signed_req = {}
|
| 793 |
+
aws_signed_req["url"] = request_options.get("url")
|
| 794 |
+
aws_signed_req["method"] = request_options.get("method")
|
| 795 |
+
aws_signed_req["headers"] = []
|
| 796 |
+
# Reformat header to GCP STS expected format.
|
| 797 |
+
for key in request_headers.keys():
|
| 798 |
+
aws_signed_req["headers"].append(
|
| 799 |
+
{"key": key, "value": request_headers[key]}
|
| 800 |
+
)
|
| 801 |
+
|
| 802 |
+
return urllib.parse.quote(
|
| 803 |
+
json.dumps(aws_signed_req, separators=(",", ":"), sort_keys=True)
|
| 804 |
+
)
|
| 805 |
+
|
| 806 |
+
def _create_default_metrics_options(self):
|
| 807 |
+
metrics_options = super(Credentials, self)._create_default_metrics_options()
|
| 808 |
+
metrics_options["source"] = "aws"
|
| 809 |
+
if self._has_custom_supplier():
|
| 810 |
+
metrics_options["source"] = "programmatic"
|
| 811 |
+
return metrics_options
|
| 812 |
+
|
| 813 |
+
def _has_custom_supplier(self):
|
| 814 |
+
return self._credential_source is None
|
| 815 |
+
|
| 816 |
+
def _constructor_args(self):
|
| 817 |
+
args = super(Credentials, self)._constructor_args()
|
| 818 |
+
# If a custom supplier was used, append it to the args dict.
|
| 819 |
+
if self._has_custom_supplier():
|
| 820 |
+
args.update(
|
| 821 |
+
{
|
| 822 |
+
"aws_security_credentials_supplier": self._aws_security_credentials_supplier
|
| 823 |
+
}
|
| 824 |
+
)
|
| 825 |
+
return args
|
| 826 |
+
|
| 827 |
+
@classmethod
|
| 828 |
+
def from_info(cls, info, **kwargs):
|
| 829 |
+
"""Creates an AWS Credentials instance from parsed external account info.
|
| 830 |
+
|
| 831 |
+
Args:
|
| 832 |
+
info (Mapping[str, str]): The AWS external account info in Google
|
| 833 |
+
format.
|
| 834 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 835 |
+
|
| 836 |
+
Returns:
|
| 837 |
+
google.auth.aws.Credentials: The constructed credentials.
|
| 838 |
+
|
| 839 |
+
Raises:
|
| 840 |
+
ValueError: For invalid parameters.
|
| 841 |
+
"""
|
| 842 |
+
aws_security_credentials_supplier = info.get(
|
| 843 |
+
"aws_security_credentials_supplier"
|
| 844 |
+
)
|
| 845 |
+
kwargs.update(
|
| 846 |
+
{"aws_security_credentials_supplier": aws_security_credentials_supplier}
|
| 847 |
+
)
|
| 848 |
+
return super(Credentials, cls).from_info(info, **kwargs)
|
| 849 |
+
|
| 850 |
+
@classmethod
|
| 851 |
+
def from_file(cls, filename, **kwargs):
|
| 852 |
+
"""Creates an AWS Credentials instance from an external account json file.
|
| 853 |
+
|
| 854 |
+
Args:
|
| 855 |
+
filename (str): The path to the AWS external account json file.
|
| 856 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 857 |
+
|
| 858 |
+
Returns:
|
| 859 |
+
google.auth.aws.Credentials: The constructed credentials.
|
| 860 |
+
"""
|
| 861 |
+
return super(Credentials, cls).from_file(filename, **kwargs)
|
lib/python3.10/site-packages/google/auth/compute_engine/_metadata.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 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 |
+
"""Provides helper methods for talking to the Compute Engine metadata server.
|
| 16 |
+
|
| 17 |
+
See https://cloud.google.com/compute/docs/metadata for more details.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import datetime
|
| 21 |
+
import http.client as http_client
|
| 22 |
+
import json
|
| 23 |
+
import logging
|
| 24 |
+
import os
|
| 25 |
+
from urllib.parse import urljoin
|
| 26 |
+
|
| 27 |
+
from google.auth import _helpers
|
| 28 |
+
from google.auth import environment_vars
|
| 29 |
+
from google.auth import exceptions
|
| 30 |
+
from google.auth import metrics
|
| 31 |
+
from google.auth import transport
|
| 32 |
+
from google.auth._exponential_backoff import ExponentialBackoff
|
| 33 |
+
|
| 34 |
+
_LOGGER = logging.getLogger(__name__)
|
| 35 |
+
|
| 36 |
+
# Environment variable GCE_METADATA_HOST is originally named
|
| 37 |
+
# GCE_METADATA_ROOT. For compatibility reasons, here it checks
|
| 38 |
+
# the new variable first; if not set, the system falls back
|
| 39 |
+
# to the old variable.
|
| 40 |
+
_GCE_METADATA_HOST = os.getenv(environment_vars.GCE_METADATA_HOST, None)
|
| 41 |
+
if not _GCE_METADATA_HOST:
|
| 42 |
+
_GCE_METADATA_HOST = os.getenv(
|
| 43 |
+
environment_vars.GCE_METADATA_ROOT, "metadata.google.internal"
|
| 44 |
+
)
|
| 45 |
+
_METADATA_ROOT = "http://{}/computeMetadata/v1/".format(_GCE_METADATA_HOST)
|
| 46 |
+
|
| 47 |
+
# This is used to ping the metadata server, it avoids the cost of a DNS
|
| 48 |
+
# lookup.
|
| 49 |
+
_METADATA_IP_ROOT = "http://{}".format(
|
| 50 |
+
os.getenv(environment_vars.GCE_METADATA_IP, "169.254.169.254")
|
| 51 |
+
)
|
| 52 |
+
_METADATA_FLAVOR_HEADER = "metadata-flavor"
|
| 53 |
+
_METADATA_FLAVOR_VALUE = "Google"
|
| 54 |
+
_METADATA_HEADERS = {_METADATA_FLAVOR_HEADER: _METADATA_FLAVOR_VALUE}
|
| 55 |
+
|
| 56 |
+
# Timeout in seconds to wait for the GCE metadata server when detecting the
|
| 57 |
+
# GCE environment.
|
| 58 |
+
try:
|
| 59 |
+
_METADATA_DEFAULT_TIMEOUT = int(os.getenv("GCE_METADATA_TIMEOUT", 3))
|
| 60 |
+
except ValueError: # pragma: NO COVER
|
| 61 |
+
_METADATA_DEFAULT_TIMEOUT = 3
|
| 62 |
+
|
| 63 |
+
# Detect GCE Residency
|
| 64 |
+
_GOOGLE = "Google"
|
| 65 |
+
_GCE_PRODUCT_NAME_FILE = "/sys/class/dmi/id/product_name"
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def is_on_gce(request):
|
| 69 |
+
"""Checks to see if the code runs on Google Compute Engine
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
request (google.auth.transport.Request): A callable used to make
|
| 73 |
+
HTTP requests.
|
| 74 |
+
|
| 75 |
+
Returns:
|
| 76 |
+
bool: True if the code runs on Google Compute Engine, False otherwise.
|
| 77 |
+
"""
|
| 78 |
+
if ping(request):
|
| 79 |
+
return True
|
| 80 |
+
|
| 81 |
+
if os.name == "nt":
|
| 82 |
+
# TODO: implement GCE residency detection on Windows
|
| 83 |
+
return False
|
| 84 |
+
|
| 85 |
+
# Detect GCE residency on Linux
|
| 86 |
+
return detect_gce_residency_linux()
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def detect_gce_residency_linux():
|
| 90 |
+
"""Detect Google Compute Engine residency by smbios check on Linux
|
| 91 |
+
|
| 92 |
+
Returns:
|
| 93 |
+
bool: True if the GCE product name file is detected, False otherwise.
|
| 94 |
+
"""
|
| 95 |
+
try:
|
| 96 |
+
with open(_GCE_PRODUCT_NAME_FILE, "r") as file_obj:
|
| 97 |
+
content = file_obj.read().strip()
|
| 98 |
+
|
| 99 |
+
except Exception:
|
| 100 |
+
return False
|
| 101 |
+
|
| 102 |
+
return content.startswith(_GOOGLE)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3):
|
| 106 |
+
"""Checks to see if the metadata server is available.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
request (google.auth.transport.Request): A callable used to make
|
| 110 |
+
HTTP requests.
|
| 111 |
+
timeout (int): How long to wait for the metadata server to respond.
|
| 112 |
+
retry_count (int): How many times to attempt connecting to metadata
|
| 113 |
+
server using above timeout.
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
bool: True if the metadata server is reachable, False otherwise.
|
| 117 |
+
"""
|
| 118 |
+
# NOTE: The explicit ``timeout`` is a workaround. The underlying
|
| 119 |
+
# issue is that resolving an unknown host on some networks will take
|
| 120 |
+
# 20-30 seconds; making this timeout short fixes the issue, but
|
| 121 |
+
# could lead to false negatives in the event that we are on GCE, but
|
| 122 |
+
# the metadata resolution was particularly slow. The latter case is
|
| 123 |
+
# "unlikely".
|
| 124 |
+
headers = _METADATA_HEADERS.copy()
|
| 125 |
+
headers[metrics.API_CLIENT_HEADER] = metrics.mds_ping()
|
| 126 |
+
|
| 127 |
+
backoff = ExponentialBackoff(total_attempts=retry_count)
|
| 128 |
+
|
| 129 |
+
for attempt in backoff:
|
| 130 |
+
try:
|
| 131 |
+
response = request(
|
| 132 |
+
url=_METADATA_IP_ROOT, method="GET", headers=headers, timeout=timeout
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
metadata_flavor = response.headers.get(_METADATA_FLAVOR_HEADER)
|
| 136 |
+
return (
|
| 137 |
+
response.status == http_client.OK
|
| 138 |
+
and metadata_flavor == _METADATA_FLAVOR_VALUE
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
except exceptions.TransportError as e:
|
| 142 |
+
_LOGGER.warning(
|
| 143 |
+
"Compute Engine Metadata server unavailable on "
|
| 144 |
+
"attempt %s of %s. Reason: %s",
|
| 145 |
+
attempt,
|
| 146 |
+
retry_count,
|
| 147 |
+
e,
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
return False
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def get(
|
| 154 |
+
request,
|
| 155 |
+
path,
|
| 156 |
+
root=_METADATA_ROOT,
|
| 157 |
+
params=None,
|
| 158 |
+
recursive=False,
|
| 159 |
+
retry_count=5,
|
| 160 |
+
headers=None,
|
| 161 |
+
return_none_for_not_found_error=False,
|
| 162 |
+
timeout=_METADATA_DEFAULT_TIMEOUT,
|
| 163 |
+
):
|
| 164 |
+
"""Fetch a resource from the metadata server.
|
| 165 |
+
|
| 166 |
+
Args:
|
| 167 |
+
request (google.auth.transport.Request): A callable used to make
|
| 168 |
+
HTTP requests.
|
| 169 |
+
path (str): The resource to retrieve. For example,
|
| 170 |
+
``'instance/service-accounts/default'``.
|
| 171 |
+
root (str): The full path to the metadata server root.
|
| 172 |
+
params (Optional[Mapping[str, str]]): A mapping of query parameter
|
| 173 |
+
keys to values.
|
| 174 |
+
recursive (bool): Whether to do a recursive query of metadata. See
|
| 175 |
+
https://cloud.google.com/compute/docs/metadata#aggcontents for more
|
| 176 |
+
details.
|
| 177 |
+
retry_count (int): How many times to attempt connecting to metadata
|
| 178 |
+
server using above timeout.
|
| 179 |
+
headers (Optional[Mapping[str, str]]): Headers for the request.
|
| 180 |
+
return_none_for_not_found_error (Optional[bool]): If True, returns None
|
| 181 |
+
for 404 error instead of throwing an exception.
|
| 182 |
+
timeout (int): How long to wait, in seconds for the metadata server to respond.
|
| 183 |
+
|
| 184 |
+
Returns:
|
| 185 |
+
Union[Mapping, str]: If the metadata server returns JSON, a mapping of
|
| 186 |
+
the decoded JSON is returned. Otherwise, the response content is
|
| 187 |
+
returned as a string.
|
| 188 |
+
|
| 189 |
+
Raises:
|
| 190 |
+
google.auth.exceptions.TransportError: if an error occurred while
|
| 191 |
+
retrieving metadata.
|
| 192 |
+
"""
|
| 193 |
+
base_url = urljoin(root, path)
|
| 194 |
+
query_params = {} if params is None else params
|
| 195 |
+
|
| 196 |
+
headers_to_use = _METADATA_HEADERS.copy()
|
| 197 |
+
if headers:
|
| 198 |
+
headers_to_use.update(headers)
|
| 199 |
+
|
| 200 |
+
if recursive:
|
| 201 |
+
query_params["recursive"] = "true"
|
| 202 |
+
|
| 203 |
+
url = _helpers.update_query(base_url, query_params)
|
| 204 |
+
|
| 205 |
+
backoff = ExponentialBackoff(total_attempts=retry_count)
|
| 206 |
+
failure_reason = None
|
| 207 |
+
for attempt in backoff:
|
| 208 |
+
try:
|
| 209 |
+
response = request(
|
| 210 |
+
url=url, method="GET", headers=headers_to_use, timeout=timeout
|
| 211 |
+
)
|
| 212 |
+
if response.status in transport.DEFAULT_RETRYABLE_STATUS_CODES:
|
| 213 |
+
_LOGGER.warning(
|
| 214 |
+
"Compute Engine Metadata server unavailable on "
|
| 215 |
+
"attempt %s of %s. Response status: %s",
|
| 216 |
+
attempt,
|
| 217 |
+
retry_count,
|
| 218 |
+
response.status,
|
| 219 |
+
)
|
| 220 |
+
failure_reason = (
|
| 221 |
+
response.data.decode("utf-8")
|
| 222 |
+
if hasattr(response.data, "decode")
|
| 223 |
+
else response.data
|
| 224 |
+
)
|
| 225 |
+
continue
|
| 226 |
+
else:
|
| 227 |
+
break
|
| 228 |
+
|
| 229 |
+
except exceptions.TransportError as e:
|
| 230 |
+
_LOGGER.warning(
|
| 231 |
+
"Compute Engine Metadata server unavailable on "
|
| 232 |
+
"attempt %s of %s. Reason: %s",
|
| 233 |
+
attempt,
|
| 234 |
+
retry_count,
|
| 235 |
+
e,
|
| 236 |
+
)
|
| 237 |
+
failure_reason = e
|
| 238 |
+
else:
|
| 239 |
+
raise exceptions.TransportError(
|
| 240 |
+
"Failed to retrieve {} from the Google Compute Engine "
|
| 241 |
+
"metadata service. Compute Engine Metadata server unavailable due to {}".format(
|
| 242 |
+
url, failure_reason
|
| 243 |
+
)
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
content = _helpers.from_bytes(response.data)
|
| 247 |
+
|
| 248 |
+
if response.status == http_client.NOT_FOUND and return_none_for_not_found_error:
|
| 249 |
+
return None
|
| 250 |
+
|
| 251 |
+
if response.status == http_client.OK:
|
| 252 |
+
if (
|
| 253 |
+
_helpers.parse_content_type(response.headers["content-type"])
|
| 254 |
+
== "application/json"
|
| 255 |
+
):
|
| 256 |
+
try:
|
| 257 |
+
return json.loads(content)
|
| 258 |
+
except ValueError as caught_exc:
|
| 259 |
+
new_exc = exceptions.TransportError(
|
| 260 |
+
"Received invalid JSON from the Google Compute Engine "
|
| 261 |
+
"metadata service: {:.20}".format(content)
|
| 262 |
+
)
|
| 263 |
+
raise new_exc from caught_exc
|
| 264 |
+
else:
|
| 265 |
+
return content
|
| 266 |
+
|
| 267 |
+
raise exceptions.TransportError(
|
| 268 |
+
"Failed to retrieve {} from the Google Compute Engine "
|
| 269 |
+
"metadata service. Status: {} Response:\n{}".format(
|
| 270 |
+
url, response.status, response.data
|
| 271 |
+
),
|
| 272 |
+
response,
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def get_project_id(request):
|
| 277 |
+
"""Get the Google Cloud Project ID from the metadata server.
|
| 278 |
+
|
| 279 |
+
Args:
|
| 280 |
+
request (google.auth.transport.Request): A callable used to make
|
| 281 |
+
HTTP requests.
|
| 282 |
+
|
| 283 |
+
Returns:
|
| 284 |
+
str: The project ID
|
| 285 |
+
|
| 286 |
+
Raises:
|
| 287 |
+
google.auth.exceptions.TransportError: if an error occurred while
|
| 288 |
+
retrieving metadata.
|
| 289 |
+
"""
|
| 290 |
+
return get(request, "project/project-id")
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def get_universe_domain(request):
|
| 294 |
+
"""Get the universe domain value from the metadata server.
|
| 295 |
+
|
| 296 |
+
Args:
|
| 297 |
+
request (google.auth.transport.Request): A callable used to make
|
| 298 |
+
HTTP requests.
|
| 299 |
+
|
| 300 |
+
Returns:
|
| 301 |
+
str: The universe domain value. If the universe domain endpoint is not
|
| 302 |
+
not found, return the default value, which is googleapis.com
|
| 303 |
+
|
| 304 |
+
Raises:
|
| 305 |
+
google.auth.exceptions.TransportError: if an error other than
|
| 306 |
+
404 occurs while retrieving metadata.
|
| 307 |
+
"""
|
| 308 |
+
universe_domain = get(
|
| 309 |
+
request, "universe/universe-domain", return_none_for_not_found_error=True
|
| 310 |
+
)
|
| 311 |
+
if not universe_domain:
|
| 312 |
+
return "googleapis.com"
|
| 313 |
+
return universe_domain
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def get_service_account_info(request, service_account="default"):
|
| 317 |
+
"""Get information about a service account from the metadata server.
|
| 318 |
+
|
| 319 |
+
Args:
|
| 320 |
+
request (google.auth.transport.Request): A callable used to make
|
| 321 |
+
HTTP requests.
|
| 322 |
+
service_account (str): The string 'default' or a service account email
|
| 323 |
+
address. The determines which service account for which to acquire
|
| 324 |
+
information.
|
| 325 |
+
|
| 326 |
+
Returns:
|
| 327 |
+
Mapping: The service account's information, for example::
|
| 328 |
+
|
| 329 |
+
{
|
| 330 |
+
'email': '...',
|
| 331 |
+
'scopes': ['scope', ...],
|
| 332 |
+
'aliases': ['default', '...']
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
Raises:
|
| 336 |
+
google.auth.exceptions.TransportError: if an error occurred while
|
| 337 |
+
retrieving metadata.
|
| 338 |
+
"""
|
| 339 |
+
path = "instance/service-accounts/{0}/".format(service_account)
|
| 340 |
+
# See https://cloud.google.com/compute/docs/metadata#aggcontents
|
| 341 |
+
# for more on the use of 'recursive'.
|
| 342 |
+
return get(request, path, params={"recursive": "true"})
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def get_service_account_token(request, service_account="default", scopes=None):
|
| 346 |
+
"""Get the OAuth 2.0 access token for a service account.
|
| 347 |
+
|
| 348 |
+
Args:
|
| 349 |
+
request (google.auth.transport.Request): A callable used to make
|
| 350 |
+
HTTP requests.
|
| 351 |
+
service_account (str): The string 'default' or a service account email
|
| 352 |
+
address. The determines which service account for which to acquire
|
| 353 |
+
an access token.
|
| 354 |
+
scopes (Optional[Union[str, List[str]]]): Optional string or list of
|
| 355 |
+
strings with auth scopes.
|
| 356 |
+
Returns:
|
| 357 |
+
Tuple[str, datetime]: The access token and its expiration.
|
| 358 |
+
|
| 359 |
+
Raises:
|
| 360 |
+
google.auth.exceptions.TransportError: if an error occurred while
|
| 361 |
+
retrieving metadata.
|
| 362 |
+
"""
|
| 363 |
+
if scopes:
|
| 364 |
+
if not isinstance(scopes, str):
|
| 365 |
+
scopes = ",".join(scopes)
|
| 366 |
+
params = {"scopes": scopes}
|
| 367 |
+
else:
|
| 368 |
+
params = None
|
| 369 |
+
|
| 370 |
+
metrics_header = {
|
| 371 |
+
metrics.API_CLIENT_HEADER: metrics.token_request_access_token_mds()
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
path = "instance/service-accounts/{0}/token".format(service_account)
|
| 375 |
+
token_json = get(request, path, params=params, headers=metrics_header)
|
| 376 |
+
token_expiry = _helpers.utcnow() + datetime.timedelta(
|
| 377 |
+
seconds=token_json["expires_in"]
|
| 378 |
+
)
|
| 379 |
+
return token_json["access_token"], token_expiry
|
lib/python3.10/site-packages/google/auth/compute_engine/credentials.py
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 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 |
+
"""Google Compute Engine credentials.
|
| 16 |
+
|
| 17 |
+
This module provides authentication for an application running on Google
|
| 18 |
+
Compute Engine using the Compute Engine metadata server.
|
| 19 |
+
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import datetime
|
| 23 |
+
|
| 24 |
+
from google.auth import _helpers
|
| 25 |
+
from google.auth import credentials
|
| 26 |
+
from google.auth import exceptions
|
| 27 |
+
from google.auth import iam
|
| 28 |
+
from google.auth import jwt
|
| 29 |
+
from google.auth import metrics
|
| 30 |
+
from google.auth.compute_engine import _metadata
|
| 31 |
+
from google.oauth2 import _client
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class Credentials(
|
| 35 |
+
credentials.Scoped,
|
| 36 |
+
credentials.CredentialsWithQuotaProject,
|
| 37 |
+
credentials.CredentialsWithUniverseDomain,
|
| 38 |
+
):
|
| 39 |
+
"""Compute Engine Credentials.
|
| 40 |
+
|
| 41 |
+
These credentials use the Google Compute Engine metadata server to obtain
|
| 42 |
+
OAuth 2.0 access tokens associated with the instance's service account,
|
| 43 |
+
and are also used for Cloud Run, Flex and App Engine (except for the Python
|
| 44 |
+
2.7 runtime, which is supported only on older versions of this library).
|
| 45 |
+
|
| 46 |
+
For more information about Compute Engine authentication, including how
|
| 47 |
+
to configure scopes, see the `Compute Engine authentication
|
| 48 |
+
documentation`_.
|
| 49 |
+
|
| 50 |
+
.. note:: On Compute Engine the metadata server ignores requested scopes.
|
| 51 |
+
On Cloud Run, Flex and App Engine the server honours requested scopes.
|
| 52 |
+
|
| 53 |
+
.. _Compute Engine authentication documentation:
|
| 54 |
+
https://cloud.google.com/compute/docs/authentication#using
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
def __init__(
|
| 58 |
+
self,
|
| 59 |
+
service_account_email="default",
|
| 60 |
+
quota_project_id=None,
|
| 61 |
+
scopes=None,
|
| 62 |
+
default_scopes=None,
|
| 63 |
+
universe_domain=None,
|
| 64 |
+
):
|
| 65 |
+
"""
|
| 66 |
+
Args:
|
| 67 |
+
service_account_email (str): The service account email to use, or
|
| 68 |
+
'default'. A Compute Engine instance may have multiple service
|
| 69 |
+
accounts.
|
| 70 |
+
quota_project_id (Optional[str]): The project ID used for quota and
|
| 71 |
+
billing.
|
| 72 |
+
scopes (Optional[Sequence[str]]): The list of scopes for the credentials.
|
| 73 |
+
default_scopes (Optional[Sequence[str]]): Default scopes passed by a
|
| 74 |
+
Google client library. Use 'scopes' for user-defined scopes.
|
| 75 |
+
universe_domain (Optional[str]): The universe domain. If not
|
| 76 |
+
provided or None, credential will attempt to fetch the value
|
| 77 |
+
from metadata server. If metadata server doesn't have universe
|
| 78 |
+
domain endpoint, then the default googleapis.com will be used.
|
| 79 |
+
"""
|
| 80 |
+
super(Credentials, self).__init__()
|
| 81 |
+
self._service_account_email = service_account_email
|
| 82 |
+
self._quota_project_id = quota_project_id
|
| 83 |
+
self._scopes = scopes
|
| 84 |
+
self._default_scopes = default_scopes
|
| 85 |
+
self._universe_domain_cached = False
|
| 86 |
+
if universe_domain:
|
| 87 |
+
self._universe_domain = universe_domain
|
| 88 |
+
self._universe_domain_cached = True
|
| 89 |
+
|
| 90 |
+
def _retrieve_info(self, request):
|
| 91 |
+
"""Retrieve information about the service account.
|
| 92 |
+
|
| 93 |
+
Updates the scopes and retrieves the full service account email.
|
| 94 |
+
|
| 95 |
+
Args:
|
| 96 |
+
request (google.auth.transport.Request): The object used to make
|
| 97 |
+
HTTP requests.
|
| 98 |
+
"""
|
| 99 |
+
info = _metadata.get_service_account_info(
|
| 100 |
+
request, service_account=self._service_account_email
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
self._service_account_email = info["email"]
|
| 104 |
+
|
| 105 |
+
# Don't override scopes requested by the user.
|
| 106 |
+
if self._scopes is None:
|
| 107 |
+
self._scopes = info["scopes"]
|
| 108 |
+
|
| 109 |
+
def _metric_header_for_usage(self):
|
| 110 |
+
return metrics.CRED_TYPE_SA_MDS
|
| 111 |
+
|
| 112 |
+
def refresh(self, request):
|
| 113 |
+
"""Refresh the access token and scopes.
|
| 114 |
+
|
| 115 |
+
Args:
|
| 116 |
+
request (google.auth.transport.Request): The object used to make
|
| 117 |
+
HTTP requests.
|
| 118 |
+
|
| 119 |
+
Raises:
|
| 120 |
+
google.auth.exceptions.RefreshError: If the Compute Engine metadata
|
| 121 |
+
service can't be reached if if the instance has not
|
| 122 |
+
credentials.
|
| 123 |
+
"""
|
| 124 |
+
scopes = self._scopes if self._scopes is not None else self._default_scopes
|
| 125 |
+
try:
|
| 126 |
+
self._retrieve_info(request)
|
| 127 |
+
# Always fetch token with default service account email.
|
| 128 |
+
self.token, self.expiry = _metadata.get_service_account_token(
|
| 129 |
+
request, service_account="default", scopes=scopes
|
| 130 |
+
)
|
| 131 |
+
except exceptions.TransportError as caught_exc:
|
| 132 |
+
new_exc = exceptions.RefreshError(caught_exc)
|
| 133 |
+
raise new_exc from caught_exc
|
| 134 |
+
|
| 135 |
+
@property
|
| 136 |
+
def service_account_email(self):
|
| 137 |
+
"""The service account email.
|
| 138 |
+
|
| 139 |
+
.. note:: This is not guaranteed to be set until :meth:`refresh` has been
|
| 140 |
+
called.
|
| 141 |
+
"""
|
| 142 |
+
return self._service_account_email
|
| 143 |
+
|
| 144 |
+
@property
|
| 145 |
+
def requires_scopes(self):
|
| 146 |
+
return not self._scopes
|
| 147 |
+
|
| 148 |
+
@property
|
| 149 |
+
def universe_domain(self):
|
| 150 |
+
if self._universe_domain_cached:
|
| 151 |
+
return self._universe_domain
|
| 152 |
+
|
| 153 |
+
from google.auth.transport import requests as google_auth_requests
|
| 154 |
+
|
| 155 |
+
self._universe_domain = _metadata.get_universe_domain(
|
| 156 |
+
google_auth_requests.Request()
|
| 157 |
+
)
|
| 158 |
+
self._universe_domain_cached = True
|
| 159 |
+
return self._universe_domain
|
| 160 |
+
|
| 161 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 162 |
+
def get_cred_info(self):
|
| 163 |
+
return {
|
| 164 |
+
"credential_source": "metadata server",
|
| 165 |
+
"credential_type": "VM credentials",
|
| 166 |
+
"principal": self.service_account_email,
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
|
| 170 |
+
def with_quota_project(self, quota_project_id):
|
| 171 |
+
creds = self.__class__(
|
| 172 |
+
service_account_email=self._service_account_email,
|
| 173 |
+
quota_project_id=quota_project_id,
|
| 174 |
+
scopes=self._scopes,
|
| 175 |
+
default_scopes=self._default_scopes,
|
| 176 |
+
)
|
| 177 |
+
creds._universe_domain = self._universe_domain
|
| 178 |
+
creds._universe_domain_cached = self._universe_domain_cached
|
| 179 |
+
return creds
|
| 180 |
+
|
| 181 |
+
@_helpers.copy_docstring(credentials.Scoped)
|
| 182 |
+
def with_scopes(self, scopes, default_scopes=None):
|
| 183 |
+
# Compute Engine credentials can not be scoped (the metadata service
|
| 184 |
+
# ignores the scopes parameter). App Engine, Cloud Run and Flex support
|
| 185 |
+
# requesting scopes.
|
| 186 |
+
creds = self.__class__(
|
| 187 |
+
scopes=scopes,
|
| 188 |
+
default_scopes=default_scopes,
|
| 189 |
+
service_account_email=self._service_account_email,
|
| 190 |
+
quota_project_id=self._quota_project_id,
|
| 191 |
+
)
|
| 192 |
+
creds._universe_domain = self._universe_domain
|
| 193 |
+
creds._universe_domain_cached = self._universe_domain_cached
|
| 194 |
+
return creds
|
| 195 |
+
|
| 196 |
+
@_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
|
| 197 |
+
def with_universe_domain(self, universe_domain):
|
| 198 |
+
return self.__class__(
|
| 199 |
+
scopes=self._scopes,
|
| 200 |
+
default_scopes=self._default_scopes,
|
| 201 |
+
service_account_email=self._service_account_email,
|
| 202 |
+
quota_project_id=self._quota_project_id,
|
| 203 |
+
universe_domain=universe_domain,
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
|
| 208 |
+
_DEFAULT_TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token"
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
class IDTokenCredentials(
|
| 212 |
+
credentials.CredentialsWithQuotaProject,
|
| 213 |
+
credentials.Signing,
|
| 214 |
+
credentials.CredentialsWithTokenUri,
|
| 215 |
+
):
|
| 216 |
+
"""Open ID Connect ID Token-based service account credentials.
|
| 217 |
+
|
| 218 |
+
These credentials relies on the default service account of a GCE instance.
|
| 219 |
+
|
| 220 |
+
ID token can be requested from `GCE metadata server identity endpoint`_, IAM
|
| 221 |
+
token endpoint or other token endpoints you specify. If metadata server
|
| 222 |
+
identity endpoint is not used, the GCE instance must have been started with
|
| 223 |
+
a service account that has access to the IAM Cloud API.
|
| 224 |
+
|
| 225 |
+
.. _GCE metadata server identity endpoint:
|
| 226 |
+
https://cloud.google.com/compute/docs/instances/verifying-instance-identity
|
| 227 |
+
"""
|
| 228 |
+
|
| 229 |
+
def __init__(
|
| 230 |
+
self,
|
| 231 |
+
request,
|
| 232 |
+
target_audience,
|
| 233 |
+
token_uri=None,
|
| 234 |
+
additional_claims=None,
|
| 235 |
+
service_account_email=None,
|
| 236 |
+
signer=None,
|
| 237 |
+
use_metadata_identity_endpoint=False,
|
| 238 |
+
quota_project_id=None,
|
| 239 |
+
):
|
| 240 |
+
"""
|
| 241 |
+
Args:
|
| 242 |
+
request (google.auth.transport.Request): The object used to make
|
| 243 |
+
HTTP requests.
|
| 244 |
+
target_audience (str): The intended audience for these credentials,
|
| 245 |
+
used when requesting the ID Token. The ID Token's ``aud`` claim
|
| 246 |
+
will be set to this string.
|
| 247 |
+
token_uri (str): The OAuth 2.0 Token URI.
|
| 248 |
+
additional_claims (Mapping[str, str]): Any additional claims for
|
| 249 |
+
the JWT assertion used in the authorization grant.
|
| 250 |
+
service_account_email (str): Optional explicit service account to
|
| 251 |
+
use to sign JWT tokens.
|
| 252 |
+
By default, this is the default GCE service account.
|
| 253 |
+
signer (google.auth.crypt.Signer): The signer used to sign JWTs.
|
| 254 |
+
In case the signer is specified, the request argument will be
|
| 255 |
+
ignored.
|
| 256 |
+
use_metadata_identity_endpoint (bool): Whether to use GCE metadata
|
| 257 |
+
identity endpoint. For backward compatibility the default value
|
| 258 |
+
is False. If set to True, ``token_uri``, ``additional_claims``,
|
| 259 |
+
``service_account_email``, ``signer`` argument should not be set;
|
| 260 |
+
otherwise ValueError will be raised.
|
| 261 |
+
quota_project_id (Optional[str]): The project ID used for quota and
|
| 262 |
+
billing.
|
| 263 |
+
|
| 264 |
+
Raises:
|
| 265 |
+
ValueError:
|
| 266 |
+
If ``use_metadata_identity_endpoint`` is set to True, and one of
|
| 267 |
+
``token_uri``, ``additional_claims``, ``service_account_email``,
|
| 268 |
+
``signer`` arguments is set.
|
| 269 |
+
"""
|
| 270 |
+
super(IDTokenCredentials, self).__init__()
|
| 271 |
+
|
| 272 |
+
self._quota_project_id = quota_project_id
|
| 273 |
+
self._use_metadata_identity_endpoint = use_metadata_identity_endpoint
|
| 274 |
+
self._target_audience = target_audience
|
| 275 |
+
|
| 276 |
+
if use_metadata_identity_endpoint:
|
| 277 |
+
if token_uri or additional_claims or service_account_email or signer:
|
| 278 |
+
raise exceptions.MalformedError(
|
| 279 |
+
"If use_metadata_identity_endpoint is set, token_uri, "
|
| 280 |
+
"additional_claims, service_account_email, signer arguments"
|
| 281 |
+
" must not be set"
|
| 282 |
+
)
|
| 283 |
+
self._token_uri = None
|
| 284 |
+
self._additional_claims = None
|
| 285 |
+
self._signer = None
|
| 286 |
+
|
| 287 |
+
if service_account_email is None:
|
| 288 |
+
sa_info = _metadata.get_service_account_info(request)
|
| 289 |
+
self._service_account_email = sa_info["email"]
|
| 290 |
+
else:
|
| 291 |
+
self._service_account_email = service_account_email
|
| 292 |
+
|
| 293 |
+
if not use_metadata_identity_endpoint:
|
| 294 |
+
if signer is None:
|
| 295 |
+
signer = iam.Signer(
|
| 296 |
+
request=request,
|
| 297 |
+
credentials=Credentials(),
|
| 298 |
+
service_account_email=self._service_account_email,
|
| 299 |
+
)
|
| 300 |
+
self._signer = signer
|
| 301 |
+
self._token_uri = token_uri or _DEFAULT_TOKEN_URI
|
| 302 |
+
|
| 303 |
+
if additional_claims is not None:
|
| 304 |
+
self._additional_claims = additional_claims
|
| 305 |
+
else:
|
| 306 |
+
self._additional_claims = {}
|
| 307 |
+
|
| 308 |
+
def with_target_audience(self, target_audience):
|
| 309 |
+
"""Create a copy of these credentials with the specified target
|
| 310 |
+
audience.
|
| 311 |
+
Args:
|
| 312 |
+
target_audience (str): The intended audience for these credentials,
|
| 313 |
+
used when requesting the ID Token.
|
| 314 |
+
Returns:
|
| 315 |
+
google.auth.service_account.IDTokenCredentials: A new credentials
|
| 316 |
+
instance.
|
| 317 |
+
"""
|
| 318 |
+
# since the signer is already instantiated,
|
| 319 |
+
# the request is not needed
|
| 320 |
+
if self._use_metadata_identity_endpoint:
|
| 321 |
+
return self.__class__(
|
| 322 |
+
None,
|
| 323 |
+
target_audience=target_audience,
|
| 324 |
+
use_metadata_identity_endpoint=True,
|
| 325 |
+
quota_project_id=self._quota_project_id,
|
| 326 |
+
)
|
| 327 |
+
else:
|
| 328 |
+
return self.__class__(
|
| 329 |
+
None,
|
| 330 |
+
service_account_email=self._service_account_email,
|
| 331 |
+
token_uri=self._token_uri,
|
| 332 |
+
target_audience=target_audience,
|
| 333 |
+
additional_claims=self._additional_claims.copy(),
|
| 334 |
+
signer=self.signer,
|
| 335 |
+
use_metadata_identity_endpoint=False,
|
| 336 |
+
quota_project_id=self._quota_project_id,
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
|
| 340 |
+
def with_quota_project(self, quota_project_id):
|
| 341 |
+
|
| 342 |
+
# since the signer is already instantiated,
|
| 343 |
+
# the request is not needed
|
| 344 |
+
if self._use_metadata_identity_endpoint:
|
| 345 |
+
return self.__class__(
|
| 346 |
+
None,
|
| 347 |
+
target_audience=self._target_audience,
|
| 348 |
+
use_metadata_identity_endpoint=True,
|
| 349 |
+
quota_project_id=quota_project_id,
|
| 350 |
+
)
|
| 351 |
+
else:
|
| 352 |
+
return self.__class__(
|
| 353 |
+
None,
|
| 354 |
+
service_account_email=self._service_account_email,
|
| 355 |
+
token_uri=self._token_uri,
|
| 356 |
+
target_audience=self._target_audience,
|
| 357 |
+
additional_claims=self._additional_claims.copy(),
|
| 358 |
+
signer=self.signer,
|
| 359 |
+
use_metadata_identity_endpoint=False,
|
| 360 |
+
quota_project_id=quota_project_id,
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
@_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
|
| 364 |
+
def with_token_uri(self, token_uri):
|
| 365 |
+
|
| 366 |
+
# since the signer is already instantiated,
|
| 367 |
+
# the request is not needed
|
| 368 |
+
if self._use_metadata_identity_endpoint:
|
| 369 |
+
raise exceptions.MalformedError(
|
| 370 |
+
"If use_metadata_identity_endpoint is set, token_uri" " must not be set"
|
| 371 |
+
)
|
| 372 |
+
else:
|
| 373 |
+
return self.__class__(
|
| 374 |
+
None,
|
| 375 |
+
service_account_email=self._service_account_email,
|
| 376 |
+
token_uri=token_uri,
|
| 377 |
+
target_audience=self._target_audience,
|
| 378 |
+
additional_claims=self._additional_claims.copy(),
|
| 379 |
+
signer=self.signer,
|
| 380 |
+
use_metadata_identity_endpoint=False,
|
| 381 |
+
quota_project_id=self.quota_project_id,
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
def _make_authorization_grant_assertion(self):
|
| 385 |
+
"""Create the OAuth 2.0 assertion.
|
| 386 |
+
This assertion is used during the OAuth 2.0 grant to acquire an
|
| 387 |
+
ID token.
|
| 388 |
+
Returns:
|
| 389 |
+
bytes: The authorization grant assertion.
|
| 390 |
+
"""
|
| 391 |
+
now = _helpers.utcnow()
|
| 392 |
+
lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
|
| 393 |
+
expiry = now + lifetime
|
| 394 |
+
|
| 395 |
+
payload = {
|
| 396 |
+
"iat": _helpers.datetime_to_secs(now),
|
| 397 |
+
"exp": _helpers.datetime_to_secs(expiry),
|
| 398 |
+
# The issuer must be the service account email.
|
| 399 |
+
"iss": self.service_account_email,
|
| 400 |
+
# The audience must be the auth token endpoint's URI
|
| 401 |
+
"aud": self._token_uri,
|
| 402 |
+
# The target audience specifies which service the ID token is
|
| 403 |
+
# intended for.
|
| 404 |
+
"target_audience": self._target_audience,
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
payload.update(self._additional_claims)
|
| 408 |
+
|
| 409 |
+
token = jwt.encode(self._signer, payload)
|
| 410 |
+
|
| 411 |
+
return token
|
| 412 |
+
|
| 413 |
+
def _call_metadata_identity_endpoint(self, request):
|
| 414 |
+
"""Request ID token from metadata identity endpoint.
|
| 415 |
+
|
| 416 |
+
Args:
|
| 417 |
+
request (google.auth.transport.Request): The object used to make
|
| 418 |
+
HTTP requests.
|
| 419 |
+
|
| 420 |
+
Returns:
|
| 421 |
+
Tuple[str, datetime.datetime]: The ID token and the expiry of the ID token.
|
| 422 |
+
|
| 423 |
+
Raises:
|
| 424 |
+
google.auth.exceptions.RefreshError: If the Compute Engine metadata
|
| 425 |
+
service can't be reached or if the instance has no credentials.
|
| 426 |
+
ValueError: If extracting expiry from the obtained ID token fails.
|
| 427 |
+
"""
|
| 428 |
+
try:
|
| 429 |
+
path = "instance/service-accounts/default/identity"
|
| 430 |
+
params = {"audience": self._target_audience, "format": "full"}
|
| 431 |
+
metrics_header = {
|
| 432 |
+
metrics.API_CLIENT_HEADER: metrics.token_request_id_token_mds()
|
| 433 |
+
}
|
| 434 |
+
id_token = _metadata.get(
|
| 435 |
+
request, path, params=params, headers=metrics_header
|
| 436 |
+
)
|
| 437 |
+
except exceptions.TransportError as caught_exc:
|
| 438 |
+
new_exc = exceptions.RefreshError(caught_exc)
|
| 439 |
+
raise new_exc from caught_exc
|
| 440 |
+
|
| 441 |
+
_, payload, _, _ = jwt._unverified_decode(id_token)
|
| 442 |
+
return id_token, datetime.datetime.utcfromtimestamp(payload["exp"])
|
| 443 |
+
|
| 444 |
+
def refresh(self, request):
|
| 445 |
+
"""Refreshes the ID token.
|
| 446 |
+
|
| 447 |
+
Args:
|
| 448 |
+
request (google.auth.transport.Request): The object used to make
|
| 449 |
+
HTTP requests.
|
| 450 |
+
|
| 451 |
+
Raises:
|
| 452 |
+
google.auth.exceptions.RefreshError: If the credentials could
|
| 453 |
+
not be refreshed.
|
| 454 |
+
ValueError: If extracting expiry from the obtained ID token fails.
|
| 455 |
+
"""
|
| 456 |
+
if self._use_metadata_identity_endpoint:
|
| 457 |
+
self.token, self.expiry = self._call_metadata_identity_endpoint(request)
|
| 458 |
+
else:
|
| 459 |
+
assertion = self._make_authorization_grant_assertion()
|
| 460 |
+
access_token, expiry, _ = _client.id_token_jwt_grant(
|
| 461 |
+
request, self._token_uri, assertion
|
| 462 |
+
)
|
| 463 |
+
self.token = access_token
|
| 464 |
+
self.expiry = expiry
|
| 465 |
+
|
| 466 |
+
@property # type: ignore
|
| 467 |
+
@_helpers.copy_docstring(credentials.Signing)
|
| 468 |
+
def signer(self):
|
| 469 |
+
return self._signer
|
| 470 |
+
|
| 471 |
+
def sign_bytes(self, message):
|
| 472 |
+
"""Signs the given message.
|
| 473 |
+
|
| 474 |
+
Args:
|
| 475 |
+
message (bytes): The message to sign.
|
| 476 |
+
|
| 477 |
+
Returns:
|
| 478 |
+
bytes: The message's cryptographic signature.
|
| 479 |
+
|
| 480 |
+
Raises:
|
| 481 |
+
ValueError:
|
| 482 |
+
Signer is not available if metadata identity endpoint is used.
|
| 483 |
+
"""
|
| 484 |
+
if self._use_metadata_identity_endpoint:
|
| 485 |
+
raise exceptions.InvalidOperation(
|
| 486 |
+
"Signer is not available if metadata identity endpoint is used"
|
| 487 |
+
)
|
| 488 |
+
return self._signer.sign(message)
|
| 489 |
+
|
| 490 |
+
@property
|
| 491 |
+
def service_account_email(self):
|
| 492 |
+
"""The service account email."""
|
| 493 |
+
return self._service_account_email
|
| 494 |
+
|
| 495 |
+
@property
|
| 496 |
+
def signer_email(self):
|
| 497 |
+
return self._service_account_email
|
lib/python3.10/site-packages/google/auth/credentials.py
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 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 |
+
|
| 16 |
+
"""Interfaces for credentials."""
|
| 17 |
+
|
| 18 |
+
import abc
|
| 19 |
+
from enum import Enum
|
| 20 |
+
import os
|
| 21 |
+
|
| 22 |
+
from google.auth import _helpers, environment_vars
|
| 23 |
+
from google.auth import exceptions
|
| 24 |
+
from google.auth import metrics
|
| 25 |
+
from google.auth._credentials_base import _BaseCredentials
|
| 26 |
+
from google.auth._refresh_worker import RefreshThreadManager
|
| 27 |
+
|
| 28 |
+
DEFAULT_UNIVERSE_DOMAIN = "googleapis.com"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class Credentials(_BaseCredentials):
|
| 32 |
+
"""Base class for all credentials.
|
| 33 |
+
|
| 34 |
+
All credentials have a :attr:`token` that is used for authentication and
|
| 35 |
+
may also optionally set an :attr:`expiry` to indicate when the token will
|
| 36 |
+
no longer be valid.
|
| 37 |
+
|
| 38 |
+
Most credentials will be :attr:`invalid` until :meth:`refresh` is called.
|
| 39 |
+
Credentials can do this automatically before the first HTTP request in
|
| 40 |
+
:meth:`before_request`.
|
| 41 |
+
|
| 42 |
+
Although the token and expiration will change as the credentials are
|
| 43 |
+
:meth:`refreshed <refresh>` and used, credentials should be considered
|
| 44 |
+
immutable. Various credentials will accept configuration such as private
|
| 45 |
+
keys, scopes, and other options. These options are not changeable after
|
| 46 |
+
construction. Some classes will provide mechanisms to copy the credentials
|
| 47 |
+
with modifications such as :meth:`ScopedCredentials.with_scopes`.
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
def __init__(self):
|
| 51 |
+
super(Credentials, self).__init__()
|
| 52 |
+
|
| 53 |
+
self.expiry = None
|
| 54 |
+
"""Optional[datetime]: When the token expires and is no longer valid.
|
| 55 |
+
If this is None, the token is assumed to never expire."""
|
| 56 |
+
self._quota_project_id = None
|
| 57 |
+
"""Optional[str]: Project to use for quota and billing purposes."""
|
| 58 |
+
self._trust_boundary = None
|
| 59 |
+
"""Optional[dict]: Cache of a trust boundary response which has a list
|
| 60 |
+
of allowed regions and an encoded string representation of credentials
|
| 61 |
+
trust boundary."""
|
| 62 |
+
self._universe_domain = DEFAULT_UNIVERSE_DOMAIN
|
| 63 |
+
"""Optional[str]: The universe domain value, default is googleapis.com
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
self._use_non_blocking_refresh = False
|
| 67 |
+
self._refresh_worker = RefreshThreadManager()
|
| 68 |
+
|
| 69 |
+
@property
|
| 70 |
+
def expired(self):
|
| 71 |
+
"""Checks if the credentials are expired.
|
| 72 |
+
|
| 73 |
+
Note that credentials can be invalid but not expired because
|
| 74 |
+
Credentials with :attr:`expiry` set to None is considered to never
|
| 75 |
+
expire.
|
| 76 |
+
|
| 77 |
+
.. deprecated:: v2.24.0
|
| 78 |
+
Prefer checking :attr:`token_state` instead.
|
| 79 |
+
"""
|
| 80 |
+
if not self.expiry:
|
| 81 |
+
return False
|
| 82 |
+
# Remove some threshold from expiry to err on the side of reporting
|
| 83 |
+
# expiration early so that we avoid the 401-refresh-retry loop.
|
| 84 |
+
skewed_expiry = self.expiry - _helpers.REFRESH_THRESHOLD
|
| 85 |
+
return _helpers.utcnow() >= skewed_expiry
|
| 86 |
+
|
| 87 |
+
@property
|
| 88 |
+
def valid(self):
|
| 89 |
+
"""Checks the validity of the credentials.
|
| 90 |
+
|
| 91 |
+
This is True if the credentials have a :attr:`token` and the token
|
| 92 |
+
is not :attr:`expired`.
|
| 93 |
+
|
| 94 |
+
.. deprecated:: v2.24.0
|
| 95 |
+
Prefer checking :attr:`token_state` instead.
|
| 96 |
+
"""
|
| 97 |
+
return self.token is not None and not self.expired
|
| 98 |
+
|
| 99 |
+
@property
|
| 100 |
+
def token_state(self):
|
| 101 |
+
"""
|
| 102 |
+
See `:obj:`TokenState`
|
| 103 |
+
"""
|
| 104 |
+
if self.token is None:
|
| 105 |
+
return TokenState.INVALID
|
| 106 |
+
|
| 107 |
+
# Credentials that can't expire are always treated as fresh.
|
| 108 |
+
if self.expiry is None:
|
| 109 |
+
return TokenState.FRESH
|
| 110 |
+
|
| 111 |
+
expired = _helpers.utcnow() >= self.expiry
|
| 112 |
+
if expired:
|
| 113 |
+
return TokenState.INVALID
|
| 114 |
+
|
| 115 |
+
is_stale = _helpers.utcnow() >= (self.expiry - _helpers.REFRESH_THRESHOLD)
|
| 116 |
+
if is_stale:
|
| 117 |
+
return TokenState.STALE
|
| 118 |
+
|
| 119 |
+
return TokenState.FRESH
|
| 120 |
+
|
| 121 |
+
@property
|
| 122 |
+
def quota_project_id(self):
|
| 123 |
+
"""Project to use for quota and billing purposes."""
|
| 124 |
+
return self._quota_project_id
|
| 125 |
+
|
| 126 |
+
@property
|
| 127 |
+
def universe_domain(self):
|
| 128 |
+
"""The universe domain value."""
|
| 129 |
+
return self._universe_domain
|
| 130 |
+
|
| 131 |
+
def get_cred_info(self):
|
| 132 |
+
"""The credential information JSON.
|
| 133 |
+
|
| 134 |
+
The credential information will be added to auth related error messages
|
| 135 |
+
by client library.
|
| 136 |
+
|
| 137 |
+
Returns:
|
| 138 |
+
Mapping[str, str]: The credential information JSON.
|
| 139 |
+
"""
|
| 140 |
+
return None
|
| 141 |
+
|
| 142 |
+
@abc.abstractmethod
|
| 143 |
+
def refresh(self, request):
|
| 144 |
+
"""Refreshes the access token.
|
| 145 |
+
|
| 146 |
+
Args:
|
| 147 |
+
request (google.auth.transport.Request): The object used to make
|
| 148 |
+
HTTP requests.
|
| 149 |
+
|
| 150 |
+
Raises:
|
| 151 |
+
google.auth.exceptions.RefreshError: If the credentials could
|
| 152 |
+
not be refreshed.
|
| 153 |
+
"""
|
| 154 |
+
# pylint: disable=missing-raises-doc
|
| 155 |
+
# (pylint doesn't recognize that this is abstract)
|
| 156 |
+
raise NotImplementedError("Refresh must be implemented")
|
| 157 |
+
|
| 158 |
+
def _metric_header_for_usage(self):
|
| 159 |
+
"""The x-goog-api-client header for token usage metric.
|
| 160 |
+
|
| 161 |
+
This header will be added to the API service requests in before_request
|
| 162 |
+
method. For example, "cred-type/sa-jwt" means service account self
|
| 163 |
+
signed jwt access token is used in the API service request
|
| 164 |
+
authorization header. Children credentials classes need to override
|
| 165 |
+
this method to provide the header value, if the token usage metric is
|
| 166 |
+
needed.
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
str: The x-goog-api-client header value.
|
| 170 |
+
"""
|
| 171 |
+
return None
|
| 172 |
+
|
| 173 |
+
def apply(self, headers, token=None):
|
| 174 |
+
"""Apply the token to the authentication header.
|
| 175 |
+
|
| 176 |
+
Args:
|
| 177 |
+
headers (Mapping): The HTTP request headers.
|
| 178 |
+
token (Optional[str]): If specified, overrides the current access
|
| 179 |
+
token.
|
| 180 |
+
"""
|
| 181 |
+
self._apply(headers, token=token)
|
| 182 |
+
"""Trust boundary value will be a cached value from global lookup.
|
| 183 |
+
|
| 184 |
+
The response of trust boundary will be a list of regions and a hex
|
| 185 |
+
encoded representation.
|
| 186 |
+
|
| 187 |
+
An example of global lookup response:
|
| 188 |
+
{
|
| 189 |
+
"locations": [
|
| 190 |
+
"us-central1", "us-east1", "europe-west1", "asia-east1"
|
| 191 |
+
]
|
| 192 |
+
"encoded_locations": "0xA30"
|
| 193 |
+
}
|
| 194 |
+
"""
|
| 195 |
+
if self._trust_boundary is not None:
|
| 196 |
+
headers["x-allowed-locations"] = self._trust_boundary["encoded_locations"]
|
| 197 |
+
if self.quota_project_id:
|
| 198 |
+
headers["x-goog-user-project"] = self.quota_project_id
|
| 199 |
+
|
| 200 |
+
def _blocking_refresh(self, request):
|
| 201 |
+
if not self.valid:
|
| 202 |
+
self.refresh(request)
|
| 203 |
+
|
| 204 |
+
def _non_blocking_refresh(self, request):
|
| 205 |
+
use_blocking_refresh_fallback = False
|
| 206 |
+
|
| 207 |
+
if self.token_state == TokenState.STALE:
|
| 208 |
+
use_blocking_refresh_fallback = not self._refresh_worker.start_refresh(
|
| 209 |
+
self, request
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
if self.token_state == TokenState.INVALID or use_blocking_refresh_fallback:
|
| 213 |
+
self.refresh(request)
|
| 214 |
+
# If the blocking refresh succeeds then we can clear the error info
|
| 215 |
+
# on the background refresh worker, and perform refreshes in a
|
| 216 |
+
# background thread.
|
| 217 |
+
self._refresh_worker.clear_error()
|
| 218 |
+
|
| 219 |
+
def before_request(self, request, method, url, headers):
|
| 220 |
+
"""Performs credential-specific before request logic.
|
| 221 |
+
|
| 222 |
+
Refreshes the credentials if necessary, then calls :meth:`apply` to
|
| 223 |
+
apply the token to the authentication header.
|
| 224 |
+
|
| 225 |
+
Args:
|
| 226 |
+
request (google.auth.transport.Request): The object used to make
|
| 227 |
+
HTTP requests.
|
| 228 |
+
method (str): The request's HTTP method or the RPC method being
|
| 229 |
+
invoked.
|
| 230 |
+
url (str): The request's URI or the RPC service's URI.
|
| 231 |
+
headers (Mapping): The request's headers.
|
| 232 |
+
"""
|
| 233 |
+
# pylint: disable=unused-argument
|
| 234 |
+
# (Subclasses may use these arguments to ascertain information about
|
| 235 |
+
# the http request.)
|
| 236 |
+
if self._use_non_blocking_refresh:
|
| 237 |
+
self._non_blocking_refresh(request)
|
| 238 |
+
else:
|
| 239 |
+
self._blocking_refresh(request)
|
| 240 |
+
|
| 241 |
+
metrics.add_metric_header(headers, self._metric_header_for_usage())
|
| 242 |
+
self.apply(headers)
|
| 243 |
+
|
| 244 |
+
def with_non_blocking_refresh(self):
|
| 245 |
+
self._use_non_blocking_refresh = True
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
class CredentialsWithQuotaProject(Credentials):
|
| 249 |
+
"""Abstract base for credentials supporting ``with_quota_project`` factory"""
|
| 250 |
+
|
| 251 |
+
def with_quota_project(self, quota_project_id):
|
| 252 |
+
"""Returns a copy of these credentials with a modified quota project.
|
| 253 |
+
|
| 254 |
+
Args:
|
| 255 |
+
quota_project_id (str): The project to use for quota and
|
| 256 |
+
billing purposes
|
| 257 |
+
|
| 258 |
+
Returns:
|
| 259 |
+
google.auth.credentials.Credentials: A new credentials instance.
|
| 260 |
+
"""
|
| 261 |
+
raise NotImplementedError("This credential does not support quota project.")
|
| 262 |
+
|
| 263 |
+
def with_quota_project_from_environment(self):
|
| 264 |
+
quota_from_env = os.environ.get(environment_vars.GOOGLE_CLOUD_QUOTA_PROJECT)
|
| 265 |
+
if quota_from_env:
|
| 266 |
+
return self.with_quota_project(quota_from_env)
|
| 267 |
+
return self
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
class CredentialsWithTokenUri(Credentials):
|
| 271 |
+
"""Abstract base for credentials supporting ``with_token_uri`` factory"""
|
| 272 |
+
|
| 273 |
+
def with_token_uri(self, token_uri):
|
| 274 |
+
"""Returns a copy of these credentials with a modified token uri.
|
| 275 |
+
|
| 276 |
+
Args:
|
| 277 |
+
token_uri (str): The uri to use for fetching/exchanging tokens
|
| 278 |
+
|
| 279 |
+
Returns:
|
| 280 |
+
google.auth.credentials.Credentials: A new credentials instance.
|
| 281 |
+
"""
|
| 282 |
+
raise NotImplementedError("This credential does not use token uri.")
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
class CredentialsWithUniverseDomain(Credentials):
|
| 286 |
+
"""Abstract base for credentials supporting ``with_universe_domain`` factory"""
|
| 287 |
+
|
| 288 |
+
def with_universe_domain(self, universe_domain):
|
| 289 |
+
"""Returns a copy of these credentials with a modified universe domain.
|
| 290 |
+
|
| 291 |
+
Args:
|
| 292 |
+
universe_domain (str): The universe domain to use
|
| 293 |
+
|
| 294 |
+
Returns:
|
| 295 |
+
google.auth.credentials.Credentials: A new credentials instance.
|
| 296 |
+
"""
|
| 297 |
+
raise NotImplementedError(
|
| 298 |
+
"This credential does not support with_universe_domain."
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
class AnonymousCredentials(Credentials):
|
| 303 |
+
"""Credentials that do not provide any authentication information.
|
| 304 |
+
|
| 305 |
+
These are useful in the case of services that support anonymous access or
|
| 306 |
+
local service emulators that do not use credentials.
|
| 307 |
+
"""
|
| 308 |
+
|
| 309 |
+
@property
|
| 310 |
+
def expired(self):
|
| 311 |
+
"""Returns `False`, anonymous credentials never expire."""
|
| 312 |
+
return False
|
| 313 |
+
|
| 314 |
+
@property
|
| 315 |
+
def valid(self):
|
| 316 |
+
"""Returns `True`, anonymous credentials are always valid."""
|
| 317 |
+
return True
|
| 318 |
+
|
| 319 |
+
def refresh(self, request):
|
| 320 |
+
"""Raises :class:``InvalidOperation``, anonymous credentials cannot be
|
| 321 |
+
refreshed."""
|
| 322 |
+
raise exceptions.InvalidOperation("Anonymous credentials cannot be refreshed.")
|
| 323 |
+
|
| 324 |
+
def apply(self, headers, token=None):
|
| 325 |
+
"""Anonymous credentials do nothing to the request.
|
| 326 |
+
|
| 327 |
+
The optional ``token`` argument is not supported.
|
| 328 |
+
|
| 329 |
+
Raises:
|
| 330 |
+
google.auth.exceptions.InvalidValue: If a token was specified.
|
| 331 |
+
"""
|
| 332 |
+
if token is not None:
|
| 333 |
+
raise exceptions.InvalidValue("Anonymous credentials don't support tokens.")
|
| 334 |
+
|
| 335 |
+
def before_request(self, request, method, url, headers):
|
| 336 |
+
"""Anonymous credentials do nothing to the request."""
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
class ReadOnlyScoped(metaclass=abc.ABCMeta):
|
| 340 |
+
"""Interface for credentials whose scopes can be queried.
|
| 341 |
+
|
| 342 |
+
OAuth 2.0-based credentials allow limiting access using scopes as described
|
| 343 |
+
in `RFC6749 Section 3.3`_.
|
| 344 |
+
If a credential class implements this interface then the credentials either
|
| 345 |
+
use scopes in their implementation.
|
| 346 |
+
|
| 347 |
+
Some credentials require scopes in order to obtain a token. You can check
|
| 348 |
+
if scoping is necessary with :attr:`requires_scopes`::
|
| 349 |
+
|
| 350 |
+
if credentials.requires_scopes:
|
| 351 |
+
# Scoping is required.
|
| 352 |
+
credentials = credentials.with_scopes(scopes=['one', 'two'])
|
| 353 |
+
|
| 354 |
+
Credentials that require scopes must either be constructed with scopes::
|
| 355 |
+
|
| 356 |
+
credentials = SomeScopedCredentials(scopes=['one', 'two'])
|
| 357 |
+
|
| 358 |
+
Or must copy an existing instance using :meth:`with_scopes`::
|
| 359 |
+
|
| 360 |
+
scoped_credentials = credentials.with_scopes(scopes=['one', 'two'])
|
| 361 |
+
|
| 362 |
+
Some credentials have scopes but do not allow or require scopes to be set,
|
| 363 |
+
these credentials can be used as-is.
|
| 364 |
+
|
| 365 |
+
.. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
|
| 366 |
+
"""
|
| 367 |
+
|
| 368 |
+
def __init__(self):
|
| 369 |
+
super(ReadOnlyScoped, self).__init__()
|
| 370 |
+
self._scopes = None
|
| 371 |
+
self._default_scopes = None
|
| 372 |
+
|
| 373 |
+
@property
|
| 374 |
+
def scopes(self):
|
| 375 |
+
"""Sequence[str]: the credentials' current set of scopes."""
|
| 376 |
+
return self._scopes
|
| 377 |
+
|
| 378 |
+
@property
|
| 379 |
+
def default_scopes(self):
|
| 380 |
+
"""Sequence[str]: the credentials' current set of default scopes."""
|
| 381 |
+
return self._default_scopes
|
| 382 |
+
|
| 383 |
+
@abc.abstractproperty
|
| 384 |
+
def requires_scopes(self):
|
| 385 |
+
"""True if these credentials require scopes to obtain an access token.
|
| 386 |
+
"""
|
| 387 |
+
return False
|
| 388 |
+
|
| 389 |
+
def has_scopes(self, scopes):
|
| 390 |
+
"""Checks if the credentials have the given scopes.
|
| 391 |
+
|
| 392 |
+
.. warning: This method is not guaranteed to be accurate if the
|
| 393 |
+
credentials are :attr:`~Credentials.invalid`.
|
| 394 |
+
|
| 395 |
+
Args:
|
| 396 |
+
scopes (Sequence[str]): The list of scopes to check.
|
| 397 |
+
|
| 398 |
+
Returns:
|
| 399 |
+
bool: True if the credentials have the given scopes.
|
| 400 |
+
"""
|
| 401 |
+
credential_scopes = (
|
| 402 |
+
self._scopes if self._scopes is not None else self._default_scopes
|
| 403 |
+
)
|
| 404 |
+
return set(scopes).issubset(set(credential_scopes or []))
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
class Scoped(ReadOnlyScoped):
|
| 408 |
+
"""Interface for credentials whose scopes can be replaced while copying.
|
| 409 |
+
|
| 410 |
+
OAuth 2.0-based credentials allow limiting access using scopes as described
|
| 411 |
+
in `RFC6749 Section 3.3`_.
|
| 412 |
+
If a credential class implements this interface then the credentials either
|
| 413 |
+
use scopes in their implementation.
|
| 414 |
+
|
| 415 |
+
Some credentials require scopes in order to obtain a token. You can check
|
| 416 |
+
if scoping is necessary with :attr:`requires_scopes`::
|
| 417 |
+
|
| 418 |
+
if credentials.requires_scopes:
|
| 419 |
+
# Scoping is required.
|
| 420 |
+
credentials = credentials.create_scoped(['one', 'two'])
|
| 421 |
+
|
| 422 |
+
Credentials that require scopes must either be constructed with scopes::
|
| 423 |
+
|
| 424 |
+
credentials = SomeScopedCredentials(scopes=['one', 'two'])
|
| 425 |
+
|
| 426 |
+
Or must copy an existing instance using :meth:`with_scopes`::
|
| 427 |
+
|
| 428 |
+
scoped_credentials = credentials.with_scopes(scopes=['one', 'two'])
|
| 429 |
+
|
| 430 |
+
Some credentials have scopes but do not allow or require scopes to be set,
|
| 431 |
+
these credentials can be used as-is.
|
| 432 |
+
|
| 433 |
+
.. _RFC6749 Section 3.3: https://tools.ietf.org/html/rfc6749#section-3.3
|
| 434 |
+
"""
|
| 435 |
+
|
| 436 |
+
@abc.abstractmethod
|
| 437 |
+
def with_scopes(self, scopes, default_scopes=None):
|
| 438 |
+
"""Create a copy of these credentials with the specified scopes.
|
| 439 |
+
|
| 440 |
+
Args:
|
| 441 |
+
scopes (Sequence[str]): The list of scopes to attach to the
|
| 442 |
+
current credentials.
|
| 443 |
+
|
| 444 |
+
Raises:
|
| 445 |
+
NotImplementedError: If the credentials' scopes can not be changed.
|
| 446 |
+
This can be avoided by checking :attr:`requires_scopes` before
|
| 447 |
+
calling this method.
|
| 448 |
+
"""
|
| 449 |
+
raise NotImplementedError("This class does not require scoping.")
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
def with_scopes_if_required(credentials, scopes, default_scopes=None):
|
| 453 |
+
"""Creates a copy of the credentials with scopes if scoping is required.
|
| 454 |
+
|
| 455 |
+
This helper function is useful when you do not know (or care to know) the
|
| 456 |
+
specific type of credentials you are using (such as when you use
|
| 457 |
+
:func:`google.auth.default`). This function will call
|
| 458 |
+
:meth:`Scoped.with_scopes` if the credentials are scoped credentials and if
|
| 459 |
+
the credentials require scoping. Otherwise, it will return the credentials
|
| 460 |
+
as-is.
|
| 461 |
+
|
| 462 |
+
Args:
|
| 463 |
+
credentials (google.auth.credentials.Credentials): The credentials to
|
| 464 |
+
scope if necessary.
|
| 465 |
+
scopes (Sequence[str]): The list of scopes to use.
|
| 466 |
+
default_scopes (Sequence[str]): Default scopes passed by a
|
| 467 |
+
Google client library. Use 'scopes' for user-defined scopes.
|
| 468 |
+
|
| 469 |
+
Returns:
|
| 470 |
+
google.auth.credentials.Credentials: Either a new set of scoped
|
| 471 |
+
credentials, or the passed in credentials instance if no scoping
|
| 472 |
+
was required.
|
| 473 |
+
"""
|
| 474 |
+
if isinstance(credentials, Scoped) and credentials.requires_scopes:
|
| 475 |
+
return credentials.with_scopes(scopes, default_scopes=default_scopes)
|
| 476 |
+
else:
|
| 477 |
+
return credentials
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
class Signing(metaclass=abc.ABCMeta):
|
| 481 |
+
"""Interface for credentials that can cryptographically sign messages."""
|
| 482 |
+
|
| 483 |
+
@abc.abstractmethod
|
| 484 |
+
def sign_bytes(self, message):
|
| 485 |
+
"""Signs the given message.
|
| 486 |
+
|
| 487 |
+
Args:
|
| 488 |
+
message (bytes): The message to sign.
|
| 489 |
+
|
| 490 |
+
Returns:
|
| 491 |
+
bytes: The message's cryptographic signature.
|
| 492 |
+
"""
|
| 493 |
+
# pylint: disable=missing-raises-doc,redundant-returns-doc
|
| 494 |
+
# (pylint doesn't recognize that this is abstract)
|
| 495 |
+
raise NotImplementedError("Sign bytes must be implemented.")
|
| 496 |
+
|
| 497 |
+
@abc.abstractproperty
|
| 498 |
+
def signer_email(self):
|
| 499 |
+
"""Optional[str]: An email address that identifies the signer."""
|
| 500 |
+
# pylint: disable=missing-raises-doc
|
| 501 |
+
# (pylint doesn't recognize that this is abstract)
|
| 502 |
+
raise NotImplementedError("Signer email must be implemented.")
|
| 503 |
+
|
| 504 |
+
@abc.abstractproperty
|
| 505 |
+
def signer(self):
|
| 506 |
+
"""google.auth.crypt.Signer: The signer used to sign bytes."""
|
| 507 |
+
# pylint: disable=missing-raises-doc
|
| 508 |
+
# (pylint doesn't recognize that this is abstract)
|
| 509 |
+
raise NotImplementedError("Signer must be implemented.")
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
class TokenState(Enum):
|
| 513 |
+
"""
|
| 514 |
+
Tracks the state of a token.
|
| 515 |
+
FRESH: The token is valid. It is not expired or close to expired, or the token has no expiry.
|
| 516 |
+
STALE: The token is close to expired, and should be refreshed. The token can be used normally.
|
| 517 |
+
INVALID: The token is expired or invalid. The token cannot be used for a normal operation.
|
| 518 |
+
"""
|
| 519 |
+
|
| 520 |
+
FRESH = 1
|
| 521 |
+
STALE = 2
|
| 522 |
+
INVALID = 3
|
lib/python3.10/site-packages/google/auth/downscoped.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2021 Google LLC
|
| 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 |
+
"""Downscoping with Credential Access Boundaries
|
| 16 |
+
|
| 17 |
+
This module provides the ability to downscope credentials using
|
| 18 |
+
`Downscoping with Credential Access Boundaries`_. This is useful to restrict the
|
| 19 |
+
Identity and Access Management (IAM) permissions that a short-lived credential
|
| 20 |
+
can use.
|
| 21 |
+
|
| 22 |
+
To downscope permissions of a source credential, a Credential Access Boundary
|
| 23 |
+
that specifies which resources the new credential can access, as well as
|
| 24 |
+
an upper bound on the permissions that are available on each resource, has to
|
| 25 |
+
be defined. A downscoped credential can then be instantiated using the source
|
| 26 |
+
credential and the Credential Access Boundary.
|
| 27 |
+
|
| 28 |
+
The common pattern of usage is to have a token broker with elevated access
|
| 29 |
+
generate these downscoped credentials from higher access source credentials and
|
| 30 |
+
pass the downscoped short-lived access tokens to a token consumer via some
|
| 31 |
+
secure authenticated channel for limited access to Google Cloud Storage
|
| 32 |
+
resources.
|
| 33 |
+
|
| 34 |
+
For example, a token broker can be set up on a server in a private network.
|
| 35 |
+
Various workloads (token consumers) in the same network will send authenticated
|
| 36 |
+
requests to that broker for downscoped tokens to access or modify specific google
|
| 37 |
+
cloud storage buckets.
|
| 38 |
+
|
| 39 |
+
The broker will instantiate downscoped credentials instances that can be used to
|
| 40 |
+
generate short lived downscoped access tokens that can be passed to the token
|
| 41 |
+
consumer. These downscoped access tokens can be injected by the consumer into
|
| 42 |
+
google.oauth2.Credentials and used to initialize a storage client instance to
|
| 43 |
+
access Google Cloud Storage resources with restricted access.
|
| 44 |
+
|
| 45 |
+
Note: Only Cloud Storage supports Credential Access Boundaries. Other Google
|
| 46 |
+
Cloud services do not support this feature.
|
| 47 |
+
|
| 48 |
+
.. _Downscoping with Credential Access Boundaries: https://cloud.google.com/iam/docs/downscoping-short-lived-credentials
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
import datetime
|
| 52 |
+
|
| 53 |
+
from google.auth import _helpers
|
| 54 |
+
from google.auth import credentials
|
| 55 |
+
from google.auth import exceptions
|
| 56 |
+
from google.oauth2 import sts
|
| 57 |
+
|
| 58 |
+
# The maximum number of access boundary rules a Credential Access Boundary can
|
| 59 |
+
# contain.
|
| 60 |
+
_MAX_ACCESS_BOUNDARY_RULES_COUNT = 10
|
| 61 |
+
# The token exchange grant_type used for exchanging credentials.
|
| 62 |
+
_STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
|
| 63 |
+
# The token exchange requested_token_type. This is always an access_token.
|
| 64 |
+
_STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
|
| 65 |
+
# The STS token URL used to exchanged a short lived access token for a downscoped one.
|
| 66 |
+
_STS_TOKEN_URL_PATTERN = "https://sts.{}/v1/token"
|
| 67 |
+
# The subject token type to use when exchanging a short lived access token for a
|
| 68 |
+
# downscoped token.
|
| 69 |
+
_STS_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class CredentialAccessBoundary(object):
|
| 73 |
+
"""Defines a Credential Access Boundary which contains a list of access boundary
|
| 74 |
+
rules. Each rule contains information on the resource that the rule applies to,
|
| 75 |
+
the upper bound of the permissions that are available on that resource and an
|
| 76 |
+
optional condition to further restrict permissions.
|
| 77 |
+
"""
|
| 78 |
+
|
| 79 |
+
def __init__(self, rules=[]):
|
| 80 |
+
"""Instantiates a Credential Access Boundary. A Credential Access Boundary
|
| 81 |
+
can contain up to 10 access boundary rules.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
rules (Sequence[google.auth.downscoped.AccessBoundaryRule]): The list of
|
| 85 |
+
access boundary rules limiting the access that a downscoped credential
|
| 86 |
+
will have.
|
| 87 |
+
Raises:
|
| 88 |
+
InvalidType: If any of the rules are not a valid type.
|
| 89 |
+
InvalidValue: If the provided rules exceed the maximum allowed.
|
| 90 |
+
"""
|
| 91 |
+
self.rules = rules
|
| 92 |
+
|
| 93 |
+
@property
|
| 94 |
+
def rules(self):
|
| 95 |
+
"""Returns the list of access boundary rules defined on the Credential
|
| 96 |
+
Access Boundary.
|
| 97 |
+
|
| 98 |
+
Returns:
|
| 99 |
+
Tuple[google.auth.downscoped.AccessBoundaryRule, ...]: The list of access
|
| 100 |
+
boundary rules defined on the Credential Access Boundary. These are returned
|
| 101 |
+
as an immutable tuple to prevent modification.
|
| 102 |
+
"""
|
| 103 |
+
return tuple(self._rules)
|
| 104 |
+
|
| 105 |
+
@rules.setter
|
| 106 |
+
def rules(self, value):
|
| 107 |
+
"""Updates the current rules on the Credential Access Boundary. This will overwrite
|
| 108 |
+
the existing set of rules.
|
| 109 |
+
|
| 110 |
+
Args:
|
| 111 |
+
value (Sequence[google.auth.downscoped.AccessBoundaryRule]): The list of
|
| 112 |
+
access boundary rules limiting the access that a downscoped credential
|
| 113 |
+
will have.
|
| 114 |
+
Raises:
|
| 115 |
+
InvalidType: If any of the rules are not a valid type.
|
| 116 |
+
InvalidValue: If the provided rules exceed the maximum allowed.
|
| 117 |
+
"""
|
| 118 |
+
if len(value) > _MAX_ACCESS_BOUNDARY_RULES_COUNT:
|
| 119 |
+
raise exceptions.InvalidValue(
|
| 120 |
+
"Credential access boundary rules can have a maximum of {} rules.".format(
|
| 121 |
+
_MAX_ACCESS_BOUNDARY_RULES_COUNT
|
| 122 |
+
)
|
| 123 |
+
)
|
| 124 |
+
for access_boundary_rule in value:
|
| 125 |
+
if not isinstance(access_boundary_rule, AccessBoundaryRule):
|
| 126 |
+
raise exceptions.InvalidType(
|
| 127 |
+
"List of rules provided do not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."
|
| 128 |
+
)
|
| 129 |
+
# Make a copy of the original list.
|
| 130 |
+
self._rules = list(value)
|
| 131 |
+
|
| 132 |
+
def add_rule(self, rule):
|
| 133 |
+
"""Adds a single access boundary rule to the existing rules.
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
rule (google.auth.downscoped.AccessBoundaryRule): The access boundary rule,
|
| 137 |
+
limiting the access that a downscoped credential will have, to be added to
|
| 138 |
+
the existing rules.
|
| 139 |
+
Raises:
|
| 140 |
+
InvalidType: If any of the rules are not a valid type.
|
| 141 |
+
InvalidValue: If the provided rules exceed the maximum allowed.
|
| 142 |
+
"""
|
| 143 |
+
if len(self.rules) == _MAX_ACCESS_BOUNDARY_RULES_COUNT:
|
| 144 |
+
raise exceptions.InvalidValue(
|
| 145 |
+
"Credential access boundary rules can have a maximum of {} rules.".format(
|
| 146 |
+
_MAX_ACCESS_BOUNDARY_RULES_COUNT
|
| 147 |
+
)
|
| 148 |
+
)
|
| 149 |
+
if not isinstance(rule, AccessBoundaryRule):
|
| 150 |
+
raise exceptions.InvalidType(
|
| 151 |
+
"The provided rule does not contain a valid 'google.auth.downscoped.AccessBoundaryRule'."
|
| 152 |
+
)
|
| 153 |
+
self._rules.append(rule)
|
| 154 |
+
|
| 155 |
+
def to_json(self):
|
| 156 |
+
"""Generates the dictionary representation of the Credential Access Boundary.
|
| 157 |
+
This uses the format expected by the Security Token Service API as documented in
|
| 158 |
+
`Defining a Credential Access Boundary`_.
|
| 159 |
+
|
| 160 |
+
.. _Defining a Credential Access Boundary:
|
| 161 |
+
https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary
|
| 162 |
+
|
| 163 |
+
Returns:
|
| 164 |
+
Mapping: Credential Access Boundary Rule represented in a dictionary object.
|
| 165 |
+
"""
|
| 166 |
+
rules = []
|
| 167 |
+
for access_boundary_rule in self.rules:
|
| 168 |
+
rules.append(access_boundary_rule.to_json())
|
| 169 |
+
|
| 170 |
+
return {"accessBoundary": {"accessBoundaryRules": rules}}
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
class AccessBoundaryRule(object):
|
| 174 |
+
"""Defines an access boundary rule which contains information on the resource that
|
| 175 |
+
the rule applies to, the upper bound of the permissions that are available on that
|
| 176 |
+
resource and an optional condition to further restrict permissions.
|
| 177 |
+
"""
|
| 178 |
+
|
| 179 |
+
def __init__(
|
| 180 |
+
self, available_resource, available_permissions, availability_condition=None
|
| 181 |
+
):
|
| 182 |
+
"""Instantiates a single access boundary rule.
|
| 183 |
+
|
| 184 |
+
Args:
|
| 185 |
+
available_resource (str): The full resource name of the Cloud Storage bucket
|
| 186 |
+
that the rule applies to. Use the format
|
| 187 |
+
"//storage.googleapis.com/projects/_/buckets/bucket-name".
|
| 188 |
+
available_permissions (Sequence[str]): A list defining the upper bound that
|
| 189 |
+
the downscoped token will have on the available permissions for the
|
| 190 |
+
resource. Each value is the identifier for an IAM predefined role or
|
| 191 |
+
custom role, with the prefix "inRole:". For example:
|
| 192 |
+
"inRole:roles/storage.objectViewer".
|
| 193 |
+
Only the permissions in these roles will be available.
|
| 194 |
+
availability_condition (Optional[google.auth.downscoped.AvailabilityCondition]):
|
| 195 |
+
Optional condition that restricts the availability of permissions to
|
| 196 |
+
specific Cloud Storage objects.
|
| 197 |
+
|
| 198 |
+
Raises:
|
| 199 |
+
InvalidType: If any of the parameters are not of the expected types.
|
| 200 |
+
InvalidValue: If any of the parameters are not of the expected values.
|
| 201 |
+
"""
|
| 202 |
+
self.available_resource = available_resource
|
| 203 |
+
self.available_permissions = available_permissions
|
| 204 |
+
self.availability_condition = availability_condition
|
| 205 |
+
|
| 206 |
+
@property
|
| 207 |
+
def available_resource(self):
|
| 208 |
+
"""Returns the current available resource.
|
| 209 |
+
|
| 210 |
+
Returns:
|
| 211 |
+
str: The current available resource.
|
| 212 |
+
"""
|
| 213 |
+
return self._available_resource
|
| 214 |
+
|
| 215 |
+
@available_resource.setter
|
| 216 |
+
def available_resource(self, value):
|
| 217 |
+
"""Updates the current available resource.
|
| 218 |
+
|
| 219 |
+
Args:
|
| 220 |
+
value (str): The updated value of the available resource.
|
| 221 |
+
|
| 222 |
+
Raises:
|
| 223 |
+
google.auth.exceptions.InvalidType: If the value is not a string.
|
| 224 |
+
"""
|
| 225 |
+
if not isinstance(value, str):
|
| 226 |
+
raise exceptions.InvalidType(
|
| 227 |
+
"The provided available_resource is not a string."
|
| 228 |
+
)
|
| 229 |
+
self._available_resource = value
|
| 230 |
+
|
| 231 |
+
@property
|
| 232 |
+
def available_permissions(self):
|
| 233 |
+
"""Returns the current available permissions.
|
| 234 |
+
|
| 235 |
+
Returns:
|
| 236 |
+
Tuple[str, ...]: The current available permissions. These are returned
|
| 237 |
+
as an immutable tuple to prevent modification.
|
| 238 |
+
"""
|
| 239 |
+
return tuple(self._available_permissions)
|
| 240 |
+
|
| 241 |
+
@available_permissions.setter
|
| 242 |
+
def available_permissions(self, value):
|
| 243 |
+
"""Updates the current available permissions.
|
| 244 |
+
|
| 245 |
+
Args:
|
| 246 |
+
value (Sequence[str]): The updated value of the available permissions.
|
| 247 |
+
|
| 248 |
+
Raises:
|
| 249 |
+
InvalidType: If the value is not a list of strings.
|
| 250 |
+
InvalidValue: If the value is not valid.
|
| 251 |
+
"""
|
| 252 |
+
for available_permission in value:
|
| 253 |
+
if not isinstance(available_permission, str):
|
| 254 |
+
raise exceptions.InvalidType(
|
| 255 |
+
"Provided available_permissions are not a list of strings."
|
| 256 |
+
)
|
| 257 |
+
if available_permission.find("inRole:") != 0:
|
| 258 |
+
raise exceptions.InvalidValue(
|
| 259 |
+
"available_permissions must be prefixed with 'inRole:'."
|
| 260 |
+
)
|
| 261 |
+
# Make a copy of the original list.
|
| 262 |
+
self._available_permissions = list(value)
|
| 263 |
+
|
| 264 |
+
@property
|
| 265 |
+
def availability_condition(self):
|
| 266 |
+
"""Returns the current availability condition.
|
| 267 |
+
|
| 268 |
+
Returns:
|
| 269 |
+
Optional[google.auth.downscoped.AvailabilityCondition]: The current
|
| 270 |
+
availability condition.
|
| 271 |
+
"""
|
| 272 |
+
return self._availability_condition
|
| 273 |
+
|
| 274 |
+
@availability_condition.setter
|
| 275 |
+
def availability_condition(self, value):
|
| 276 |
+
"""Updates the current availability condition.
|
| 277 |
+
|
| 278 |
+
Args:
|
| 279 |
+
value (Optional[google.auth.downscoped.AvailabilityCondition]): The updated
|
| 280 |
+
value of the availability condition.
|
| 281 |
+
|
| 282 |
+
Raises:
|
| 283 |
+
google.auth.exceptions.InvalidType: If the value is not of type google.auth.downscoped.AvailabilityCondition
|
| 284 |
+
or None.
|
| 285 |
+
"""
|
| 286 |
+
if not isinstance(value, AvailabilityCondition) and value is not None:
|
| 287 |
+
raise exceptions.InvalidType(
|
| 288 |
+
"The provided availability_condition is not a 'google.auth.downscoped.AvailabilityCondition' or None."
|
| 289 |
+
)
|
| 290 |
+
self._availability_condition = value
|
| 291 |
+
|
| 292 |
+
def to_json(self):
|
| 293 |
+
"""Generates the dictionary representation of the access boundary rule.
|
| 294 |
+
This uses the format expected by the Security Token Service API as documented in
|
| 295 |
+
`Defining a Credential Access Boundary`_.
|
| 296 |
+
|
| 297 |
+
.. _Defining a Credential Access Boundary:
|
| 298 |
+
https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary
|
| 299 |
+
|
| 300 |
+
Returns:
|
| 301 |
+
Mapping: The access boundary rule represented in a dictionary object.
|
| 302 |
+
"""
|
| 303 |
+
json = {
|
| 304 |
+
"availablePermissions": list(self.available_permissions),
|
| 305 |
+
"availableResource": self.available_resource,
|
| 306 |
+
}
|
| 307 |
+
if self.availability_condition:
|
| 308 |
+
json["availabilityCondition"] = self.availability_condition.to_json()
|
| 309 |
+
return json
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
class AvailabilityCondition(object):
|
| 313 |
+
"""An optional condition that can be used as part of a Credential Access Boundary
|
| 314 |
+
to further restrict permissions."""
|
| 315 |
+
|
| 316 |
+
def __init__(self, expression, title=None, description=None):
|
| 317 |
+
"""Instantiates an availability condition using the provided expression and
|
| 318 |
+
optional title or description.
|
| 319 |
+
|
| 320 |
+
Args:
|
| 321 |
+
expression (str): A condition expression that specifies the Cloud Storage
|
| 322 |
+
objects where permissions are available. For example, this expression
|
| 323 |
+
makes permissions available for objects whose name starts with "customer-a":
|
| 324 |
+
"resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a')"
|
| 325 |
+
title (Optional[str]): An optional short string that identifies the purpose of
|
| 326 |
+
the condition.
|
| 327 |
+
description (Optional[str]): Optional details about the purpose of the condition.
|
| 328 |
+
|
| 329 |
+
Raises:
|
| 330 |
+
InvalidType: If any of the parameters are not of the expected types.
|
| 331 |
+
InvalidValue: If any of the parameters are not of the expected values.
|
| 332 |
+
"""
|
| 333 |
+
self.expression = expression
|
| 334 |
+
self.title = title
|
| 335 |
+
self.description = description
|
| 336 |
+
|
| 337 |
+
@property
|
| 338 |
+
def expression(self):
|
| 339 |
+
"""Returns the current condition expression.
|
| 340 |
+
|
| 341 |
+
Returns:
|
| 342 |
+
str: The current conditon expression.
|
| 343 |
+
"""
|
| 344 |
+
return self._expression
|
| 345 |
+
|
| 346 |
+
@expression.setter
|
| 347 |
+
def expression(self, value):
|
| 348 |
+
"""Updates the current condition expression.
|
| 349 |
+
|
| 350 |
+
Args:
|
| 351 |
+
value (str): The updated value of the condition expression.
|
| 352 |
+
|
| 353 |
+
Raises:
|
| 354 |
+
google.auth.exceptions.InvalidType: If the value is not of type string.
|
| 355 |
+
"""
|
| 356 |
+
if not isinstance(value, str):
|
| 357 |
+
raise exceptions.InvalidType("The provided expression is not a string.")
|
| 358 |
+
self._expression = value
|
| 359 |
+
|
| 360 |
+
@property
|
| 361 |
+
def title(self):
|
| 362 |
+
"""Returns the current title.
|
| 363 |
+
|
| 364 |
+
Returns:
|
| 365 |
+
Optional[str]: The current title.
|
| 366 |
+
"""
|
| 367 |
+
return self._title
|
| 368 |
+
|
| 369 |
+
@title.setter
|
| 370 |
+
def title(self, value):
|
| 371 |
+
"""Updates the current title.
|
| 372 |
+
|
| 373 |
+
Args:
|
| 374 |
+
value (Optional[str]): The updated value of the title.
|
| 375 |
+
|
| 376 |
+
Raises:
|
| 377 |
+
google.auth.exceptions.InvalidType: If the value is not of type string or None.
|
| 378 |
+
"""
|
| 379 |
+
if not isinstance(value, str) and value is not None:
|
| 380 |
+
raise exceptions.InvalidType("The provided title is not a string or None.")
|
| 381 |
+
self._title = value
|
| 382 |
+
|
| 383 |
+
@property
|
| 384 |
+
def description(self):
|
| 385 |
+
"""Returns the current description.
|
| 386 |
+
|
| 387 |
+
Returns:
|
| 388 |
+
Optional[str]: The current description.
|
| 389 |
+
"""
|
| 390 |
+
return self._description
|
| 391 |
+
|
| 392 |
+
@description.setter
|
| 393 |
+
def description(self, value):
|
| 394 |
+
"""Updates the current description.
|
| 395 |
+
|
| 396 |
+
Args:
|
| 397 |
+
value (Optional[str]): The updated value of the description.
|
| 398 |
+
|
| 399 |
+
Raises:
|
| 400 |
+
google.auth.exceptions.InvalidType: If the value is not of type string or None.
|
| 401 |
+
"""
|
| 402 |
+
if not isinstance(value, str) and value is not None:
|
| 403 |
+
raise exceptions.InvalidType(
|
| 404 |
+
"The provided description is not a string or None."
|
| 405 |
+
)
|
| 406 |
+
self._description = value
|
| 407 |
+
|
| 408 |
+
def to_json(self):
|
| 409 |
+
"""Generates the dictionary representation of the availability condition.
|
| 410 |
+
This uses the format expected by the Security Token Service API as documented in
|
| 411 |
+
`Defining a Credential Access Boundary`_.
|
| 412 |
+
|
| 413 |
+
.. _Defining a Credential Access Boundary:
|
| 414 |
+
https://cloud.google.com/iam/docs/downscoping-short-lived-credentials#define-boundary
|
| 415 |
+
|
| 416 |
+
Returns:
|
| 417 |
+
Mapping[str, str]: The availability condition represented in a dictionary
|
| 418 |
+
object.
|
| 419 |
+
"""
|
| 420 |
+
json = {"expression": self.expression}
|
| 421 |
+
if self.title:
|
| 422 |
+
json["title"] = self.title
|
| 423 |
+
if self.description:
|
| 424 |
+
json["description"] = self.description
|
| 425 |
+
return json
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
class Credentials(credentials.CredentialsWithQuotaProject):
|
| 429 |
+
"""Defines a set of Google credentials that are downscoped from an existing set
|
| 430 |
+
of Google OAuth2 credentials. This is useful to restrict the Identity and Access
|
| 431 |
+
Management (IAM) permissions that a short-lived credential can use.
|
| 432 |
+
The common pattern of usage is to have a token broker with elevated access
|
| 433 |
+
generate these downscoped credentials from higher access source credentials and
|
| 434 |
+
pass the downscoped short-lived access tokens to a token consumer via some
|
| 435 |
+
secure authenticated channel for limited access to Google Cloud Storage
|
| 436 |
+
resources.
|
| 437 |
+
"""
|
| 438 |
+
|
| 439 |
+
def __init__(
|
| 440 |
+
self,
|
| 441 |
+
source_credentials,
|
| 442 |
+
credential_access_boundary,
|
| 443 |
+
quota_project_id=None,
|
| 444 |
+
universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,
|
| 445 |
+
):
|
| 446 |
+
"""Instantiates a downscoped credentials object using the provided source
|
| 447 |
+
credentials and credential access boundary rules.
|
| 448 |
+
To downscope permissions of a source credential, a Credential Access Boundary
|
| 449 |
+
that specifies which resources the new credential can access, as well as an
|
| 450 |
+
upper bound on the permissions that are available on each resource, has to be
|
| 451 |
+
defined. A downscoped credential can then be instantiated using the source
|
| 452 |
+
credential and the Credential Access Boundary.
|
| 453 |
+
|
| 454 |
+
Args:
|
| 455 |
+
source_credentials (google.auth.credentials.Credentials): The source credentials
|
| 456 |
+
to be downscoped based on the provided Credential Access Boundary rules.
|
| 457 |
+
credential_access_boundary (google.auth.downscoped.CredentialAccessBoundary):
|
| 458 |
+
The Credential Access Boundary which contains a list of access boundary
|
| 459 |
+
rules. Each rule contains information on the resource that the rule applies to,
|
| 460 |
+
the upper bound of the permissions that are available on that resource and an
|
| 461 |
+
optional condition to further restrict permissions.
|
| 462 |
+
quota_project_id (Optional[str]): The optional quota project ID.
|
| 463 |
+
universe_domain (Optional[str]): The universe domain value, default is googleapis.com
|
| 464 |
+
Raises:
|
| 465 |
+
google.auth.exceptions.RefreshError: If the source credentials
|
| 466 |
+
return an error on token refresh.
|
| 467 |
+
google.auth.exceptions.OAuthError: If the STS token exchange
|
| 468 |
+
endpoint returned an error during downscoped token generation.
|
| 469 |
+
"""
|
| 470 |
+
|
| 471 |
+
super(Credentials, self).__init__()
|
| 472 |
+
self._source_credentials = source_credentials
|
| 473 |
+
self._credential_access_boundary = credential_access_boundary
|
| 474 |
+
self._quota_project_id = quota_project_id
|
| 475 |
+
self._universe_domain = universe_domain or credentials.DEFAULT_UNIVERSE_DOMAIN
|
| 476 |
+
self._sts_client = sts.Client(
|
| 477 |
+
_STS_TOKEN_URL_PATTERN.format(self.universe_domain)
|
| 478 |
+
)
|
| 479 |
+
|
| 480 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 481 |
+
def refresh(self, request):
|
| 482 |
+
# Generate an access token from the source credentials.
|
| 483 |
+
self._source_credentials.refresh(request)
|
| 484 |
+
now = _helpers.utcnow()
|
| 485 |
+
# Exchange the access token for a downscoped access token.
|
| 486 |
+
response_data = self._sts_client.exchange_token(
|
| 487 |
+
request=request,
|
| 488 |
+
grant_type=_STS_GRANT_TYPE,
|
| 489 |
+
subject_token=self._source_credentials.token,
|
| 490 |
+
subject_token_type=_STS_SUBJECT_TOKEN_TYPE,
|
| 491 |
+
requested_token_type=_STS_REQUESTED_TOKEN_TYPE,
|
| 492 |
+
additional_options=self._credential_access_boundary.to_json(),
|
| 493 |
+
)
|
| 494 |
+
self.token = response_data.get("access_token")
|
| 495 |
+
# For downscoping CAB flow, the STS endpoint may not return the expiration
|
| 496 |
+
# field for some flows. The generated downscoped token should always have
|
| 497 |
+
# the same expiration time as the source credentials. When no expires_in
|
| 498 |
+
# field is returned in the response, we can just get the expiration time
|
| 499 |
+
# from the source credentials.
|
| 500 |
+
if response_data.get("expires_in"):
|
| 501 |
+
lifetime = datetime.timedelta(seconds=response_data.get("expires_in"))
|
| 502 |
+
self.expiry = now + lifetime
|
| 503 |
+
else:
|
| 504 |
+
self.expiry = self._source_credentials.expiry
|
| 505 |
+
|
| 506 |
+
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
|
| 507 |
+
def with_quota_project(self, quota_project_id):
|
| 508 |
+
return self.__class__(
|
| 509 |
+
self._source_credentials,
|
| 510 |
+
self._credential_access_boundary,
|
| 511 |
+
quota_project_id=quota_project_id,
|
| 512 |
+
)
|
lib/python3.10/site-packages/google/auth/environment_vars.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 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 |
+
"""Environment variables used by :mod:`google.auth`."""
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
PROJECT = "GOOGLE_CLOUD_PROJECT"
|
| 19 |
+
"""Environment variable defining default project.
|
| 20 |
+
|
| 21 |
+
This used by :func:`google.auth.default` to explicitly set a project ID. This
|
| 22 |
+
environment variable is also used by the Google Cloud Python Library.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
LEGACY_PROJECT = "GCLOUD_PROJECT"
|
| 26 |
+
"""Previously used environment variable defining the default project.
|
| 27 |
+
|
| 28 |
+
This environment variable is used instead of the current one in some
|
| 29 |
+
situations (such as Google App Engine).
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
GOOGLE_CLOUD_QUOTA_PROJECT = "GOOGLE_CLOUD_QUOTA_PROJECT"
|
| 33 |
+
"""Environment variable defining the project to be used for
|
| 34 |
+
quota and billing."""
|
| 35 |
+
|
| 36 |
+
CREDENTIALS = "GOOGLE_APPLICATION_CREDENTIALS"
|
| 37 |
+
"""Environment variable defining the location of Google application default
|
| 38 |
+
credentials."""
|
| 39 |
+
|
| 40 |
+
# The environment variable name which can replace ~/.config if set.
|
| 41 |
+
CLOUD_SDK_CONFIG_DIR = "CLOUDSDK_CONFIG"
|
| 42 |
+
"""Environment variable defines the location of Google Cloud SDK's config
|
| 43 |
+
files."""
|
| 44 |
+
|
| 45 |
+
# These two variables allow for customization of the addresses used when
|
| 46 |
+
# contacting the GCE metadata service.
|
| 47 |
+
GCE_METADATA_HOST = "GCE_METADATA_HOST"
|
| 48 |
+
"""Environment variable providing an alternate hostname or host:port to be
|
| 49 |
+
used for GCE metadata requests.
|
| 50 |
+
|
| 51 |
+
This environment variable was originally named GCE_METADATA_ROOT. The system will
|
| 52 |
+
check this environemnt variable first; should there be no value present,
|
| 53 |
+
the system will fall back to the old variable.
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
GCE_METADATA_ROOT = "GCE_METADATA_ROOT"
|
| 57 |
+
"""Old environment variable for GCE_METADATA_HOST."""
|
| 58 |
+
|
| 59 |
+
GCE_METADATA_IP = "GCE_METADATA_IP"
|
| 60 |
+
"""Environment variable providing an alternate ip:port to be used for ip-only
|
| 61 |
+
GCE metadata requests."""
|
| 62 |
+
|
| 63 |
+
GOOGLE_API_USE_CLIENT_CERTIFICATE = "GOOGLE_API_USE_CLIENT_CERTIFICATE"
|
| 64 |
+
"""Environment variable controlling whether to use client certificate or not.
|
| 65 |
+
|
| 66 |
+
The default value is false. Users have to explicitly set this value to true
|
| 67 |
+
in order to use client certificate to establish a mutual TLS channel."""
|
| 68 |
+
|
| 69 |
+
LEGACY_APPENGINE_RUNTIME = "APPENGINE_RUNTIME"
|
| 70 |
+
"""Gen1 environment variable defining the App Engine Runtime.
|
| 71 |
+
|
| 72 |
+
Used to distinguish between GAE gen1 and GAE gen2+.
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
# AWS environment variables used with AWS workload identity pools to retrieve
|
| 76 |
+
# AWS security credentials and the AWS region needed to create a serialized
|
| 77 |
+
# signed requests to the AWS STS GetCalledIdentity API that can be exchanged
|
| 78 |
+
# for a Google access tokens via the GCP STS endpoint.
|
| 79 |
+
# When not available the AWS metadata server is used to retrieve these values.
|
| 80 |
+
AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID"
|
| 81 |
+
AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"
|
| 82 |
+
AWS_SESSION_TOKEN = "AWS_SESSION_TOKEN"
|
| 83 |
+
AWS_REGION = "AWS_REGION"
|
| 84 |
+
AWS_DEFAULT_REGION = "AWS_DEFAULT_REGION"
|
lib/python3.10/site-packages/google/auth/exceptions.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 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 |
+
"""Exceptions used in the google.auth package."""
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class GoogleAuthError(Exception):
|
| 19 |
+
"""Base class for all google.auth errors."""
|
| 20 |
+
|
| 21 |
+
def __init__(self, *args, **kwargs):
|
| 22 |
+
super(GoogleAuthError, self).__init__(*args)
|
| 23 |
+
retryable = kwargs.get("retryable", False)
|
| 24 |
+
self._retryable = retryable
|
| 25 |
+
|
| 26 |
+
@property
|
| 27 |
+
def retryable(self):
|
| 28 |
+
return self._retryable
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class TransportError(GoogleAuthError):
|
| 32 |
+
"""Used to indicate an error occurred during an HTTP request."""
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class RefreshError(GoogleAuthError):
|
| 36 |
+
"""Used to indicate that an refreshing the credentials' access token
|
| 37 |
+
failed."""
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class UserAccessTokenError(GoogleAuthError):
|
| 41 |
+
"""Used to indicate ``gcloud auth print-access-token`` command failed."""
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class DefaultCredentialsError(GoogleAuthError):
|
| 45 |
+
"""Used to indicate that acquiring default credentials failed."""
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class MutualTLSChannelError(GoogleAuthError):
|
| 49 |
+
"""Used to indicate that mutual TLS channel creation is failed, or mutual
|
| 50 |
+
TLS channel credentials is missing or invalid."""
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class ClientCertError(GoogleAuthError):
|
| 54 |
+
"""Used to indicate that client certificate is missing or invalid."""
|
| 55 |
+
|
| 56 |
+
@property
|
| 57 |
+
def retryable(self):
|
| 58 |
+
return False
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class OAuthError(GoogleAuthError):
|
| 62 |
+
"""Used to indicate an error occurred during an OAuth related HTTP
|
| 63 |
+
request."""
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class ReauthFailError(RefreshError):
|
| 67 |
+
"""An exception for when reauth failed."""
|
| 68 |
+
|
| 69 |
+
def __init__(self, message=None, **kwargs):
|
| 70 |
+
super(ReauthFailError, self).__init__(
|
| 71 |
+
"Reauthentication failed. {0}".format(message), **kwargs
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class ReauthSamlChallengeFailError(ReauthFailError):
|
| 76 |
+
"""An exception for SAML reauth challenge failures."""
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class MalformedError(DefaultCredentialsError, ValueError):
|
| 80 |
+
"""An exception for malformed data."""
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class InvalidResource(DefaultCredentialsError, ValueError):
|
| 84 |
+
"""An exception for URL error."""
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class InvalidOperation(DefaultCredentialsError, ValueError):
|
| 88 |
+
"""An exception for invalid operation."""
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class InvalidValue(DefaultCredentialsError, ValueError):
|
| 92 |
+
"""Used to wrap general ValueError of python."""
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class InvalidType(DefaultCredentialsError, TypeError):
|
| 96 |
+
"""Used to wrap general TypeError of python."""
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class OSError(DefaultCredentialsError, EnvironmentError):
|
| 100 |
+
"""Used to wrap EnvironmentError(OSError after python3.3)."""
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class TimeoutError(GoogleAuthError):
|
| 104 |
+
"""Used to indicate a timeout error occurred during an HTTP request."""
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class ResponseError(GoogleAuthError):
|
| 108 |
+
"""Used to indicate an error occurred when reading an HTTP response."""
|
lib/python3.10/site-packages/google/auth/external_account.py
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020 Google LLC
|
| 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 |
+
"""External Account Credentials.
|
| 16 |
+
|
| 17 |
+
This module provides credentials that exchange workload identity pool external
|
| 18 |
+
credentials for Google access tokens. This facilitates accessing Google Cloud
|
| 19 |
+
Platform resources from on-prem and non-Google Cloud platforms (e.g. AWS,
|
| 20 |
+
Microsoft Azure, OIDC identity providers), using native credentials retrieved
|
| 21 |
+
from the current environment without the need to copy, save and manage
|
| 22 |
+
long-lived service account credentials.
|
| 23 |
+
|
| 24 |
+
Specifically, this is intended to use access tokens acquired using the GCP STS
|
| 25 |
+
token exchange endpoint following the `OAuth 2.0 Token Exchange`_ spec.
|
| 26 |
+
|
| 27 |
+
.. _OAuth 2.0 Token Exchange: https://tools.ietf.org/html/rfc8693
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
import abc
|
| 31 |
+
import copy
|
| 32 |
+
from dataclasses import dataclass
|
| 33 |
+
import datetime
|
| 34 |
+
import functools
|
| 35 |
+
import io
|
| 36 |
+
import json
|
| 37 |
+
import re
|
| 38 |
+
|
| 39 |
+
from google.auth import _helpers
|
| 40 |
+
from google.auth import credentials
|
| 41 |
+
from google.auth import exceptions
|
| 42 |
+
from google.auth import impersonated_credentials
|
| 43 |
+
from google.auth import metrics
|
| 44 |
+
from google.oauth2 import sts
|
| 45 |
+
from google.oauth2 import utils
|
| 46 |
+
|
| 47 |
+
# External account JSON type identifier.
|
| 48 |
+
_EXTERNAL_ACCOUNT_JSON_TYPE = "external_account"
|
| 49 |
+
# The token exchange grant_type used for exchanging credentials.
|
| 50 |
+
_STS_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
|
| 51 |
+
# The token exchange requested_token_type. This is always an access_token.
|
| 52 |
+
_STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
|
| 53 |
+
# Cloud resource manager URL used to retrieve project information.
|
| 54 |
+
_CLOUD_RESOURCE_MANAGER = "https://cloudresourcemanager.googleapis.com/v1/projects/"
|
| 55 |
+
# Default Google sts token url.
|
| 56 |
+
_DEFAULT_TOKEN_URL = "https://sts.{universe_domain}/v1/token"
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@dataclass
|
| 60 |
+
class SupplierContext:
|
| 61 |
+
"""A context class that contains information about the requested third party credential that is passed
|
| 62 |
+
to AWS security credential and subject token suppliers.
|
| 63 |
+
|
| 64 |
+
Attributes:
|
| 65 |
+
subject_token_type (str): The requested subject token type based on the Oauth2.0 token exchange spec.
|
| 66 |
+
Expected values include::
|
| 67 |
+
|
| 68 |
+
“urn:ietf:params:oauth:token-type:jwt”
|
| 69 |
+
“urn:ietf:params:oauth:token-type:id-token”
|
| 70 |
+
“urn:ietf:params:oauth:token-type:saml2”
|
| 71 |
+
“urn:ietf:params:aws:token-type:aws4_request”
|
| 72 |
+
|
| 73 |
+
audience (str): The requested audience for the subject token.
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
subject_token_type: str
|
| 77 |
+
audience: str
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class Credentials(
|
| 81 |
+
credentials.Scoped,
|
| 82 |
+
credentials.CredentialsWithQuotaProject,
|
| 83 |
+
credentials.CredentialsWithTokenUri,
|
| 84 |
+
metaclass=abc.ABCMeta,
|
| 85 |
+
):
|
| 86 |
+
"""Base class for all external account credentials.
|
| 87 |
+
|
| 88 |
+
This is used to instantiate Credentials for exchanging external account
|
| 89 |
+
credentials for Google access token and authorizing requests to Google APIs.
|
| 90 |
+
The base class implements the common logic for exchanging external account
|
| 91 |
+
credentials for Google access tokens.
|
| 92 |
+
"""
|
| 93 |
+
|
| 94 |
+
def __init__(
|
| 95 |
+
self,
|
| 96 |
+
audience,
|
| 97 |
+
subject_token_type,
|
| 98 |
+
token_url,
|
| 99 |
+
credential_source,
|
| 100 |
+
service_account_impersonation_url=None,
|
| 101 |
+
service_account_impersonation_options=None,
|
| 102 |
+
client_id=None,
|
| 103 |
+
client_secret=None,
|
| 104 |
+
token_info_url=None,
|
| 105 |
+
quota_project_id=None,
|
| 106 |
+
scopes=None,
|
| 107 |
+
default_scopes=None,
|
| 108 |
+
workforce_pool_user_project=None,
|
| 109 |
+
universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,
|
| 110 |
+
trust_boundary=None,
|
| 111 |
+
):
|
| 112 |
+
"""Instantiates an external account credentials object.
|
| 113 |
+
|
| 114 |
+
Args:
|
| 115 |
+
audience (str): The STS audience field.
|
| 116 |
+
subject_token_type (str): The subject token type based on the Oauth2.0 token exchange spec.
|
| 117 |
+
Expected values include::
|
| 118 |
+
|
| 119 |
+
“urn:ietf:params:oauth:token-type:jwt”
|
| 120 |
+
“urn:ietf:params:oauth:token-type:id-token”
|
| 121 |
+
“urn:ietf:params:oauth:token-type:saml2”
|
| 122 |
+
“urn:ietf:params:aws:token-type:aws4_request”
|
| 123 |
+
|
| 124 |
+
token_url (str): The STS endpoint URL.
|
| 125 |
+
credential_source (Mapping): The credential source dictionary.
|
| 126 |
+
service_account_impersonation_url (Optional[str]): The optional service account
|
| 127 |
+
impersonation generateAccessToken URL.
|
| 128 |
+
client_id (Optional[str]): The optional client ID.
|
| 129 |
+
client_secret (Optional[str]): The optional client secret.
|
| 130 |
+
token_info_url (str): The optional STS endpoint URL for token introspection.
|
| 131 |
+
quota_project_id (Optional[str]): The optional quota project ID.
|
| 132 |
+
scopes (Optional[Sequence[str]]): Optional scopes to request during the
|
| 133 |
+
authorization grant.
|
| 134 |
+
default_scopes (Optional[Sequence[str]]): Default scopes passed by a
|
| 135 |
+
Google client library. Use 'scopes' for user-defined scopes.
|
| 136 |
+
workforce_pool_user_project (Optona[str]): The optional workforce pool user
|
| 137 |
+
project number when the credential corresponds to a workforce pool and not
|
| 138 |
+
a workload identity pool. The underlying principal must still have
|
| 139 |
+
serviceusage.services.use IAM permission to use the project for
|
| 140 |
+
billing/quota.
|
| 141 |
+
universe_domain (str): The universe domain. The default universe
|
| 142 |
+
domain is googleapis.com.
|
| 143 |
+
trust_boundary (str): String representation of trust boundary meta.
|
| 144 |
+
Raises:
|
| 145 |
+
google.auth.exceptions.RefreshError: If the generateAccessToken
|
| 146 |
+
endpoint returned an error.
|
| 147 |
+
"""
|
| 148 |
+
super(Credentials, self).__init__()
|
| 149 |
+
self._audience = audience
|
| 150 |
+
self._subject_token_type = subject_token_type
|
| 151 |
+
self._universe_domain = universe_domain
|
| 152 |
+
self._token_url = token_url
|
| 153 |
+
if self._token_url == _DEFAULT_TOKEN_URL:
|
| 154 |
+
self._token_url = self._token_url.replace(
|
| 155 |
+
"{universe_domain}", self._universe_domain
|
| 156 |
+
)
|
| 157 |
+
self._token_info_url = token_info_url
|
| 158 |
+
self._credential_source = credential_source
|
| 159 |
+
self._service_account_impersonation_url = service_account_impersonation_url
|
| 160 |
+
self._service_account_impersonation_options = (
|
| 161 |
+
service_account_impersonation_options or {}
|
| 162 |
+
)
|
| 163 |
+
self._client_id = client_id
|
| 164 |
+
self._client_secret = client_secret
|
| 165 |
+
self._quota_project_id = quota_project_id
|
| 166 |
+
self._scopes = scopes
|
| 167 |
+
self._default_scopes = default_scopes
|
| 168 |
+
self._workforce_pool_user_project = workforce_pool_user_project
|
| 169 |
+
self._trust_boundary = {
|
| 170 |
+
"locations": [],
|
| 171 |
+
"encoded_locations": "0x0",
|
| 172 |
+
} # expose a placeholder trust boundary value.
|
| 173 |
+
|
| 174 |
+
if self._client_id:
|
| 175 |
+
self._client_auth = utils.ClientAuthentication(
|
| 176 |
+
utils.ClientAuthType.basic, self._client_id, self._client_secret
|
| 177 |
+
)
|
| 178 |
+
else:
|
| 179 |
+
self._client_auth = None
|
| 180 |
+
self._sts_client = sts.Client(self._token_url, self._client_auth)
|
| 181 |
+
|
| 182 |
+
self._metrics_options = self._create_default_metrics_options()
|
| 183 |
+
|
| 184 |
+
self._impersonated_credentials = None
|
| 185 |
+
self._project_id = None
|
| 186 |
+
self._supplier_context = SupplierContext(
|
| 187 |
+
self._subject_token_type, self._audience
|
| 188 |
+
)
|
| 189 |
+
self._cred_file_path = None
|
| 190 |
+
|
| 191 |
+
if not self.is_workforce_pool and self._workforce_pool_user_project:
|
| 192 |
+
# Workload identity pools do not support workforce pool user projects.
|
| 193 |
+
raise exceptions.InvalidValue(
|
| 194 |
+
"workforce_pool_user_project should not be set for non-workforce pool "
|
| 195 |
+
"credentials"
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
@property
|
| 199 |
+
def info(self):
|
| 200 |
+
"""Generates the dictionary representation of the current credentials.
|
| 201 |
+
|
| 202 |
+
Returns:
|
| 203 |
+
Mapping: The dictionary representation of the credentials. This is the
|
| 204 |
+
reverse of "from_info" defined on the subclasses of this class. It is
|
| 205 |
+
useful for serializing the current credentials so it can deserialized
|
| 206 |
+
later.
|
| 207 |
+
"""
|
| 208 |
+
config_info = self._constructor_args()
|
| 209 |
+
config_info.update(
|
| 210 |
+
type=_EXTERNAL_ACCOUNT_JSON_TYPE,
|
| 211 |
+
service_account_impersonation=config_info.pop(
|
| 212 |
+
"service_account_impersonation_options", None
|
| 213 |
+
),
|
| 214 |
+
)
|
| 215 |
+
config_info.pop("scopes", None)
|
| 216 |
+
config_info.pop("default_scopes", None)
|
| 217 |
+
return {key: value for key, value in config_info.items() if value is not None}
|
| 218 |
+
|
| 219 |
+
def _constructor_args(self):
|
| 220 |
+
args = {
|
| 221 |
+
"audience": self._audience,
|
| 222 |
+
"subject_token_type": self._subject_token_type,
|
| 223 |
+
"token_url": self._token_url,
|
| 224 |
+
"token_info_url": self._token_info_url,
|
| 225 |
+
"service_account_impersonation_url": self._service_account_impersonation_url,
|
| 226 |
+
"service_account_impersonation_options": copy.deepcopy(
|
| 227 |
+
self._service_account_impersonation_options
|
| 228 |
+
)
|
| 229 |
+
or None,
|
| 230 |
+
"credential_source": copy.deepcopy(self._credential_source),
|
| 231 |
+
"quota_project_id": self._quota_project_id,
|
| 232 |
+
"client_id": self._client_id,
|
| 233 |
+
"client_secret": self._client_secret,
|
| 234 |
+
"workforce_pool_user_project": self._workforce_pool_user_project,
|
| 235 |
+
"scopes": self._scopes,
|
| 236 |
+
"default_scopes": self._default_scopes,
|
| 237 |
+
"universe_domain": self._universe_domain,
|
| 238 |
+
}
|
| 239 |
+
if not self.is_workforce_pool:
|
| 240 |
+
args.pop("workforce_pool_user_project")
|
| 241 |
+
return args
|
| 242 |
+
|
| 243 |
+
@property
|
| 244 |
+
def service_account_email(self):
|
| 245 |
+
"""Returns the service account email if service account impersonation is used.
|
| 246 |
+
|
| 247 |
+
Returns:
|
| 248 |
+
Optional[str]: The service account email if impersonation is used. Otherwise
|
| 249 |
+
None is returned.
|
| 250 |
+
"""
|
| 251 |
+
if self._service_account_impersonation_url:
|
| 252 |
+
# Parse email from URL. The formal looks as follows:
|
| 253 |
+
# https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/name@project-id.iam.gserviceaccount.com:generateAccessToken
|
| 254 |
+
url = self._service_account_impersonation_url
|
| 255 |
+
start_index = url.rfind("/")
|
| 256 |
+
end_index = url.find(":generateAccessToken")
|
| 257 |
+
if start_index != -1 and end_index != -1 and start_index < end_index:
|
| 258 |
+
start_index = start_index + 1
|
| 259 |
+
return url[start_index:end_index]
|
| 260 |
+
return None
|
| 261 |
+
|
| 262 |
+
@property
|
| 263 |
+
def is_user(self):
|
| 264 |
+
"""Returns whether the credentials represent a user (True) or workload (False).
|
| 265 |
+
Workloads behave similarly to service accounts. Currently workloads will use
|
| 266 |
+
service account impersonation but will eventually not require impersonation.
|
| 267 |
+
As a result, this property is more reliable than the service account email
|
| 268 |
+
property in determining if the credentials represent a user or workload.
|
| 269 |
+
|
| 270 |
+
Returns:
|
| 271 |
+
bool: True if the credentials represent a user. False if they represent a
|
| 272 |
+
workload.
|
| 273 |
+
"""
|
| 274 |
+
# If service account impersonation is used, the credentials will always represent a
|
| 275 |
+
# service account.
|
| 276 |
+
if self._service_account_impersonation_url:
|
| 277 |
+
return False
|
| 278 |
+
return self.is_workforce_pool
|
| 279 |
+
|
| 280 |
+
@property
|
| 281 |
+
def is_workforce_pool(self):
|
| 282 |
+
"""Returns whether the credentials represent a workforce pool (True) or
|
| 283 |
+
workload (False) based on the credentials' audience.
|
| 284 |
+
|
| 285 |
+
This will also return True for impersonated workforce pool credentials.
|
| 286 |
+
|
| 287 |
+
Returns:
|
| 288 |
+
bool: True if the credentials represent a workforce pool. False if they
|
| 289 |
+
represent a workload.
|
| 290 |
+
"""
|
| 291 |
+
# Workforce pools representing users have the following audience format:
|
| 292 |
+
# //iam.googleapis.com/locations/$location/workforcePools/$poolId/providers/$providerId
|
| 293 |
+
p = re.compile(r"//iam\.googleapis\.com/locations/[^/]+/workforcePools/")
|
| 294 |
+
return p.match(self._audience or "") is not None
|
| 295 |
+
|
| 296 |
+
@property
|
| 297 |
+
def requires_scopes(self):
|
| 298 |
+
"""Checks if the credentials requires scopes.
|
| 299 |
+
|
| 300 |
+
Returns:
|
| 301 |
+
bool: True if there are no scopes set otherwise False.
|
| 302 |
+
"""
|
| 303 |
+
return not self._scopes and not self._default_scopes
|
| 304 |
+
|
| 305 |
+
@property
|
| 306 |
+
def project_number(self):
|
| 307 |
+
"""Optional[str]: The project number corresponding to the workload identity pool."""
|
| 308 |
+
|
| 309 |
+
# STS audience pattern:
|
| 310 |
+
# //iam.googleapis.com/projects/$PROJECT_NUMBER/locations/...
|
| 311 |
+
components = self._audience.split("/")
|
| 312 |
+
try:
|
| 313 |
+
project_index = components.index("projects")
|
| 314 |
+
if project_index + 1 < len(components):
|
| 315 |
+
return components[project_index + 1] or None
|
| 316 |
+
except ValueError:
|
| 317 |
+
return None
|
| 318 |
+
|
| 319 |
+
@property
|
| 320 |
+
def token_info_url(self):
|
| 321 |
+
"""Optional[str]: The STS token introspection endpoint."""
|
| 322 |
+
|
| 323 |
+
return self._token_info_url
|
| 324 |
+
|
| 325 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 326 |
+
def get_cred_info(self):
|
| 327 |
+
if self._cred_file_path:
|
| 328 |
+
cred_info_json = {
|
| 329 |
+
"credential_source": self._cred_file_path,
|
| 330 |
+
"credential_type": "external account credentials",
|
| 331 |
+
}
|
| 332 |
+
if self.service_account_email:
|
| 333 |
+
cred_info_json["principal"] = self.service_account_email
|
| 334 |
+
return cred_info_json
|
| 335 |
+
return None
|
| 336 |
+
|
| 337 |
+
@_helpers.copy_docstring(credentials.Scoped)
|
| 338 |
+
def with_scopes(self, scopes, default_scopes=None):
|
| 339 |
+
kwargs = self._constructor_args()
|
| 340 |
+
kwargs.update(scopes=scopes, default_scopes=default_scopes)
|
| 341 |
+
scoped = self.__class__(**kwargs)
|
| 342 |
+
scoped._cred_file_path = self._cred_file_path
|
| 343 |
+
scoped._metrics_options = self._metrics_options
|
| 344 |
+
return scoped
|
| 345 |
+
|
| 346 |
+
@abc.abstractmethod
|
| 347 |
+
def retrieve_subject_token(self, request):
|
| 348 |
+
"""Retrieves the subject token using the credential_source object.
|
| 349 |
+
|
| 350 |
+
Args:
|
| 351 |
+
request (google.auth.transport.Request): A callable used to make
|
| 352 |
+
HTTP requests.
|
| 353 |
+
Returns:
|
| 354 |
+
str: The retrieved subject token.
|
| 355 |
+
"""
|
| 356 |
+
# pylint: disable=missing-raises-doc
|
| 357 |
+
# (pylint doesn't recognize that this is abstract)
|
| 358 |
+
raise NotImplementedError("retrieve_subject_token must be implemented")
|
| 359 |
+
|
| 360 |
+
def get_project_id(self, request):
|
| 361 |
+
"""Retrieves the project ID corresponding to the workload identity or workforce pool.
|
| 362 |
+
For workforce pool credentials, it returns the project ID corresponding to
|
| 363 |
+
the workforce_pool_user_project.
|
| 364 |
+
|
| 365 |
+
When not determinable, None is returned.
|
| 366 |
+
|
| 367 |
+
This is introduced to support the current pattern of using the Auth library:
|
| 368 |
+
|
| 369 |
+
credentials, project_id = google.auth.default()
|
| 370 |
+
|
| 371 |
+
The resource may not have permission (resourcemanager.projects.get) to
|
| 372 |
+
call this API or the required scopes may not be selected:
|
| 373 |
+
https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes
|
| 374 |
+
|
| 375 |
+
Args:
|
| 376 |
+
request (google.auth.transport.Request): A callable used to make
|
| 377 |
+
HTTP requests.
|
| 378 |
+
Returns:
|
| 379 |
+
Optional[str]: The project ID corresponding to the workload identity pool
|
| 380 |
+
or workforce pool if determinable.
|
| 381 |
+
"""
|
| 382 |
+
if self._project_id:
|
| 383 |
+
# If already retrieved, return the cached project ID value.
|
| 384 |
+
return self._project_id
|
| 385 |
+
scopes = self._scopes if self._scopes is not None else self._default_scopes
|
| 386 |
+
# Scopes are required in order to retrieve a valid access token.
|
| 387 |
+
project_number = self.project_number or self._workforce_pool_user_project
|
| 388 |
+
if project_number and scopes:
|
| 389 |
+
headers = {}
|
| 390 |
+
url = _CLOUD_RESOURCE_MANAGER + project_number
|
| 391 |
+
self.before_request(request, "GET", url, headers)
|
| 392 |
+
response = request(url=url, method="GET", headers=headers)
|
| 393 |
+
|
| 394 |
+
response_body = (
|
| 395 |
+
response.data.decode("utf-8")
|
| 396 |
+
if hasattr(response.data, "decode")
|
| 397 |
+
else response.data
|
| 398 |
+
)
|
| 399 |
+
response_data = json.loads(response_body)
|
| 400 |
+
|
| 401 |
+
if response.status == 200:
|
| 402 |
+
# Cache result as this field is immutable.
|
| 403 |
+
self._project_id = response_data.get("projectId")
|
| 404 |
+
return self._project_id
|
| 405 |
+
|
| 406 |
+
return None
|
| 407 |
+
|
| 408 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 409 |
+
def refresh(self, request):
|
| 410 |
+
scopes = self._scopes if self._scopes is not None else self._default_scopes
|
| 411 |
+
|
| 412 |
+
# Inject client certificate into request.
|
| 413 |
+
if self._mtls_required():
|
| 414 |
+
request = functools.partial(
|
| 415 |
+
request, cert=self._get_mtls_cert_and_key_paths()
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
if self._should_initialize_impersonated_credentials():
|
| 419 |
+
self._impersonated_credentials = self._initialize_impersonated_credentials()
|
| 420 |
+
|
| 421 |
+
if self._impersonated_credentials:
|
| 422 |
+
self._impersonated_credentials.refresh(request)
|
| 423 |
+
self.token = self._impersonated_credentials.token
|
| 424 |
+
self.expiry = self._impersonated_credentials.expiry
|
| 425 |
+
else:
|
| 426 |
+
now = _helpers.utcnow()
|
| 427 |
+
additional_options = None
|
| 428 |
+
# Do not pass workforce_pool_user_project when client authentication
|
| 429 |
+
# is used. The client ID is sufficient for determining the user project.
|
| 430 |
+
if self._workforce_pool_user_project and not self._client_id:
|
| 431 |
+
additional_options = {"userProject": self._workforce_pool_user_project}
|
| 432 |
+
additional_headers = {
|
| 433 |
+
metrics.API_CLIENT_HEADER: metrics.byoid_metrics_header(
|
| 434 |
+
self._metrics_options
|
| 435 |
+
)
|
| 436 |
+
}
|
| 437 |
+
response_data = self._sts_client.exchange_token(
|
| 438 |
+
request=request,
|
| 439 |
+
grant_type=_STS_GRANT_TYPE,
|
| 440 |
+
subject_token=self.retrieve_subject_token(request),
|
| 441 |
+
subject_token_type=self._subject_token_type,
|
| 442 |
+
audience=self._audience,
|
| 443 |
+
scopes=scopes,
|
| 444 |
+
requested_token_type=_STS_REQUESTED_TOKEN_TYPE,
|
| 445 |
+
additional_options=additional_options,
|
| 446 |
+
additional_headers=additional_headers,
|
| 447 |
+
)
|
| 448 |
+
self.token = response_data.get("access_token")
|
| 449 |
+
expires_in = response_data.get("expires_in")
|
| 450 |
+
# Some services do not respect the OAUTH2.0 RFC and send expires_in as a
|
| 451 |
+
# JSON String.
|
| 452 |
+
if isinstance(expires_in, str):
|
| 453 |
+
expires_in = int(expires_in)
|
| 454 |
+
|
| 455 |
+
lifetime = datetime.timedelta(seconds=expires_in)
|
| 456 |
+
|
| 457 |
+
self.expiry = now + lifetime
|
| 458 |
+
|
| 459 |
+
def _make_copy(self):
|
| 460 |
+
kwargs = self._constructor_args()
|
| 461 |
+
new_cred = self.__class__(**kwargs)
|
| 462 |
+
new_cred._cred_file_path = self._cred_file_path
|
| 463 |
+
new_cred._metrics_options = self._metrics_options
|
| 464 |
+
return new_cred
|
| 465 |
+
|
| 466 |
+
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
|
| 467 |
+
def with_quota_project(self, quota_project_id):
|
| 468 |
+
# Return copy of instance with the provided quota project ID.
|
| 469 |
+
cred = self._make_copy()
|
| 470 |
+
cred._quota_project_id = quota_project_id
|
| 471 |
+
return cred
|
| 472 |
+
|
| 473 |
+
@_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
|
| 474 |
+
def with_token_uri(self, token_uri):
|
| 475 |
+
cred = self._make_copy()
|
| 476 |
+
cred._token_url = token_uri
|
| 477 |
+
return cred
|
| 478 |
+
|
| 479 |
+
@_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
|
| 480 |
+
def with_universe_domain(self, universe_domain):
|
| 481 |
+
cred = self._make_copy()
|
| 482 |
+
cred._universe_domain = universe_domain
|
| 483 |
+
return cred
|
| 484 |
+
|
| 485 |
+
def _should_initialize_impersonated_credentials(self):
|
| 486 |
+
return (
|
| 487 |
+
self._service_account_impersonation_url is not None
|
| 488 |
+
and self._impersonated_credentials is None
|
| 489 |
+
)
|
| 490 |
+
|
| 491 |
+
def _initialize_impersonated_credentials(self):
|
| 492 |
+
"""Generates an impersonated credentials.
|
| 493 |
+
|
| 494 |
+
For more details, see `projects.serviceAccounts.generateAccessToken`_.
|
| 495 |
+
|
| 496 |
+
.. _projects.serviceAccounts.generateAccessToken: https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken
|
| 497 |
+
|
| 498 |
+
Returns:
|
| 499 |
+
impersonated_credentials.Credential: The impersonated credentials
|
| 500 |
+
object.
|
| 501 |
+
|
| 502 |
+
Raises:
|
| 503 |
+
google.auth.exceptions.RefreshError: If the generateAccessToken
|
| 504 |
+
endpoint returned an error.
|
| 505 |
+
"""
|
| 506 |
+
# Return copy of instance with no service account impersonation.
|
| 507 |
+
kwargs = self._constructor_args()
|
| 508 |
+
kwargs.update(
|
| 509 |
+
service_account_impersonation_url=None,
|
| 510 |
+
service_account_impersonation_options={},
|
| 511 |
+
)
|
| 512 |
+
source_credentials = self.__class__(**kwargs)
|
| 513 |
+
source_credentials._metrics_options = self._metrics_options
|
| 514 |
+
|
| 515 |
+
# Determine target_principal.
|
| 516 |
+
target_principal = self.service_account_email
|
| 517 |
+
if not target_principal:
|
| 518 |
+
raise exceptions.RefreshError(
|
| 519 |
+
"Unable to determine target principal from service account impersonation URL."
|
| 520 |
+
)
|
| 521 |
+
|
| 522 |
+
scopes = self._scopes if self._scopes is not None else self._default_scopes
|
| 523 |
+
# Initialize and return impersonated credentials.
|
| 524 |
+
return impersonated_credentials.Credentials(
|
| 525 |
+
source_credentials=source_credentials,
|
| 526 |
+
target_principal=target_principal,
|
| 527 |
+
target_scopes=scopes,
|
| 528 |
+
quota_project_id=self._quota_project_id,
|
| 529 |
+
iam_endpoint_override=self._service_account_impersonation_url,
|
| 530 |
+
lifetime=self._service_account_impersonation_options.get(
|
| 531 |
+
"token_lifetime_seconds"
|
| 532 |
+
),
|
| 533 |
+
)
|
| 534 |
+
|
| 535 |
+
def _create_default_metrics_options(self):
|
| 536 |
+
metrics_options = {}
|
| 537 |
+
if self._service_account_impersonation_url:
|
| 538 |
+
metrics_options["sa-impersonation"] = "true"
|
| 539 |
+
else:
|
| 540 |
+
metrics_options["sa-impersonation"] = "false"
|
| 541 |
+
if self._service_account_impersonation_options.get("token_lifetime_seconds"):
|
| 542 |
+
metrics_options["config-lifetime"] = "true"
|
| 543 |
+
else:
|
| 544 |
+
metrics_options["config-lifetime"] = "false"
|
| 545 |
+
|
| 546 |
+
return metrics_options
|
| 547 |
+
|
| 548 |
+
def _mtls_required(self):
|
| 549 |
+
"""Returns a boolean representing whether the current credential is configured
|
| 550 |
+
for mTLS and should add a certificate to the outgoing calls to the sts and service
|
| 551 |
+
account impersonation endpoint.
|
| 552 |
+
|
| 553 |
+
Returns:
|
| 554 |
+
bool: True if the credential is configured for mTLS, False if it is not.
|
| 555 |
+
"""
|
| 556 |
+
return False
|
| 557 |
+
|
| 558 |
+
def _get_mtls_cert_and_key_paths(self):
|
| 559 |
+
"""Gets the file locations for a certificate and private key file
|
| 560 |
+
to be used for configuring mTLS for the sts and service account
|
| 561 |
+
impersonation calls. Currently only expected to return a value when using
|
| 562 |
+
X509 workload identity federation.
|
| 563 |
+
|
| 564 |
+
Returns:
|
| 565 |
+
Tuple[str, str]: The cert and key file locations as strings in a tuple.
|
| 566 |
+
|
| 567 |
+
Raises:
|
| 568 |
+
NotImplementedError: When the current credential is not configured for
|
| 569 |
+
mTLS.
|
| 570 |
+
"""
|
| 571 |
+
raise NotImplementedError(
|
| 572 |
+
"_get_mtls_cert_and_key_location must be implemented."
|
| 573 |
+
)
|
| 574 |
+
|
| 575 |
+
@classmethod
|
| 576 |
+
def from_info(cls, info, **kwargs):
|
| 577 |
+
"""Creates a Credentials instance from parsed external account info.
|
| 578 |
+
|
| 579 |
+
Args:
|
| 580 |
+
info (Mapping[str, str]): The external account info in Google
|
| 581 |
+
format.
|
| 582 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 583 |
+
|
| 584 |
+
Returns:
|
| 585 |
+
google.auth.identity_pool.Credentials: The constructed
|
| 586 |
+
credentials.
|
| 587 |
+
|
| 588 |
+
Raises:
|
| 589 |
+
InvalidValue: For invalid parameters.
|
| 590 |
+
"""
|
| 591 |
+
return cls(
|
| 592 |
+
audience=info.get("audience"),
|
| 593 |
+
subject_token_type=info.get("subject_token_type"),
|
| 594 |
+
token_url=info.get("token_url"),
|
| 595 |
+
token_info_url=info.get("token_info_url"),
|
| 596 |
+
service_account_impersonation_url=info.get(
|
| 597 |
+
"service_account_impersonation_url"
|
| 598 |
+
),
|
| 599 |
+
service_account_impersonation_options=info.get(
|
| 600 |
+
"service_account_impersonation"
|
| 601 |
+
)
|
| 602 |
+
or {},
|
| 603 |
+
client_id=info.get("client_id"),
|
| 604 |
+
client_secret=info.get("client_secret"),
|
| 605 |
+
credential_source=info.get("credential_source"),
|
| 606 |
+
quota_project_id=info.get("quota_project_id"),
|
| 607 |
+
workforce_pool_user_project=info.get("workforce_pool_user_project"),
|
| 608 |
+
universe_domain=info.get(
|
| 609 |
+
"universe_domain", credentials.DEFAULT_UNIVERSE_DOMAIN
|
| 610 |
+
),
|
| 611 |
+
**kwargs
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
+
@classmethod
|
| 615 |
+
def from_file(cls, filename, **kwargs):
|
| 616 |
+
"""Creates a Credentials instance from an external account json file.
|
| 617 |
+
|
| 618 |
+
Args:
|
| 619 |
+
filename (str): The path to the external account json file.
|
| 620 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 621 |
+
|
| 622 |
+
Returns:
|
| 623 |
+
google.auth.identity_pool.Credentials: The constructed
|
| 624 |
+
credentials.
|
| 625 |
+
"""
|
| 626 |
+
with io.open(filename, "r", encoding="utf-8") as json_file:
|
| 627 |
+
data = json.load(json_file)
|
| 628 |
+
return cls.from_info(data, **kwargs)
|
lib/python3.10/site-packages/google/auth/external_account_authorized_user.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2022 Google LLC
|
| 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 |
+
"""External Account Authorized User Credentials.
|
| 16 |
+
This module provides credentials based on OAuth 2.0 access and refresh tokens.
|
| 17 |
+
These credentials usually access resources on behalf of a user (resource
|
| 18 |
+
owner).
|
| 19 |
+
|
| 20 |
+
Specifically, these are sourced using external identities via Workforce Identity Federation.
|
| 21 |
+
|
| 22 |
+
Obtaining the initial access and refresh token can be done through the Google Cloud CLI.
|
| 23 |
+
|
| 24 |
+
Example credential:
|
| 25 |
+
{
|
| 26 |
+
"type": "external_account_authorized_user",
|
| 27 |
+
"audience": "//iam.googleapis.com/locations/global/workforcePools/$WORKFORCE_POOL_ID/providers/$PROVIDER_ID",
|
| 28 |
+
"refresh_token": "refreshToken",
|
| 29 |
+
"token_url": "https://sts.googleapis.com/v1/oauth/token",
|
| 30 |
+
"token_info_url": "https://sts.googleapis.com/v1/instrospect",
|
| 31 |
+
"client_id": "clientId",
|
| 32 |
+
"client_secret": "clientSecret"
|
| 33 |
+
}
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
import datetime
|
| 37 |
+
import io
|
| 38 |
+
import json
|
| 39 |
+
|
| 40 |
+
from google.auth import _helpers
|
| 41 |
+
from google.auth import credentials
|
| 42 |
+
from google.auth import exceptions
|
| 43 |
+
from google.oauth2 import sts
|
| 44 |
+
from google.oauth2 import utils
|
| 45 |
+
|
| 46 |
+
_EXTERNAL_ACCOUNT_AUTHORIZED_USER_JSON_TYPE = "external_account_authorized_user"
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class Credentials(
|
| 50 |
+
credentials.CredentialsWithQuotaProject,
|
| 51 |
+
credentials.ReadOnlyScoped,
|
| 52 |
+
credentials.CredentialsWithTokenUri,
|
| 53 |
+
):
|
| 54 |
+
"""Credentials for External Account Authorized Users.
|
| 55 |
+
|
| 56 |
+
This is used to instantiate Credentials for exchanging refresh tokens from
|
| 57 |
+
authorized users for Google access token and authorizing requests to Google
|
| 58 |
+
APIs.
|
| 59 |
+
|
| 60 |
+
The credentials are considered immutable. If you want to modify the
|
| 61 |
+
quota project, use `with_quota_project` and if you want to modify the token
|
| 62 |
+
uri, use `with_token_uri`.
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
def __init__(
|
| 66 |
+
self,
|
| 67 |
+
token=None,
|
| 68 |
+
expiry=None,
|
| 69 |
+
refresh_token=None,
|
| 70 |
+
audience=None,
|
| 71 |
+
client_id=None,
|
| 72 |
+
client_secret=None,
|
| 73 |
+
token_url=None,
|
| 74 |
+
token_info_url=None,
|
| 75 |
+
revoke_url=None,
|
| 76 |
+
scopes=None,
|
| 77 |
+
quota_project_id=None,
|
| 78 |
+
universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,
|
| 79 |
+
):
|
| 80 |
+
"""Instantiates a external account authorized user credentials object.
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
token (str): The OAuth 2.0 access token. Can be None if refresh information
|
| 84 |
+
is provided.
|
| 85 |
+
expiry (datetime.datetime): The optional expiration datetime of the OAuth 2.0 access
|
| 86 |
+
token.
|
| 87 |
+
refresh_token (str): The optional OAuth 2.0 refresh token. If specified,
|
| 88 |
+
credentials can be refreshed.
|
| 89 |
+
audience (str): The optional STS audience which contains the resource name for the workforce
|
| 90 |
+
pool and the provider identifier in that pool.
|
| 91 |
+
client_id (str): The OAuth 2.0 client ID. Must be specified for refresh, can be left as
|
| 92 |
+
None if the token can not be refreshed.
|
| 93 |
+
client_secret (str): The OAuth 2.0 client secret. Must be specified for refresh, can be
|
| 94 |
+
left as None if the token can not be refreshed.
|
| 95 |
+
token_url (str): The optional STS token exchange endpoint for refresh. Must be specified for
|
| 96 |
+
refresh, can be left as None if the token can not be refreshed.
|
| 97 |
+
token_info_url (str): The optional STS endpoint URL for token introspection.
|
| 98 |
+
revoke_url (str): The optional STS endpoint URL for revoking tokens.
|
| 99 |
+
quota_project_id (str): The optional project ID used for quota and billing.
|
| 100 |
+
This project may be different from the project used to
|
| 101 |
+
create the credentials.
|
| 102 |
+
universe_domain (Optional[str]): The universe domain. The default value
|
| 103 |
+
is googleapis.com.
|
| 104 |
+
|
| 105 |
+
Returns:
|
| 106 |
+
google.auth.external_account_authorized_user.Credentials: The
|
| 107 |
+
constructed credentials.
|
| 108 |
+
"""
|
| 109 |
+
super(Credentials, self).__init__()
|
| 110 |
+
|
| 111 |
+
self.token = token
|
| 112 |
+
self.expiry = expiry
|
| 113 |
+
self._audience = audience
|
| 114 |
+
self._refresh_token = refresh_token
|
| 115 |
+
self._token_url = token_url
|
| 116 |
+
self._token_info_url = token_info_url
|
| 117 |
+
self._client_id = client_id
|
| 118 |
+
self._client_secret = client_secret
|
| 119 |
+
self._revoke_url = revoke_url
|
| 120 |
+
self._quota_project_id = quota_project_id
|
| 121 |
+
self._scopes = scopes
|
| 122 |
+
self._universe_domain = universe_domain or credentials.DEFAULT_UNIVERSE_DOMAIN
|
| 123 |
+
self._cred_file_path = None
|
| 124 |
+
|
| 125 |
+
if not self.valid and not self.can_refresh:
|
| 126 |
+
raise exceptions.InvalidOperation(
|
| 127 |
+
"Token should be created with fields to make it valid (`token` and "
|
| 128 |
+
"`expiry`), or fields to allow it to refresh (`refresh_token`, "
|
| 129 |
+
"`token_url`, `client_id`, `client_secret`)."
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
self._client_auth = None
|
| 133 |
+
if self._client_id:
|
| 134 |
+
self._client_auth = utils.ClientAuthentication(
|
| 135 |
+
utils.ClientAuthType.basic, self._client_id, self._client_secret
|
| 136 |
+
)
|
| 137 |
+
self._sts_client = sts.Client(self._token_url, self._client_auth)
|
| 138 |
+
|
| 139 |
+
@property
|
| 140 |
+
def info(self):
|
| 141 |
+
"""Generates the serializable dictionary representation of the current
|
| 142 |
+
credentials.
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
Mapping: The dictionary representation of the credentials. This is the
|
| 146 |
+
reverse of the "from_info" method defined in this class. It is
|
| 147 |
+
useful for serializing the current credentials so it can deserialized
|
| 148 |
+
later.
|
| 149 |
+
"""
|
| 150 |
+
config_info = self.constructor_args()
|
| 151 |
+
config_info.update(type=_EXTERNAL_ACCOUNT_AUTHORIZED_USER_JSON_TYPE)
|
| 152 |
+
if config_info["expiry"]:
|
| 153 |
+
config_info["expiry"] = config_info["expiry"].isoformat() + "Z"
|
| 154 |
+
|
| 155 |
+
return {key: value for key, value in config_info.items() if value is not None}
|
| 156 |
+
|
| 157 |
+
def constructor_args(self):
|
| 158 |
+
return {
|
| 159 |
+
"audience": self._audience,
|
| 160 |
+
"refresh_token": self._refresh_token,
|
| 161 |
+
"token_url": self._token_url,
|
| 162 |
+
"token_info_url": self._token_info_url,
|
| 163 |
+
"client_id": self._client_id,
|
| 164 |
+
"client_secret": self._client_secret,
|
| 165 |
+
"token": self.token,
|
| 166 |
+
"expiry": self.expiry,
|
| 167 |
+
"revoke_url": self._revoke_url,
|
| 168 |
+
"scopes": self._scopes,
|
| 169 |
+
"quota_project_id": self._quota_project_id,
|
| 170 |
+
"universe_domain": self._universe_domain,
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
@property
|
| 174 |
+
def scopes(self):
|
| 175 |
+
"""Optional[str]: The OAuth 2.0 permission scopes."""
|
| 176 |
+
return self._scopes
|
| 177 |
+
|
| 178 |
+
@property
|
| 179 |
+
def requires_scopes(self):
|
| 180 |
+
""" False: OAuth 2.0 credentials have their scopes set when
|
| 181 |
+
the initial token is requested and can not be changed."""
|
| 182 |
+
return False
|
| 183 |
+
|
| 184 |
+
@property
|
| 185 |
+
def client_id(self):
|
| 186 |
+
"""Optional[str]: The OAuth 2.0 client ID."""
|
| 187 |
+
return self._client_id
|
| 188 |
+
|
| 189 |
+
@property
|
| 190 |
+
def client_secret(self):
|
| 191 |
+
"""Optional[str]: The OAuth 2.0 client secret."""
|
| 192 |
+
return self._client_secret
|
| 193 |
+
|
| 194 |
+
@property
|
| 195 |
+
def audience(self):
|
| 196 |
+
"""Optional[str]: The STS audience which contains the resource name for the
|
| 197 |
+
workforce pool and the provider identifier in that pool."""
|
| 198 |
+
return self._audience
|
| 199 |
+
|
| 200 |
+
@property
|
| 201 |
+
def refresh_token(self):
|
| 202 |
+
"""Optional[str]: The OAuth 2.0 refresh token."""
|
| 203 |
+
return self._refresh_token
|
| 204 |
+
|
| 205 |
+
@property
|
| 206 |
+
def token_url(self):
|
| 207 |
+
"""Optional[str]: The STS token exchange endpoint for refresh."""
|
| 208 |
+
return self._token_url
|
| 209 |
+
|
| 210 |
+
@property
|
| 211 |
+
def token_info_url(self):
|
| 212 |
+
"""Optional[str]: The STS endpoint for token info."""
|
| 213 |
+
return self._token_info_url
|
| 214 |
+
|
| 215 |
+
@property
|
| 216 |
+
def revoke_url(self):
|
| 217 |
+
"""Optional[str]: The STS endpoint for token revocation."""
|
| 218 |
+
return self._revoke_url
|
| 219 |
+
|
| 220 |
+
@property
|
| 221 |
+
def is_user(self):
|
| 222 |
+
""" True: This credential always represents a user."""
|
| 223 |
+
return True
|
| 224 |
+
|
| 225 |
+
@property
|
| 226 |
+
def can_refresh(self):
|
| 227 |
+
return all(
|
| 228 |
+
(self._refresh_token, self._token_url, self._client_id, self._client_secret)
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
def get_project_id(self, request=None):
|
| 232 |
+
"""Retrieves the project ID corresponding to the workload identity or workforce pool.
|
| 233 |
+
For workforce pool credentials, it returns the project ID corresponding to
|
| 234 |
+
the workforce_pool_user_project.
|
| 235 |
+
|
| 236 |
+
When not determinable, None is returned.
|
| 237 |
+
|
| 238 |
+
Args:
|
| 239 |
+
request (google.auth.transport.requests.Request): Request object.
|
| 240 |
+
Unused here, but passed from _default.default().
|
| 241 |
+
|
| 242 |
+
Return:
|
| 243 |
+
str: project ID is not determinable for this credential type so it returns None
|
| 244 |
+
"""
|
| 245 |
+
|
| 246 |
+
return None
|
| 247 |
+
|
| 248 |
+
def to_json(self, strip=None):
|
| 249 |
+
"""Utility function that creates a JSON representation of this
|
| 250 |
+
credential.
|
| 251 |
+
Args:
|
| 252 |
+
strip (Sequence[str]): Optional list of members to exclude from the
|
| 253 |
+
generated JSON.
|
| 254 |
+
Returns:
|
| 255 |
+
str: A JSON representation of this instance. When converted into
|
| 256 |
+
a dictionary, it can be passed to from_info()
|
| 257 |
+
to create a new instance.
|
| 258 |
+
"""
|
| 259 |
+
strip = strip if strip else []
|
| 260 |
+
return json.dumps({k: v for (k, v) in self.info.items() if k not in strip})
|
| 261 |
+
|
| 262 |
+
def refresh(self, request):
|
| 263 |
+
"""Refreshes the access token.
|
| 264 |
+
|
| 265 |
+
Args:
|
| 266 |
+
request (google.auth.transport.Request): The object used to make
|
| 267 |
+
HTTP requests.
|
| 268 |
+
|
| 269 |
+
Raises:
|
| 270 |
+
google.auth.exceptions.RefreshError: If the credentials could
|
| 271 |
+
not be refreshed.
|
| 272 |
+
"""
|
| 273 |
+
if not self.can_refresh:
|
| 274 |
+
raise exceptions.RefreshError(
|
| 275 |
+
"The credentials do not contain the necessary fields need to "
|
| 276 |
+
"refresh the access token. You must specify refresh_token, "
|
| 277 |
+
"token_url, client_id, and client_secret."
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
now = _helpers.utcnow()
|
| 281 |
+
response_data = self._make_sts_request(request)
|
| 282 |
+
|
| 283 |
+
self.token = response_data.get("access_token")
|
| 284 |
+
|
| 285 |
+
lifetime = datetime.timedelta(seconds=response_data.get("expires_in"))
|
| 286 |
+
self.expiry = now + lifetime
|
| 287 |
+
|
| 288 |
+
if "refresh_token" in response_data:
|
| 289 |
+
self._refresh_token = response_data["refresh_token"]
|
| 290 |
+
|
| 291 |
+
def _make_sts_request(self, request):
|
| 292 |
+
return self._sts_client.refresh_token(request, self._refresh_token)
|
| 293 |
+
|
| 294 |
+
@_helpers.copy_docstring(credentials.Credentials)
|
| 295 |
+
def get_cred_info(self):
|
| 296 |
+
if self._cred_file_path:
|
| 297 |
+
return {
|
| 298 |
+
"credential_source": self._cred_file_path,
|
| 299 |
+
"credential_type": "external account authorized user credentials",
|
| 300 |
+
}
|
| 301 |
+
return None
|
| 302 |
+
|
| 303 |
+
def _make_copy(self):
|
| 304 |
+
kwargs = self.constructor_args()
|
| 305 |
+
cred = self.__class__(**kwargs)
|
| 306 |
+
cred._cred_file_path = self._cred_file_path
|
| 307 |
+
return cred
|
| 308 |
+
|
| 309 |
+
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
|
| 310 |
+
def with_quota_project(self, quota_project_id):
|
| 311 |
+
cred = self._make_copy()
|
| 312 |
+
cred._quota_project_id = quota_project_id
|
| 313 |
+
return cred
|
| 314 |
+
|
| 315 |
+
@_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
|
| 316 |
+
def with_token_uri(self, token_uri):
|
| 317 |
+
cred = self._make_copy()
|
| 318 |
+
cred._token_url = token_uri
|
| 319 |
+
return cred
|
| 320 |
+
|
| 321 |
+
@_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)
|
| 322 |
+
def with_universe_domain(self, universe_domain):
|
| 323 |
+
cred = self._make_copy()
|
| 324 |
+
cred._universe_domain = universe_domain
|
| 325 |
+
return cred
|
| 326 |
+
|
| 327 |
+
@classmethod
|
| 328 |
+
def from_info(cls, info, **kwargs):
|
| 329 |
+
"""Creates a Credentials instance from parsed external account info.
|
| 330 |
+
|
| 331 |
+
Args:
|
| 332 |
+
info (Mapping[str, str]): The external account info in Google
|
| 333 |
+
format.
|
| 334 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 335 |
+
|
| 336 |
+
Returns:
|
| 337 |
+
google.auth.external_account_authorized_user.Credentials: The
|
| 338 |
+
constructed credentials.
|
| 339 |
+
|
| 340 |
+
Raises:
|
| 341 |
+
ValueError: For invalid parameters.
|
| 342 |
+
"""
|
| 343 |
+
expiry = info.get("expiry")
|
| 344 |
+
if expiry:
|
| 345 |
+
expiry = datetime.datetime.strptime(
|
| 346 |
+
expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S"
|
| 347 |
+
)
|
| 348 |
+
return cls(
|
| 349 |
+
audience=info.get("audience"),
|
| 350 |
+
refresh_token=info.get("refresh_token"),
|
| 351 |
+
token_url=info.get("token_url"),
|
| 352 |
+
token_info_url=info.get("token_info_url"),
|
| 353 |
+
client_id=info.get("client_id"),
|
| 354 |
+
client_secret=info.get("client_secret"),
|
| 355 |
+
token=info.get("token"),
|
| 356 |
+
expiry=expiry,
|
| 357 |
+
revoke_url=info.get("revoke_url"),
|
| 358 |
+
quota_project_id=info.get("quota_project_id"),
|
| 359 |
+
scopes=info.get("scopes"),
|
| 360 |
+
universe_domain=info.get(
|
| 361 |
+
"universe_domain", credentials.DEFAULT_UNIVERSE_DOMAIN
|
| 362 |
+
),
|
| 363 |
+
**kwargs
|
| 364 |
+
)
|
| 365 |
+
|
| 366 |
+
@classmethod
|
| 367 |
+
def from_file(cls, filename, **kwargs):
|
| 368 |
+
"""Creates a Credentials instance from an external account json file.
|
| 369 |
+
|
| 370 |
+
Args:
|
| 371 |
+
filename (str): The path to the external account json file.
|
| 372 |
+
kwargs: Additional arguments to pass to the constructor.
|
| 373 |
+
|
| 374 |
+
Returns:
|
| 375 |
+
google.auth.external_account_authorized_user.Credentials: The
|
| 376 |
+
constructed credentials.
|
| 377 |
+
"""
|
| 378 |
+
with io.open(filename, "r", encoding="utf-8") as json_file:
|
| 379 |
+
data = json.load(json_file)
|
| 380 |
+
return cls.from_info(data, **kwargs)
|
lib/python3.10/site-packages/google/auth/iam.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2017 Google LLC
|
| 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 |
+
"""Tools for using the Google `Cloud Identity and Access Management (IAM)
|
| 16 |
+
API`_'s auth-related functionality.
|
| 17 |
+
|
| 18 |
+
.. _Cloud Identity and Access Management (IAM) API:
|
| 19 |
+
https://cloud.google.com/iam/docs/
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import base64
|
| 23 |
+
import http.client as http_client
|
| 24 |
+
import json
|
| 25 |
+
|
| 26 |
+
from google.auth import _exponential_backoff
|
| 27 |
+
from google.auth import _helpers
|
| 28 |
+
from google.auth import credentials
|
| 29 |
+
from google.auth import crypt
|
| 30 |
+
from google.auth import exceptions
|
| 31 |
+
|
| 32 |
+
IAM_RETRY_CODES = {
|
| 33 |
+
http_client.INTERNAL_SERVER_ERROR,
|
| 34 |
+
http_client.BAD_GATEWAY,
|
| 35 |
+
http_client.SERVICE_UNAVAILABLE,
|
| 36 |
+
http_client.GATEWAY_TIMEOUT,
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
_IAM_SCOPE = ["https://www.googleapis.com/auth/iam"]
|
| 40 |
+
|
| 41 |
+
_IAM_ENDPOINT = (
|
| 42 |
+
"https://iamcredentials.googleapis.com/v1/projects/-"
|
| 43 |
+
+ "/serviceAccounts/{}:generateAccessToken"
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
_IAM_SIGN_ENDPOINT = (
|
| 47 |
+
"https://iamcredentials.googleapis.com/v1/projects/-"
|
| 48 |
+
+ "/serviceAccounts/{}:signBlob"
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
_IAM_SIGNJWT_ENDPOINT = (
|
| 52 |
+
"https://iamcredentials.googleapis.com/v1/projects/-"
|
| 53 |
+
+ "/serviceAccounts/{}:signJwt"
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
_IAM_IDTOKEN_ENDPOINT = (
|
| 57 |
+
"https://iamcredentials.googleapis.com/v1/"
|
| 58 |
+
+ "projects/-/serviceAccounts/{}:generateIdToken"
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class Signer(crypt.Signer):
|
| 63 |
+
"""Signs messages using the IAM `signBlob API`_.
|
| 64 |
+
|
| 65 |
+
This is useful when you need to sign bytes but do not have access to the
|
| 66 |
+
credential's private key file.
|
| 67 |
+
|
| 68 |
+
.. _signBlob API:
|
| 69 |
+
https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts
|
| 70 |
+
/signBlob
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
def __init__(self, request, credentials, service_account_email):
|
| 74 |
+
"""
|
| 75 |
+
Args:
|
| 76 |
+
request (google.auth.transport.Request): The object used to make
|
| 77 |
+
HTTP requests.
|
| 78 |
+
credentials (google.auth.credentials.Credentials): The credentials
|
| 79 |
+
that will be used to authenticate the request to the IAM API.
|
| 80 |
+
The credentials must have of one the following scopes:
|
| 81 |
+
|
| 82 |
+
- https://www.googleapis.com/auth/iam
|
| 83 |
+
- https://www.googleapis.com/auth/cloud-platform
|
| 84 |
+
service_account_email (str): The service account email identifying
|
| 85 |
+
which service account to use to sign bytes. Often, this can
|
| 86 |
+
be the same as the service account email in the given
|
| 87 |
+
credentials.
|
| 88 |
+
"""
|
| 89 |
+
self._request = request
|
| 90 |
+
self._credentials = credentials
|
| 91 |
+
self._service_account_email = service_account_email
|
| 92 |
+
|
| 93 |
+
def _make_signing_request(self, message):
|
| 94 |
+
"""Makes a request to the API signBlob API."""
|
| 95 |
+
message = _helpers.to_bytes(message)
|
| 96 |
+
|
| 97 |
+
method = "POST"
|
| 98 |
+
url = _IAM_SIGN_ENDPOINT.replace(
|
| 99 |
+
credentials.DEFAULT_UNIVERSE_DOMAIN, self._credentials.universe_domain
|
| 100 |
+
).format(self._service_account_email)
|
| 101 |
+
headers = {"Content-Type": "application/json"}
|
| 102 |
+
body = json.dumps(
|
| 103 |
+
{"payload": base64.b64encode(message).decode("utf-8")}
|
| 104 |
+
).encode("utf-8")
|
| 105 |
+
|
| 106 |
+
retries = _exponential_backoff.ExponentialBackoff()
|
| 107 |
+
for _ in retries:
|
| 108 |
+
self._credentials.before_request(self._request, method, url, headers)
|
| 109 |
+
|
| 110 |
+
response = self._request(url=url, method=method, body=body, headers=headers)
|
| 111 |
+
|
| 112 |
+
if response.status in IAM_RETRY_CODES:
|
| 113 |
+
continue
|
| 114 |
+
|
| 115 |
+
if response.status != http_client.OK:
|
| 116 |
+
raise exceptions.TransportError(
|
| 117 |
+
"Error calling the IAM signBlob API: {}".format(response.data)
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
return json.loads(response.data.decode("utf-8"))
|
| 121 |
+
raise exceptions.TransportError("exhausted signBlob endpoint retries")
|
| 122 |
+
|
| 123 |
+
@property
|
| 124 |
+
def key_id(self):
|
| 125 |
+
"""Optional[str]: The key ID used to identify this private key.
|
| 126 |
+
|
| 127 |
+
.. warning::
|
| 128 |
+
This is always ``None``. The key ID used by IAM can not
|
| 129 |
+
be reliably determined ahead of time.
|
| 130 |
+
"""
|
| 131 |
+
return None
|
| 132 |
+
|
| 133 |
+
@_helpers.copy_docstring(crypt.Signer)
|
| 134 |
+
def sign(self, message):
|
| 135 |
+
response = self._make_signing_request(message)
|
| 136 |
+
return base64.b64decode(response["signedBlob"])
|