From 7f7533a94c39b3176430e0aed55b7d66515dbd40 Mon Sep 17 00:00:00 2001 From: Trevor Edwards Date: Fri, 5 Oct 2018 17:09:54 -0700 Subject: [PATCH] Refresh GCP tokens on retrieval by overriding client config method. [Fix #59] --- config/kube_config.py | 16 +++++++ config/kube_config_test.py | 91 +++++++++++++++++++++++++++++++------- 2 files changed, 92 insertions(+), 15 deletions(-) diff --git a/config/kube_config.py b/config/kube_config.py index a5396b95..305b2e0a 100644 --- a/config/kube_config.py +++ b/config/kube_config.py @@ -392,8 +392,24 @@ def _load_cluster_info(self): if 'insecure-skip-tls-verify' in self._cluster: self.verify_ssl = not self._cluster['insecure-skip-tls-verify'] + def _using_gcp_auth_provider(self): + return self._user and \ + 'auth-provider' in self._user and \ + 'name' in self._user['auth-provider'] and \ + self._user['auth-provider']['name'] == 'gcp' + def _set_config(self, client_configuration): + if self._using_gcp_auth_provider(): + # GCP auth tokens must be refreshed regularly, but swagger expects + # a constant token. Replace the swagger-generated client config's + # get_api_key_with_prefix method with our own to allow automatic + # token refresh. + def _gcp_get_api_key(*args): + return self._load_gcp_token(self._user['auth-provider']) + client_configuration.get_api_key_with_prefix = _gcp_get_api_key if 'token' in self.__dict__: + # Note: this line runs for GCP auth tokens as well, but this entry + # will not be updated upon GCP token refresh. client_configuration.api_key['authorization'] = self.token # copy these keys directly from self to configuration object keys = ['host', 'ssl_ca_cert', 'cert_file', 'key_file', 'verify_ssl'] diff --git a/config/kube_config_test.py b/config/kube_config_test.py index 7c9921ed..f68aadf9 100644 --- a/config/kube_config_test.py +++ b/config/kube_config_test.py @@ -34,7 +34,9 @@ EXPIRY_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" # should be less than kube_config.EXPIRY_SKEW_PREVENTION_DELAY -EXPIRY_TIMEDELTA = 2 +PAST_EXPIRY_TIMEDELTA = 2 +# should be more than kube_config.EXPIRY_SKEW_PREVENTION_DELAY +FUTURE_EXPIRY_TIMEDELTA = 60 NON_EXISTING_FILE = "zz_non_existing_file_472398324" @@ -47,9 +49,9 @@ def _format_expiry_datetime(dt): return dt.strftime(EXPIRY_DATETIME_FORMAT) -def _get_expiry(loader): +def _get_expiry(loader, active_context): expired_gcp_conf = (item for item in loader._config.value.get("users") - if item.get("name") == "expired_gcp") + if item.get("name") == active_context) return next(expired_gcp_conf).get("user").get("auth-provider") \ .get("config").get("expiry") @@ -73,8 +75,11 @@ def _raise_exception(st): TEST_PASSWORD = "pass" # token for me:pass TEST_BASIC_TOKEN = "Basic bWU6cGFzcw==" -TEST_TOKEN_EXPIRY = _format_expiry_datetime( - datetime.datetime.utcnow() - datetime.timedelta(minutes=EXPIRY_TIMEDELTA)) +DATETIME_EXPIRY_PAST = datetime.datetime.utcnow( +) - datetime.timedelta(minutes=PAST_EXPIRY_TIMEDELTA) +DATETIME_EXPIRY_FUTURE = datetime.datetime.utcnow( +) + datetime.timedelta(minutes=FUTURE_EXPIRY_TIMEDELTA) +TEST_TOKEN_EXPIRY_PAST = _format_expiry_datetime(DATETIME_EXPIRY_PAST) TEST_SSL_HOST = "https://test-host" TEST_CERTIFICATE_AUTH = "cert-auth" @@ -371,6 +376,13 @@ class TestKubeConfigLoader(BaseTestCase): "user": "expired_gcp" } }, + { + "name": "expired_gcp_refresh", + "context": { + "cluster": "default", + "user": "expired_gcp_refresh" + } + }, { "name": "oidc", "context": { @@ -509,7 +521,24 @@ class TestKubeConfigLoader(BaseTestCase): "name": "gcp", "config": { "access-token": TEST_DATA_BASE64, - "expiry": TEST_TOKEN_EXPIRY, # always in past + "expiry": TEST_TOKEN_EXPIRY_PAST, # always in past + } + }, + "token": TEST_DATA_BASE64, # should be ignored + "username": TEST_USERNAME, # should be ignored + "password": TEST_PASSWORD, # should be ignored + } + }, + # Duplicated from "expired_gcp" so test_load_gcp_token_with_refresh + # is isolated from test_gcp_get_api_key_with_prefix. + { + "name": "expired_gcp_refresh", + "user": { + "auth-provider": { + "name": "gcp", + "config": { + "access-token": TEST_DATA_BASE64, + "expiry": TEST_TOKEN_EXPIRY_PAST, # always in past } }, "token": TEST_DATA_BASE64, # should be ignored @@ -630,16 +659,20 @@ def test_load_user_token(self): self.assertEqual(BEARER_TOKEN_FORMAT % TEST_DATA_BASE64, loader.token) def test_gcp_no_refresh(self): - expected = FakeConfig( - host=TEST_HOST, - token=BEARER_TOKEN_FORMAT % TEST_DATA_BASE64) - actual = FakeConfig() + fake_config = FakeConfig() + # swagger-generated config has this, but FakeConfig does not. + self.assertFalse(hasattr(fake_config, 'get_api_key_with_prefix')) KubeConfigLoader( config_dict=self.TEST_KUBE_CONFIG, active_context="gcp", get_google_credentials=lambda: _raise_exception( - "SHOULD NOT BE CALLED")).load_and_set(actual) - self.assertEqual(expected, actual) + "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.assertEqual(TEST_HOST, fake_config.host) + # For backwards compatibility, authorization field should still be set. + self.assertEqual(BEARER_TOKEN_FORMAT % TEST_DATA_BASE64, + fake_config.api_key['authorization']) def test_load_gcp_token_no_refresh(self): loader = KubeConfigLoader( @@ -654,20 +687,48 @@ def test_load_gcp_token_no_refresh(self): def test_load_gcp_token_with_refresh(self): def cred(): return None cred.token = TEST_ANOTHER_DATA_BASE64 - cred.expiry = datetime.datetime.now() + cred.expiry = datetime.datetime.utcnow() loader = KubeConfigLoader( config_dict=self.TEST_KUBE_CONFIG, active_context="expired_gcp", get_google_credentials=lambda: cred) - original_expiry = _get_expiry(loader) + original_expiry = _get_expiry(loader, "expired_gcp") self.assertTrue(loader._load_auth_provider_token()) - new_expiry = _get_expiry(loader) + new_expiry = _get_expiry(loader, "expired_gcp") # assert that the configs expiry actually updates self.assertTrue(new_expiry > original_expiry) self.assertEqual(BEARER_TOKEN_FORMAT % TEST_ANOTHER_DATA_BASE64, loader.token) + def test_gcp_get_api_key_with_prefix(self): + class cred_old: + token = TEST_DATA_BASE64 + expiry = DATETIME_EXPIRY_PAST + + class cred_new: + token = TEST_ANOTHER_DATA_BASE64 + expiry = DATETIME_EXPIRY_FUTURE + fake_config = FakeConfig() + _get_google_credentials = mock.Mock() + _get_google_credentials.side_effect = [cred_old, cred_new] + + loader = KubeConfigLoader( + config_dict=self.TEST_KUBE_CONFIG, + active_context="expired_gcp_refresh", + 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() + 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( config_dict=self.TEST_KUBE_CONFIG,