Skip to content

Commit a70e522

Browse files
authored
Merge branch 'main' into parthea-patch-1
2 parents f2d5c57 + f89188a commit a70e522

File tree

16 files changed

+499
-48
lines changed

16 files changed

+499
-48
lines changed

.github/.OwlBot.lock.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2024 Google LLC
1+
# Copyright 2025 Google LLC
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -13,5 +13,5 @@
1313
# limitations under the License.
1414
docker:
1515
image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest
16-
digest: sha256:5efdf8d38e5a22c1ec9e5541cbdfde56399bdffcb6f531183f84ac66052a8024
17-
# created: 2024-10-25
16+
digest: sha256:023a21377a2a00008057f99f0118edadc30a19d1636a3fee47189ebec2f3921c
17+
# created: 2025-03-31T16:51:40.130756953Z

.kokoro/build-systests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export PYTHONUNBUFFERED=1
2828
python3 -m pip uninstall --yes --quiet nox-automation
2929

3030
# Install nox
31-
python3 -m pip install --upgrade --quiet nox
31+
python3 -m pip install nox==2024.10.9
3232
python3 -m nox --version
3333

3434
# Setup service account credentials.

.kokoro/test-samples-impl.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export PYTHONUNBUFFERED=1
3333
env | grep KOKORO
3434

3535
# Install nox
36-
python3.9 -m pip install --upgrade --quiet nox
36+
# `virtualenv==20.26.6` is added for Python 3.7 compatibility
37+
python3.9 -m pip install --upgrade --quiet nox virtualenv==20.26.6
3738

3839
# Use secrets acessor service account to get secrets
3940
if [[ -f "${KOKORO_GFILE_DIR}/secrets_viewer_service_account.json" ]]; then

