diff --git a/CHANGELOG.md b/CHANGELOG.md index c465e6c78..268077fdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/google-auth/#history +## [1.14.0](https://www.github.com/googleapis/google-auth-library-python/compare/v1.13.1...v1.14.0) (2020-04-13) + + +### Features + +* add default client cert source util ([#486](https://www.github.com/googleapis/google-auth-library-python/issues/486)) ([ed41b49](https://www.github.com/googleapis/google-auth-library-python/commit/ed41b49e9d7ba7402b27107b7aa47eed06ac6c55)) + ### [1.13.1](https://www.github.com/googleapis/google-auth-library-python/compare/v1.13.0...v1.13.1) (2020-04-01) diff --git a/google/auth/exceptions.py b/google/auth/exceptions.py index 95013860a..5b614602b 100644 --- a/google/auth/exceptions.py +++ b/google/auth/exceptions.py @@ -37,4 +37,5 @@ class DefaultCredentialsError(GoogleAuthError): class MutualTLSChannelError(GoogleAuthError): - """Used to indicate that mutual TLS channel creation is failed.""" + """Used to indicate that mutual TLS channel creation is failed, or mutual + TLS channel credentials is missing or invalid.""" diff --git a/google/auth/transport/mtls.py b/google/auth/transport/mtls.py new file mode 100644 index 000000000..063b26504 --- /dev/null +++ b/google/auth/transport/mtls.py @@ -0,0 +1,60 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utilites for mutual TLS.""" + +import six + +from google.auth import exceptions +from google.auth.transport import _mtls_helper + + +def has_default_client_cert_source(): + """Check if default client SSL credentials exists on the device. + + Returns: + bool: indicating if the default client cert source exists. + """ + metadata_path = _mtls_helper._check_dca_metadata_path( + _mtls_helper.CONTEXT_AWARE_METADATA_PATH + ) + return metadata_path is not None + + +def default_client_cert_source(): + """Get a callback which returns the default client SSL credentials. + + Returns: + Callable[[], [bytes, bytes]]: A callback which returns the default + client certificate bytes and private key bytes, both in PEM format. + + Raises: + google.auth.exceptions.DefaultClientCertSourceError: If the default + client SSL credentials don't exist or are malformed. + """ + if not has_default_client_cert_source(): + raise exceptions.MutualTLSChannelError( + "Default client cert source doesn't exist" + ) + + def callback(): + try: + _, cert_bytes, key_bytes = _mtls_helper.get_client_cert_and_key() + except (OSError, RuntimeError, ValueError) as caught_exc: + new_exc = exceptions.MutualTLSChannelError(caught_exc) + six.raise_from(new_exc, caught_exc) + + return cert_bytes, key_bytes + + return callback diff --git a/google/auth/transport/requests.py b/google/auth/transport/requests.py index 26096e213..cc0e93b6f 100644 --- a/google/auth/transport/requests.py +++ b/google/auth/transport/requests.py @@ -249,8 +249,8 @@ 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_callabck is provided, client certificate and private - key are loaded using the callback; if client_cert_callabck is None, + method. 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. @@ -344,11 +344,11 @@ 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_callabck or from application default SSL credentials), a + client_cert_callback or from application default SSL credentials), a :class:`_MutualTlsAdapter` instance will be mounted to "https://" prefix. Args: - client_cert_callabck (Optional[Callable[[], (bytes, bytes)]]): + client_cert_callback (Optional[Callable[[], (bytes, bytes)]]): The optional callback returns the client certificate and private key bytes both in PEM format. If the callback is None, application default SSL credentials diff --git a/google/auth/transport/urllib3.py b/google/auth/transport/urllib3.py index c359f3592..3771d84c4 100644 --- a/google/auth/transport/urllib3.py +++ b/google/auth/transport/urllib3.py @@ -202,8 +202,8 @@ 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_callabck is provided, client certificate and private - key are loaded using the callback; if client_cert_callabck is None, + method. 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. @@ -280,14 +280,14 @@ def __init__( super(AuthorizedHttp, self).__init__() - def configure_mtls_channel(self, client_cert_callabck=None): - """Configures mutual TLS channel using the given client_cert_callabck or + 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. Args: - client_cert_callabck (Optional[Callable[[], (bytes, bytes)]]): + client_cert_callback (Optional[Callable[[], (bytes, bytes)]]): The optional callback returns the client certificate and private key bytes both in PEM format. If the callback is None, application default SSL credentials @@ -308,7 +308,7 @@ def configure_mtls_channel(self, client_cert_callabck=None): try: found_cert_key, cert, key = transport._mtls_helper.get_client_cert_and_key( - client_cert_callabck + client_cert_callback ) if found_cert_key: diff --git a/setup.py b/setup.py index cb6ed5aa6..2acef446b 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ with io.open("README.rst", "r") as fh: long_description = fh.read() -version = "1.13.1" +version = "1.14.0" setup( name="google-auth", diff --git a/system_tests/test_mtls_http.py b/system_tests/test_mtls_http.py index 1fd80311d..4a6a9c4bc 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.transport import mtls import google.auth.transport.requests import google.auth.transport.urllib3 @@ -25,11 +26,6 @@ REGULAR_ENDPOINT = "https://pubsub.googleapis.com/v1/projects/{}/topics" -def check_context_aware_metadata(): - metadata_path = path.expanduser("~/.secureConnect/context_aware_metadata.json") - return path.exists(metadata_path) - - def test_requests(): credentials, project_id = google.auth.default() credentials = google.auth.credentials.with_scopes_if_required( @@ -39,9 +35,9 @@ def test_requests(): authed_session = google.auth.transport.requests.AuthorizedSession(credentials) authed_session.configure_mtls_channel() - # If the devices has context aware metadata, then a mutual TLS channel is - # supposed to be created. - assert authed_session.is_mtls == check_context_aware_metadata() + # If the devices has default client cert source, then a mutual TLS channel + # is supposed to be created. + assert authed_session.is_mtls == mtls.has_default_client_cert_source() # Sleep 1 second to avoid 503 error. time.sleep(1) @@ -63,9 +59,9 @@ def test_urllib3(): authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials) is_mtls = authed_http.configure_mtls_channel() - # If the devices has context aware metadata, then a mutual TLS channel is - # supposed to be created. - assert is_mtls == check_context_aware_metadata() + # If the devices has default client cert source, then a mutual TLS channel + # is supposed to be created. + assert is_mtls == mtls.has_default_client_cert_source() # Sleep 1 second to avoid 503 error. time.sleep(1) @@ -76,3 +72,45 @@ def test_urllib3(): response = authed_http.request("GET", REGULAR_ENDPOINT.format(project_id)) assert response.status == 200 + + +def test_requests_with_default_client_cert_source(): + credentials, project_id = google.auth.default() + credentials = google.auth.credentials.with_scopes_if_required( + credentials, ["https://www.googleapis.com/auth/pubsub"] + ) + + 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() + ) + + assert authed_session.is_mtls + + # Sleep 1 second to avoid 503 error. + time.sleep(1) + + response = authed_session.get(MTLS_ENDPOINT.format(project_id)) + assert response.ok + + +def test_urllib3_with_default_client_cert_source(): + credentials, project_id = google.auth.default() + credentials = google.auth.credentials.with_scopes_if_required( + credentials, ["https://www.googleapis.com/auth/pubsub"] + ) + + 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() + ) + + # Sleep 1 second to avoid 503 error. + time.sleep(1) + + response = authed_http.request("GET", MTLS_ENDPOINT.format(project_id)) + assert response.status == 200 diff --git a/tests/transport/test_mtls.py b/tests/transport/test_mtls.py new file mode 100644 index 000000000..d3bc3915a --- /dev/null +++ b/tests/transport/test_mtls.py @@ -0,0 +1,55 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock +import pytest + +from google.auth import exceptions +from google.auth.transport import mtls + + +@mock.patch( + "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True +) +def test_has_default_client_cert_source(check_dca_metadata_path): + check_dca_metadata_path.return_value = mock.Mock() + assert mtls.has_default_client_cert_source() + + check_dca_metadata_path.return_value = None + assert not mtls.has_default_client_cert_source() + + +@mock.patch("google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True) +@mock.patch("google.auth.transport.mtls.has_default_client_cert_source", autospec=True) +def test_default_client_cert_source( + has_default_client_cert_source, get_client_cert_and_key +): + # Test default client cert source doesn't exist. + has_default_client_cert_source.return_value = False + with pytest.raises(exceptions.MutualTLSChannelError): + mtls.default_client_cert_source() + + # The following tests will assume default client cert source exists. + has_default_client_cert_source.return_value = True + + # Test good callback. + get_client_cert_and_key.return_value = (True, b"cert", b"key") + callback = mtls.default_client_cert_source() + assert callback() == (b"cert", b"key") + + # Test bad callback which throws exception. + get_client_cert_and_key.side_effect = ValueError() + callback = mtls.default_client_cert_source() + with pytest.raises(exceptions.MutualTLSChannelError): + callback()