Skip to content

Commit

Permalink
update token in file listed in KUBECONFIG env var (#6240)
Browse files Browse the repository at this point in the history
Signed-off-by: Andre Dietisheim <[email protected]>
  • Loading branch information
adietish committed Aug 30, 2024
1 parent 35295a2 commit b89fa98
Show file tree
Hide file tree
Showing 16 changed files with 557 additions and 195 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,8 @@ public Config build() {
fluent.getOauthTokenProvider(), fluent.getCustomHeaders(), fluent.getRequestRetryBackoffLimit(),
fluent.getRequestRetryBackoffInterval(), fluent.getUploadRequestTimeout(), fluent.getOnlyHttpWatches(),
fluent.getCurrentContext(), fluent.getContexts(),
Optional.ofNullable(fluent.getAutoConfigure()).orElse(!disableAutoConfig()), true);
Optional.ofNullable(fluent.getAutoConfigure()).orElse(!disableAutoConfig()), true, fluent.getFiles());
buildable.setAuthProvider(fluent.getAuthProvider());
buildable.setFile(fluent.getFile());
return buildable;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public void copyInstance(Config instance) {
this.withContexts(instance.getContexts());
this.withAutoConfigure(instance.getAutoConfigure());
this.withAuthProvider(instance.getAuthProvider());
this.withFile(instance.getFile());
this.withFiles(instance.getFiles());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.fabric8.kubernetes.client.http.TlsVersion;
import io.sundr.builder.annotations.Buildable;

import java.io.File;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -50,14 +51,14 @@ public SundrioConfig(String masterUrl, String apiVersion, String namespace, Bool
String impersonateUsername, String[] impersonateGroups, Map<String, List<String>> impersonateExtras,
OAuthTokenProvider oauthTokenProvider, Map<String, String> customHeaders, Integer requestRetryBackoffLimit,
Integer requestRetryBackoffInterval, Integer uploadRequestTimeout, Boolean onlyHttpWatches, NamedContext currentContext,
List<NamedContext> contexts, Boolean autoConfigure) {
List<NamedContext> contexts, Boolean autoConfigure, List<File> files) {
super(masterUrl, apiVersion, namespace, trustCerts, disableHostnameVerification, caCertFile, caCertData,
clientCertFile, clientCertData, clientKeyFile, clientKeyData, clientKeyAlgo, clientKeyPassphrase, username,
password, oauthToken, autoOAuthToken, watchReconnectInterval, watchReconnectLimit, connectionTimeout, requestTimeout,
scaleTimeout, loggingInterval, maxConcurrentRequests, maxConcurrentRequestsPerHost, http2Disable,
httpProxy, httpsProxy, noProxy, userAgent, tlsVersions, websocketPingInterval, proxyUsername, proxyPassword,
trustStoreFile, trustStorePassphrase, keyStoreFile, keyStorePassphrase, impersonateUsername, impersonateGroups,
impersonateExtras, oauthTokenProvider, customHeaders, requestRetryBackoffLimit, requestRetryBackoffInterval,
uploadRequestTimeout, onlyHttpWatches, currentContext, contexts, autoConfigure, true);
uploadRequestTimeout, onlyHttpWatches, currentContext, contexts, autoConfigure, true, files);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,21 @@
import io.fabric8.kubernetes.api.model.AuthInfo;
import io.fabric8.kubernetes.api.model.Cluster;
import io.fabric8.kubernetes.api.model.Config;
import io.fabric8.kubernetes.api.model.ConfigBuilder;
import io.fabric8.kubernetes.api.model.Context;
import io.fabric8.kubernetes.api.model.NamedAuthInfo;
import io.fabric8.kubernetes.api.model.NamedCluster;
import io.fabric8.kubernetes.api.model.NamedContext;
import io.fabric8.kubernetes.api.model.NamedExtension;
import io.fabric8.kubernetes.api.model.PreferencesBuilder;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.fabric8.kubernetes.client.utils.Utils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
* Helper class for working with the YAML config file thats located in
Expand Down Expand Up @@ -165,27 +166,40 @@ public static void persistKubeConfigIntoFile(Config kubeConfig, String kubeConfi
}
}

