| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 89fc6f2 commit 3368f27
6 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,137 @@ | |||
| 1 | + # Copyright 2026 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 | + Helper functions for mTLS in async for discovery of certs. | ||
| 17 | + """ | ||
| 18 | + | ||
| 19 | + import asyncio | ||
| 20 | + import logging | ||
| 21 | + | ||
| 22 | + from google.auth import exceptions | ||
| 23 | + import google.auth.transport._mtls_helper | ||
| 24 | + import google.auth.transport.mtls | ||
| 25 | + | ||
| 26 | + _LOGGER = logging.getLogger(__name__) | ||
| 27 | + | ||
| 28 | + | ||
| 29 | + async def _run_in_executor(func, *args): | ||
| 30 | + """Run a blocking function in an executor to avoid blocking the event loop. | ||
| 31 | + | ||
| 32 | + This implements the non-blocking execution strategy for disk I/O operations. | ||
| 33 | + """ | ||
| 34 | + try: | ||
| 35 | + # For python versions 3.9 and newer versions | ||
| 36 | + return await asyncio.to_thread(func, *args) | ||
| 37 | + except AttributeError: | ||
| 38 | + # Fallback for older Python versions | ||
| 39 | + loop = asyncio.get_running_loop() | ||
| 40 | + return await loop.run_in_executor(None, func, *args) | ||
| 41 | + | ||
| 42 | + | ||
| 43 | + def default_client_cert_source(): | ||
| 44 | + """Get a callback which returns the default client SSL credentials. | ||
| 45 | + | ||
| 46 | + Returns: | ||
| 47 | + Awaitable[Callable[[], [bytes, bytes]]]: A callback which returns the default | ||
| 48 | + client certificate bytes and private key bytes, both in PEM format. | ||
| 49 | + | ||
| 50 | + Raises: | ||
| 51 | + google.auth.exceptions.DefaultClientCertSourceError: If the default | ||
| 52 | + client SSL credentials don't exist or are malformed. | ||
| 53 | + """ | ||
| 54 | + if not google.auth.transport.mtls.has_default_client_cert_source( | ||
| 55 | + include_context_aware=False | ||
| 56 | + ): | ||
| 57 | + raise exceptions.MutualTLSChannelError( | ||
| 58 | + "Default client cert source doesn't exist" | ||
| 59 | + ) | ||
| 60 | + | ||
| 61 | + async def callback(): | ||
| 62 | + try: | ||
| 63 | + _, cert_bytes, key_bytes = await get_client_cert_and_key() | ||
| 64 | + except (OSError, RuntimeError, ValueError) as caught_exc: | ||
| 65 | + new_exc = exceptions.MutualTLSChannelError(caught_exc) | ||
| 66 | + raise new_exc from caught_exc | ||
| 67 | + | ||
| 68 | + return cert_bytes, key_bytes | ||
| 69 | + | ||
| 70 | + return callback | ||
| 71 | + | ||
| 72 | + | ||
| 73 | + async def get_client_ssl_credentials( | ||
| 74 | + certificate_config_path=None, | ||
| 75 | + ): | ||
| 76 | + """Returns the client side certificate, private key and passphrase. | ||
| 77 | + | ||
| 78 | + We look for certificates and keys with the following order of priority: | ||
| 79 | + 1. Certificate and key specified by certificate_config.json. | ||
| 80 | + Currently, only X.509 workload certificates are supported. | ||
| 81 | + | ||
| 82 | + Args: | ||
| 83 | + certificate_config_path (str): The certificate_config.json file path. | ||
| 84 | + | ||
| 85 | + Returns: | ||
| 86 | + Tuple[bool, bytes, bytes, bytes]: | ||
| 87 | + A boolean indicating if cert, key and passphrase are obtained, the | ||
| 88 | + cert bytes and key bytes both in PEM format, and passphrase bytes. | ||
| 89 | + | ||
| 90 | + Raises: | ||
| 91 | + google.auth.exceptions.ClientCertError: if problems occurs when getting | ||
| 92 | + the cert, key and passphrase. | ||
| 93 | + """ | ||
| 94 | + | ||
| 95 | + # Attempt to retrieve X.509 Workload cert and key. | ||
| 96 | + cert, key = await _run_in_executor( | ||
| 97 | + google.auth.transport._mtls_helper._get_workload_cert_and_key, | ||
| 98 | + certificate_config_path, | ||
| 99 | + False, | ||
| 100 | + ) | ||
| 101 | + | ||
| 102 | + if cert and key: | ||
| 103 | + return True, cert, key, None | ||
| 104 | + | ||
| 105 | + return False, None, None, None | ||
| 106 | + | ||
| 107 | + | ||
| 108 | + async def get_client_cert_and_key(client_cert_callback=None): | ||
| 109 | + """Returns the client side certificate and private key. The function first | ||
| 110 | + tries to get certificate and key from client_cert_callback; if the callback | ||
| 111 | + is None or doesn't provide certificate and key, the function tries application | ||
| 112 | + default SSL credentials. | ||
| 113 | + | ||
| 114 | + Args: | ||
| 115 | + client_cert_callback (Optional[Callable[[], (bytes, bytes)]]): An | ||
| 116 | + optional callback which returns client certificate bytes and private | ||
| 117 | + key bytes both in PEM format. | ||
| 118 | + | ||
| 119 | + Returns: | ||
| 120 | + Tuple[bool, bytes, bytes]: | ||
| 121 | + A boolean indicating if cert and key are obtained, the cert bytes | ||
| 122 | + and key bytes both in PEM format. | ||
| 123 | + | ||
| 124 | + Raises: | ||
| 125 | + google.auth.exceptions.ClientCertError: if problems occurs when getting | ||
| 126 | + the cert and key. | ||
| 127 | + """ | ||
| 128 | + if client_cert_callback: | ||
| 129 | + result = client_cert_callback() | ||
| 130 | + try: | ||
| 131 | + cert, key = await result | ||
| 132 | + except TypeError: | ||
| 133 | + cert, key = result | ||
| 134 | + return True, cert, key | ||
| 135 | + | ||
| 136 | + has_cert, cert, key, _ = await get_client_ssl_credentials() | ||
| 137 | + return has_cert, cert, key | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -25,6 +25,8 @@ | |||
| 25 | 25 | from google.auth import exceptions | |
| 26 | 26 | ||
| 27 | 27 | CONTEXT_AWARE_METADATA_PATH = "~/.secureConnect/context_aware_metadata.json" | |
| 28 | + | ||
| 29 | + # Default gcloud config path, to be used with path.expanduser for cross-platform compatibility. | ||
| 28 | 30 | CERTIFICATE_CONFIGURATION_DEFAULT_PATH = "~/.config/gcloud/certificate_config.json" | |
| 29 | 31 | _CERT_PROVIDER_COMMAND = "cert_provider_command" | |
| 30 | 32 | _CERT_REGEX = re.compile( | |
@@ -103,14 +105,18 @@ def _load_json_file(path): | |||
| 103 | 105 | return json_data | |
| 104 | 106 | ||
| 105 | 107 | ||
| 106 | - def _get_workload_cert_and_key(certificate_config_path=None): | ||
| 108 | + def _get_workload_cert_and_key( | ||
| 109 | + certificate_config_path=None, include_context_aware=True | ||
| 110 | + ): | ||
| 107 | 111 | """Read the workload identity cert and key files specified in the certificate config provided. | |
| 108 | 112 | If no config path is provided, check the environment variable: "GOOGLE_API_CERTIFICATE_CONFIG" | |
| 109 | 113 | first, then the well known gcloud location: "~/.config/gcloud/certificate_config.json". | |
| 110 | 114 | ||
| 111 | 115 | Args: | |
| 112 | 116 | certificate_config_path (string): The certificate config path. If no path is provided, | |
| 113 | 117 | the environment variable will be checked first, then the well known gcloud location. | |
| 118 | + include_context_aware (bool): If context aware metadata path should be checked for the | ||
| 119 | + SecureConnect mTLS configuration. | ||
| 114 | 120 | ||
| 115 | 121 | Returns: | |
| 116 | 122 | Tuple[Optional[bytes], Optional[bytes]]: client certificate bytes in PEM format and key | |
@@ -121,15 +127,17 @@ def _get_workload_cert_and_key(certificate_config_path=None): | |||
| 121 | 127 | the certificate or key information. | |
| 122 | 128 | """ | |
| 123 | 129 | ||
| 124 | - cert_path, key_path = _get_workload_cert_and_key_paths(certificate_config_path) | ||
| 130 | + cert_path, key_path = _get_workload_cert_and_key_paths( | ||
| 131 | + certificate_config_path, include_context_aware | ||
| 132 | + ) | ||
| 125 | 133 | ||
| 126 | 134 | if cert_path is None and key_path is None: | |
| 127 | 135 | return None, None | |
| 128 | 136 | ||
| 129 | 137 | return _read_cert_and_key_files(cert_path, key_path) | |
| 130 | 138 | ||
| 131 | 139 | ||
| 132 | - def _get_cert_config_path(certificate_config_path=None): | ||
| 140 | + def _get_cert_config_path(certificate_config_path=None, include_context_aware=True): | ||
| 133 | 141 | """Get the certificate configuration path based on the following order: | |
| 134 | 142 | ||
| 135 | 143 | 1: Explicit override, if set | |
@@ -141,6 +149,8 @@ def _get_cert_config_path(certificate_config_path=None): | |||
| 141 | 149 | Args: | |
| 142 | 150 | certificate_config_path (string): The certificate config path. If provided, the well known | |
| 143 | 151 | location and environment variable will be ignored. | |
| 152 | + include_context_aware (bool): If context aware metadata path should be checked for the | ||
| 153 | + SecureConnect mTLS configuration. | ||
| 144 | 154 | ||
| 145 | 155 | Returns: | |
| 146 | 156 | The absolute path of the certificate config file, and None if the file does not exist. | |
@@ -155,7 +165,7 @@ def _get_cert_config_path(certificate_config_path=None): | |||
| 155 | 165 | environment_vars.CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH, | |
| 156 | 166 | None, | |
| 157 | 167 | ) | |
| 158 | - if env_path is not None and env_path != "": | ||
| 168 | + if include_context_aware and env_path is not None and env_path != "": | ||
| 159 | 169 | certificate_config_path = env_path | |
| 160 | 170 | else: | |
| 161 | 171 | certificate_config_path = CERTIFICATE_CONFIGURATION_DEFAULT_PATH | |
@@ -166,8 +176,8 @@ def _get_cert_config_path(certificate_config_path=None): | |||
| 166 | 176 | return certificate_config_path | |
| 167 | 177 | ||
| 168 | 178 | ||
| 169 | - def _get_workload_cert_and_key_paths(config_path): | ||
| 170 | - absolute_path = _get_cert_config_path(config_path) | ||
| 179 | + def _get_workload_cert_and_key_paths(config_path, include_context_aware=True): | ||
| 180 | + absolute_path = _get_cert_config_path(config_path, include_context_aware) | ||
| 171 | 181 | if absolute_path is None: | |
| 172 | 182 | return None, None | |
| 173 | 183 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -20,14 +20,19 @@ | |||
| 20 | 20 | from google.auth.transport import _mtls_helper | |
| 21 | 21 | ||
| 22 | 22 | ||
| 23 | - def has_default_client_cert_source(): | ||
| 23 | + def has_default_client_cert_source(include_context_aware=True): | ||
| 24 | 24 | """Check if default client SSL credentials exists on the device. | |
| 25 | 25 | ||
| 26 | + Args: | ||
| 27 | + include_context_aware (bool): include_context_aware indicates if context_aware | ||
| 28 | + path location will be checked or should it be skipped. | ||
| 29 | + | ||
| 26 | 30 | Returns: | |
| 27 | 31 | bool: indicating if the default client cert source exists. | |
| 28 | 32 | """ | |
| 29 | 33 | if ( | |
| 30 | - _mtls_helper._check_config_path(_mtls_helper.CONTEXT_AWARE_METADATA_PATH) | ||
| 34 | + include_context_aware | ||
| 35 | + and _mtls_helper._check_config_path(_mtls_helper.CONTEXT_AWARE_METADATA_PATH) | ||
| 31 | 36 | is not None | |
| 32 | 37 | ): | |
| 33 | 38 | return True | |
@@ -58,7 +63,7 @@ def default_client_cert_source(): | |||
| 58 | 63 | google.auth.exceptions.DefaultClientCertSourceError: If the default | |
| 59 | 64 | client SSL credentials don't exist or are malformed. | |
| 60 | 65 | """ | |
| 61 | - if not has_default_client_cert_source(): | ||
| 66 | + if not has_default_client_cert_source(include_context_aware=True): | ||
| 62 | 67 | raise exceptions.MutualTLSChannelError( | |
| 63 | 68 | "Default client cert source doesn't exist" | |
| 64 | 69 | ) | |
@@ -94,7 +99,7 @@ def default_client_encrypted_cert_source(cert_path, key_path): | |||
| 94 | 99 | google.auth.exceptions.DefaultClientCertSourceError: If any problem | |
| 95 | 100 | occurs when loading or saving the client certificate and key. | |
| 96 | 101 | """ | |
| 97 | - if not has_default_client_cert_source(): | ||
| 102 | + if not has_default_client_cert_source(include_context_aware=True): | ||
| 98 | 103 | raise exceptions.MutualTLSChannelError( | |
| 99 | 104 | "Default client encrypted cert source doesn't exist" | |
| 100 | 105 | ) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -41,12 +41,12 @@ def test_refresh_success(http_request, credentials, token_info): | |||
| 41 | 41 | ||
| 42 | 42 | assert info["email"] == credentials.service_account_email | |
| 43 | 43 | info_scopes = _helpers.string_to_scopes(info["scope"]) | |
| 44 | - assert set(info_scopes) == set( | ||
| 44 | + assert set(info_scopes).issubset(set( | ||
| 45 | 45 | [ | |
| 46 | 46 | "https://www.googleapis.com/auth/userinfo.email", | |
| 47 | 47 | "https://www.googleapis.com/auth/userinfo.profile", | |
| 48 | 48 | ] | |
| 49 | - ) | ||
| 49 | + )) | ||
| 50 | 50 | ||
| 51 | 51 | def test_iam_signer(http_request, credentials): | |
| 52 | 52 | credentials = credentials.with_scopes( | |
| Back | FazBrowse Home | New Git URL |
0 commit comments