Skip to content

Commit

Permalink
update token in file listed in KUBECONFIG env var (fabric8io#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 22, 2024
1 parent 7dc77ee commit 450e0c1
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ public class Config {
public static final String KUBERNETES_NAMESPACE_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/namespace";
public static final String KUBERNETES_NAMESPACE_FILE = "kubenamespace";
public static final String KUBERNETES_NAMESPACE_SYSTEM_PROPERTY = "kubernetes.namespace";
@Deprecated
public static final String KUBERNETES_KUBECONFIG_FILE = "kubeconfig";
public static final String KUBERNETES_KUBECONFIG_FILES = "kubeconfig";
public static final String KUBERNETES_SERVICE_HOST_PROPERTY = "KUBERNETES_SERVICE_HOST";
public static final String KUBERNETES_SERVICE_PORT_PROPERTY = "KUBERNETES_SERVICE_PORT";
public static final String KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token";
Expand Down Expand Up @@ -854,40 +856,41 @@ private static boolean tryKubeConfig(Config config, String context) {
if (!Utils.getSystemPropertyOrEnvVar(KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, true)) {
return false;
}
List<String> kubeConfigFilenames = Arrays.asList(getKubeconfigFilenames());
if (kubeConfigFilenames.isEmpty()) {
String[] kubeConfigFilenames = getKubeconfigFilenames();
if (kubeConfigFilenames == null
|| kubeConfigFilenames.length == 0) {
return false;
}
List<File> allKubeConfigFiles = kubeConfigFilenames.stream()
.map(File::new)
.collect(Collectors.toList());
File mainKubeConfig = allKubeConfigFiles.get(0);
io.fabric8.kubernetes.api.model.Config kubeConfig = createKubeconfig(mainKubeConfig);
List<File> allFiles = Arrays.stream(kubeConfigFilenames)
.map(File::new)
.collect(Collectors.toList());
File mainFile = allFiles.get(0);
io.fabric8.kubernetes.api.model.Config kubeConfig = createKubeconfig(mainFile);
if (kubeConfig == null) {
return false;
}
config.file = mainKubeConfig;
config.files = allKubeConfigFiles;
config.file = mainFile;
config.files = allFiles;

List<File> additionalConfigs = config.files.subList(1, allKubeConfigFiles.size());
List<File> additionalConfigs = config.files.subList(1, allFiles.size());
addAdditionalConfigs(kubeConfig, additionalConfigs);

return loadFromKubeconfig(config, context, mainKubeConfig);
return loadFromKubeconfig(config, context, kubeConfig);
}

private static void addAdditionalConfigs(io.fabric8.kubernetes.api.model.Config kubeConfig, List<File> files) {
if (files == null
|| files.isEmpty()) {
|| files.isEmpty()) {
return;
}
files.stream()
.map(Config::createKubeconfig)
.filter(Objects::nonNull)
.forEach(additionalConfig -> {
addTo(additionalConfig.getContexts(), kubeConfig::getContexts, kubeConfig::setContexts);
addTo(additionalConfig.getClusters(), kubeConfig::getClusters, kubeConfig::setClusters);
addTo(additionalConfig.getUsers(), kubeConfig::getUsers, kubeConfig::setUsers);
});
.map(Config::createKubeconfig)
.filter(Objects::nonNull)
.forEach(additionalConfig -> {
addTo(additionalConfig.getContexts(), kubeConfig::getContexts, kubeConfig::setContexts);
addTo(additionalConfig.getClusters(), kubeConfig::getClusters, kubeConfig::setClusters);
addTo(additionalConfig.getUsers(), kubeConfig::getUsers, kubeConfig::setUsers);
});
}

private static io.fabric8.kubernetes.api.model.Config createKubeconfig(File file) {
Expand All @@ -903,7 +906,7 @@ private static io.fabric8.kubernetes.api.model.Config createKubeconfig(File file
try {
String content = getKubeconfigContents(file);
if (content != null
&& !content.isEmpty()) {
&& !content.isEmpty()) {
kubeConfig = KubeConfigUtils.parseConfigFromString(content);
}
} catch (KubernetesClientException e) {
Expand All @@ -926,19 +929,21 @@ public static String getKubeconfigFilename() {
fileName = fileNames[0];
if (fileNames.length > 1) {
LOGGER.info("Found multiple Kubernetes config files [{}], returning the first one. Use #getKubeconfigFilenames instead",
fileNames[0]);
fileNames[0]);
}
}
return fileName;
}

public static String[] getKubeconfigFilenames() {
String[] fileNames = null;
String fileName = Utils.getSystemPropertyOrEnvVar(KUBERNETES_KUBECONFIG_FILE);

fileNames = fileName.split(File.pathSeparator);
if (fileNames.length == 0) {
fileNames = new String[] { new File(getHomeDir(), ".kube" + File.separator + "config").toString() };
String fileName = Utils.getSystemPropertyOrEnvVar(KUBERNETES_KUBECONFIG_FILES);
if (fileName != null
&& !fileName.isEmpty()) {
fileNames = fileName.split(File.pathSeparator);
if (fileNames.length == 0) {
fileNames = new String[] { new File(getHomeDir(), ".kube" + File.separator + "config").toString() };
}
}
return fileNames;
}
Expand All @@ -954,21 +959,20 @@ private static String getKubeconfigContents(File kubeConfigFile) {
return kubeconfigContents;
}

private static boolean loadFromKubeconfig(Config config, String context, File kubeConfigFile) {
String contents = getKubeconfigContents(kubeConfigFile);
if (contents == null) {
return false;
}
return loadFromKubeconfig(config, context, contents);
}

// Note: kubeconfigPath is optional
// It is only used to rewrite relative tls asset paths inside kubeconfig when a file is passed, and in the case that
// the kubeconfig references some assets via relative paths.
private static boolean loadFromKubeconfig(Config config, String context, String kubeconfigContents) {
if (kubeconfigContents != null && !kubeconfigContents.isEmpty()) {
return loadFromKubeconfig(config, context, KubeConfigUtils.parseConfigFromString(kubeconfigContents));
} else {
return false;
}
}

private static boolean loadFromKubeconfig(Config config, String context, io.fabric8.kubernetes.api.model.Config kubeConfig) {
try {
if (kubeconfigContents != null && !kubeconfigContents.isEmpty()) {
io.fabric8.kubernetes.api.model.Config kubeConfig = KubeConfigUtils.parseConfigFromString(kubeconfigContents);
if (kubeConfig != null) {
mergeKubeConfigContents(config, context, kubeConfig);
return true;
}
Expand Down Expand Up @@ -1144,6 +1148,7 @@ protected static String getCommandWithFullyQualifiedPath(String command, String

private static Context setCurrentContext(String context, Config config, io.fabric8.kubernetes.api.model.Config kubeConfig) {
if (context != null) {
// override existing current-context
kubeConfig.setCurrentContext(context);
}
Context currentContext = null;
Expand Down Expand Up @@ -1738,13 +1743,70 @@ public void setCurrentContext(NamedContext context) {
/**
*
* Returns the path to the file that this configuration was loaded from. Returns {@code null} if no file was used.
*
* @deprecated use {@link #getFiles} instead.
*
* @return the path to the kubeConfig file
* @return the kubeConfig file
*/
@Deprecated
public File getFile() {
return file;
}

/**
* Returns the kube config files that are used to configure this client.
* Returns the files that are listed in the KUBERNETES_KUBECONFIG_FILES env or system variables.
* Returns the default kube config file if it's not set'.
*
* @return
*/
public List<File> getFiles() {
return files;
}

public KubeConfigFile getFile(String username) {
if (username == null
|| username.isEmpty()) {
return null;
}
return Arrays.stream(getKubeconfigFilenames())
.map(filename -> {
try {
return new KubeConfigFile(file, KubeConfigUtils.parseConfig(file));
} catch (IOException e) {
return null;
}
})
.filter(entry -> entry != null
&& entry.getConfig() != null
&& hasAuthInfo(username, entry.getConfig()))
.findFirst()
.orElse(null);
}

private boolean hasAuthInfo(String username, io.fabric8.kubernetes.api.model.Config kubeConfig) {
return kubeConfig.getUsers().stream()
.anyMatch(namedAuthInfo -> username.equals(namedAuthInfo.getUser().getUsername()));
}

public static class KubeConfigFile {
private final File file;
private final io.fabric8.kubernetes.api.model.Config config;

private KubeConfigFile(File file, io.fabric8.kubernetes.api.model.Config config) {
this.file = file;
this.config = config;
}

public File getFile() {
return file;
}

public io.fabric8.kubernetes.api.model.Config getConfig() {
return config;
}
}

@JsonIgnore
public Readiness getReadiness() {
return Readiness.getInstance();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import io.fabric8.kubernetes.api.model.AuthProviderConfig;
import io.fabric8.kubernetes.api.model.NamedAuthInfo;
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 +83,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 +127,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 +150,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 @@ -202,7 +201,7 @@ private static void persistOAuthTokenToFile(Config currentConfig, String token,
if (currentConfig.getFile() != null && currentConfig.getCurrentContext() != null) {
try {
final String userName = currentConfig.getCurrentContext().getContext().getUser();
KubeConfigFile kubeConfigFile = currentConfig.getFile(userName);
Config.KubeConfigFile kubeConfigFile = currentConfig.getFile(userName);
if (kubeConfigFile == null) {
LOGGER.warn("oidc: failure while persisting new tokens into KUBECONFIG: file for user {} not found", userName);
return;
Expand All @@ -217,7 +216,8 @@ private static void persistOAuthTokenToFile(Config currentConfig, String token,
}
}

private static void setAuthProviderAndToken(String token, Map<String, String> authProviderConfig, NamedAuthInfo namedAuthInfo) {
private static void setAuthProviderAndToken(String token, Map<String, String> authProviderConfig,
NamedAuthInfo namedAuthInfo) {
if (namedAuthInfo.getUser() == null) {
namedAuthInfo.setUser(new AuthInfo());
}
Expand All @@ -232,19 +232,19 @@ private static void setAuthProviderAndToken(String token, Map<String, String> au

private static NamedAuthInfo getOrCreateNamedAuthInfo(String userName, 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(userName))
.findFirst()
.orElseGet(() -> {
NamedAuthInfo result = new NamedAuthInfo(userName, new AuthInfo());
kubeConfig.getUsers().add(result);
return result;
});
}

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

/**
Expand All @@ -268,19 +268,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 +305,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
Loading

0 comments on commit 450e0c1

Please sign in to comment.