Skip to content

Commit fa1853e

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/gapic-generator-centralization-transcoding
2 parents a8a1968 + edc0423 commit fa1853e

7 files changed

Lines changed: 480 additions & 392 deletions

File tree

packages/google-auth/google/auth/_agent_identity_utils.py

Lines changed: 90 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -67,93 +67,88 @@ def _is_certificate_file_ready(path):
6767
st = os.stat(path)
6868
return stat.S_ISREG(st.st_mode) and st.st_size > 0
6969
except PermissionError:
70-
# Propagate PermissionError to let caller handle it (fail-fast or fallback)
70+
# Propagate PermissionError to let caller handle it (e.g., return early or fallback)
7171
raise
7272
except OSError:
7373
return False
7474

7575

7676
def get_agent_identity_certificate_path():
77-
"""Gets the certificate path from the certificate config file.
77+
"""Gets the agent certificate path from the certificate config file.
7878
7979
The path to the certificate config file is read from the
8080
GOOGLE_API_CERTIFICATE_CONFIG environment variable. This function
81-
implements a retry mechanism to handle cases where the environment
81+
can optionally trigger polling to handle cases where the environment
8282
variable is set before the files are available on the filesystem.
8383
8484
Returns:
85-
str: The path to the leaf certificate file.
85+
Optional[str]: The path to the agent's certificate file, or None if unavailable.
8686
8787
Raises:
8888
google.auth.exceptions.RefreshError: If the certificate config file
8989
or the certificate file cannot be found after retries.
9090
"""
91-
import json
92-
9391
cert_config_path = os.environ.get(environment_vars.GOOGLE_API_CERTIFICATE_CONFIG)
9492

95-
# Check if the well-known workload directory is mounted.
93+
if not cert_config_path:
94+
return None
95+
96+
# We trigger polling only if the config path points to the well-known directory.
97+
# Cloud Run dynamically generates these files in this directory, and both the
98+
# config file and the certificate file may experience a brief startup latency.
99+
# For all other paths, we return early to avoid introducing unnecessary startup
100+
# delays.
96101
well_known_dir = os.path.dirname(_WELL_KNOWN_CERT_PATH)
97-
has_well_known_dir = os.path.exists(well_known_dir)
102+
try:
103+
abs_cert_path = os.path.abspath(cert_config_path)
104+
abs_well_known_dir = os.path.abspath(well_known_dir)
105+
should_poll = (
106+
os.path.commonpath([abs_well_known_dir, abs_cert_path])
107+
== abs_well_known_dir
108+
)
109+
except ValueError:
110+
should_poll = False
98111

99-
# If we have neither a config path nor a well-known mount directory, exit immediately.
100-
if not cert_config_path and not has_well_known_dir:
101-
return None
112+
return _get_cert_path_with_optional_polling(cert_config_path, should_poll)
102113

103-
# If ECP config path is specified but does not exist, and we are on a workstation, fail-fast immediately.
104-
if (
105-
cert_config_path
106-
and not has_well_known_dir
107-
and not os.path.exists(cert_config_path)
108-
):
109-
return None
110114

115+
def _get_cert_path_with_optional_polling(cert_config_path, should_poll):
116+
"""Gets the certificate path, optionally polling until it is ready.
117+
118+
Args:
119+
cert_config_path (str): The path to the certificate configuration file.
120+
should_poll (bool): If True, the function will poll for the file and
121+
certificate to be ready. If False, it will check only once and
122+
return early if they are not immediately available.
123+
124+
Returns:
125+
str: The path to the certificate file, or None if unavailable.
126+
127+
Raises:
128+
google.auth.exceptions.RefreshError: If the certificate config file
129+
or the certificate file cannot be found after retries.
130+
"""
111131
has_logged_config_warning = False
112132
has_logged_cert_warning = False
113133

114134
for interval in _POLLING_INTERVALS:
115135
try:
116-
# Path A: Config file is explicitly set
117-
if cert_config_path:
118-
with open(cert_config_path, "r") as f:
119-
cert_config = json.load(f)
120-
121-
cert_configs = (
122-
cert_config.get("cert_configs")
123-
if isinstance(cert_config, dict)
124-
else None
125-
)
126-
workload_config = (
127-
cert_configs.get("workload")
128-
if isinstance(cert_configs, dict)
129-
else None
130-
)
131-
132-
if (
133-
not isinstance(workload_config, dict)
134-
or "cert_path" not in workload_config
135-
):
136-
return None
137-
138-
cert_path = workload_config["cert_path"]
139-
if _is_certificate_file_ready(cert_path):
140-
return cert_path
136+
cert_path = _parse_cert_path_from_config(cert_config_path)
141137

142-
# The config was parsed, but the cert file is not ready yet
143-
target_path = cert_path
138+
if cert_path is None:
139+
return None
144140

145-
# Path B: Config is NOT set, fallback to the well-known path
146-
else:
147-
if _is_certificate_file_ready(_WELL_KNOWN_CERT_PATH):
148-
return _WELL_KNOWN_CERT_PATH
141+
if _is_certificate_file_ready(cert_path):
142+
return cert_path
149143

150-
# The well-known cert file is not ready yet
151-
target_path = _WELL_KNOWN_CERT_PATH
144+
# The config was parsed, but the cert file is not ready yet
145+
if not should_poll:
146+
# If polling is disabled, return early.
147+
return None
152148

153-
# Log a warning on the first failed attempt to load the certificate file
154149
if not has_logged_cert_warning:
155150
warnings.warn(
156-
f"Certificate file not ready at {target_path}. Retrying until startup timeout (up to {_TOTAL_TIMEOUT} seconds total)..."
151+
f"Certificate file not ready at {cert_path}. Retrying until startup timeout (up to {_TOTAL_TIMEOUT} seconds total)..."
157152
)
158153
has_logged_cert_warning = True
159154

@@ -164,25 +159,24 @@ def get_agent_identity_certificate_path():
164159
)
165160
return None
166161
except (IOError, ValueError, KeyError) as e:
167-
if cert_config_path and os.path.exists(cert_config_path):
162+
if os.path.exists(cert_config_path):
168163
# If the file exists but has invalid JSON or is unreadable,
169-
# we assume it is in its final format and fail-fast by returning None.
164+
# we assume it is in its final format and return early (returning None).
165+
return None
166+
167+
if not should_poll:
168+
# If polling is disabled, return early if the file doesn't exist.
170169
return None
171170

172-
if not has_logged_config_warning and cert_config_path:
171+
if not has_logged_config_warning:
173172
warnings.warn(
174173
f"Certificate config file not found or incomplete: {e} (from "
175174
f"{environment_vars.GOOGLE_API_CERTIFICATE_CONFIG} environment variable). "
176175
f"Retrying until startup timeout (up to {_TOTAL_TIMEOUT} seconds total)..."
177176
)
178177
has_logged_config_warning = True
179-
pass
180178

181-
# A sleep is required in two cases:
182-
# 1. The config file is not found (the except block).
183-
# 2. The config file/well-known path is found, but the certificate is not yet available.
184-
# In both cases, we need to poll, so we sleep on every iteration
185-
# that doesn't return a certificate.
179+
# Sleep before the next polling attempt.
186180
time.sleep(interval)
187181

188182
raise exceptions.RefreshError(
@@ -193,6 +187,40 @@ def get_agent_identity_certificate_path():
193187
)
194188

195189

190+
def _parse_cert_path_from_config(cert_config_path):
191+
"""Reads the cert config file and returns the cert_path.
192+
193+
Args:
194+
cert_config_path (str): The path to the certificate configuration file.
195+
196+
Returns:
197+
Optional[str]: The path to the certificate file, or None if not found
198+
in the config.
199+
200+
Raises:
201+
IOError: If the certificate config file cannot be read.
202+
ValueError: If the certificate config file contains invalid JSON.
203+
KeyError: If the certificate config file does not contain the
204+
expected structure.
205+
"""
206+
import json
207+
208+
with open(cert_config_path, "r", encoding="utf-8") as f:
209+
cert_config = json.load(f)
210+
211+
cert_configs = (
212+
cert_config.get("cert_configs") if isinstance(cert_config, dict) else None
213+
)
214+
workload_config = (
215+
cert_configs.get("workload") if isinstance(cert_configs, dict) else None
216+
)
217+
218+
if not isinstance(workload_config, dict) or "cert_path" not in workload_config:
219+
return None
220+
221+
return workload_config["cert_path"]
222+
223+
196224
def get_and_parse_agent_identity_certificate():
197225
"""Gets and parses the agent identity certificate if not opted out.
198226

