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 10, 2023
1 parent 13d6dfd commit 8f986a4
Show file tree
Hide file tree
Showing 9 changed files with 437 additions and 81 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 @@ -231,6 +232,8 @@ public class Config {

private File file;

private String lastAutoconfiguredOAuthToken;

@JsonIgnore
protected Map<String, Object> additionalProperties = new HashMap<String, Object>();

Expand Down Expand Up @@ -330,7 +333,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, null, DEFAULT_REQUEST_RETRY_BACKOFFLIMIT, DEFAULT_REQUEST_RETRY_BACKOFFINTERVAL,
DEFAULT_UPLOAD_REQUEST_TIMEOUT);
}

Expand All @@ -344,7 +347,9 @@ 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,
String lastAutoconfiguredOAuthToken, Map<String, String> customHeaders,
int requestRetryBackoffLimit,
int requestRetryBackoffInterval, int uploadRequestTimeout) {
this.apiVersion = apiVersion;
this.namespace = namespace;
Expand All @@ -361,6 +366,7 @@ public Config(String masterUrl, String apiVersion, String namespace, boolean tru
this.username = username;
this.password = password;
this.oauthToken = oauthToken;
this.oAuthTokenSource = determineOAuthTokenSource(oauthToken, lastAutoconfiguredOAuthToken, oAuthTokenSource, oauthTokenProvider);
this.websocketPingInterval = websocketPingInterval;
this.connectionTimeout = connectionTimeout;

Expand Down Expand Up @@ -426,7 +432,8 @@ 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()));
config.setOAuthTokenAndSource(Utils.getSystemPropertyOrEnvVar(KUBERNETES_OAUTH_TOKEN_SYSTEM_PROPERTY),
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 @@ -551,7 +558,7 @@ private static boolean tryServiceAccount(Config config) {
try {
String serviceTokenCandidate = new String(Files.readAllBytes(saTokenPathFile.toPath()));
LOGGER.debug("Found service account token at: [{}].", saTokenPathLocation);
config.setOauthToken(serviceTokenCandidate);
config.setOAuthTokenAndSource(serviceTokenCandidate, 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 +639,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 @@ -730,7 +740,7 @@ private static boolean loadFromKubeconfig(Config config, String context, String
config.setClientKeyFile(clientKeyFile);
config.setClientKeyData(currentAuthInfo.getClientKeyData());
config.setClientKeyAlgo(getKeyAlgorithm(config.getClientKeyFile(), config.getClientKeyData()));
config.setOauthToken(currentAuthInfo.getToken());
config.setOAuthTokenAndSource(currentAuthInfo.getToken(), OAuthTokenSource.KUBECONFIG);
config.setUsername(currentAuthInfo.getUsername());
config.setPassword(currentAuthInfo.getPassword());

Expand All @@ -739,18 +749,20 @@ private static boolean loadFromKubeconfig(Config config, String context, String
config.setAuthProvider(currentAuthInfo.getAuthProvider());
if (!Utils.isNullOrEmpty(currentAuthInfo.getAuthProvider().getConfig().get(ACCESS_TOKEN))) {
// GKE token
config.setOauthToken(currentAuthInfo.getAuthProvider().getConfig().get(ACCESS_TOKEN));
config.setOAuthTokenAndSource(currentAuthInfo.getAuthProvider().getConfig().get(ACCESS_TOKEN),
OAuthTokenSource.KUBECONFIG);
} else if (!Utils.isNullOrEmpty(currentAuthInfo.getAuthProvider().getConfig().get(ID_TOKEN))) {
// OpenID Connect token
config.setOauthToken(currentAuthInfo.getAuthProvider().getConfig().get(ID_TOKEN));
config.setOAuthTokenAndSource(currentAuthInfo.getAuthProvider().getConfig().get(ID_TOKEN),
OAuthTokenSource.KUBECONFIG);
}
}
} else if (config.getOauthTokenProvider() == null) { // https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins
ExecConfig exec = currentAuthInfo.getExec();
if (exec != null) {
ExecCredential ec = getExecCredentialFromExecConfig(exec, configFile);
if (ec != null && ec.status != null && ec.status.token != null) {
config.setOauthToken(ec.status.token);
config.setOAuthTokenAndSource(ec.status.token, OAuthTokenSource.EXEC_CREDENTIAL_PLUGIN);
} else {
LOGGER.warn("No token returned");
}
Expand Down Expand Up @@ -1405,6 +1417,26 @@ public boolean getAutoConfigure() {
return autoConfigure;
}

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

public OAuthTokenSource getOAuthTokenSource() {
return oAuthTokenSource;
}

private void setOAuthTokenAndSource(String oAuthToken, OAuthTokenSource oAuthTokenSource) {
if (oAuthToken != null && !oAuthToken.isEmpty()) {
setOauthToken(oAuthToken);
setOAuthTokenSource(oAuthTokenSource);
this.lastAutoconfiguredOAuthToken = oAuthToken;
}
}

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

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

public String getLastAutoconfiguredOAuthToken() {
return lastAutoconfiguredOAuthToken;
}

private static OAuthTokenSource determineOAuthTokenSource(String oAuthToken, String lastAutoconfiguredOAuthToken, OAuthTokenSource oAuthTokenSource,
OAuthTokenProvider oAuthTokenProvider) {
if (oAuthTokenProvider != null) {
return OAuthTokenSource.OAUTHTOKEN_PROVIDER;
} else if (oAuthToken != null && !oAuthToken.equals(lastAutoconfiguredOAuthToken)) { // token modified via user
return OAuthTokenSource.USER;
}
return oAuthTokenSource;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* 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,15 @@ public CompletableFuture<Boolean> afterFailure(BasicBuilder headerBuilder, HttpR
if (isBasicAuth()) {
return CompletableFuture.completedFuture(false);
}
if (config.isTokenNonRefreshable()) {
return CompletableFuture.completedFuture(false);
}
if (config.getOAuthTokenSource() != null && config.getOAuthTokenSource().equals(OAuthTokenSource.OAUTHTOKEN_PROVIDER)) {
String tokenFromProvider = config.getOauthTokenProvider().getToken();
if (tokenFromProvider != null && !tokenFromProvider.isEmpty()) {
return CompletableFuture.completedFuture(overrideNewAccessTokenToConfig(tokenFromProvider, headerBuilder, config));
}
}
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
return refreshToken(headerBuilder);
}
Expand All @@ -85,13 +95,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 +141,7 @@ private void updateLatestRefreshTimestamp() {
latestRefreshTimestamp = Instant.now();
}

private static boolean isAuthProviderOidc(Config newestConfig) {
return newestConfig.getAuthProvider() != null && newestConfig.getAuthProvider().getName().equalsIgnoreCase("oidc");
}
}
Loading

0 comments on commit 8f986a4

Please sign in to comment.