/**
* Adds the given source list to the destination list that's provided by the given supplier
* and then set to the destination by the given setter.
* Creates the list if it doesn't exist yet (supplier returns {@code null}.
* Does not copy if the given list is {@code null}.
*
* @param source the source list to add to the destination
* @param destinationSupplier supplies the list that the source shall be added to
* @param destinationSetter sets the list, once the source was added to it
*/
public static <T> void addTo(List<T> source, Supplier<List<T>> destinationSupplier, Consumer<List<T>> destinationSetter) {
if (source == null) {
return;
public static Config merge(Config thisConfig, Config thatConfig) {
if (thisConfig == null) {
return thatConfig;
}

List<T> list = destinationSupplier.get();
if (list == null) {
list = new ArrayList<>();
ConfigBuilder builder = new ConfigBuilder(thatConfig);
if (thisConfig.getClusters() != null) {
builder.addAllToClusters(thisConfig.getClusters());
}
if (thisConfig.getContexts() != null) {
builder.addAllToContexts(thisConfig.getContexts());
}
if (thisConfig.getUsers() != null) {
builder.addAllToUsers(thisConfig.getUsers());
}
list.addAll(source);
destinationSetter.accept(list);
if (thisConfig.getExtensions() != null) {
builder.addAllToExtensions(thisConfig.getExtensions());
}
if (!builder.hasCurrentContext()
&& Utils.isNotNullOrEmpty(thisConfig.getCurrentContext())) {
builder.withCurrentContext(thisConfig.getCurrentContext());
}
Config merged = builder.build();
mergePreferences(thisConfig, merged);
return merged;
}

public static void mergePreferences(io.fabric8.kubernetes.api.model.Config source,
io.fabric8.kubernetes.api.model.Config destination) {
if (source.getPreferences() != null) {
PreferencesBuilder builder = new PreferencesBuilder(destination.getPreferences());
if (source.getPreferences() != null) {
builder.addToExtensions(source.getExtensions().toArray(new NamedExtension[] {}));
}
destination.setPreferences(builder.build());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
import io.fabric8.kubernetes.api.model.AuthInfo;
import io.fabric8.kubernetes.api.model.AuthProviderConfig;
import io.fabric8.kubernetes.api.model.NamedAuthInfo;
import io.fabric8.kubernetes.api.model.NamedAuthInfoBuilder;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.Config.KubeConfigFile;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.http.HttpClient;
import io.fabric8.kubernetes.client.http.HttpRequest;
Expand Down Expand Up @@ -84,22 +84,22 @@ private OpenIDConnectionUtils() {
* @return access token for interacting with Kubernetes API
*/
public static CompletableFuture<String> resolveOIDCTokenFromAuthConfig(
Config currentConfig, Map<String, String> currentAuthProviderConfig, HttpClient.Builder clientBuilder) {
Config currentConfig, Map<String, String> currentAuthProviderConfig, HttpClient.Builder clientBuilder) {
String originalToken = currentAuthProviderConfig.get(ID_TOKEN_KUBECONFIG);
String idpCert = currentAuthProviderConfig.getOrDefault(IDP_CERT_DATA, getClientCertDataFromConfig(currentConfig));
if (isTokenRefreshSupported(currentAuthProviderConfig)) {
final HttpClient httpClient = initHttpClientWithPemCert(idpCert, clientBuilder);
final CompletableFuture<String> result = getOpenIdConfiguration(httpClient, currentAuthProviderConfig)
.thenCompose(openIdConfiguration -> refreshOpenIdToken(httpClient, currentAuthProviderConfig, openIdConfiguration))
.thenApply(oAuthToken -> persistOAuthToken(currentConfig, oAuthToken, null))
.thenApply(oAuthToken -> {
if (oAuthToken == null || Utils.isNullOrEmpty(oAuthToken.idToken)) {
LOGGER.warn("token response did not contain an id_token, either the scope \\\"openid\\\" wasn't " +
"requested upon login, or the provider doesn't support id_tokens as part of the refresh response.");
return originalToken;
}
return oAuthToken.idToken;
});
.thenCompose(openIdConfiguration -> refreshOpenIdToken(httpClient, currentAuthProviderConfig, openIdConfiguration))
.thenApply(oAuthToken -> persistOAuthToken(currentConfig, oAuthToken, null))
.thenApply(oAuthToken -> {
if (oAuthToken == null || Utils.isNullOrEmpty(oAuthToken.idToken)) {
LOGGER.warn("token response did not contain an id_token, either the scope \\\"openid\\\" wasn't " +
"requested upon login, or the provider doesn't support id_tokens as part of the refresh response.");
return originalToken;
}
return oAuthToken.idToken;
});
result.whenComplete((s, t) -> httpClient.close());
return result;
}
Expand Down Expand Up @@ -128,9 +128,9 @@ static boolean isTokenRefreshSupported(Map<String, String> currentAuthProviderCo
* @return the OpenID Configuration as returned by the OpenID provider
*/
private static CompletableFuture<OpenIdConfiguration> getOpenIdConfiguration(HttpClient client,
Map<String, String> authProviderConfig) {
Map<String, String> authProviderConfig) {
final HttpRequest request = client.newHttpRequestBuilder()
.uri(resolveWellKnownUrlForOpenIDIssuer(authProviderConfig)).build();
.uri(resolveWellKnownUrlForOpenIDIssuer(authProviderConfig)).build();
return client.sendAsync(request, String.class).thenApply(response -> {
try {
if (response.isSuccessful() && response.body() != null) {
Expand All @@ -151,13 +151,13 @@ private static CompletableFuture<OpenIdConfiguration> getOpenIdConfiguration(Htt
* Issue Token Refresh HTTP Request to OIDC Provider
*/
private static CompletableFuture<OAuthToken> refreshOpenIdToken(
HttpClient httpClient, Map<String, String> authProviderConfig, OpenIdConfiguration openIdConfiguration) {
HttpClient httpClient, Map<String, String> authProviderConfig, OpenIdConfiguration openIdConfiguration) {
if (openIdConfiguration == null || Utils.isNullOrEmpty(openIdConfiguration.tokenEndpoint)) {
LOGGER.warn("oidc: discovery object doesn't contain a valid token endpoint: {}", openIdConfiguration);
return CompletableFuture.completedFuture(null);
}
final HttpRequest request = initTokenRefreshHttpRequest(httpClient, authProviderConfig,
openIdConfiguration.tokenEndpoint);
openIdConfiguration.tokenEndpoint);
return httpClient.sendAsync(request, String.class).thenApply(r -> {
String body = r.body();
if (body != null) {
Expand Down Expand Up @@ -199,10 +199,11 @@ public static OAuthToken persistOAuthToken(Config currentConfig, OAuthToken oAut
}

private static void persistOAuthTokenToFile(Config currentConfig, String token, Map<String, String> authProviderConfig) {
if (currentConfig.getFile() != null && currentConfig.getCurrentContext() != null) {
if (currentConfig.getCurrentContext() != null
&& currentConfig.getCurrentContext().getContext() != null) {
try {
final String userName = currentConfig.getCurrentContext().getContext().getUser();
KubeConfigFile kubeConfigFile = currentConfig.getFile(userName);
Config.KubeConfigFile kubeConfigFile = currentConfig.getFileWithAuthInfo(userName);
if (kubeConfigFile == null) {
LOGGER.warn("oidc: failure while persisting new tokens into KUBECONFIG: file for user {} not found", userName);
return;
Expand Down Expand Up @@ -230,21 +231,28 @@ private static void setAuthProviderAndToken(String token, Map<String, String> au
}
}

private static NamedAuthInfo getOrCreateNamedAuthInfo(String userName, io.fabric8.kubernetes.api.model.Config kubeConfig) {
private static NamedAuthInfo getOrCreateNamedAuthInfo(String name, io.fabric8.kubernetes.api.model.Config kubeConfig) {
return kubeConfig.getUsers().stream()
.filter(n -> n.getName().equals(userName))
.findFirst()
.orElseGet(() -> {
NamedAuthInfo result = new NamedAuthInfo(userName, new AuthInfo());
kubeConfig.getUsers().add(result);
return result;
});
.filter(n -> n.getName().equals(name))
.findFirst()
.orElseGet(() -> {
NamedAuthInfo authInfo = new NamedAuthInfoBuilder()
.withName(name)
.withNewUser()
.endUser()
.build();
kubeConfig.getUsers().add(authInfo);
return authInfo;
});
}

private static void persistOAuthTokenToFile(AuthProviderConfig config, Map<String, String> authProviderConfig) {
if (config == null) {
return;
}
Optional.of(config)
.map(AuthProviderConfig::getConfig)
.ifPresent(c -> c.putAll(authProviderConfig));
.map(AuthProviderConfig::getConfig)
.ifPresent(c -> c.putAll(authProviderConfig));
}

/**
Expand All @@ -268,19 +276,19 @@ private static HttpClient initHttpClientWithPemCert(String idpCert, HttpClient.B
clientBuilder.sslContext(keyManagers, trustManagers);
return clientBuilder.build();
} catch (KeyStoreException | InvalidKeySpecException | NoSuchAlgorithmException | IOException | UnrecoverableKeyException
| CertificateException e) {
| CertificateException e) {
throw KubernetesClientException.launderThrowable("Could not import idp certificate", e);
}
}

private static HttpRequest initTokenRefreshHttpRequest(
HttpClient client, Map<String, String> authProviderConfig, String tokenRefreshUrl) {
HttpClient client, Map<String, String> authProviderConfig, String tokenRefreshUrl) {

final String clientId = authProviderConfig.get(CLIENT_ID_KUBECONFIG);
final String clientSecret = authProviderConfig.getOrDefault(CLIENT_SECRET_KUBECONFIG, "");
final HttpRequest.Builder httpRequestBuilder = client.newHttpRequestBuilder().uri(tokenRefreshUrl);
final String credentials = java.util.Base64.getEncoder().encodeToString((clientId + ':' + clientSecret)
.getBytes(StandardCharsets.UTF_8));
.getBytes(StandardCharsets.UTF_8));
httpRequestBuilder.header("Authorization", "Basic " + credentials);

final Map<String, String> requestBody = new LinkedHashMap<>();
Expand All @@ -305,8 +313,8 @@ public static boolean idTokenExpired(Config config) {
Map<String, Object> jwtPayloadMap = Serialization.unmarshal(jwtPayloadDecoded, Map.class);
int expiryTimestampInSeconds = (Integer) jwtPayloadMap.get(JWT_TOKEN_EXPIRY_TIMESTAMP_KEY);
return Instant.ofEpochSecond(expiryTimestampInSeconds)
.minusSeconds(TOKEN_EXPIRY_DELTA)
.isBefore(Instant.now());
.minusSeconds(TOKEN_EXPIRY_DELTA)
.isBefore(Instant.now());
} catch (Exception e) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -291,16 +292,32 @@ public static boolean isNullOrEmpty(String str) {
return str == null || str.isEmpty();
}

public static boolean isNotNullOrEmpty(Map map) {
return !(map == null || map.isEmpty());
public static boolean isNotNullOrEmpty(Map<?, ?> map) {
return !isNullOrEmpty(map);
}

public static boolean isNullOrEmpty(Map<?, ?> map) {
return map == null || map.isEmpty();
}

public static boolean isNotNullOrEmpty(Collection<?> collection) {
return !isNullOrEmpty(collection);
}

public static boolean isNullOrEmpty(Collection<?> collection) {
return collection == null || collection.isEmpty();
}

public static boolean isNotNullOrEmpty(String str) {
return !isNullOrEmpty(str);
}

public static boolean isNotNullOrEmpty(String[] array) {
return !(array == null || array.length == 0);
return !isNullOrEmpty(array);
}

public static boolean isNullOrEmpty(String[] array) {
return array == null || array.length == 0;
}

public static <T> boolean isNotNull(T... refList) {
Expand Down
Loading

0 comments on commit b89fa98

Please sign in to comment.