diff --git a/helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/utils/Command.java b/helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/utils/Command.java index d0519caa..33369c67 100644 --- a/helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/utils/Command.java +++ b/helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/utils/Command.java @@ -2,6 +2,8 @@ import io.github.inseefrlab.helmwrapper.configuration.HelmConfiguration; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeoutException; @@ -30,7 +32,9 @@ private static ProcessExecutor getProcessExecutor() { new ProcessListener() { @Override public void afterStart(Process process, ProcessExecutor executor) { - process.info().commandLine().ifPresent(cli -> LOGGER.info(cli)); + process.info() + .commandLine() + .ifPresent(cli -> LOGGER.info(redactToken(cli))); super.afterStart(process, executor); } }); @@ -114,6 +118,29 @@ private static String addConfigToCommand(String command, HelmConfiguration helmC if (helmConfiguration.getAsKubeUser() != null) { newCommand = newCommand.concat(" --kube-as-user " + helmConfiguration.getAsKubeUser() + " "); + + // Temp fix for https://github.com/kubernetes/kubernetes/issues/93474 + try { + String token = + Files.readString( + Path.of( + "/var/run/secrets/kubernetes.io/serviceaccount/token")) + .trim(); + String apiserver = + "https://" + + System.getenv("KUBERNETES_SERVICE_HOST") + + ":" + + System.getenv("KUBERNETES_SERVICE_PORT"); + newCommand = + newCommand.concat( + " --kube-ca-file=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt --kube-token=" + + token + + " --kube-apiserver=" + + apiserver + + " "); + } catch (IOException e) { + LOGGER.debug("Could not retrieve in-cluster token"); + } } String kubeConfig = null; if (StringUtils.isNotEmpty(helmConfiguration.getApiserverUrl())) { @@ -146,6 +173,10 @@ private static String addConfigToCommand(String command, HelmConfiguration helmC return newCommand; } + private static String redactToken(String commandLine) { + return commandLine.replaceAll("--kube-token[= ]+[^\\s]+", "--kube-token=[REDACTED]"); + } + public static void safeConcat(StringBuilder currentCommand, String toConcat) throws IllegalArgumentException { if (!safeToConcatenate.matcher(toConcat).matches()) { diff --git a/helm-wrapper/src/test/java/io/github/inseefrlab/helmwrapper/utils/RedactTokenTest.java b/helm-wrapper/src/test/java/io/github/inseefrlab/helmwrapper/utils/RedactTokenTest.java new file mode 100644 index 00000000..d7868c38 --- /dev/null +++ b/helm-wrapper/src/test/java/io/github/inseefrlab/helmwrapper/utils/RedactTokenTest.java @@ -0,0 +1,62 @@ +package io.github.inseefrlab.helmwrapper.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Method; +import org.junit.jupiter.api.Test; + +public class RedactTokenTest { + + private String invokeRedactToken(String commandLine) throws Exception { + Method method = Command.class.getDeclaredMethod("redactToken", String.class); + method.setAccessible(true); + return (String) method.invoke(null, commandLine); + } + + @Test + public void shouldRedactKubeTokenWithEquals() throws Exception { + String input = + "helm ls --kube-token=eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0In0.test"; + String result = invokeRedactToken(input); + assertEquals("helm ls --kube-token=[REDACTED]", result); + } + + @Test + public void shouldRedactKubeTokenWithSpace() throws Exception { + String input = + "helm ls --kube-token eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0In0.test"; + String result = invokeRedactToken(input); + assertEquals("helm ls --kube-token=[REDACTED]", result); + } + + @Test + public void shouldRedactMultipleKubeTokens() throws Exception { + String input = "helm ls --kube-token=token1 --kube-token=token2"; + String result = invokeRedactToken(input); + assertEquals("helm ls --kube-token=[REDACTED] --kube-token=[REDACTED]", result); + } + + @Test + public void shouldLeaveOtherOptionsUnchanged() throws Exception { + String input = + "helm ls --kubeconfig=/path/to/config --kube-apiserver=https://api.example.com"; + String result = invokeRedactToken(input); + assertEquals( + "helm ls --kubeconfig=/path/to/config --kube-apiserver=https://api.example.com", + result); + } + + @Test + public void shouldNotRedactIfNoToken() throws Exception { + String input = "helm ls --namespace default"; + String result = invokeRedactToken(input); + assertEquals("helm ls --namespace default", result); + } + + @Test + public void shouldRedactTokenWithTrailingOptions() throws Exception { + String input = "helm ls --kube-token=secret123 --namespace myns --output json"; + String result = invokeRedactToken(input); + assertEquals("helm ls --kube-token=[REDACTED] --namespace myns --output json", result); + } +} diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/kubernetes/HelmClientProvider.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/kubernetes/HelmClientProvider.java index 9eb34f73..a5177ea3 100644 --- a/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/kubernetes/HelmClientProvider.java +++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/kubernetes/HelmClientProvider.java @@ -27,7 +27,7 @@ public HelmVersionService defaultHelmVersionService() { return new HelmVersionService(); } - public HelmConfiguration getConfiguration(Region region, User user) { + public HelmConfiguration getConfiguration(Region region, User user, String namespace) { final HelmConfiguration helmConfiguration = new HelmConfiguration(); if (region.getServices().getServer() != null) { final Region.Auth auth = region.getServices().getServer().getAuth(); @@ -47,6 +47,12 @@ public HelmConfiguration getConfiguration(Region region, User user) { helmConfiguration.setAsKubeUser(username); } + if (region.getServices().getAuthenticationMode() + == Region.Services.AuthenticationMode.SERVICEACCOUNTINTARGETNAMESPACE) { + helmConfiguration.setAsKubeUser( + String.format("system:serviceaccount:%s:onyxia-local", namespace)); + } + if (region.getServices().getAuthenticationMode() == Region.Services.AuthenticationMode.TOKEN_PASSTHROUGH) { helmConfiguration.setKubeToken((String) user.getAttributes().get("access_token")); diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/kubernetes/KubernetesClientProvider.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/kubernetes/KubernetesClientProvider.java index 085cc8b7..9cbc89c1 100644 --- a/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/kubernetes/KubernetesClientProvider.java +++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/kubernetes/KubernetesClientProvider.java @@ -42,7 +42,9 @@ public KubernetesClient getUserClient(Region region, User user) { // In case of SERVICEACCOUNT authentication, we can safely mutualize and use a single // KubernetesClient if (region.getServices().getAuthenticationMode() - == Region.Services.AuthenticationMode.SERVICEACCOUNT) { + == Region.Services.AuthenticationMode.SERVICEACCOUNT + || region.getServices().getAuthenticationMode() + == Region.Services.AuthenticationMode.SERVICEACCOUNTINTARGETNAMESPACE) { if (userKubernetesClientCache != null) { return userKubernetesClientCache; } diff --git a/onyxia-api/src/main/java/fr/insee/onyxia/api/services/impl/HelmAppsService.java b/onyxia-api/src/main/java/fr/insee/onyxia/api/services/impl/HelmAppsService.java index d0e69d5a..cc650d41 100644 --- a/onyxia-api/src/main/java/fr/insee/onyxia/api/services/impl/HelmAppsService.java +++ b/onyxia-api/src/main/java/fr/insee/onyxia/api/services/impl/HelmAppsService.java @@ -88,8 +88,8 @@ public HelmAppsService( this.helmReleaseHealthResolver = helmReleaseHealthResolver; } - private HelmConfiguration getHelmConfiguration(Region region, User user) { - return helmClientProvider.getConfiguration(region, user); + private HelmConfiguration getHelmConfiguration(Region region, User user, String namespace) { + return helmClientProvider.getConfiguration(region, user, namespace); } private HelmInstallService getHelmInstallService() { @@ -118,7 +118,7 @@ public Collection installApp( HelmInstaller res = getHelmInstallService() .installChart( - getHelmConfiguration(region, user), + getHelmConfiguration(region, user, namespaceId), catalogId + "/" + pkg.getName(), namespaceId, requestDTO.getName(), @@ -180,7 +180,8 @@ public CompletableFuture getUserServices( Arrays.asList( getHelmInstallService() .listChartInstall( - getHelmConfiguration(region, user), + getHelmConfiguration( + region, user, project.getNamespace()), project.getNamespace())); } catch (Exception e) { return CompletableFuture.completedFuture(new ServicesListing()); @@ -234,13 +235,14 @@ public Service getUserService(Region region, Project project, User user, String if (serviceId.startsWith("/")) { serviceId = serviceId.substring(1); } + String namespace = + kubernetesService.determineNamespaceAndCreateIfNeeded(region, project, user); HelmLs result = getHelmInstallService() .getAppById( - getHelmConfiguration(region, user), + getHelmConfiguration(region, user, namespace), serviceId, - kubernetesService.determineNamespaceAndCreateIfNeeded( - region, project, user)); + namespace); return getHelmApp(region, user, result); } @@ -257,14 +259,15 @@ public UninstallService destroyService( // If bulk in kub we ignore the path and delete every helm release HelmLs[] releases = getHelmInstallService() - .listChartInstall(getHelmConfiguration(region, user), namespace); + .listChartInstall( + getHelmConfiguration(region, user, namespace), namespace); for (HelmLs release : releases) { status = Math.max( 0, getHelmInstallService() .uninstaller( - getHelmConfiguration(region, user), + getHelmConfiguration(region, user, namespace), release.getName(), namespace)); UninstallServiceEvent uninstallServiceEvent = @@ -277,7 +280,9 @@ public UninstallService destroyService( status = getHelmInstallService() .uninstaller( - getHelmConfiguration(region, user), cannonicalPath, namespace); + getHelmConfiguration(region, user, namespace), + cannonicalPath, + namespace); UninstallServiceEvent uninstallServiceEvent = new UninstallServiceEvent(namespace, cannonicalPath, user.getIdep()); onyxiaEventPublisher.publishEvent(uninstallServiceEvent); @@ -290,7 +295,7 @@ private Service getHelmApp(Region region, User user, HelmLs release) { HelmReleaseInfo helmReleaseInfo = getHelmInstallService() .getAll( - getHelmConfiguration(region, user), + getHelmConfiguration(region, user, release.getNamespace()), release.getName(), release.getNamespace()); Service service = @@ -481,7 +486,7 @@ public void suspendOrResume( if (suspend) { getHelmInstallService() .suspend( - getHelmConfiguration(region, user), + getHelmConfiguration(region, user, namespaceId), catalogId + "/" + chartName, namespaceId, serviceId, @@ -493,7 +498,7 @@ public void suspendOrResume( } else { getHelmInstallService() .resume( - getHelmConfiguration(region, user), + getHelmConfiguration(region, user, namespaceId), catalogId + "/" + chartName, namespaceId, serviceId, diff --git a/onyxia-model/src/main/java/fr/insee/onyxia/model/region/Region.java b/onyxia-model/src/main/java/fr/insee/onyxia/model/region/Region.java index 8fb6f6ed..a1660091 100644 --- a/onyxia-model/src/main/java/fr/insee/onyxia/model/region/Region.java +++ b/onyxia-model/src/main/java/fr/insee/onyxia/model/region/Region.java @@ -352,7 +352,8 @@ public static enum AuthenticationMode { @JsonProperty("serviceAccount") @JsonAlias("admin") SERVICEACCOUNT, - + @JsonProperty("serviceAccountInTargetNamespace") + SERVICEACCOUNTINTARGETNAMESPACE, @JsonProperty("tokenPassthrough") TOKEN_PASSTHROUGH }