Skip to content
This repository was archived by the owner on Mar 6, 2026. It is now read-only.

Commit 2062792

Browse files
authored
Merge branch 'main' into renovate/all
2 parents e80bc27 + ef86015 commit 2062792

15 files changed

Lines changed: 945 additions & 69 deletions

File tree

.github/CODEOWNERS

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,27 @@
44
# For syntax help see:
55
# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
66

7-
# The @googleapis/googleapis-auth and @googleapis/python-core-client-libraries is the default owner for changes in this repo
8-
* @googleapis/googleapis-auth @googleapis/python-core-client-libraries
9-
google/auth/_default.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
10-
google/auth/aws.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
11-
google/auth/credentials.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
12-
google/auth/downscoped.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
13-
google/auth/external_account.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
14-
google/auth/external_account_authorized_user.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
15-
google/auth/identity_pool.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
16-
google/auth/pluggable.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
17-
google/auth/sts.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
18-
google/auth/impersonated_credentials.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
19-
tests/test__default.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
20-
tests/test_aws.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
21-
tests/test_credentials.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
22-
tests/test_downscoped.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
23-
tests/test_external_account.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
24-
tests/test_external_account_authorized_user.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
25-
tests/test_identity_pool.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
26-
tests/test_pluggable.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
27-
tests/test_sts.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
28-
tests/test_impersonated_credentials.py @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-core-client-libraries
29-
/samples/ @googleapis/googleapis-auth @googleapis/aion-sdk @googleapis/python-samples-owners @googleapis/python-core-client-libraries
7+
# The @googleapis/googleapis-auth and @googleapis/cloud-sdk-python-team is the default owner for changes in this repo
8+
* @googleapis/googleapis-auth @googleapis/cloud-sdk-python-team
9+
google/auth/_default.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
10+
google/auth/aws.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
11+
google/auth/credentials.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
12+
google/auth/downscoped.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
13+
google/auth/external_account.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
14+
google/auth/external_account_authorized_user.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
15+
google/auth/identity_pool.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
16+
google/auth/pluggable.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
17+
google/auth/sts.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
18+
google/auth/impersonated_credentials.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
19+
tests/test__default.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
20+
tests/test_aws.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
21+
tests/test_credentials.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
22+
tests/test_downscoped.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
23+
tests/test_external_account.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
24+
tests/test_external_account_authorized_user.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
25+
tests/test_identity_pool.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
26+
tests/test_pluggable.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
27+
tests/test_sts.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
28+
tests/test_impersonated_credentials.py @googleapis/googleapis-auth @googleapis/aion-team @googleapis/cloud-sdk-python-team
29+
/samples/ @googleapis/googleapis-auth @googleapis/aion-team @googleapis/python-samples-owners @googleapis/cloud-sdk-python-team
3030
system_tests/secrets.tar.enc # Remove noise from test creds.

.kokoro/trampoline_v2.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
# To run this script, first download few files from gcs to /dev/shm.
2727
# (/dev/shm is passed into the container as KOKORO_GFILE_DIR).
2828
#
29-
# gsutil cp gs://cloud-devrel-kokoro-resources/python-docs-samples/secrets_viewer_service_account.json /dev/shm
30-
# gsutil cp gs://cloud-devrel-kokoro-resources/python-docs-samples/automl_secrets.txt /dev/shm
29+
# gcloud storage cp gs://cloud-devrel-kokoro-resources/python-docs-samples/secrets_viewer_service_account.json /dev/shm
30+
# gcloud storage cp gs://cloud-devrel-kokoro-resources/python-docs-samples/automl_secrets.txt /dev/shm
3131
#
3232
# Then run the script.
3333
# .kokoro/trampoline_v2.sh

google/auth/aio/transport/aiohttp.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,11 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
"""Transport adapter for Asynchronous HTTP Requests based on aiohttp.
16-
"""
15+
"""Transport adapter for Asynchronous HTTP Requests based on aiohttp."""
1716

1817
import asyncio
1918
import logging
20-
from typing import AsyncGenerator, Mapping, Optional
19+
from typing import AsyncGenerator, Mapping, Optional, TYPE_CHECKING, Union
2120

2221
try:
2322
import aiohttp # type: ignore
@@ -31,6 +30,15 @@
3130
from google.auth.aio import _helpers as _helpers_async
3231
from google.auth.aio import transport
3332