google/auth/compute_engine/_metadata.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ def get(
159159
retry_count=5,
160160
headers=None,
161161
return_none_for_not_found_error=False,
162+
timeout=_METADATA_DEFAULT_TIMEOUT,
162163
):
163164
"""Fetch a resource from the metadata server.
164165
@@ -178,6 +179,7 @@ def get(
178179
headers (Optional[Mapping[str, str]]): Headers for the request.
179180
return_none_for_not_found_error (Optional[bool]): If True, returns None
180181
for 404 error instead of throwing an exception.
182+
timeout (int): How long to wait, in seconds for the metadata server to respond.
181183
182184
Returns:
183185
Union[Mapping, str]: If the metadata server returns JSON, a mapping of
@@ -204,7 +206,9 @@ def get(
204206
failure_reason = None
205207
for attempt in backoff:
206208
try:
207-
response = request(url=url, method="GET", headers=headers_to_use)
209+
response = request(
210+
url=url, method="GET", headers=headers_to_use, timeout=timeout
211+
)
208212
if response.status in transport.DEFAULT_RETRYABLE_STATUS_CODES:
209213
_LOGGER.warning(
210214
"Compute Engine Metadata server unavailable on "

google/auth/identity_pool.py

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
except ImportError: # pragma: NO COVER
4242
from collections import Mapping # type: ignore
4343
import abc
44+
import base64
4445
import json
4546
import os
4647
from typing import NamedTuple
@@ -145,9 +146,88 @@ def get_subject_token(self, context, request):
145146
class _X509Supplier(SubjectTokenSupplier):
146147
"""Internal supplier for X509 workload credentials. This class is used internally and always returns an empty string as the subject token."""
147148

149+
def __init__(self, trust_chain_path, leaf_cert_callback):
150+
self._trust_chain_path = trust_chain_path
151+
self._leaf_cert_callback = leaf_cert_callback
152+
148153
@_helpers.copy_docstring(SubjectTokenSupplier)
149154
def get_subject_token(self, context, request):
150-
return ""
155+
# Import OpennSSL inline because it is an extra import only required by customers
156+
# using mTLS.
157+
from OpenSSL import crypto
158+
159+
leaf_cert = crypto.load_certificate(
160+
crypto.FILETYPE_PEM, self._leaf_cert_callback()
161+
)
162+
trust_chain = self._read_trust_chain()
163+
cert_chain = []
164+
165+
cert_chain.append(_X509Supplier._encode_cert(leaf_cert))
166+
167+
if trust_chain is None or len(trust_chain) == 0:
168+
return json.dumps(cert_chain)
169+
170+
# Append the first cert if it is not the leaf cert.
171+
first_cert = _X509Supplier._encode_cert(trust_chain[0])
172+
if first_cert != cert_chain[0]:
173+
cert_chain.append(first_cert)
174+
175+
for i in range(1, len(trust_chain)):
176+
encoded = _X509Supplier._encode_cert(trust_chain[i])
177+
# Check if the current cert is the leaf cert and raise an exception if it is.
178+
if encoded == cert_chain[0]:
179+
raise exceptions.RefreshError(
180+
"The leaf certificate must be at the top of the trust chain file"
181+
)
182+
else:
183+
cert_chain.append(encoded)
184+
return json.dumps(cert_chain)
185+
186+
def _read_trust_chain(self):
187+
# Import OpennSSL inline because it is an extra import only required by customers
188+
# using mTLS.
189+
from OpenSSL import crypto
190+
191+
certificate_trust_chain = []
192+
# If no trust chain path was provided, return an empty list.
193+
if self._trust_chain_path is None or self._trust_chain_path == "":
194+
return certificate_trust_chain
195+
try:
196+
# Open the trust chain file.
197+
with open(self._trust_chain_path, "rb") as f:
198+
trust_chain_data = f.read()
199+
# Split PEM data into individual certificates.
200+
cert_blocks = trust_chain_data.split(b"-----BEGIN CERTIFICATE-----")
201+
for cert_block in cert_blocks:
202+
# Skip empty blocks.
203+
if cert_block.strip():
204+
cert_data = b"-----BEGIN CERTIFICATE-----" + cert_block
205+
try:
206+
# Load each certificate and add it to the trust chain.
207+
cert = crypto.load_certificate(
208+
crypto.FILETYPE_PEM, cert_data
209+
)
210+
certificate_trust_chain.append(cert)
211+
except Exception as e:
212+
raise exceptions.RefreshError(
213+
"Error loading PEM certificates from the trust chain file '{}'".format(
214+
self._trust_chain_path
215+
)
216+
) from e
217+
return certificate_trust_chain
218+
except FileNotFoundError:
219+
raise exceptions.RefreshError(
220+
"Trust chain file '{}' was not found.".format(self._trust_chain_path)
221+
)
222+
223+
def _encode_cert(cert):
224+
# Import OpennSSL inline because it is an extra import only required by customers
225+
# using mTLS.
226+
from OpenSSL import crypto
227+
228+
return base64.b64encode(
229+
crypto.dump_certificate(crypto.FILETYPE_ASN1, cert)
230+
).decode("utf-8")
151231

152232

153233
def _parse_token_data(token_content, format_type="text", subject_token_field_name=None):
@@ -296,7 +376,9 @@ def __init__(
296376
self._credential_source_headers,
297377
)
298378
else: # self._credential_source_certificate
299-
self._subject_token_supplier = _X509Supplier()
379+
self._subject_token_supplier = _X509Supplier(
380+
self._trust_chain_path, self._get_cert_bytes
381+
)
300382

301383
@_helpers.copy_docstring(external_account.Credentials)
302384
def retrieve_subject_token(self, request):
@@ -314,6 +396,10 @@ def _get_mtls_cert_and_key_paths(self):
314396
self._certificate_config_location
315397
)
316398

399+
def _get_cert_bytes(self):
400+
cert_path, _ = self._get_mtls_cert_and_key_paths()
401+
return _mtls_helper._read_cert_file(cert_path)
402+
317403
def _mtls_required(self):
318404
return self._credential_source_certificate is not None
319405

@@ -350,6 +436,9 @@ def _validate_certificate_config(self):
350436
use_default = self._credential_source_certificate.get(
351437
"use_default_certificate_config"
352438
)
439+
self._trust_chain_path = self._credential_source_certificate.get(
440+
"trust_chain_path"
441+
)
353442
if self._certificate_config_location and use_default:
354443
raise exceptions.MalformedError(
355444
"Invalid certificate configuration, certificate_config_location cannot be specified when use_default_certificate_config = true."

google/auth/transport/urllib3.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,21 @@
3434
try:
3535
import urllib3 # type: ignore
3636
import urllib3.exceptions # type: ignore
37+
from packaging import version # type: ignore
3738
except ImportError as caught_exc: # pragma: NO COVER
3839
raise ImportError(
39-
"The urllib3 library is not installed from please install the "
40-
"urllib3 package to use the urllib3 transport."
40+
""
41+
f"Error: {caught_exc}."
42+
" The 'google-auth' library requires the extras installed "
43+
"for urllib3 network transport."
44+
"\n"
45+
"Please install the necessary dependencies using pip:\n"
46+
" pip install google-auth[urllib3]\n"
47+
"\n"
48+
"(Note: Using '[urllib3]' ensures the specific dependencies needed for this feature are installed. "
49+
"We recommend running this command in your virtual environment.)"
4150
) from caught_exc
4251

43-
from packaging import version # type: ignore
4452

4553
from google.auth import environment_vars
4654
from google.auth import exceptions
@@ -414,7 +422,7 @@ def urlopen(self, method, url, body=None, headers=None, **kwargs):
414422
body=body,
415423
headers=headers,
416424
_credential_refresh_attempt=_credential_refresh_attempt + 1,
417-
**kwargs
425+
**kwargs,
418426
)
419427

420428
return response

noxfile.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,7 @@ def unit(session):
8989
constraints_path = str(
9090
CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt"
9191
)
92-
session.install("-r", "testing/requirements.txt", "-c", constraints_path)
93-
session.install("-e", ".", "-c", constraints_path)
92+
session.install("-e", ".[testing]", "-c", constraints_path)
9493
session.run(
9594
"pytest",
9695
f"--junitxml=unit_{session.python}_sponge_log.xml",
@@ -105,8 +104,7 @@ def unit(session):
105104

106105
@nox.session(python="3.8")
107106
def cover(session):
108-
session.install("-r", "testing/requirements.txt")
109-
session.install("-e", ".")
107+
session.install("-e", ".[testing]")
110108
session.run(
111109
"pytest",
112110
"--cov=google.auth",
@@ -144,8 +142,7 @@ def docs(session):
144142

145143
@nox.session(python="pypy")
146144
def pypy(session):
147-
session.install("-r", "testing/requirements.txt")
148-
session.install("-e", ".")
145+
session.install("-e", ".[testing]")
149146
session.run(
150147
"pytest",
151148
f"--junitxml=unit_{session.python}_sponge_log.xml",

renovate.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
":preserveSemverRanges",
66
":disableDependencyDashboard"
77
],
8-
"ignorePaths": [".pre-commit-config.yaml", ".kokoro/requirements.txt", "setup.py"],
8+
"ignorePaths": [".pre-commit-config.yaml", ".kokoro/requirements.txt", "setup.py", ".github/workflows/unittest.yml"],
99
"pip_requirements": {
1010
"fileMatch": ["requirements-test.txt", "samples/[\\S/]*constraints.txt", "samples/[\\S/]*constraints-test.txt"]
1111
}

setup.py

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,70 @@
2727
"rsa>=3.1.4,<5",
2828
)
2929

30+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1737): Unit test fails with
31+
# `No module named 'cryptography.hazmat.backends.openssl.x509' for Python 3.7``.
32+
cryptography_base_require = [
33+
"cryptography >= 38.0.3",
34+
"cryptography < 39.0.0; python_version < '3.8'",
35+
]
36+
37+
requests_extra_require = ["requests >= 2.20.0, < 3.0.0"]
38+
39+
aiohttp_extra_require = ["aiohttp >= 3.6.2, < 4.0.0", *requests_extra_require]
40+
41+
pyjwt_extra_require = ["pyjwt>=2.0", *cryptography_base_require]
42+
43+
reauth_extra_require = ["pyu2f>=0.1.5"]
44+
45+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1738): Add bounds for cryptography and pyopenssl dependencies.
46+
enterprise_cert_extra_require = ["cryptography", "pyopenssl"]
47+
48+
pyopenssl_extra_require = ["pyopenssl>=20.0.0", cryptography_base_require]
49+
50+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1739): Add bounds for urllib3 and packaging dependencies.
51+
urllib3_extra_require = ["urllib3", "packaging"]
52+
53+
# Unit test requirements.
54+
testing_extra_require = [
55+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1735): Remove `grpcio` from testing requirements once an extra is added for `grpcio` dependency.
56+
"grpcio",
57+
"flask",
58+
"freezegun",
59+
"mock",
60+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1736): Remove `oauth2client` from testing requirements once an extra is added for `oauth2client` dependency.
61+
"oauth2client",
62+
*pyjwt_extra_require,
63+
"pytest",
64+
"pytest-cov",
65+
"pytest-localserver",
66+
*pyopenssl_extra_require,
67+
*reauth_extra_require,
68+
"responses",
69+
*urllib3_extra_require,
70+
# Async Dependencies
71+
*aiohttp_extra_require,
72+
"aioresponses",
73+
"pytest-asyncio",
74+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1665): Remove the pinned version of pyopenssl
75+
# once `TestDecryptPrivateKey::test_success` is updated to remove the deprecated `OpenSSL.crypto.sign` and
76+
# `OpenSSL.crypto.verify` methods. See: https://www.pyopenssl.org/en/latest/changelog.html#id3.
77+
"pyopenssl < 24.3.0",
78+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1722): `test_aiohttp_requests` depend on
79+
# aiohttp < 3.10.0 which is a bug. Investigate and remove the pinned aiohttp version.
80+
"aiohttp < 3.10.0",
81+
]
82+
3083
extras = {
31-
"aiohttp": ["aiohttp >= 3.6.2, < 4.0.0.dev0", "requests >= 2.20.0, < 3.0.0.dev0"],
32-
"pyopenssl": ["pyopenssl>=20.0.0", "cryptography>=38.0.3"],
33-
"requests": "requests >= 2.20.0, < 3.0.0.dev0",
34-
"reauth": "pyu2f>=0.1.5",
35-
"enterprise_cert": ["cryptography", "pyopenssl"],
36-
"pyjwt": ["pyjwt>=2.0", "cryptography>=38.0.3"],
84+
"aiohttp": aiohttp_extra_require,
85+
"enterprise_cert": enterprise_cert_extra_require,
86+
"pyopenssl": pyopenssl_extra_require,
87+
"pyjwt": pyjwt_extra_require,
88+
"reauth": reauth_extra_require,
89+
"requests": requests_extra_require,
90+
"testing": testing_extra_require,
91+
"urllib3": urllib3_extra_require,
92+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1735): Add an extra for `grpcio` dependency.
93+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1736): Add an extra for `oauth2client` dependency.
3794
}
3895

3996
with io.open("README.rst", "r") as fh:

system_tests/secrets.tar.enc

0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)