diff --git a/packages/google-auth/google/auth/transport/_mtls_helper.py b/packages/google-auth/google/auth/transport/_mtls_helper.py index d6450291c7f2..ff2fb4491fb4 100644 --- a/packages/google-auth/google/auth/transport/_mtls_helper.py +++ b/packages/google-auth/google/auth/transport/_mtls_helper.py @@ -448,6 +448,22 @@ def client_cert_callback(): return crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey) +def _get_use_client_cert_env(): + """Returns the configured client certificate opt-in environment value.""" + use_client_cert = getenv(environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE) + if use_client_cert is None or use_client_cert == "": + use_client_cert = getenv( + environment_vars.CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE + ) + return use_client_cert + + +def _is_client_cert_explicitly_enabled(): + """Returns True if an environment variable explicitly enables client certs.""" + use_client_cert = _get_use_client_cert_env() + return bool(use_client_cert and use_client_cert.lower() == "true") + + def check_use_client_cert(): """Returns boolean for whether the client certificate should be used for mTLS. @@ -462,11 +478,7 @@ def check_use_client_cert(): Returns: bool: Whether the client certificate should be used for mTLS connection. """ - use_client_cert = getenv(environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE) - if use_client_cert is None or use_client_cert == "": - use_client_cert = getenv( - environment_vars.CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE - ) + use_client_cert = _get_use_client_cert_env() # Check if the value of GOOGLE_API_USE_CLIENT_CERTIFICATE is set. if use_client_cert: diff --git a/packages/google-auth/google/auth/transport/requests.py b/packages/google-auth/google/auth/transport/requests.py index 9735762c4414..af0c20b4d468 100644 --- a/packages/google-auth/google/auth/transport/requests.py +++ b/packages/google-auth/google/auth/transport/requests.py @@ -313,10 +313,10 @@ class AuthorizedSession(requests.Session): credentials' headers to the request and refreshing credentials as needed. This class also supports mutual TLS via :meth:`configure_mtls_channel` - method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE` - environment variable must be explicitly set to ``true``, otherwise it does - nothing. Assume the environment is set to ``true``, the method behaves in the - following manner: + method. Client certificate use is enabled when + `GOOGLE_API_USE_CLIENT_CERTIFICATE` is set to ``true`` or when it is inferred + from a certificate configuration. When client certificate use is enabled, the + method behaves in the following manner: If client_cert_callback is provided, client certificate and private key are loaded using the callback; if client_cert_callback is None, @@ -428,11 +428,12 @@ def __init__( def configure_mtls_channel(self, client_cert_callback=None): """Configure the client certificate and key for SSL connection. - The function does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE` is - explicitly set to `true`. In this case if client certificate and key are - successfully obtained (from the given client_cert_callback or from application - default SSL credentials), a :class:`_MutualTlsAdapter` instance will be mounted - to "https://" prefix. + The function does nothing unless client certificate use is enabled by + `GOOGLE_API_USE_CLIENT_CERTIFICATE` or inferred from certificate + configuration. In this case if client certificate and key are + successfully obtained (from the given client_cert_callback or from + application default SSL credentials), a :class:`_MutualTlsAdapter` + instance will be mounted to "https://" prefix. Args: client_cert_callback (Optional[Callable[[], (bytes, bytes)]]): @@ -452,6 +453,10 @@ def configure_mtls_channel(self, client_cert_callback=None): try: import OpenSSL except ImportError as caught_exc: + if not _mtls_helper._is_client_cert_explicitly_enabled(): + _LOGGER.debug("pyOpenSSL is unavailable; disabling inferred mTLS.") + self._is_mtls = False + return new_exc = exceptions.MutualTLSChannelError(caught_exc) raise new_exc from caught_exc diff --git a/packages/google-auth/google/auth/transport/urllib3.py b/packages/google-auth/google/auth/transport/urllib3.py index de07007a946c..50e07d7586dc 100644 --- a/packages/google-auth/google/auth/transport/urllib3.py +++ b/packages/google-auth/google/auth/transport/urllib3.py @@ -215,10 +215,10 @@ class AuthorizedHttp(RequestMethods): # type: ignore credentials' headers to the request and refreshing credentials as needed. This class also supports mutual TLS via :meth:`configure_mtls_channel` - method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE` - environment variable must be explicitly set to `true`, otherwise it does - nothing. Assume the environment is set to `true`, the method behaves in the - following manner: + method. Client certificate use is enabled when + `GOOGLE_API_USE_CLIENT_CERTIFICATE` is set to `true` or when it is inferred + from a certificate configuration. When client certificate use is enabled, the + method behaves in the following manner: If client_cert_callback is provided, client certificate and private key are loaded using the callback; if client_cert_callback is None, application default SSL credentials will be used. Exceptions are raised if @@ -313,13 +313,14 @@ def __init__( def configure_mtls_channel(self, client_cert_callback=None): """Configures mutual TLS channel using the given client_cert_callback or - application default SSL credentials. The behavior is controlled by - `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable. - (1) If the environment variable value is `true`, the function returns True - if the channel is mutual TLS and False otherwise. The `http` provided - in the constructor will be overwritten. - (2) If the environment variable is not set or `false`, the function does - nothing and it always return False. + application default SSL credentials. Client certificate use is enabled + when `GOOGLE_API_USE_CLIENT_CERTIFICATE` is set to `true` or when it is + inferred from a certificate configuration. + (1) If client certificate use is enabled, the function returns True if + the channel is mutual TLS and False otherwise. The `http` provided in + the constructor will be overwritten. + (2) If client certificate use is disabled, the function does nothing and + it always returns False. Args: client_cert_callback (Optional[Callable[[], (bytes, bytes)]]): @@ -335,7 +336,7 @@ def configure_mtls_channel(self, client_cert_callback=None): google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel creation failed for any reason. """ - use_client_cert = transport._mtls_helper.check_use_client_cert() + use_client_cert = _mtls_helper.check_use_client_cert() if not use_client_cert: self._is_mtls = False return False @@ -344,11 +345,15 @@ def configure_mtls_channel(self, client_cert_callback=None): try: import OpenSSL except ImportError as caught_exc: + if not _mtls_helper._is_client_cert_explicitly_enabled(): + _LOGGER.debug("pyOpenSSL is unavailable; disabling inferred mTLS.") + self._is_mtls = False + return False new_exc = exceptions.MutualTLSChannelError(caught_exc) raise new_exc from caught_exc try: - found_cert_key, cert, key = transport._mtls_helper.get_client_cert_and_key( + found_cert_key, cert, key = _mtls_helper.get_client_cert_and_key( client_cert_callback ) diff --git a/packages/google-auth/tests/transport/test_requests.py b/packages/google-auth/tests/transport/test_requests.py index c9fab036e17b..b94f1956a544 100644 --- a/packages/google-auth/tests/transport/test_requests.py +++ b/packages/google-auth/tests/transport/test_requests.py @@ -501,6 +501,32 @@ def test_configure_mtls_channel_exceptions(self, mock_get_client_cert_and_key): ): auth_session.configure_mtls_channel() + @mock.patch("builtins.open", autospec=True) + @mock.patch( + "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True + ) + def test_configure_mtls_channel_inferred_missing_openssl( + self, mock_get_client_cert_and_key, mock_file + ): + mock_file.side_effect = mock.mock_open( + read_data='{"cert_configs": {"workload": "exists"}}' + ) + auth_session = google.auth.transport.requests.AuthorizedSession( + credentials=mock.Mock() + ) + + with mock.patch.dict("sys.modules"): + sys.modules["OpenSSL"] = None + with mock.patch.dict( + os.environ, + {environment_vars.GOOGLE_API_CERTIFICATE_CONFIG: "/path/to/config"}, + clear=True, + ): + auth_session.configure_mtls_channel() + + assert not auth_session.is_mtls + mock_get_client_cert_and_key.assert_not_called() + @mock.patch( "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True ) diff --git a/packages/google-auth/tests/transport/test_urllib3.py b/packages/google-auth/tests/transport/test_urllib3.py index b29e4e950433..960e509ce1e8 100644 --- a/packages/google-auth/tests/transport/test_urllib3.py +++ b/packages/google-auth/tests/transport/test_urllib3.py @@ -290,6 +290,33 @@ def test_configure_mtls_channel_exceptions(self, mock_get_client_cert_and_key): ): authed_http.configure_mtls_channel() + @mock.patch("builtins.open", autospec=True) + @mock.patch( + "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True + ) + def test_configure_mtls_channel_inferred_missing_openssl( + self, mock_get_client_cert_and_key, mock_file + ): + mock_file.side_effect = mock.mock_open( + read_data='{"cert_configs": {"workload": "exists"}}' + ) + authed_http = google.auth.transport.urllib3.AuthorizedHttp( + credentials=mock.Mock() + ) + + with mock.patch.dict("sys.modules"): + sys.modules["OpenSSL"] = None + with mock.patch.dict( + os.environ, + {environment_vars.GOOGLE_API_CERTIFICATE_CONFIG: "/path/to/config"}, + clear=True, + ): + is_mtls = authed_http.configure_mtls_channel() + + assert not is_mtls + assert not authed_http._is_mtls + mock_get_client_cert_and_key.assert_not_called() + @mock.patch( "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True )