33+
if TYPE_CHECKING: # pragma: NO COVER
34+
from aiohttp import ClientTimeout # type: ignore
35+
36+
else:
37+
try:
38+
from aiohttp import ClientTimeout
39+
except (ImportError, AttributeError):
40+
ClientTimeout = None
41+
3442
_LOGGER = logging.getLogger(__name__)
3543

3644

@@ -113,7 +121,7 @@ class Request(transport.Request):
113121
.. automethod:: __call__
114122
"""
115123

116-
def __init__(self, session: aiohttp.ClientSession = None):
124+
def __init__(self, session: Optional[aiohttp.ClientSession] = None):
117125
self._session = session
118126
self._closed = False
119127

@@ -123,7 +131,7 @@ async def __call__(
123131
method: str = "GET",
124132
body: Optional[bytes] = None,
125133
headers: Optional[Mapping[str, str]] = None,
126-
timeout: float = transport._DEFAULT_TIMEOUT_SECONDS,
134+
timeout: Union[float, ClientTimeout] = transport._DEFAULT_TIMEOUT_SECONDS,
127135
**kwargs,
128136
) -> transport.Response:
129137
"""
@@ -158,7 +166,10 @@ async def __call__(
158166
if not self._session:
159167
self._session = aiohttp.ClientSession()
160168

161-
client_timeout = aiohttp.ClientTimeout(total=timeout)
169+
if isinstance(timeout, aiohttp.ClientTimeout):
170+
client_timeout = timeout
171+
else:
172+
client_timeout = aiohttp.ClientTimeout(total=timeout)
162173
_helpers.request_log(_LOGGER, method, url, body, headers)
163174
response = await self._session.request(
164175
method,
@@ -176,8 +187,12 @@ async def __call__(
176187
raise client_exc from caught_exc
177188

178189
except asyncio.TimeoutError as caught_exc:
190+
if isinstance(timeout, aiohttp.ClientTimeout):
191+
timeout_seconds = timeout.total
192+
else:
193+
timeout_seconds = timeout
179194
timeout_exc = exceptions.TimeoutError(
180-
f"Request timed out after {timeout} seconds."
195+
f"Request timed out after {timeout_seconds} seconds."
181196
)
182197
raise timeout_exc from caught_exc
183198

google/auth/aio/transport/mtls.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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 contextlib
21+
import logging
22+
import os
23+
import ssl
24+
import tempfile
25+
from typing import Optional
26+
27+
from google.auth import exceptions
28+
import google.auth.transport._mtls_helper
29+
import google.auth.transport.mtls
30+
31+
_LOGGER = logging.getLogger(__name__)
32+
33+
34+
@contextlib.contextmanager
35+
def _create_temp_file(content: bytes):
36+
"""Creates a temporary file with the given content.
37+
38+
Args:
39+
content (bytes): The content to write to the file.
40+
41+
Yields:
42+
str: The path to the temporary file.
43+
"""
44+
# Create a temporary file that is readable only by the owner.
45+
fd, file_path = tempfile.mkstemp()
46+
try:
47+
with os.fdopen(fd, "wb") as f:
48+
f.write(content)
49+
yield file_path
50+
finally:
51+
# Securely delete the file after use.
52+
if os.path.exists(file_path):
53+
os.remove(file_path)
54+
55+
56+
def make_client_cert_ssl_context(
57+
cert_bytes: bytes, key_bytes: bytes, passphrase: Optional[bytes] = None
58+
) -> ssl.SSLContext:
59+
"""Creates an SSLContext with the given client certificate and key.
60+
This function writes the certificate and key to temporary files so that
61+
ssl.create_default_context can load them, as the ssl module requires
62+
file paths for client certificates. These temporary files are deleted
63+
immediately after the SSL context is created.
64+
Args:
65+
cert_bytes (bytes): The client certificate content in PEM format.
66+
key_bytes (bytes): The client private key content in PEM format.
67+
passphrase (Optional[bytes]): The passphrase for the private key, if any.
68+
Returns:
69+
ssl.SSLContext: The configured SSL context with client certificate.
70+
71+
Raises:
72+
google.auth.exceptions.TransportError: If there is an error loading the certificate.
73+
"""
74+
with _create_temp_file(cert_bytes) as cert_path, _create_temp_file(
75+
key_bytes
76+
) as key_path:
77+
try:
78+
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
79+
context.load_cert_chain(
80+
certfile=cert_path, keyfile=key_path, password=passphrase
81+
)
82+
return context
83+
except (ssl.SSLError, OSError, IOError, ValueError, RuntimeError) as exc:
84+
raise exceptions.TransportError(
85+
"Failed to load client certificate and key for mTLS."
86+
) from exc
87+
88+
89+
async def _run_in_executor(func, *args):
90+
"""Run a blocking function in an executor to avoid blocking the event loop.
91+
92+
This implements the non-blocking execution strategy for disk I/O operations.
93+
"""
94+
try:
95+
# For python versions 3.9 and newer versions
96+
return await asyncio.to_thread(func, *args)
97+
except AttributeError:
98+
# Fallback for older Python versions
99+
loop = asyncio.get_running_loop()
100+
return await loop.run_in_executor(None, func, *args)
101+
102+
103+
def default_client_cert_source():
104+
"""Get a callback which returns the default client SSL credentials.
105+
106+
Returns:
107+
Awaitable[Callable[[], Tuple[bytes, bytes]]]: A callback which returns the default
108+
client certificate bytes and private key bytes, both in PEM format.
109+
110+
Raises:
111+
google.auth.exceptions.DefaultClientCertSourceError: If the default
112+
client SSL credentials don't exist or are malformed.
113+
"""
114+
if not google.auth.transport.mtls.has_default_client_cert_source(
115+
include_context_aware=False
116+
):
117+
raise exceptions.MutualTLSChannelError(
118+
"Default client cert source doesn't exist"
119+
)
120+
121+
async def callback():
122+
try:
123+
_, cert_bytes, key_bytes = await get_client_cert_and_key()
124+
except (OSError, RuntimeError, ValueError) as caught_exc:
125+
new_exc = exceptions.MutualTLSChannelError(caught_exc)
126+
raise new_exc from caught_exc
127+
128+
return cert_bytes, key_bytes
129+
130+
return callback
131+
132+
133+
async def get_client_ssl_credentials(
134+
certificate_config_path=None,
135+
):
136+
"""Returns the client side certificate, private key and passphrase.
137+
138+
We look for certificates and keys with the following order of priority:
139+
1. Certificate and key specified by certificate_config.json.
140+
Currently, only X.509 workload certificates are supported.
141+
142+
Args:
143+
certificate_config_path (str): The certificate_config.json file path.
144+
145+
Returns:
146+
Tuple[bool, bytes, bytes, bytes]:
147+
A boolean indicating if cert, key and passphrase are obtained, the
148+
cert bytes and key bytes both in PEM format, and passphrase bytes.
149+
150+
Raises:
151+
google.auth.exceptions.ClientCertError: if problems occurs when getting
152+
the cert, key and passphrase.
153+
"""
154+
155+
# Attempt to retrieve X.509 Workload cert and key.
156+
cert, key = await _run_in_executor(
157+
google.auth.transport._mtls_helper._get_workload_cert_and_key,
158+
certificate_config_path,
159+
False,
160+
)
161+
162+
if cert and key:
163+
return True, cert, key, None
164+
165+
return False, None, None, None
166+
167+
168+
async def get_client_cert_and_key(client_cert_callback=None):
169+
"""Returns the client side certificate and private key. The function first
170+
tries to get certificate and key from client_cert_callback; if the callback
171+
is None or doesn't provide certificate and key, the function tries application
172+
default SSL credentials.
173+
174+
Args:
175+
client_cert_callback (Optional[Callable[[], (bytes, bytes)]]): An
176+
optional callback which returns client certificate bytes and private
177+
key bytes both in PEM format.
178+
179+
Returns:
180+
Tuple[bool, bytes, bytes]:
181+
A boolean indicating if cert and key are obtained, the cert bytes
182+
and key bytes both in PEM format.
183+
184+
Raises:
185+
google.auth.exceptions.ClientCertError: if problems occurs when getting
186+
the cert and key.
187+
"""
188+
if client_cert_callback:
189+
result = client_cert_callback()
190+
try:
191+
cert, key = await result
192+
except TypeError:
193+
cert, key = result
194+
return True, cert, key
195+
196+
has_cert, cert, key, _ = await get_client_ssl_credentials()
197+
return has_cert, cert, key

0 commit comments

Comments
 (0)