Skip to content

Commit

Permalink
fix (kubernetes-client-api) : Track sources from where OAuthToken get…
Browse files Browse the repository at this point in the history
…s set (fabric8io#4802)

+ Add enum OAuthTokenSource in Config which tell us about the source from
  which Config's OAuthToken was set.
+ Skip refresh in case current OAuthTokenSource is set to USER (the
  scenario reported in the linked issue)

Signed-off-by: Rohan Kumar <rohaan@redhat.com>
  • Loading branch information
rohanKanojia committed Mar 6, 2023
1 parent dfa920e commit 7e7fb1c
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 29 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Fix #4794: improving the semantics of manually calling informer stop
* Fix #4797: OkHttpClientFactory.additionalConfig can be used to override the default OkHttp Dispatcher
* Fix #4798: fix leader election release on cancel
* Fix #4802: config.refresh() erases token specified when building initial config
* Fix #4815: (java-generator) create target download directory if it doesn't exist
* Fix #4846: allowed for pod read / copy operations to distinguish when the target doesn't exist
* Fix #4818: [java-generator] Escape `*/` in generated JavaDocs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ public class Config {
private int connectionTimeout = 10 * 1000;
private int maxConcurrentRequests = DEFAULT_MAX_CONCURRENT_REQUESTS;
private int maxConcurrentRequestsPerHost = DEFAULT_MAX_CONCURRENT_REQUESTS_PER_HOST;
private OAuthTokenSource oAuthTokenSource;

private RequestConfig requestConfig = new RequestConfig();

Expand Down Expand Up @@ -330,7 +331,7 @@ public Config(String masterUrl, String apiVersion, String namespace, boolean tru
loggingInterval, maxConcurrentRequests, maxConcurrentRequestsPerHost, false, httpProxy, httpsProxy, noProxy,
errorMessages, userAgent, tlsVersions, websocketTimeout, websocketPingInterval, proxyUsername, proxyPassword,
trustStoreFile, trustStorePassphrase, keyStoreFile, keyStorePassphrase, impersonateUsername, impersonateGroups,
impersonateExtras, null, null, DEFAULT_REQUEST_RETRY_BACKOFFLIMIT, DEFAULT_REQUEST_RETRY_BACKOFFINTERVAL,
impersonateExtras, null, null, null, DEFAULT_REQUEST_RETRY_BACKOFFLIMIT, DEFAULT_REQUEST_RETRY_BACKOFFINTERVAL,
DEFAULT_UPLOAD_REQUEST_TIMEOUT);
}

