Skip to content

Commit 18c2ec1

Browse files
andyrzhaoclundin25
andauthoredAug 7, 2024··
feat(auth): Update get_client_ssl_credentials to support X.509 workload certs (#1558)
* feat(auth): Update get_client_ssl_credentials to support X.509 workload certs * feat(auth): Update has_default_client_cert_source * feat(auth): Fix formatting * feat(auth): Fix test__mtls_helper.py * feat(auth): Fix function name in tests * chore: Refresh system test creds. * feat(auth): Fix style * feat(auth): Fix casing * feat(auth): Fix linter issue * feat(auth): Fix coverage issue --------- Co-authored-by: Carl Lundin <clundin@google.com> Co-authored-by: Carl Lundin <108372512+clundin25@users.noreply.github.com>
1 parent f858a15 commit 18c2ec1

File tree

6 files changed

+145
-70
lines changed

6 files changed

+145
-70
lines changed
 

‎google/auth/transport/_mtls_helper.py

+29-11
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from google.auth import exceptions
2424

2525
CONTEXT_AWARE_METADATA_PATH = "~/.secureConnect/context_aware_metadata.json"
26-
_CERTIFICATE_CONFIGURATION_DEFAULT_PATH = "~/.config/gcloud/certificate_config.json"
26+
CERTIFICATE_CONFIGURATION_DEFAULT_PATH = "~/.config/gcloud/certificate_config.json"
2727
_CERTIFICATE_CONFIGURATION_ENV = "GOOGLE_API_CERTIFICATE_CONFIG"
2828
_CERT_PROVIDER_COMMAND = "cert_provider_command"
2929
_CERT_REGEX = re.compile(
@@ -48,21 +48,21 @@
4848
)
4949

5050

51-
def _check_dca_metadata_path(metadata_path):
52-
"""Checks for context aware metadata. If it exists, returns the absolute path;
51+
def _check_config_path(config_path):
52+
"""Checks for config file path. If it exists, returns the absolute path with user expansion;
5353
otherwise returns None.
5454
5555
Args:
56-
metadata_path (str): context aware metadata path.
56+
config_path (str): The config file path for either context_aware_metadata.json or certificate_config.json for example
5757
5858
Returns:
5959
str: absolute path if exists and None otherwise.
6060
"""
61-
metadata_path = path.expanduser(metadata_path)
62-
if not path.exists(metadata_path):
63-
_LOGGER.debug("%s is not found, skip client SSL authentication.", metadata_path)
61+
config_path = path.expanduser(config_path)
62+
if not path.exists(config_path):
63+
_LOGGER.debug("%s is not found.", config_path)
6464
return None
65-
return metadata_path
65+
return config_path
6666

6767

6868
def _load_json_file(path):
@@ -136,7 +136,7 @@ def _get_cert_config_path(certificate_config_path=None):
136136
if env_path is not None and env_path != "":
137137
certificate_config_path = env_path
138138
else:
139-
certificate_config_path = _CERTIFICATE_CONFIGURATION_DEFAULT_PATH
139+
certificate_config_path = CERTIFICATE_CONFIGURATION_DEFAULT_PATH
140140

141141
certificate_config_path = path.expanduser(certificate_config_path)
142142
if not path.exists(certificate_config_path):
@@ -279,14 +279,22 @@ def _run_cert_provider_command(command, expect_encrypted_key=False):
279279
def get_client_ssl_credentials(
280280
generate_encrypted_key=False,
281281
context_aware_metadata_path=CONTEXT_AWARE_METADATA_PATH,
282+
certificate_config_path=CERTIFICATE_CONFIGURATION_DEFAULT_PATH,
282283
):
283284
"""Returns the client side certificate, private key and passphrase.
284285
286+
We look for certificates and keys with the following order of priority:
287+
1. Certificate and key specified by certificate_config.json.
288+
Currently, only X.509 workload certificates are supported.
289+
2. Certificate and key specified by context aware metadata (i.e. SecureConnect).
290+
285291
Args:
286292
generate_encrypted_key (bool): If set to True, encrypted private key
287293
and passphrase will be generated; otherwise, unencrypted private key
288-
will be generated and passphrase will be None.
294+
will be generated and passphrase will be None. This option only
295+
affects keys obtained via context_aware_metadata.json.
289296
context_aware_metadata_path (str): The context_aware_metadata.json file path.
297+
certificate_config_path (str): The certificate_config.json file path.
290298
291299
Returns:
292300
Tuple[bool, bytes, bytes, bytes]:
@@ -297,7 +305,17 @@ def get_client_ssl_credentials(
297305
google.auth.exceptions.ClientCertError: if problems occurs when getting
298306
the cert, key and passphrase.
299307
"""
300-
metadata_path = _check_dca_metadata_path(context_aware_metadata_path)
308+
309+
# 1. Check for certificate config json.
310+
cert_config_path = _check_config_path(certificate_config_path)
311+
if cert_config_path:
312+
# Attempt to retrieve X.509 Workload cert and key.
313+
cert, key = _get_workload_cert_and_key(cert_config_path)
314+
if cert and key:
315+
return True, cert, key, None
316+
317+
# 2. Check for context aware metadata json
318+
metadata_path = _check_config_path(context_aware_metadata_path)
301319

302320
if metadata_path:
303321
metadata_json = _load_json_file(metadata_path)

‎google/auth/transport/grpc.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ def __init__(self):
302302
self._is_mtls = False
303303
else:
304304
# Load client SSL credentials.
305-
metadata_path = _mtls_helper._check_dca_metadata_path(
305+
metadata_path = _mtls_helper._check_config_path(
306306
_mtls_helper.CONTEXT_AWARE_METADATA_PATH
307307
)
308308
self._is_mtls = metadata_path is not None

‎google/auth/transport/mtls.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,19 @@ def has_default_client_cert_source():
2424
Returns:
2525
bool: indicating if the default client cert source exists.
2626
"""
27-
metadata_path = _mtls_helper._check_dca_metadata_path(
28-
_mtls_helper.CONTEXT_AWARE_METADATA_PATH
29-
)
30-
return metadata_path is not None
27+
if (
28+
_mtls_helper._check_config_path(_mtls_helper.CONTEXT_AWARE_METADATA_PATH)
29+
is not None
30+
):
31+
return True
32+
if (
33+
_mtls_helper._check_config_path(
34+
_mtls_helper.CERTIFICATE_CONFIGURATION_DEFAULT_PATH
35+
)
36+
is not None
37+
):
38+
return True
39+
return False
3140

3241

3342
def default_client_cert_source():

‎tests/transport/test__mtls_helper.py

+69-28
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,15 @@ def test_key(self):
111111
)
112112

113113

114-
class TestCheckaMetadataPath(object):
114+
class TestCheckConfigPath(object):
115115
def test_success(self):
116116
metadata_path = os.path.join(pytest.data_dir, "context_aware_metadata.json")
117-
returned_path = _mtls_helper._check_dca_metadata_path(metadata_path)
117+
returned_path = _mtls_helper._check_config_path(metadata_path)
118118
assert returned_path is not None
119119

120120
def test_failure(self):
121121
metadata_path = os.path.join(pytest.data_dir, "not_exists.json")
122-
returned_path = _mtls_helper._check_dca_metadata_path(metadata_path)
122+
returned_path = _mtls_helper._check_config_path(metadata_path)
123123
assert returned_path is None
124124

125125

@@ -275,54 +275,92 @@ def test_popen_raise_exception(self, mock_popen):
275275

276276
class TestGetClientSslCredentials(object):
277277
@mock.patch(
278-
"google.auth.transport._mtls_helper._run_cert_provider_command", autospec=True
278+
"google.auth.transport._mtls_helper._get_workload_cert_and_key", autospec=True
279279
)
280-
@mock.patch("google.auth.transport._mtls_helper._load_json_file", autospec=True)
281280
@mock.patch(
282-
"google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
281+
"google.auth.transport._mtls_helper._run_cert_provider_command", autospec=True
283282
)
284-
def test_success(
283+
@mock.patch("google.auth.transport._mtls_helper._load_json_file", autospec=True)
284+
@mock.patch("google.auth.transport._mtls_helper._check_config_path", autospec=True)
285+
def test_success_with_context_aware_metadata(
285286
self,
286-
mock_check_dca_metadata_path,
287+
mock_check_config_path,
287288
mock_load_json_file,
288289
mock_run_cert_provider_command,
290+
mock_get_workload_cert_and_key,
289291
):
290-
mock_check_dca_metadata_path.return_value = True
292+
mock_check_config_path.return_value = "/path/to/config"
291293
mock_load_json_file.return_value = {"cert_provider_command": ["command"]}
292294
mock_run_cert_provider_command.return_value = (b"cert", b"key", None)
295+
mock_get_workload_cert_and_key.return_value = (None, None)
293296
has_cert, cert, key, passphrase = _mtls_helper.get_client_ssl_credentials()
294297
assert has_cert
295298
assert cert == b"cert"
296299
assert key == b"key"
297300
assert passphrase is None
298301

299302
@mock.patch(
300-
"google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
303+
"google.auth.transport._mtls_helper._read_cert_and_key_files", autospec=True
301304
)
302-
def test_success_without_metadata(self, mock_check_dca_metadata_path):
303-
mock_check_dca_metadata_path.return_value = False
305+
@mock.patch(
306+
"google.auth.transport._mtls_helper._get_cert_config_path", autospec=True
307+
)
308+
@mock.patch("google.auth.transport._mtls_helper._load_json_file", autospec=True)
309+
@mock.patch("google.auth.transport._mtls_helper._check_config_path", autospec=True)
310+
def test_success_with_certificate_config(
311+
self,
312+
mock_check_config_path,
313+
mock_load_json_file,
314+
mock_get_cert_config_path,
315+
mock_read_cert_and_key_files,
316+
):
317+
cert_config_path = "/path/to/config"
318+
mock_check_config_path.return_value = cert_config_path
319+
mock_load_json_file.return_value = {
320+
"cert_configs": {
321+
"workload": {"cert_path": "cert/path", "key_path": "key/path"}
322+
}
323+
}
324+
mock_get_cert_config_path.return_value = cert_config_path
325+
mock_read_cert_and_key_files.return_value = (
326+
pytest.public_cert_bytes,
327+
pytest.private_key_bytes,
328+
)
329+
330+
has_cert, cert, key, passphrase = _mtls_helper.get_client_ssl_credentials()
331+
assert has_cert
332+
assert cert == pytest.public_cert_bytes
333+
assert key == pytest.private_key_bytes
334+
assert passphrase is None
335+
336+
@mock.patch("google.auth.transport._mtls_helper._check_config_path", autospec=True)
337+
def test_success_without_metadata(self, mock_check_config_path):
338+
mock_check_config_path.return_value = False
304339
has_cert, cert, key, passphrase = _mtls_helper.get_client_ssl_credentials()
305340
assert not has_cert
306341
assert cert is None
307342
assert key is None
308343
assert passphrase is None
309344

310345
@mock.patch(
311-
"google.auth.transport._mtls_helper._run_cert_provider_command", autospec=True
346+
"google.auth.transport._mtls_helper._get_workload_cert_and_key", autospec=True
312347
)
313-
@mock.patch("google.auth.transport._mtls_helper._load_json_file", autospec=True)
314348
@mock.patch(
315-
"google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
349+
"google.auth.transport._mtls_helper._run_cert_provider_command", autospec=True
316350
)
351+
@mock.patch("google.auth.transport._mtls_helper._load_json_file", autospec=True)
352+
@mock.patch("google.auth.transport._mtls_helper._check_config_path", autospec=True)
317353
def test_success_with_encrypted_key(
318354
self,
319-
mock_check_dca_metadata_path,
355+
mock_check_config_path,
320356
mock_load_json_file,
321357
mock_run_cert_provider_command,
358+
mock_get_workload_cert_and_key,
322359
):
323-
mock_check_dca_metadata_path.return_value = True
360+
mock_check_config_path.return_value = "/path/to/config"
324361
mock_load_json_file.return_value = {"cert_provider_command": ["command"]}
325362
mock_run_cert_provider_command.return_value = (b"cert", b"key", b"passphrase")
363+
mock_get_workload_cert_and_key.return_value = (None, None)
326364
has_cert, cert, key, passphrase = _mtls_helper.get_client_ssl_credentials(
327365
generate_encrypted_key=True
328366
)
@@ -334,33 +372,36 @@ def test_success_with_encrypted_key(
334372
["command", "--with_passphrase"], expect_encrypted_key=True
335373
)
336374

337-
@mock.patch("google.auth.transport._mtls_helper._load_json_file", autospec=True)
338375
@mock.patch(
339-
"google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
376+
"google.auth.transport._mtls_helper._get_workload_cert_and_key", autospec=True
340377
)
378+
@mock.patch("google.auth.transport._mtls_helper._load_json_file", autospec=True)
379+
@mock.patch("google.auth.transport._mtls_helper._check_config_path", autospec=True)
341380
def test_missing_cert_command(
342-
self, mock_check_dca_metadata_path, mock_load_json_file
381+
self,
382+
mock_check_config_path,
383+
mock_load_json_file,
384+
mock_get_workload_cert_and_key,
343385
):
344-
mock_check_dca_metadata_path.return_value = True
386+
mock_check_config_path.return_value = "/path/to/config"
345387
mock_load_json_file.return_value = {}
388+
mock_get_workload_cert_and_key.return_value = (None, None)
346389
with pytest.raises(exceptions.ClientCertError):
347390
_mtls_helper.get_client_ssl_credentials()
348391

349392
@mock.patch(
350393
"google.auth.transport._mtls_helper._run_cert_provider_command", autospec=True
351394
)
352395
@mock.patch("google.auth.transport._mtls_helper._load_json_file", autospec=True)
353-
@mock.patch(
354-
"google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
355-
)
396+
@mock.patch("google.auth.transport._mtls_helper._check_config_path", autospec=True)
356397
def test_customize_context_aware_metadata_path(
357398
self,
358-
mock_check_dca_metadata_path,
399+
mock_check_config_path,
359400
mock_load_json_file,
360401
mock_run_cert_provider_command,
361402
):
362403
context_aware_metadata_path = "/path/to/metata/data"
363-
mock_check_dca_metadata_path.return_value = context_aware_metadata_path
404+
mock_check_config_path.return_value = context_aware_metadata_path
364405
mock_load_json_file.return_value = {"cert_provider_command": ["command"]}
365406
mock_run_cert_provider_command.return_value = (b"cert", b"key", None)
366407

@@ -372,7 +413,7 @@ def test_customize_context_aware_metadata_path(
372413
assert cert == b"cert"
373414
assert key == b"key"
374415
assert passphrase is None
375-
mock_check_dca_metadata_path.assert_called_with(context_aware_metadata_path)
416+
mock_check_config_path.assert_called_with(context_aware_metadata_path)
376417
mock_load_json_file.assert_called_with(context_aware_metadata_path)
377418

378419

@@ -520,7 +561,7 @@ def test_default(self, mock_path_exists):
520561
mock_path_exists.return_value = True
521562
returned_path = _mtls_helper._get_cert_config_path()
522563
expected_path = os.path.expanduser(
523-
_mtls_helper._CERTIFICATE_CONFIGURATION_DEFAULT_PATH
564+
_mtls_helper.CERTIFICATE_CONFIGURATION_DEFAULT_PATH
524565
)
525566
assert returned_path == expected_path
526567

0 commit comments

Comments
 (0)
Please sign in to comment.