diff --git a/docs/reference/google.auth.transport.mtls.rst b/docs/reference/google.auth.transport.mtls.rst new file mode 100644 index 000000000..11b50e23c --- /dev/null +++ b/docs/reference/google.auth.transport.mtls.rst @@ -0,0 +1,7 @@ +google.auth.transport.mtls module +================================= + +.. automodule:: google.auth.transport.mtls + :members: + :inherited-members: + :show-inheritance: diff --git a/google/auth/environment_vars.py b/google/auth/environment_vars.py index 9c1367f52..46a892664 100644 --- a/google/auth/environment_vars.py +++ b/google/auth/environment_vars.py @@ -53,3 +53,9 @@ GCE_METADATA_IP = "GCE_METADATA_IP" """Environment variable providing an alternate ip:port to be used for ip-only GCE metadata requests.""" + +GOOGLE_API_USE_CLIENT_CERTIFICATE = "GOOGLE_API_USE_CLIENT_CERTIFICATE" +"""Environment variable controlling whether to use client certificate or not. + +The default value is false. Users have to explicitly set this value to true +in order to use client certificate to establish a mutual TLS channel.""" diff --git a/google/auth/transport/grpc.py b/google/auth/transport/grpc.py index 13234a331..ab7d0dbf8 100644 --- a/google/auth/transport/grpc.py +++ b/google/auth/transport/grpc.py @@ -17,9 +17,11 @@ from __future__ import absolute_import import logging +import os import six +from google.auth import environment_vars from google.auth import exceptions from google.auth.transport import _mtls_helper @@ -96,6 +98,9 @@ def secure_authorized_channel( This creates a channel with SSL and :class:`AuthMetadataPlugin`. This channel can be used to create a stub that can make authorized requests. + Users can configure client certificate or rely on device certificates to + establish a mutual TLS channel, if the `GOOGLE_API_USE_CLIENT_CERTIFICATE` + variable is explicitly set to `true`. Example:: @@ -138,7 +143,9 @@ def secure_authorized_channel( ssl_credentials=regular_ssl_credentials) Option 2: create a mutual TLS channel by calling a callback which returns - the client side certificate and the key:: + the client side certificate and the key (Note that + `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be explicitly + set to `true`):: def my_client_cert_callback(): code_to_load_client_cert_and_key() @@ -155,7 +162,9 @@ def my_client_cert_callback(): Option 3: use application default SSL credentials. It searches and uses the command in a context aware metadata file, which is available on devices - with endpoint verification support. + with endpoint verification support (Note that + `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be explicitly + set to `true`). See https://cloud.google.com/endpoint-verification/docs/overview:: try: @@ -174,7 +183,8 @@ def my_client_cert_callback(): ssl_credentials=default_ssl_credentials) Option 4: not setting ssl_credentials and client_cert_callback. For devices - without endpoint verification support, a regular TLS channel is created; + without endpoint verification support or `GOOGLE_API_USE_CLIENT_CERTIFICATE` + environment variable is not `true`, a regular TLS channel is created; otherwise, a mutual TLS channel is created, however, the call should be wrapped in a try/except block in case of malformed context aware metadata. @@ -205,13 +215,15 @@ def my_client_cert_callback(): This argument is mutually exclusive with client_cert_callback; providing both will raise an exception. If ssl_credentials and client_cert_callback are None, application - default SSL credentials will be used. + default SSL credentials are used if `GOOGLE_API_USE_CLIENT_CERTIFICATE` + environment variable is explicitly set to `true`, otherwise one way TLS + SSL credentials are used. client_cert_callback (Callable[[], (bytes, bytes)]): Optional callback function to obtain client certicate and key for mutual TLS connection. This argument is mutually exclusive with ssl_credentials; providing both will raise an exception. - If ssl_credentials and client_cert_callback are None, application - default SSL credentials will be used. + This argument does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE` + environment variable is explicitly set to `true`. kwargs: Additional arguments to pass to :func:`grpc.secure_channel`. Returns: @@ -235,16 +247,21 @@ def my_client_cert_callback(): # If SSL credentials are not explicitly set, try client_cert_callback and ADC. if not ssl_credentials: - if client_cert_callback: + use_client_cert = os.getenv( + environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false" + ) + if use_client_cert == "true" and client_cert_callback: # Use the callback if provided. cert, key = client_cert_callback() ssl_credentials = grpc.ssl_channel_credentials( certificate_chain=cert, private_key=key ) - else: + elif use_client_cert == "true": # Use application default SSL credentials. adc_ssl_credentils = SslCredentials() ssl_credentials = adc_ssl_credentils.ssl_credentials + else: + ssl_credentials = grpc.ssl_channel_credentials() # Combine the ssl credentials and the authorization credentials. composite_credentials = grpc.composite_channel_credentials( @@ -257,17 +274,29 @@ def my_client_cert_callback(): class SslCredentials: """Class for application default SSL credentials. - For devices with endpoint verification support, a device certificate will be - automatically loaded and mutual TLS will be established. + The behavior is controlled by `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment + variable whose default value is `false`. Client certificate will not be used + unless the environment variable is explicitly set to `true`. See + https://google.aip.dev/auth/4114 + + If the environment variable is `true`, then for devices with endpoint verification + support, a device certificate will be automatically loaded and mutual TLS will + be established. See https://cloud.google.com/endpoint-verification/docs/overview. """ def __init__(self): - # Load client SSL credentials. - metadata_path = _mtls_helper._check_dca_metadata_path( - _mtls_helper.CONTEXT_AWARE_METADATA_PATH + use_client_cert = os.getenv( + environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false" ) - self._is_mtls = metadata_path is not None + if use_client_cert != "true": + self._is_mtls = False + else: + # Load client SSL credentials. + metadata_path = _mtls_helper._check_dca_metadata_path( + _mtls_helper.CONTEXT_AWARE_METADATA_PATH + ) + self._is_mtls = metadata_path is not None @property def ssl_credentials(self): diff --git a/google/auth/transport/requests.py b/google/auth/transport/requests.py index 4f5af7dea..9a2f3afc7 100644 --- a/google/auth/transport/requests.py +++ b/google/auth/transport/requests.py @@ -19,6 +19,7 @@ import functools import logging import numbers +import os import time try: @@ -40,6 +41,7 @@ ) # pylint: disable=ungrouped-imports import six # pylint: disable=ungrouped-imports +from google.auth import environment_vars from google.auth import exceptions from google.auth import transport import google.auth.transport._mtls_helper @@ -249,13 +251,18 @@ 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. If client_cert_callback is provided, client certificate and private + 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: + 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 there are problems with the certificate, private key, or the loading process, so it should be called within a try/except block. - First we create an :class:`AuthorizedSession` instance and specify the endpoints:: + First we set the environment variable to `true`, then create an :class:`AuthorizedSession` + instance and specify the endpoints:: regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics' mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics' @@ -343,9 +350,11 @@ def __init__( def configure_mtls_channel(self, client_cert_callback=None): """Configure the client certificate and key for SSL connection. - 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 `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. Args: client_cert_callback (Optional[Callable[[], (bytes, bytes)]]): @@ -358,6 +367,13 @@ 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 = os.getenv( + environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false" + ) + if use_client_cert != "true": + self._is_mtls = False + return + try: import OpenSSL except ImportError as caught_exc: diff --git a/google/auth/transport/urllib3.py b/google/auth/transport/urllib3.py index 3742f1a41..209fc51bc 100644 --- a/google/auth/transport/urllib3.py +++ b/google/auth/transport/urllib3.py @@ -17,6 +17,7 @@ from __future__ import absolute_import import logging +import os import warnings # Certifi is Mozilla's certificate bundle. Urllib3 needs a certificate bundle @@ -45,6 +46,7 @@ import six import urllib3.exceptions # pylint: disable=ungrouped-imports +from google.auth import environment_vars from google.auth import exceptions from google.auth import transport @@ -202,13 +204,18 @@ class AuthorizedHttp(urllib3.request.RequestMethods): credentials' headers to the request and refreshing credentials as needed. This class also supports mutual TLS via :meth:`configure_mtls_channel` - method. If client_cert_callback is provided, client certificate and private + 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: + 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 there are problems with the certificate, private key, or the loading process, so it should be called within a try/except block. - First we create an :class:`AuthorizedHttp` instance and specify the endpoints:: + First we set the environment variable to `true`, then create an :class:`AuthorizedHttp` + instance and specify the endpoints:: regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics' mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics' @@ -282,9 +289,13 @@ 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. Returns True if the channel is - mutual TLS and False otherwise. Note that the `http` provided in the - constructor will be overwritten. + 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. Args: client_cert_callback (Optional[Callable[[], (bytes, bytes)]]): @@ -300,6 +311,12 @@ 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 = os.getenv( + environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false" + ) + if use_client_cert != "true": + return False + try: import OpenSSL except ImportError as caught_exc: diff --git a/system_tests/test_mtls_http.py b/system_tests/test_mtls_http.py index 4a6a9c4bc..7c5649685 100644 --- a/system_tests/test_mtls_http.py +++ b/system_tests/test_mtls_http.py @@ -18,6 +18,7 @@ import google.auth import google.auth.credentials +from google.auth import environment_vars from google.auth.transport import mtls import google.auth.transport.requests import google.auth.transport.urllib3 @@ -33,7 +34,8 @@ def test_requests(): ) authed_session = google.auth.transport.requests.AuthorizedSession(credentials) - authed_session.configure_mtls_channel() + with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}): + authed_session.configure_mtls_channel() # If the devices has default client cert source, then a mutual TLS channel # is supposed to be created. @@ -57,7 +59,8 @@ def test_urllib3(): ) authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials) - is_mtls = authed_http.configure_mtls_channel() + with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}): + is_mtls = authed_http.configure_mtls_channel() # If the devices has default client cert source, then a mutual TLS channel # is supposed to be created. @@ -83,9 +86,10 @@ def test_requests_with_default_client_cert_source(): authed_session = google.auth.transport.requests.AuthorizedSession(credentials) if mtls.has_default_client_cert_source(): - authed_session.configure_mtls_channel( - client_cert_callback=mtls.default_client_cert_source() - ) + with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}): + authed_session.configure_mtls_channel( + client_cert_callback=mtls.default_client_cert_source() + ) assert authed_session.is_mtls @@ -105,9 +109,10 @@ def test_urllib3_with_default_client_cert_source(): authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials) if mtls.has_default_client_cert_source(): - assert authed_http.configure_mtls_channel( - client_cert_callback=mtls.default_client_cert_source() - ) + with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}): + assert authed_http.configure_mtls_channel( + client_cert_callback=mtls.default_client_cert_source() + ) # Sleep 1 second to avoid 503 error. time.sleep(1) diff --git a/tests/transport/test_grpc.py b/tests/transport/test_grpc.py index ef2e2e24f..39f8b11c8 100644 --- a/tests/transport/test_grpc.py +++ b/tests/transport/test_grpc.py @@ -21,6 +21,7 @@ from google.auth import _helpers from google.auth import credentials +from google.auth import environment_vars from google.auth import exceptions from google.auth import transport @@ -139,9 +140,13 @@ def test_secure_authorized_channel_adc( None, ) - channel = google.auth.transport.grpc.secure_authorized_channel( - credentials, request, target, options=mock.sentinel.options - ) + channel = None + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + channel = google.auth.transport.grpc.secure_authorized_channel( + credentials, request, target, options=mock.sentinel.options + ) # Check the auth plugin construction. auth_plugin = metadata_call_credentials.call_args[0][0] @@ -167,6 +172,49 @@ def test_secure_authorized_channel_adc( ) assert channel == secure_channel.return_value + @mock.patch("google.auth.transport.grpc.SslCredentials", autospec=True) + def test_secure_authorized_channel_adc_without_client_cert_env( + self, + ssl_credentials_adc_method, + secure_channel, + ssl_channel_credentials, + metadata_call_credentials, + composite_channel_credentials, + get_client_ssl_credentials, + ): + # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE + # environment variable is not set. + credentials = CredentialsStub() + request = mock.create_autospec(transport.Request) + target = "example.com:80" + + channel = google.auth.transport.grpc.secure_authorized_channel( + credentials, request, target, options=mock.sentinel.options + ) + + # Check the auth plugin construction. + auth_plugin = metadata_call_credentials.call_args[0][0] + assert isinstance(auth_plugin, google.auth.transport.grpc.AuthMetadataPlugin) + assert auth_plugin._credentials == credentials + assert auth_plugin._request == request + + # Check the ssl channel call. + ssl_channel_credentials.assert_called_once() + ssl_credentials_adc_method.assert_not_called() + + # Check the composite credentials call. + composite_channel_credentials.assert_called_once_with( + ssl_channel_credentials.return_value, metadata_call_credentials.return_value + ) + + # Check the channel call. + secure_channel.assert_called_once_with( + target, + composite_channel_credentials.return_value, + options=mock.sentinel.options, + ) + assert channel == secure_channel.return_value + def test_secure_authorized_channel_explicit_ssl( self, secure_channel, @@ -233,9 +281,12 @@ def test_secure_authorized_channel_with_client_cert_callback_success( client_cert_callback = mock.Mock() client_cert_callback.return_value = (PUBLIC_CERT_BYTES, PRIVATE_KEY_BYTES) - google.auth.transport.grpc.secure_authorized_channel( - credentials, request, target, client_cert_callback=client_cert_callback - ) + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + google.auth.transport.grpc.secure_authorized_channel( + credentials, request, target, client_cert_callback=client_cert_callback + ) client_cert_callback.assert_called_once() @@ -273,12 +324,48 @@ def test_secure_authorized_channel_with_client_cert_callback_failure( client_cert_callback.side_effect = Exception("callback exception") with pytest.raises(Exception) as excinfo: - google.auth.transport.grpc.secure_authorized_channel( - credentials, request, target, client_cert_callback=client_cert_callback - ) + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + google.auth.transport.grpc.secure_authorized_channel( + credentials, + request, + target, + client_cert_callback=client_cert_callback, + ) assert str(excinfo.value) == "callback exception" + def test_secure_authorized_channel_cert_callback_without_client_cert_env( + self, + secure_channel, + ssl_channel_credentials, + metadata_call_credentials, + composite_channel_credentials, + get_client_ssl_credentials, + ): + # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE + # environment variable is not set. + credentials = mock.Mock() + request = mock.Mock() + target = "example.com:80" + client_cert_callback = mock.Mock() + + google.auth.transport.grpc.secure_authorized_channel( + credentials, request, target, client_cert_callback=client_cert_callback + ) + + # Check client_cert_callback is not called because GOOGLE_API_USE_CLIENT_CERTIFICATE + # is not set. + client_cert_callback.assert_not_called() + + ssl_channel_credentials.assert_called_once() + + # Check the composite credentials call. + composite_channel_credentials.assert_called_once_with( + ssl_channel_credentials.return_value, metadata_call_credentials.return_value + ) + @mock.patch("grpc.ssl_channel_credentials", autospec=True) @mock.patch( @@ -299,7 +386,10 @@ def test_no_context_aware_metadata( # Mock that the metadata file doesn't exist. mock_check_dca_metadata_path.return_value = None - ssl_credentials = google.auth.transport.grpc.SslCredentials() + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + ssl_credentials = google.auth.transport.grpc.SslCredentials() # Since no context aware metadata is found, we wouldn't call # get_client_ssl_credentials, and the SSL channel credentials created is @@ -325,7 +415,10 @@ def test_get_client_ssl_credentials_failure( mock_get_client_ssl_credentials.side_effect = exceptions.ClientCertError() with pytest.raises(exceptions.MutualTLSChannelError): - assert google.auth.transport.grpc.SslCredentials().ssl_credentials + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + assert google.auth.transport.grpc.SslCredentials().ssl_credentials def test_get_client_ssl_credentials_success( self, @@ -345,7 +438,10 @@ def test_get_client_ssl_credentials_success( None, ) - ssl_credentials = google.auth.transport.grpc.SslCredentials() + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + ssl_credentials = google.auth.transport.grpc.SslCredentials() assert ssl_credentials.ssl_credentials is not None assert ssl_credentials.is_mtls @@ -353,3 +449,20 @@ def test_get_client_ssl_credentials_success( mock_ssl_channel_credentials.assert_called_once_with( certificate_chain=PUBLIC_CERT_BYTES, private_key=PRIVATE_KEY_BYTES ) + + def test_get_client_ssl_credentials_without_client_cert_env( + self, + mock_check_dca_metadata_path, + mock_read_dca_metadata_file, + mock_get_client_ssl_credentials, + mock_ssl_channel_credentials, + ): + # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE is not set. + ssl_credentials = google.auth.transport.grpc.SslCredentials() + + assert ssl_credentials.ssl_credentials is not None + assert not ssl_credentials.is_mtls + mock_check_dca_metadata_path.assert_not_called() + mock_read_dca_metadata_file.assert_not_called() + mock_get_client_ssl_credentials.assert_not_called() + mock_ssl_channel_credentials.assert_called_once() diff --git a/tests/transport/test_requests.py b/tests/transport/test_requests.py index 7ac55cebb..d56c2be55 100644 --- a/tests/transport/test_requests.py +++ b/tests/transport/test_requests.py @@ -14,6 +14,7 @@ import datetime import functools +import os import sys import freezegun @@ -24,6 +25,7 @@ import requests.adapters from six.moves import http_client +from google.auth import environment_vars from google.auth import exceptions import google.auth.credentials import google.auth.transport._mtls_helper @@ -380,7 +382,10 @@ def test_configure_mtls_channel_with_callback(self): auth_session = google.auth.transport.requests.AuthorizedSession( credentials=mock.Mock() ) - auth_session.configure_mtls_channel(mock_callback) + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + auth_session.configure_mtls_channel(mock_callback) assert auth_session.is_mtls assert isinstance( @@ -401,7 +406,10 @@ def test_configure_mtls_channel_with_metadata(self, mock_get_client_cert_and_key auth_session = google.auth.transport.requests.AuthorizedSession( credentials=mock.Mock() ) - auth_session.configure_mtls_channel() + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + auth_session.configure_mtls_channel() assert auth_session.is_mtls assert isinstance( @@ -421,7 +429,10 @@ def test_configure_mtls_channel_non_mtls( auth_session = google.auth.transport.requests.AuthorizedSession( credentials=mock.Mock() ) - auth_session.configure_mtls_channel() + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + auth_session.configure_mtls_channel() assert not auth_session.is_mtls @@ -438,10 +449,38 @@ def test_configure_mtls_channel_exceptions(self, mock_get_client_cert_and_key): credentials=mock.Mock() ) with pytest.raises(exceptions.MutualTLSChannelError): - auth_session.configure_mtls_channel() + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + auth_session.configure_mtls_channel() mock_get_client_cert_and_key.return_value = (False, None, None) with mock.patch.dict("sys.modules"): sys.modules["OpenSSL"] = None with pytest.raises(exceptions.MutualTLSChannelError): - auth_session.configure_mtls_channel() + with mock.patch.dict( + os.environ, + {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}, + ): + auth_session.configure_mtls_channel() + + @mock.patch( + "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True + ) + def test_configure_mtls_channel_without_client_cert_env( + self, get_client_cert_and_key + ): + # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE + # environment variable is not set. + auth_session = google.auth.transport.requests.AuthorizedSession( + credentials=mock.Mock() + ) + + auth_session.configure_mtls_channel() + assert not auth_session.is_mtls + get_client_cert_and_key.assert_not_called() + + mock_callback = mock.Mock() + auth_session.configure_mtls_channel(mock_callback) + assert not auth_session.is_mtls + mock_callback.assert_not_called() diff --git a/tests/transport/test_urllib3.py b/tests/transport/test_urllib3.py index 3158b92c6..29561f6d6 100644 --- a/tests/transport/test_urllib3.py +++ b/tests/transport/test_urllib3.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os import sys import mock @@ -20,6 +21,7 @@ from six.moves import http_client import urllib3 +from google.auth import environment_vars from google.auth import exceptions import google.auth.credentials import google.auth.transport._mtls_helper @@ -179,7 +181,10 @@ def test_configure_mtls_channel_with_callback(self, mock_make_mutual_tls_http): ) with pytest.warns(UserWarning): - is_mtls = authed_http.configure_mtls_channel(callback) + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + is_mtls = authed_http.configure_mtls_channel(callback) assert is_mtls mock_make_mutual_tls_http.assert_called_once_with( @@ -202,7 +207,10 @@ def test_configure_mtls_channel_with_metadata( pytest.public_cert_bytes, pytest.private_key_bytes, ) - is_mtls = authed_http.configure_mtls_channel() + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + is_mtls = authed_http.configure_mtls_channel() assert is_mtls mock_get_client_cert_and_key.assert_called_once() @@ -222,7 +230,10 @@ def test_configure_mtls_channel_non_mtls( ) mock_get_client_cert_and_key.return_value = (False, None, None) - is_mtls = authed_http.configure_mtls_channel() + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + is_mtls = authed_http.configure_mtls_channel() assert not is_mtls mock_get_client_cert_and_key.assert_called_once() @@ -238,10 +249,39 @@ def test_configure_mtls_channel_exceptions(self, mock_get_client_cert_and_key): mock_get_client_cert_and_key.side_effect = exceptions.ClientCertError() with pytest.raises(exceptions.MutualTLSChannelError): - authed_http.configure_mtls_channel() + with mock.patch.dict( + os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} + ): + authed_http.configure_mtls_channel() mock_get_client_cert_and_key.return_value = (False, None, None) with mock.patch.dict("sys.modules"): sys.modules["OpenSSL"] = None with pytest.raises(exceptions.MutualTLSChannelError): - authed_http.configure_mtls_channel() + with mock.patch.dict( + os.environ, + {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}, + ): + authed_http.configure_mtls_channel() + + @mock.patch( + "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True + ) + def test_configure_mtls_channel_without_client_cert_env( + self, get_client_cert_and_key + ): + callback = mock.Mock() + + authed_http = google.auth.transport.urllib3.AuthorizedHttp( + credentials=mock.Mock(), http=mock.Mock() + ) + + # Test the callback is not called if GOOGLE_API_USE_CLIENT_CERTIFICATE is not set. + is_mtls = authed_http.configure_mtls_channel(callback) + assert not is_mtls + callback.assert_not_called() + + # Test ADC client cert is not used if GOOGLE_API_USE_CLIENT_CERTIFICATE is not set. + is_mtls = authed_http.configure_mtls_channel(callback) + assert not is_mtls + get_client_cert_and_key.assert_not_called()