packages/google-auth/google/auth/transport/_mtls_helper.py

Lines changed: 31 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from typing import cast, Generator, List, Optional, Tuple, Union
2727

2828
from google.auth import _agent_identity_utils
29+
from google.auth import _cloud_sdk
2930
from google.auth import environment_vars
3031
from google.auth import exceptions
3132

@@ -51,30 +52,10 @@
5152
_LOGGER = logging.getLogger(__name__)
5253

5354

54-
# A flag to track if we have already logged a warning about mTLS auto-enablement failures.
55-
# This prevents log spam when client libraries create transports or session instances
56-
# frequently within a single process.
57-
_has_logged_mtls_warning = False
58-
59-
6055
_PASSPHRASE_REGEX = re.compile(
6156
b"-----BEGIN PASSPHRASE-----(.+)-----END PASSPHRASE-----", re.DOTALL
6257
)
6358

64-
# Temporary patch to accomodate incorrect cert config in Cloud Run prod environment.
65-
_WELL_KNOWN_CLOUD_RUN_CERT_PATH = (
66-
"/var/run/secrets/workload-spiffe-credentials/certificates.pem"
67-
)
68-
_WELL_KNOWN_CLOUD_RUN_KEY_PATH = (
69-
"/var/run/secrets/workload-spiffe-credentials/private_key.pem"
70-
)
71-
_INCORRECT_CLOUD_RUN_CERT_PATH = (
72-
"/var/lib/volumes/certificate/workload-certificates/certificates.pem"
73-
)
74-
_INCORRECT_CLOUD_RUN_KEY_PATH = (
75-
"/var/lib/volumes/certificate/workload-certificates/private_key.pem"
76-
)
77-
7859

