-
Notifications
You must be signed in to change notification settings - Fork 183
Refresh exec-based API credentials when they expire #250
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,7 +29,7 @@ | |
from kubernetes.client import Configuration | ||
|
||
from .config_exception import ConfigException | ||
from .dateutil import parse_rfc3339 | ||
from .dateutil import format_rfc3339, parse_rfc3339 | ||
from .kube_config import (ENV_KUBECONFIG_PATH_SEPARATOR, CommandTokenSource, | ||
ConfigNode, FileOrData, KubeConfigLoader, | ||
KubeConfigMerger, _cleanup_temp_files, | ||
|
@@ -346,9 +346,12 @@ def test_get_with_name_on_duplicate_name(self): | |
class FakeConfig: | ||
|
||
FILE_KEYS = ["ssl_ca_cert", "key_file", "cert_file"] | ||
IGNORE_KEYS = ["refresh_api_key_hook"] | ||
|
||
def __init__(self, token=None, **kwargs): | ||
self.api_key = {} | ||
# Provided by the OpenAPI-generated Configuration class | ||
self.refresh_api_key_hook = None | ||
if token: | ||
self.api_key['authorization'] = token | ||
|
||
|
@@ -358,6 +361,8 @@ def __eq__(self, other): | |
if len(self.__dict__) != len(other.__dict__): | ||
return | ||
for k, v in self.__dict__.items(): | ||
if k in self.IGNORE_KEYS: | ||
continue | ||
if k not in other.__dict__: | ||
return | ||
if k in self.FILE_KEYS: | ||
|
@@ -956,17 +961,15 @@ def test_load_user_token(self): | |
|
||
def test_gcp_no_refresh(self): | ||
fake_config = FakeConfig() | ||
# swagger-generated config has this, but FakeConfig does not. | ||
self.assertFalse(hasattr(fake_config, 'get_api_key_with_prefix')) | ||
self.assertIsNone(fake_config.refresh_api_key_hook) | ||
KubeConfigLoader( | ||
config_dict=self.TEST_KUBE_CONFIG, | ||
active_context="gcp", | ||
get_google_credentials=lambda: _raise_exception( | ||
"SHOULD NOT BE CALLED")).load_and_set(fake_config) | ||
# Should now be populated with a gcp token fetcher. | ||
self.assertIsNotNone(fake_config.get_api_key_with_prefix) | ||
self.assertIsNotNone(fake_config.refresh_api_key_hook) | ||
self.assertEqual(TEST_HOST, fake_config.host) | ||
# For backwards compatibility, authorization field should still be set. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: why do we remove this comment? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test tests GCP token refresh functionality -- before this patch, However, this patch refactors the GCP token refresh logic to use the new |
||
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_DATA_BASE64, | ||
fake_config.api_key['authorization']) | ||
|
||
|
@@ -997,7 +1000,7 @@ def cred(): return None | |
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_ANOTHER_DATA_BASE64, | ||
loader.token) | ||
|
||
def test_gcp_get_api_key_with_prefix(self): | ||
def test_gcp_refresh_api_key_hook(self): | ||
class cred_old: | ||
token = TEST_DATA_BASE64 | ||
expiry = DATETIME_EXPIRY_PAST | ||
|
@@ -1015,15 +1018,13 @@ class cred_new: | |
get_google_credentials=_get_google_credentials) | ||
loader.load_and_set(fake_config) | ||
original_expiry = _get_expiry(loader, "expired_gcp_refresh") | ||
# Call GCP token fetcher. | ||
token = fake_config.get_api_key_with_prefix() | ||
# Refresh the GCP token. | ||
fake_config.refresh_api_key_hook(fake_config) | ||
new_expiry = _get_expiry(loader, "expired_gcp_refresh") | ||
|
||
self.assertTrue(new_expiry > original_expiry) | ||
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_ANOTHER_DATA_BASE64, | ||
loader.token) | ||
self.assertEqual(BEARER_TOKEN_FORMAT % TEST_ANOTHER_DATA_BASE64, | ||
token) | ||
|
||
def test_oidc_no_refresh(self): | ||
loader = KubeConfigLoader( | ||
|
@@ -1383,6 +1384,38 @@ def test_user_exec_auth(self, mock): | |
active_context="exec_cred_user").load_and_set(actual) | ||
self.assertEqual(expected, actual) | ||
|
||
@mock.patch('kubernetes.config.kube_config.ExecProvider.run') | ||
def test_user_exec_auth_with_expiry(self, mock): | ||
expired_token = "expired" | ||
current_token = "current" | ||
mock.side_effect = [ | ||
{ | ||
"token": expired_token, | ||
"expirationTimestamp": format_rfc3339(DATETIME_EXPIRY_PAST) | ||
}, | ||
{ | ||
"token": current_token, | ||
"expirationTimestamp": format_rfc3339(DATETIME_EXPIRY_FUTURE) | ||
} | ||
] | ||
|
||
fake_config = FakeConfig() | ||
self.assertIsNone(fake_config.refresh_api_key_hook) | ||
|
||
KubeConfigLoader( | ||
config_dict=self.TEST_KUBE_CONFIG, | ||
active_context="exec_cred_user").load_and_set(fake_config) | ||
# The kube config should use the first token returned from the | ||
# exec provider. | ||
self.assertEqual(fake_config.api_key["authorization"], | ||
BEARER_TOKEN_FORMAT % expired_token) | ||
# Should now be populated with a method to refresh expired tokens. | ||
self.assertIsNotNone(fake_config.refresh_api_key_hook) | ||
# Refresh the token; the kube config should be updated. | ||
fake_config.refresh_api_key_hook(fake_config) | ||
self.assertEqual(fake_config.api_key["authorization"], | ||
BEARER_TOKEN_FORMAT % current_token) | ||
|
||
@mock.patch('kubernetes.config.kube_config.ExecProvider.run') | ||
def test_user_exec_auth_certificates(self, mock): | ||
mock.return_value = { | ||
|
@@ -1412,7 +1445,6 @@ def test_user_cmd_path(self): | |
KubeConfigLoader( | ||
config_dict=self.TEST_KUBE_CONFIG, | ||
active_context="contexttestcmdpath").load_and_set(actual) | ||
del actual.get_api_key_with_prefix | ||
self.assertEqual(expected, actual) | ||
|
||
def test_user_cmd_path_empty(self): | ||
|
@@ -1490,31 +1522,28 @@ def test__get_kube_config_loader_dict_no_persist(self): | |
class TestKubernetesClientConfiguration(BaseTestCase): | ||
# Verifies properties of kubernetes.client.Configuration. | ||
# These tests guard against changes to the upstream configuration class, | ||
# since GCP authorization overrides get_api_key_with_prefix to refresh its | ||
# token regularly. | ||
# since GCP and Exec authorization use refresh_api_key_hook to refresh | ||
# their tokens regularly. | ||
|
||
def test_get_api_key_with_prefix_exists(self): | ||
self.assertTrue(hasattr(Configuration, 'get_api_key_with_prefix')) | ||
def test_refresh_api_key_hook_exists(self): | ||
self.assertTrue(hasattr(Configuration(), 'refresh_api_key_hook')) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change is made because this patch changes the way this code interacts with the upstream Because that property is only created when |
||
|
||
def test_get_api_key_with_prefix_returns_token(self): | ||
expected_token = 'expected_token' | ||
config = Configuration() | ||
config.api_key['authorization'] = expected_token | ||
self.assertEqual(expected_token, | ||
config.get_api_key_with_prefix('authorization')) | ||
|
||
def test_auth_settings_calls_get_api_key_with_prefix(self): | ||
def test_get_api_key_calls_refresh_api_key_hook(self): | ||
identifier = 'authorization' | ||
expected_token = 'expected_token' | ||
old_token = 'old_token' | ||
config = Configuration( | ||
api_key={identifier: old_token}, | ||
api_key_prefix={identifier: 'Bearer'} | ||
) | ||
|
||
def refresh_api_key_hook(client_config): | ||
self.assertEqual(client_config, config) | ||
client_config.api_key[identifier] = expected_token | ||
config.refresh_api_key_hook = refresh_api_key_hook | ||
|
||
def fake_get_api_key_with_prefix(identifier): | ||
self.assertEqual('authorization', identifier) | ||
return expected_token | ||
config = Configuration() | ||
config.api_key['authorization'] = old_token | ||
config.get_api_key_with_prefix = fake_get_api_key_with_prefix | ||
self.assertEqual(expected_token, | ||
config.auth_settings()['BearerToken']['value']) | ||
self.assertEqual('Bearer ' + expected_token, | ||
config.get_api_key_with_prefix(identifier)) | ||
|
||
|
||
class TestKubeConfigMerger(BaseTestCase): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the indentation correct? Do we want to install the hook only when
token
is set?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My reasoning here was that only token-based authentication types would require refreshing -- other types (for example, certificate-based authentication) are expected to use stable credentials and therefore wouldn't need this hook.
However, if that's not a valid assumption, I don't think it would cause any harm to install this hook in all cases, other than perhaps running the refresh hook too often. Happy to update in that case!