Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/region-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ Users can work on Onyxia as a User or as a Group to which they belong. Each user
| `groupNamespacePrefix` | "projet-" | User in a group groupId can access the namespace groupeNamespacePrefix + groupId. This prefix is also used for the Vault group directory. | |
| `usernamePrefix` | | If set, the Kubernetes user corresponding to the Onyxia user is named usernamePrefix + userId on impersonation mode, otherwise it is identified only as userId | "user-" |
| `groupPrefix` | | not used | |
| `authenticationMode` | serviceAccount | serviceAccount, impersonate or tokenPassthrough : on serviceAccount mode Onyxia API uses its own serviceAccount (by default admin or cluster-admin), with impersonate mode Onyxia requests the API with user's permissions (helm option `--kube-as-user`). With tokenPassthrough, the authentication token is passed to the API server. | |
| `authenticationMode` | serviceAccount | serviceAccount, serviceAccountInTargetNamespace, impersonate or tokenPassthrough : on serviceAccount mode Onyxia API uses its own serviceAccount (by default admin or cluster-admin), with serviceAccountInTargetNamespace mode Onyxia impersonates the service account named `targetNamespaceServiceAccount` created by the onboarding API in each namespace (its own serviceAccount only needs the `impersonate` verb on `serviceaccounts` with that name), with impersonate mode Onyxia requests the API with user's permissions (helm option `--kube-as-user`). With tokenPassthrough, the authentication token is passed to the API server. | |
| `targetNamespaceServiceAccount` | onyxia-local | Name of the service account, created by the onboarding API in each namespace, impersonated with serviceAccountInTargetNamespace authentication. | |
| `expose` | | When users request to expose their service, only subdomain of this object domain are allowed | See [Expose properties](#expose-properties) |
| `monitoring` | | Define the URL pattern of the monitoring service that is to be launched with each service. Only for client purposes. | {URLPattern: "https://$NAMESPACE-$INSTANCE.mymonitoring.sspcloud.fr"} | |
| `allowedURIPattern` | "^https://" | Init scripts set by the user have to respect this pattern. | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -30,7 +32,9 @@
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);
}
});
Expand Down Expand Up @@ -114,6 +118,29 @@
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) {

Check warning on line 141 in helm-wrapper/src/main/java/io/github/inseefrlab/helmwrapper/utils/Command.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace "e" with an unnamed pattern.

See more on https://sonarcloud.io/project/issues?id=InseeFrLab_onyxia-api&issues=AaFpHZ79OvglcMGQI2hZ&open=AaFpHZ79OvglcMGQI2hZ&pullRequest=693
LOGGER.debug("Could not retrieve in-cluster token");
}
}
String kubeConfig = null;
if (StringUtils.isNotEmpty(helmConfiguration.getApiserverUrl())) {
Expand Down Expand Up @@ -146,6 +173,10 @@
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()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {

Check warning on line 8 in helm-wrapper/src/test/java/io/github/inseefrlab/helmwrapper/utils/RedactTokenTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove redundant visibility modifiers from the methods of this test class.

See more on https://sonarcloud.io/project/issues?id=InseeFrLab_onyxia-api&issues=AaFpQBPU2AsIuq_znXzR&open=AaFpQBPU2AsIuq_znXzR&pullRequest=693

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 {

Check warning on line 17 in helm-wrapper/src/test/java/io/github/inseefrlab/helmwrapper/utils/RedactTokenTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace these 6 tests with a single Parameterized one.

See more on https://sonarcloud.io/project/issues?id=InseeFrLab_onyxia-api&issues=AaFpQBPU2AsIuq_znXzS&open=AaFpQBPU2AsIuq_znXzS&pullRequest=693
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -47,6 +47,13 @@ public HelmConfiguration getConfiguration(Region region, User user) {
helmConfiguration.setAsKubeUser(username);
}

if (region.getServices().getAuthenticationMode()
== Region.Services.AuthenticationMode.SERVICEACCOUNTINTARGETNAMESPACE) {
helmConfiguration.setAsKubeUser(
KubernetesClientProvider.getTargetNamespaceServiceAccountUsername(
region, namespace));
}

if (region.getServices().getAuthenticationMode()
== Region.Services.AuthenticationMode.TOKEN_PASSTHROUGH) {
helmConfiguration.setKubeToken((String) user.getAttributes().get("access_token"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,23 @@
return rootKubernetesClientCache.get(region.getId());
}

public KubernetesClient getUserClient(Region region, User user) {
/**
* Username of the service account created by the onboarding in the given namespace,
* impersonated in SERVICEACCOUNTINTARGETNAMESPACE mode.
*/
public static String getTargetNamespaceServiceAccountUsername(Region region, String namespace) {
return String.format(
"system:serviceaccount:%s:%s",
namespace, region.getServices().getTargetNamespaceServiceAccount());
}

/**
* Client to act on the given namespace on behalf of the user.
*
* @param namespace the namespace the client will act on, used to impersonate its service
* account in SERVICEACCOUNTINTARGETNAMESPACE mode
*/
public KubernetesClient getUserClient(Region region, User user, String namespace) {
// In case of SERVICEACCOUNT authentication, we can safely mutualize and use a single
// KubernetesClient
if (region.getServices().getAuthenticationMode()
Expand All @@ -52,6 +68,20 @@
return client;
}
final Config config = getDefaultConfiguration(region).build();

if (region.getServices().getAuthenticationMode()
== Region.Services.AuthenticationMode.SERVICEACCOUNTINTARGETNAMESPACE) {
if (StringUtils.isEmpty(namespace)) {
throw new IllegalArgumentException(
"A namespace is required to impersonate its service account");
}
config.setImpersonateUsername(
getTargetNamespaceServiceAccountUsername(region, namespace));
// Without explicit groups, the API server adds the service account groups itself
config.setImpersonateGroups(null);

Check warning on line 81 in onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/kubernetes/KubernetesClientProvider.java

View workflow job for this annotation

GitHub Actions / build

non-varargs call of varargs method with inexact argument type for last parameter;
return new KubernetesClientBuilder().withConfig(config).build();
}

String username = user.getIdep();
if (region.getServices().getUsernamePrefix() != null) {
username = region.getServices().getUsernamePrefix() + user.getIdep();
Expand All @@ -60,7 +90,7 @@
if (region.getServices().getAuthenticationMode()
== Region.Services.AuthenticationMode.IMPERSONATE) {
config.setImpersonateUsername(username);
config.setImpersonateGroups(null);

Check warning on line 93 in onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/kubernetes/KubernetesClientProvider.java

View workflow job for this annotation

GitHub Actions / build

non-varargs call of varargs method with inexact argument type for last parameter;
}

if (region.getServices().getAuthenticationMode()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -39,7 +40,7 @@
}

@PostConstruct
public void load() throws Exception {

Check failure on line 43 in onyxia-api/src/main/java/fr/insee/onyxia/api/configuration/properties/RegionsConfiguration.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=InseeFrLab_onyxia-api&issues=AaGclYM7O3RpFBaCG7A3&open=AaGclYM7O3RpFBaCG7A3&pullRequest=693
resolvedRegions = Arrays.asList(mapper.readValue(regions, Region[].class));
resolvedRegions.forEach(
region -> {
Expand All @@ -66,6 +67,37 @@
"Using token passthrough authentication for region {}. User token will be used by Onyxia to interact with the API Server.",
region.getId());
}

if (region.getServices()
.getAuthenticationMode()
.equals(
Region.Services.AuthenticationMode
.SERVICEACCOUNTINTARGETNAMESPACE)) {
if (StringUtils.isBlank(
region.getServices().getTargetNamespaceServiceAccount())) {
throw new IllegalStateException(
"services.targetNamespaceServiceAccount is required with serviceAccountInTargetNamespace authentication for region "
+ region.getId());
}
LOGGER.info(
"Using the {} service account of each namespace for region {}.",
region.getServices().getTargetNamespaceServiceAccount(),
region.getId());
if (!region.getServices().isSingleNamespace()
&& region.getServices().isAllowNamespaceCreation()) {
// Namespaces and their service account are created by the onboarding
// API, the API has no permission to do it in this mode
LOGGER.warn(
"Namespace creation is disabled for region {}: the onboarding API must be used with serviceAccountInTargetNamespace authentication.",
region.getId());
region.getServices().setAllowNamespaceCreation(false);
}
} else if (!region.getServices().isSingleNamespace()
&& region.getServices().isAllowNamespaceCreation()) {
LOGGER.warn(
"Namespace creation by Onyxia API (allowNamespaceCreation) is deprecated for region {} and will be removed, use the onboarding API instead.",
region.getId());
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,16 @@
this.userProvider = userProvider;
}

/**
* @deprecated namespaces should be created by the onboarding API. Disabled with
* serviceAccountInTargetNamespace authentication.
*/
@Deprecated

Check warning on line 42 in onyxia-api/src/main/java/fr/insee/onyxia/api/controller/api/onboarding/OnboardingController.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add 'since' and/or 'forRemoval' arguments to the @Deprecated annotation.

See more on https://sonarcloud.io/project/issues?id=InseeFrLab_onyxia-api&issues=AaGclYPMO3RpFBaCG7A4&open=AaGclYPMO3RpFBaCG7A4&pullRequest=693
@Operation(
summary = "Init a namespace for a user or a group.",
description =
"create or replace the namespace of the user or the namespace of a group if the user is in the requested group and the according rbac policies. with the group prefix / user prefix of the region",
"Deprecated: use the onboarding API instead. create or replace the namespace of the user or the namespace of a group if the user is in the requested group and the according rbac policies. with the group prefix / user prefix of the region",
deprecated = true,
parameters = {
@Parameter(
name = "ONYXIA-REGION",
Expand All @@ -48,7 +54,7 @@
schema = @Schema(name = "ONYXIA-REGION", type = "string"))
})
@PostMapping
public void onboard(

Check warning on line 57 in onyxia-api/src/main/java/fr/insee/onyxia/api/controller/api/onboarding/OnboardingController.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=InseeFrLab_onyxia-api&issues=AaGclYPMO3RpFBaCG7A5&open=AaGclYPMO3RpFBaCG7A5&pullRequest=693
@Parameter(hidden = true) Region region, @RequestBody OnboardingRequest request) {
if (region.getServices().isSingleNamespace()) {
throw new NamespaceAlreadyExistException();
Expand Down
Loading
Loading