7960
class _MemfdCreationError(OSError):
8061
"""Raised when Linux in-memory virtual file creation (memfd) fails."""
@@ -436,22 +417,37 @@ def _get_cert_config_path(certificate_config_path=None, include_context_aware=Tr
436417
The absolute path of the certificate config file, and None if the file does not exist.
437418
"""
438419

420+
source = "function argument"
421+
is_explicit = True
439422
if certificate_config_path is None:
440423
env_path = environ.get(environment_vars.GOOGLE_API_CERTIFICATE_CONFIG, None)
441424
if env_path is not None and env_path != "":
442425
certificate_config_path = env_path
426+
source = (
427+
f"environment variable {environment_vars.GOOGLE_API_CERTIFICATE_CONFIG}"
428+
)
443429
else:
444430
env_path = environ.get(
445431
environment_vars.CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH,
446432
None,
447433
)
448434
if include_context_aware and env_path is not None and env_path != "":
449435
certificate_config_path = env_path
436+
source = f"environment variable {environment_vars.CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH}"
450437
else:
451-
certificate_config_path = CERTIFICATE_CONFIGURATION_DEFAULT_PATH
438+
certificate_config_path = os.path.join(
439+
_cloud_sdk.get_config_path(), "certificate_config.json"
440+
)
441+
is_explicit = False
452442

453443
certificate_config_path = path.expanduser(certificate_config_path)
454444
if not path.exists(certificate_config_path):
445+
if is_explicit:
446+
_LOGGER.debug(
447+
"Certificate configuration file explicitly specified via %s at %s does not exist",
448+
source,
449+
certificate_config_path,
450+
)
455451
return None
456452
return certificate_config_path
457453

@@ -489,25 +485,6 @@ def _get_workload_cert_and_key_paths(config_path, include_context_aware=True):
489485
cert_path = workload["cert_path"]
490486
key_path = workload["key_path"]
491487

492-
# == BEGIN Temporary Cloud Run PATCH ==
493-
# See https://github.com/googleapis/google-auth-library-python/issues/1881
494-
if (cert_path == _INCORRECT_CLOUD_RUN_CERT_PATH) and (
495-
key_path == _INCORRECT_CLOUD_RUN_KEY_PATH
496-
):
497-
if not path.exists(cert_path) and not path.exists(key_path):
498-
_LOGGER.debug(
499-
"Applying Cloud Run certificate path patch. "
500-
"Configured paths not found: %s, %s. "
501-
"Using well-known paths: %s, %s",
502-
cert_path,
503-
key_path,
504-
_WELL_KNOWN_CLOUD_RUN_CERT_PATH,
505-
_WELL_KNOWN_CLOUD_RUN_KEY_PATH,
506-
)
507-
cert_path = _WELL_KNOWN_CLOUD_RUN_CERT_PATH
508-
key_path = _WELL_KNOWN_CLOUD_RUN_KEY_PATH
509-
# == END Temporary Cloud Run PATCH ==
510-
511488
return cert_path, key_path
512489

513490

@@ -755,36 +732,33 @@ def check_use_client_cert():
755732
will default to False.
756733
If GOOGLE_API_USE_CLIENT_CERTIFICATE is unset, the value will be inferred
757734
as True (auto-enabled) if a workload config file exists (pointed at by
758-
GOOGLE_API_CERTIFICATE_CONFIG) containing a "workload" section.
735+
GOOGLE_API_CERTIFICATE_CONFIG or CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH,
736+
or the default path like ~/.config/gcloud/certificate_config.json)
737+
containing a "workload" section.
759738
Otherwise, it returns False.
760739
761740
Returns:
762741
bool: Whether the client certificate should be used for mTLS connection.
763742
"""
764-
global _has_logged_mtls_warning
765743
env_override = _check_use_client_cert_env()
766744
if env_override is not None:
767745
return env_override
768746

769747
# Auto-enablement checks (when GOOGLE_API_USE_CLIENT_CERTIFICATE is not set)
770748

771-
# Check if the value of GOOGLE_API_CERTIFICATE_CONFIG is set.
772-
cert_path = getenv(environment_vars.GOOGLE_API_CERTIFICATE_CONFIG) or getenv(
773-
environment_vars.CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH
774-
)
749+
# Check if a workload config file exists.
750+
cert_path = _get_cert_config_path(include_context_aware=True)
775751

776752
if cert_path:
777753
try:
778754
with open(cert_path, "r") as f:
779755
content = json.load(f)
780756
except (FileNotFoundError, OSError, json.JSONDecodeError) as e:
781-
if not _has_logged_mtls_warning:
782-
_LOGGER.warning(
783-
"mTLS auto-enablement failed: Could not read/parse certificate file at %s. Error: %s",
784-
cert_path,
785-
e,
786-
)
787-
_has_logged_mtls_warning = True
757+
_LOGGER.debug(
758+
"mTLS auto-enablement failed: Could not read/parse certificate file at %s. Error: %s",
759+
cert_path,
760+
e,
761+
)
788762
return False
789763

790764
# Structural validation
@@ -794,12 +768,10 @@ def check_use_client_cert():
794768
return True
795769

796770
# If we got here, the file exists but the expected structure is missing
797-
if not _has_logged_mtls_warning:
798-
_LOGGER.warning(
799-
"mTLS auto-enablement failed: Certificate configuration file at %s is missing the required ['cert_configs']['workload'] section.",
800-
cert_path,
801-
)
802-
_has_logged_mtls_warning = True
771+
_LOGGER.debug(
772+
"mTLS auto-enablement failed: Certificate configuration file at %s is missing the required ['cert_configs']['workload'] section.",
773+
cert_path,
774+
)
803775
return False
804776

805777

0 commit comments

Comments
 (0)