Skip to content

Commit b2be7eb

Browse files
feat: add default client cert source util (#486)
feat: add default client cert source util
1 parent e242a84 commit b2be7eb

6 files changed

Lines changed: 176 additions & 22 deletions

File tree

packages/google-auth/google/auth/exceptions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,5 @@ class DefaultCredentialsError(GoogleAuthError):
3737

3838

3939
class MutualTLSChannelError(GoogleAuthError):
40-
"""Used to indicate that mutual TLS channel creation is failed."""
40+
"""Used to indicate that mutual TLS channel creation is failed, or mutual
41+
TLS channel credentials is missing or invalid."""
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright 2020 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Utilites for mutual TLS."""
16+
17+
import six
18+
19+
from google.auth import exceptions
20+
from google.auth.transport import _mtls_helper
21+
22+
23+
def has_default_client_cert_source():
24+
"""Check if default client SSL credentials exists on the device.
25+
26+
Returns:
27+
bool: indicating if the default client cert source exists.
28+
"""
29+
metadata_path = _mtls_helper._check_dca_metadata_path(
30+
_mtls_helper.CONTEXT_AWARE_METADATA_PATH
31+
)
32+
return metadata_path is not None
33+
34+
35+
def default_client_cert_source():
36+
"""Get a callback which returns the default client SSL credentials.
37+
38+
Returns:
39+
Callable[[], [bytes, bytes]]: A callback which returns the default
40+
client certificate bytes and private key bytes, both in PEM format.
41+
42+
Raises:
43+
google.auth.exceptions.DefaultClientCertSourceError: If the default
44+
client SSL credentials don't exist or are malformed.
45+
"""
46+
if not has_default_client_cert_source():
47+
raise exceptions.MutualTLSChannelError(
48+
"Default client cert source doesn't exist"
49+
)
50+
51+
def callback():
52+
try:
53+
_, cert_bytes, key_bytes = _mtls_helper.get_client_cert_and_key()
54+
except (OSError, RuntimeError, ValueError) as caught_exc:
55+
new_exc = exceptions.MutualTLSChannelError(caught_exc)
56+
six.raise_from(new_exc, caught_exc)
57+
58+
return cert_bytes, key_bytes
59+
60+
return callback

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,8 @@ class AuthorizedSession(requests.Session):
249249
credentials' headers to the request and refreshing credentials as needed.
250250
251251
This class also supports mutual TLS via :meth:`configure_mtls_channel`
252-
method. If client_cert_callabck is provided, client certificate and private
253-
key are loaded using the callback; if client_cert_callabck is None,
252+
method. If client_cert_callback is provided, client certificate and private
253+
key are loaded using the callback; if client_cert_callback is None,
254254
application default SSL credentials will be used. Exceptions are raised if
255255
there are problems with the certificate, private key, or the loading process,
256256
so it should be called within a try/except block.
@@ -344,11 +344,11 @@ def configure_mtls_channel(self, client_cert_callback=None):
344344
"""Configure the client certificate and key for SSL connection.
345345
346346
If client certificate and key are successfully obtained (from the given
347-
client_cert_callabck or from application default SSL credentials), a
347+
client_cert_callback or from application default SSL credentials), a
348348
:class:`_MutualTlsAdapter` instance will be mounted to "https://" prefix.
349349
350350
Args:
351-
client_cert_callabck (Optional[Callable[[], (bytes, bytes)]]):
351+
client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
352352
The optional callback returns the client certificate and private
353353
key bytes both in PEM format.
354354
If the callback is None, application default SSL credentials

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,8 @@ class AuthorizedHttp(urllib3.request.RequestMethods):
202202
credentials' headers to the request and refreshing credentials as needed.
203203
204204
This class also supports mutual TLS via :meth:`configure_mtls_channel`
205-
method. If client_cert_callabck is provided, client certificate and private
206-
key are loaded using the callback; if client_cert_callabck is None,
205+
method. If client_cert_callback is provided, client certificate and private
206+
key are loaded using the callback; if client_cert_callback is None,
207207
application default SSL credentials will be used. Exceptions are raised if
208208
there are problems with the certificate, private key, or the loading process,
209209
so it should be called within a try/except block.
@@ -280,14 +280,14 @@ def __init__(
280280

281281
super(AuthorizedHttp, self).__init__()
282282

283-
def configure_mtls_channel(self, client_cert_callabck=None):
284-
"""Configures mutual TLS channel using the given client_cert_callabck or
283+
def configure_mtls_channel(self, client_cert_callback=None):
284+
"""Configures mutual TLS channel using the given client_cert_callback or
285285
application default SSL credentials. Returns True if the channel is
286286
mutual TLS and False otherwise. Note that the `http` provided in the
287287
constructor will be overwritten.
288288
289289
Args:
290-
client_cert_callabck (Optional[Callable[[], (bytes, bytes)]]):
290+
client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
291291
The optional callback returns the client certificate and private
292292
key bytes both in PEM format.
293293
If the callback is None, application default SSL credentials
@@ -308,7 +308,7 @@ def configure_mtls_channel(self, client_cert_callabck=None):
308308

309309
try:
310310
found_cert_key, cert, key = transport._mtls_helper.get_client_cert_and_key(
311-
client_cert_callabck
311+
client_cert_callback
312312
)
313313

314314
if found_cert_key:

packages/google-auth/system_tests/test_mtls_http.py

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,14 @@
1818

1919
import google.auth
2020
import google.auth.credentials
21+
from google.auth.transport import mtls
2122
import google.auth.transport.requests
2223
import google.auth.transport.urllib3
2324

2425
MTLS_ENDPOINT = "https://pubsub.mtls.googleapis.com/v1/projects/{}/topics"
2526
REGULAR_ENDPOINT = "https://pubsub.googleapis.com/v1/projects/{}/topics"
2627

2728

28-
def check_context_aware_metadata():
29-
metadata_path = path.expanduser("~/.secureConnect/context_aware_metadata.json")
30-
return path.exists(metadata_path)
31-
32-
3329
def test_requests():
3430
credentials, project_id = google.auth.default()
3531
credentials = google.auth.credentials.with_scopes_if_required(
@@ -39,9 +35,9 @@ def test_requests():
3935
authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
4036
authed_session.configure_mtls_channel()
4137

42-
# If the devices has context aware metadata, then a mutual TLS channel is
43-
# supposed to be created.
44-
assert authed_session.is_mtls == check_context_aware_metadata()
38+
# If the devices has default client cert source, then a mutual TLS channel
39+
# is supposed to be created.
40+
assert authed_session.is_mtls == mtls.has_default_client_cert_source()
4541

4642
# Sleep 1 second to avoid 503 error.
4743
time.sleep(1)
@@ -63,9 +59,9 @@ def test_urllib3():
6359
authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials)
6460
is_mtls = authed_http.configure_mtls_channel()
6561

66-
# If the devices has context aware metadata, then a mutual TLS channel is
67-
# supposed to be created.
68-
assert is_mtls == check_context_aware_metadata()
62+
# If the devices has default client cert source, then a mutual TLS channel
63+
# is supposed to be created.
64+
assert is_mtls == mtls.has_default_client_cert_source()
6965

7066
# Sleep 1 second to avoid 503 error.
7167
time.sleep(1)
@@ -76,3 +72,45 @@ def test_urllib3():
7672
response = authed_http.request("GET", REGULAR_ENDPOINT.format(project_id))
7773

7874
assert response.status == 200
75+
76+
77+
def test_requests_with_default_client_cert_source():
78+
credentials, project_id = google.auth.default()
79+
credentials = google.auth.credentials.with_scopes_if_required(
80+
credentials, ["https://www.googleapis.com/auth/pubsub"]
81+
)
82+
83+
authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
84+
85+
if mtls.has_default_client_cert_source():
86+
authed_session.configure_mtls_channel(
87+
client_cert_callback=mtls.default_client_cert_source()
88+
)
89+
90+
assert authed_session.is_mtls
91+
92+
# Sleep 1 second to avoid 503 error.
93+
time.sleep(1)
94+
95+
response = authed_session.get(MTLS_ENDPOINT.format(project_id))
96+
assert response.ok
97+
98+
99+
def test_urllib3_with_default_client_cert_source():
100+
credentials, project_id = google.auth.default()
101+
credentials = google.auth.credentials.with_scopes_if_required(
102+
credentials, ["https://www.googleapis.com/auth/pubsub"]
103+
)
104+
105+
authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials)
106+
107+
if mtls.has_default_client_cert_source():
108+
assert authed_http.configure_mtls_channel(
109+
client_cert_callback=mtls.default_client_cert_source()
110+
)
111+
112+
# Sleep 1 second to avoid 503 error.
113+
time.sleep(1)
114+
115+
response = authed_http.request("GET", MTLS_ENDPOINT.format(project_id))
116+
assert response.status == 200
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright 2020 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import mock
16+
import pytest
17+
18+
from google.auth import exceptions
19+
from google.auth.transport import mtls
20+
21+
22+
@mock.patch(
23+
"google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
24+
)
25+
def test_has_default_client_cert_source(check_dca_metadata_path):
26+
check_dca_metadata_path.return_value = mock.Mock()
27+
assert mtls.has_default_client_cert_source()
28+
29+
check_dca_metadata_path.return_value = None
30+
assert not mtls.has_default_client_cert_source()
31+
32+
33+
@mock.patch("google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True)
34+
@mock.patch("google.auth.transport.mtls.has_default_client_cert_source", autospec=True)
35+
def test_default_client_cert_source(
36+
has_default_client_cert_source, get_client_cert_and_key
37+
):
38+
# Test default client cert source doesn't exist.
39+
has_default_client_cert_source.return_value = False
40+
with pytest.raises(exceptions.MutualTLSChannelError):
41+
mtls.default_client_cert_source()
42+
43+
# The following tests will assume default client cert source exists.
44+
has_default_client_cert_source.return_value = True
45+
46+
# Test good callback.
47+
get_client_cert_and_key.return_value = (True, b"cert", b"key")
48+
callback = mtls.default_client_cert_source()
49+
assert callback() == (b"cert", b"key")
50+
51+
# Test bad callback which throws exception.
52+
get_client_cert_and_key.side_effect = ValueError()
53+
callback = mtls.default_client_cert_source()
54+
with pytest.raises(exceptions.MutualTLSChannelError):
55+
callback()

0 commit comments

Comments
 (0)