Expand All @@ -344,7 +345,8 @@ public Config(String masterUrl, String apiVersion, String namespace, boolean tru
String userAgent, TlsVersion[] tlsVersions, long websocketTimeout, long websocketPingInterval, String proxyUsername,
String proxyPassword, String trustStoreFile, String trustStorePassphrase, String keyStoreFile, String keyStorePassphrase,
String impersonateUsername, String[] impersonateGroups, Map<String, List<String>> impersonateExtras,
OAuthTokenProvider oauthTokenProvider, Map<String, String> customHeaders, int requestRetryBackoffLimit,
OAuthTokenProvider oauthTokenProvider, OAuthTokenSource oAuthTokenSource, Map<String, String> customHeaders,
int requestRetryBackoffLimit,
int requestRetryBackoffInterval, int uploadRequestTimeout) {
this.apiVersion = apiVersion;
this.namespace = namespace;
Expand All @@ -361,6 +363,7 @@ public Config(String masterUrl, String apiVersion, String namespace, boolean tru
this.username = username;
this.password = password;
this.oauthToken = oauthToken;
this.oAuthTokenSource = determineOAuthTokenSource(oauthToken, oAuthTokenSource, oauthTokenProvider);
this.websocketPingInterval = websocketPingInterval;
this.connectionTimeout = connectionTimeout;

Expand Down Expand Up @@ -426,7 +429,11 @@ public static void configFromSysPropsOrEnvVars(Config config) {
Utils.getSystemPropertyOrEnvVar(KUBERNETES_KEYSTORE_PASSPHRASE_PROPERTY, config.getKeyStorePassphrase()));
config.setKeyStoreFile(Utils.getSystemPropertyOrEnvVar(KUBERNETES_KEYSTORE_FILE_PROPERTY, config.getKeyStoreFile()));

config.setOauthToken(Utils.getSystemPropertyOrEnvVar(KUBERNETES_OAUTH_TOKEN_SYSTEM_PROPERTY, config.getOauthToken()));
String tokenFromPropertyOrEnvVar = Utils.getSystemPropertyOrEnvVar(KUBERNETES_OAUTH_TOKEN_SYSTEM_PROPERTY, "");
if (!tokenFromPropertyOrEnvVar.isEmpty()) {
config.setOauthToken(tokenFromPropertyOrEnvVar);
config.setOAuthTokenSource(OAuthTokenSource.SYSTEM_PROPERTIES_OR_ENVIRONMENT_VARIABLES);
}
config.setUsername(Utils.getSystemPropertyOrEnvVar(KUBERNETES_AUTH_BASIC_USERNAME_SYSTEM_PROPERTY, config.getUsername()));
config.setPassword(Utils.getSystemPropertyOrEnvVar(KUBERNETES_AUTH_BASIC_PASSWORD_SYSTEM_PROPERTY, config.getPassword()));

Expand Down Expand Up @@ -552,6 +559,7 @@ private static boolean tryServiceAccount(Config config) {
String serviceTokenCandidate = new String(Files.readAllBytes(saTokenPathFile.toPath()));
LOGGER.debug("Found service account token at: [{}].", saTokenPathLocation);
config.setOauthToken(serviceTokenCandidate);
config.setOAuthTokenSource(OAuthTokenSource.SERVICEACCOUNT_TOKEN_FILE);
String txt = "Configured service account doesn't have access. Service account may have been revoked.";
config.getErrorMessages().put(401, "Unauthorized! " + txt);
config.getErrorMessages().put(403, "Forbidden!" + txt);
Expand Down Expand Up @@ -632,6 +640,9 @@ public static Config fromKubeconfig(String context, String kubeconfigContents, S
* @return
*/
public Config refresh() {
if (isTokenNonRefreshable()) {
return this;
}
final String currentContextName = this.getCurrentContext() != null ? this.getCurrentContext().getName() : null;
if (this.autoConfigure) {
return Config.autoConfigure(currentContextName);
Expand Down Expand Up @@ -731,6 +742,7 @@ private static boolean loadFromKubeconfig(Config config, String context, String
config.setClientKeyData(currentAuthInfo.getClientKeyData());
config.setClientKeyAlgo(getKeyAlgorithm(config.getClientKeyFile(), config.getClientKeyData()));
config.setOauthToken(currentAuthInfo.getToken());
config.setOAuthTokenSource(OAuthTokenSource.KUBECONFIG);
config.setUsername(currentAuthInfo.getUsername());
config.setPassword(currentAuthInfo.getPassword());

Expand All @@ -740,9 +752,11 @@ private static boolean loadFromKubeconfig(Config config, String context, String
if (!Utils.isNullOrEmpty(currentAuthInfo.getAuthProvider().getConfig().get(ACCESS_TOKEN))) {
// GKE token
config.setOauthToken(currentAuthInfo.getAuthProvider().getConfig().get(ACCESS_TOKEN));
config.setOAuthTokenSource(OAuthTokenSource.KUBECONFIG);
} else if (!Utils.isNullOrEmpty(currentAuthInfo.getAuthProvider().getConfig().get(ID_TOKEN))) {
// OpenID Connect token
config.setOauthToken(currentAuthInfo.getAuthProvider().getConfig().get(ID_TOKEN));
config.setOAuthTokenSource(OAuthTokenSource.KUBECONFIG);
}
}
} else if (config.getOauthTokenProvider() == null) { // https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins
Expand All @@ -751,6 +765,7 @@ private static boolean loadFromKubeconfig(Config config, String context, String
ExecCredential ec = getExecCredentialFromExecConfig(exec, configFile);
if (ec != null && ec.status != null && ec.status.token != null) {
config.setOauthToken(ec.status.token);
config.setOAuthTokenSource(OAuthTokenSource.EXEC_CREDENTIAL_PLUGIN);
} else {
LOGGER.warn("No token returned");
}
Expand Down Expand Up @@ -1405,6 +1420,19 @@ public boolean getAutoConfigure() {
return autoConfigure;
}

public void setOAuthTokenSource(OAuthTokenSource oAuthTokenSource) {
this.oAuthTokenSource = oAuthTokenSource;
}

public OAuthTokenSource getOAuthTokenSource() {
return oAuthTokenSource;
}

public boolean isTokenNonRefreshable() {
return (oAuthTokenSource != null && oAuthTokenSource.equals(OAuthTokenSource.USER)) ||
oauthTokenProvider != null;
}

/**
* Returns all the {@link NamedContext}s that exist in the kube config
*
Expand Down Expand Up @@ -1476,4 +1504,18 @@ public void setAutoConfigure(boolean autoConfigure) {
this.autoConfigure = autoConfigure;
}

private static OAuthTokenSource determineOAuthTokenSource(String oAuthToken, OAuthTokenSource oAuthTokenSource,
OAuthTokenProvider oAuthTokenProvider) {
if (oAuthTokenProvider != null) {
return OAuthTokenSource.OAUTHTOKEN_PROVIDER;
}
if (oAuthToken != null) {
if (Utils.getSystemPropertyOrEnvVar(KUBERNETES_OAUTH_TOKEN_SYSTEM_PROPERTY) != null) {
return OAuthTokenSource.SYSTEM_PROPERTIES_OR_ENVIRONMENT_VARIABLES;
} else {
return OAuthTokenSource.USER;
}
}
return oAuthTokenSource;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Copyright (C) 2015 Red Hat, Inc.
*
* 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.
*/
package io.fabric8.kubernetes.client;

/**
* Enum to track source of OAuthToken
*/
public enum OAuthTokenSource {
/**
* OAuthToken that comes from Config's OAuthTokenProvider. If token is coming from this source
* it would not be considered for refresh.
*/
OAUTHTOKEN_PROVIDER,
/**
* OAuthToken directly provided by user during building initial Config. It would not be considered
* for refresh. It is responsibility of caller to refresh it on its own.
*/
USER,
/**
* OAuthToken picked up from System properties or environment variables
*/
SYSTEM_PROPERTIES_OR_ENVIRONMENT_VARIABLES,
/**
* OAuthToken picked up from KubeConfig file
*/
KUBECONFIG,
/**
* OAuthToken picked from <a href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins">client-go credential plugins</a>
*/
EXEC_CREDENTIAL_PLUGIN,
/**
* OAuthToken picked from mounted ServiceAccount file inside container
*/
SERVICEACCOUNT_TOKEN_FILE
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.fabric8.kubernetes.client.utils;

import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.OAuthTokenSource;
import io.fabric8.kubernetes.client.http.BasicBuilder;
import io.fabric8.kubernetes.client.http.HttpClient;
import io.fabric8.kubernetes.client.http.HttpRequest;
Expand Down Expand Up @@ -59,7 +60,7 @@ public void before(BasicBuilder headerBuilder, HttpRequest request, RequestTags
if (Utils.isNotNullOrEmpty(config.getOauthToken())) {
headerBuilder.header(AUTHORIZATION, "Bearer " + config.getOauthToken());
}
if (isTimeToRefresh()) {
if (!config.isTokenNonRefreshable() && isTimeToRefresh()) {
refreshToken(headerBuilder);
}
}
Expand All @@ -77,6 +78,9 @@ public CompletableFuture<Boolean> afterFailure(BasicBuilder headerBuilder, HttpR
if (isBasicAuth()) {
return CompletableFuture.completedFuture(false);
}
if (config.isTokenNonRefreshable()) {
return CompletableFuture.completedFuture(true);
}
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
return refreshToken(headerBuilder);
}
Expand All @@ -85,13 +89,28 @@ public CompletableFuture<Boolean> afterFailure(BasicBuilder headerBuilder, HttpR

private CompletableFuture<Boolean> refreshToken(BasicBuilder headerBuilder) {
Config newestConfig = config.refresh();
if (shouldUseTokenFromNewestConfig(newestConfig)) {
return CompletableFuture
.completedFuture(overrideNewAccessTokenToConfig(newestConfig.getOauthToken(), headerBuilder, newestConfig));
}
final CompletableFuture<String> newAccessToken = extractNewAccessTokenFrom(newestConfig);

return newAccessToken.thenApply(token -> overrideNewAccessTokenToConfig(token, headerBuilder, config));
}

private static boolean shouldUseTokenFromNewestConfig(Config newestConfig) {
if (newestConfig.getOauthToken() != null) {
OAuthTokenSource oAuthTokenSource = newestConfig.getOAuthTokenSource();
return oAuthTokenSource != null &&
(oAuthTokenSource.equals(OAuthTokenSource.EXEC_CREDENTIAL_PLUGIN) ||
oAuthTokenSource.equals(OAuthTokenSource.SERVICEACCOUNT_TOKEN_FILE) ||
oAuthTokenSource.equals(OAuthTokenSource.SYSTEM_PROPERTIES_OR_ENVIRONMENT_VARIABLES));
}
return false;
}

private CompletableFuture<String> extractNewAccessTokenFrom(Config newestConfig) {
if (newestConfig.getAuthProvider() != null && newestConfig.getAuthProvider().getName().equalsIgnoreCase("oidc")) {
if (isAuthProviderOidc(newestConfig)) {
return OpenIDConnectionUtils.resolveOIDCTokenFromAuthConfig(config, newestConfig.getAuthProvider().getConfig(),
factory.newBuilder());
}
Expand All @@ -116,4 +135,7 @@ private void updateLatestRefreshTimestamp() {
latestRefreshTimestamp = Instant.now();
}

private static boolean isAuthProviderOidc(Config newestConfig) {
return newestConfig.getAuthProvider() != null && newestConfig.getAuthProvider().getName().equalsIgnoreCase("oidc");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
Expand All @@ -49,7 +50,7 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.condition.OS.WINDOWS;

public class ConfigTest {
class ConfigTest {

private static final String TEST_KUBECONFIG_FILE = Utils.filePath(ConfigTest.class.getResource("/test-kubeconfig"));
private static final String TEST_EC_KUBECONFIG_FILE = Utils.filePath(ConfigTest.class.getResource("/test-ec-kubeconfig"));
Expand Down Expand Up @@ -148,9 +149,11 @@ void testWithSystemProperties() {

Config config = new Config();
assertConfig(config);
assertThat(config.getOAuthTokenSource()).isEqualTo(OAuthTokenSource.SYSTEM_PROPERTIES_OR_ENVIRONMENT_VARIABLES);

config = new ConfigBuilder().build();
assertConfig(config);
assertThat(config.getOAuthTokenSource()).isEqualTo(OAuthTokenSource.SYSTEM_PROPERTIES_OR_ENVIRONMENT_VARIABLES);
}

@Test
Expand Down Expand Up @@ -230,6 +233,7 @@ void testWithBuilderAndSystemProperties() {
.build();

assertConfig(config);
assertThat(config.getOAuthTokenSource()).isEqualTo(OAuthTokenSource.SYSTEM_PROPERTIES_OR_ENVIRONMENT_VARIABLES);
}

@Test
Expand Down Expand Up @@ -281,6 +285,7 @@ void testWithKubeConfig() {
assertTrue(config.getCaCertFile().endsWith("testns/ca.pem".replace("/", File.separator)));
assertTrue(new File(config.getCaCertFile()).isAbsolute());
assertEquals(new File(TEST_KUBECONFIG_FILE), config.getFile());
assertThat(config.getOAuthTokenSource()).isEqualTo(OAuthTokenSource.KUBECONFIG);
}

@Test
Expand Down Expand Up @@ -469,6 +474,7 @@ void honorClientAuthenticatorCommands() throws Exception {
Config config = Config.autoConfigure(null);
assertNotNull(config);
assertEquals("HELLO WORLD", config.getOauthToken());
assertThat(config.getOAuthTokenSource()).isEqualTo(OAuthTokenSource.EXEC_CREDENTIAL_PLUGIN);
}

@Test
Expand Down Expand Up @@ -497,6 +503,7 @@ void shouldBeUsedTokenSuppliedByProvider() {
.build();

assertEquals("PROVIDER_TOKEN", config.getOauthToken());
assertThat(config.getOAuthTokenSource()).isEqualTo(OAuthTokenSource.OAUTHTOKEN_PROVIDER);
}

@Test
Expand All @@ -515,6 +522,7 @@ void testKubeConfigWithAuthConfigProvider() throws URISyntaxException {
assertEquals(
"eyJraWQiOiJDTj1vaWRjaWRwLnRyZW1vbG8ubGFuLCBPVT1EZW1vLCBPPVRybWVvbG8gU2VjdXJpdHksIEw9QXJsaW5ndG9uLCBTVD1WaXJnaW5pYSwgQz1VUy1DTj1rdWJlLWNhLTEyMDIxNDc5MjEwMzYwNzMyMTUyIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL29pZGNpZHAudHJlbW9sby5sYW46ODQ0My9hdXRoL2lkcC9PaWRjSWRQIiwiYXVkIjoia3ViZXJuZXRlcyIsImV4cCI6MTQ4MzU0OTUxMSwianRpIjoiMm96US15TXdFcHV4WDlHZUhQdy1hZyIsImlhdCI6MTQ4MzU0OTQ1MSwibmJmIjoxNDgzNTQ5MzMxLCJzdWIiOiI0YWViMzdiYS1iNjQ1LTQ4ZmQtYWIzMC0xYTAxZWU0MWUyMTgifQ.w6p4J_6qQ1HzTG9nrEOrubxIMb9K5hzcMPxc9IxPx2K4xO9l-oFiUw93daH3m5pluP6K7eOE6txBuRVfEcpJSwlelsOsW8gb8VJcnzMS9EnZpeA0tW_p-mnkFc3VcfyXuhe5R3G7aa5d8uHv70yJ9Y3-UhjiN9EhpMdfPAoEB9fYKKkJRzF7utTTIPGrSaSU6d2pcpfYKaxIwePzEkT4DfcQthoZdy9ucNvvLoi1DIC-UocFD8HLs8LYKEqSxQvOcvnThbObJ9af71EwmuE21fO5KzMW20KtAeget1gnldOosPtz1G5EwvaQ401-RPQzPGMVBld0_zMCAwZttJ4knw",
config.getOauthToken());
assertEquals(OAuthTokenSource.KUBECONFIG, config.getOAuthTokenSource());
}

@Test
Expand All @@ -529,7 +537,7 @@ void testEmptyConfig() {
assertNotNull(emptyConfig);
assertEquals("https://kubernetes.default.svc", emptyConfig.getMasterUrl());
assertTrue(emptyConfig.getContexts().isEmpty());
assertNull(emptyConfig.getCurrentContext());
assertThat(emptyConfig.getCurrentContext()).isNull();
assertEquals(64, emptyConfig.getMaxConcurrentRequests());
assertEquals(5, emptyConfig.getMaxConcurrentRequestsPerHost());
assertFalse(emptyConfig.isTrustCerts());
Expand All @@ -551,6 +559,7 @@ void testEmptyConfig() {
assertEquals(1, emptyConfig.getTlsVersions().length);
assertTrue(emptyConfig.getErrorMessages().isEmpty());
assertNotNull(emptyConfig.getUserAgent());
assertThat(emptyConfig.getOAuthTokenSource()).isNull();
}

private void assertConfig(Config config) {
Expand Down Expand Up @@ -750,4 +759,20 @@ void getHomeDir_shouldReturnUserHomeProp_WhenHomeEnvVariablesAreNotSet() {
System.setProperty("user.home", userHomePropToRestore);
}
}

@Test
void refresh_whenOAuthTokenSourceSetToUser_thenConfigUnchanged() {
// Given
Config config = new ConfigBuilder()
.withOauthToken("token-from-user")
.build();

// When
Config updatedConfig = config.refresh();

// Then
assertThat(updatedConfig)
.isSameAs(config)
.hasFieldOrPropertyWithValue("oauthToken", "token-from-user");
}
}
Loading

0 comments on commit 7e7fb1c

Please sign in to comment.