feat: native auth on Kubernetes (#171)

Signed-off-by: Michal Vala <mvala@redhat.com>
pull/186/head^2
Michal Vala 2021-11-25 14:48:55 +01:00 committed by GitHub
parent f289ec619f
commit 388a5183be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 2128 additions and 998 deletions

View File

@ -35,6 +35,10 @@
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
@ -275,6 +279,10 @@
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-machine-authentication</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-oidc</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-permission-devfile</artifactId>
@ -454,6 +462,7 @@
<dep>com.google.guava:guava</dep>
<dep>org.everrest:everrest-core</dep>
<dep>io.jsonwebtoken:jjwt-jackson</dep>
<dep>io.jsonwebtoken:jjwt-impl</dep>
</ignoredUnusedDeclaredDependencies>
</configuration>
</execution>

View File

@ -15,6 +15,7 @@ import static com.google.inject.matcher.Matchers.subclassesOf;
import static org.eclipse.che.inject.Matchers.names;
import static org.eclipse.che.multiuser.api.permission.server.SystemDomain.SYSTEM_DOMAIN_ACTIONS;
import com.auth0.jwk.JwkProvider;
import com.google.inject.AbstractModule;
import com.google.inject.TypeLiteral;
import com.google.inject.assistedinject.FactoryModuleBuilder;
@ -22,7 +23,7 @@ import com.google.inject.multibindings.MapBinder;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.name.Names;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.impl.DefaultJwtParser;
import io.jsonwebtoken.SigningKeyResolver;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
@ -81,7 +82,6 @@ import org.eclipse.che.commons.observability.deploy.ExecutorWrapperModule;
import org.eclipse.che.core.db.DBTermination;
import org.eclipse.che.core.db.schema.SchemaInitializer;
import org.eclipse.che.core.tracing.metrics.TracingMetricsModule;
import org.eclipse.che.inject.ConfigurationException;
import org.eclipse.che.inject.DynaModule;
import org.eclipse.che.multiuser.api.authentication.commons.token.ChainedTokenExtractor;
import org.eclipse.che.multiuser.api.authentication.commons.token.HeaderRequestTokenExtractor;
@ -93,6 +93,11 @@ import org.eclipse.che.multiuser.api.workspace.activity.MultiUserWorkspaceActivi
import org.eclipse.che.multiuser.keycloak.server.deploy.KeycloakModule;
import org.eclipse.che.multiuser.keycloak.server.deploy.KeycloakUserRemoverModule;
import org.eclipse.che.multiuser.machine.authentication.server.MachineAuthModule;
import org.eclipse.che.multiuser.oidc.OIDCInfo;
import org.eclipse.che.multiuser.oidc.OIDCInfoProvider;
import org.eclipse.che.multiuser.oidc.OIDCJwkProvider;
import org.eclipse.che.multiuser.oidc.OIDCJwtParserProvider;
import org.eclipse.che.multiuser.oidc.OIDCSigningKeyResolver;
import org.eclipse.che.multiuser.organization.api.OrganizationApiModule;
import org.eclipse.che.multiuser.organization.api.OrganizationJpaModule;
import org.eclipse.che.multiuser.permission.user.UserServicePermissionsFilter;
@ -335,18 +340,10 @@ public class WsMasterModule extends AbstractModule {
.to(org.eclipse.che.api.workspace.server.DefaultWorkspaceStatusCache.class);
}
if (OpenShiftInfrastructure.NAME.equals(infrastructure)) {
if (Boolean.parseBoolean(System.getenv("CHE_AUTH_NATIVEUSER"))) {
bind(KubernetesClientConfigFactory.class).to(KubernetesOidcProviderConfigFactory.class);
} else {
bind(KubernetesClientConfigFactory.class).to(KeycloakProviderConfigFactory.class);
}
}
if (KubernetesInfrastructure.NAME.equals(infrastructure)
&& Boolean.parseBoolean(System.getenv("CHE_AUTH_NATIVEUSER"))) {
throw new ConfigurationException(
"Native user mode is not supported on Kubernetes. It is supported only on OpenShift.");
if (Boolean.parseBoolean(System.getenv("CHE_AUTH_NATIVEUSER"))) {
bind(KubernetesClientConfigFactory.class).to(KubernetesOidcProviderConfigFactory.class);
} else if (OpenShiftInfrastructure.NAME.equals(infrastructure)) {
bind(KubernetesClientConfigFactory.class).to(KeycloakProviderConfigFactory.class);
}
persistenceProperties.put(
@ -395,11 +392,16 @@ public class WsMasterModule extends AbstractModule {
install(new OrganizationJpaModule());
if (Boolean.parseBoolean(System.getenv("CHE_AUTH_NATIVEUSER"))) {
bind(RequestTokenExtractor.class).to(HeaderRequestTokenExtractor.class);
if (KubernetesInfrastructure.NAME.equals(infrastructure)) {
bind(OIDCInfo.class).toProvider(OIDCInfoProvider.class).asEagerSingleton();
bind(SigningKeyResolver.class).to(OIDCSigningKeyResolver.class);
bind(JwtParser.class).toProvider(OIDCJwtParserProvider.class);
bind(JwkProvider.class).toProvider(OIDCJwkProvider.class);
}
bind(TokenValidator.class).to(NotImplementedTokenValidator.class);
bind(JwtParser.class).to(DefaultJwtParser.class);
bind(ProfileDao.class).to(JpaProfileDao.class);
bind(OAuthAPI.class).to(EmbeddedOAuthAPI.class);
bind(RequestTokenExtractor.class).to(HeaderRequestTokenExtractor.class);
} else {
install(new KeycloakModule());
install(new KeycloakUserRemoverModule());

View File

@ -19,6 +19,8 @@ import org.eclipse.che.inject.ConfigurationException;
import org.eclipse.che.inject.DynaModule;
import org.eclipse.che.multiuser.keycloak.server.deploy.KeycloakServletModule;
import org.eclipse.che.multiuser.machine.authentication.server.MachineLoginFilter;
import org.eclipse.che.multiuser.oidc.filter.OidcTokenInitializationFilter;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesInfrastructure;
import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftInfrastructure;
import org.eclipse.che.workspace.infrastructure.openshift.multiuser.oauth.OpenshiftTokenInitializationFilter;
import org.everrest.guice.servlet.GuiceEverrestServlet;
@ -78,6 +80,8 @@ public class WsMasterServletModule extends ServletModule {
final String infrastructure = System.getenv("CHE_INFRASTRUCTURE_ACTIVE");
if (OpenShiftInfrastructure.NAME.equals(infrastructure)) {
filter("/*").through(OpenshiftTokenInitializationFilter.class);
} else if (KubernetesInfrastructure.NAME.equals(infrastructure)) {
filter("/*").through(OidcTokenInitializationFilter.class);
} else {
throw new ConfigurationException("Native user mode is currently supported on on OpenShift.");
}

View File

@ -342,6 +342,9 @@ che.infra.kubernetes.service_account_name=NULL
# This property deprecates `che.infra.kubernetes.cluster_role_name`.
che.infra.kubernetes.workspace_sa_cluster_roles=NULL
# Cluster roles to assign to user in his namespace
che.infra.kubernetes.user_cluster_roles=NULL
# Defines wait time that limits the Kubernetes workspace start time.
che.infra.kubernetes.workspace_start_timeout_min=8
@ -545,12 +548,6 @@ che.infra.kubernetes.trusted_ca.mount_path=/public-certs
# See the `che.infra.kubernetes.trusted_ca.dest_configmap` property.
che.infra.kubernetes.trusted_ca.dest_configmap_labels=
# Enables the `/unsupported/k8s` endpoint to resolve calls on Kubernetes infrastructure.
# Provides direct access to the underlying infrastructure REST API.
# This results in huge privilege escalation.
# It impacts only Kubernetes infrastructure. Therefore it implies no security risk on OpenShift with OAuth.
# Do not enable this, unless you understand the risks.
che.infra.kubernetes.enable_unsupported_k8s=false
### OpenShift Infra parameters

View File

@ -93,16 +93,31 @@ che.limits.organization.workspaces.run.count=-1
# See: link:https://www.keycloak.org/docs/latest/server_admin/#openshift-4[OpenShift identity provider]
che.infra.openshift.oauth_identity_provider=NULL
### OIDC configuration
# Url to OIDC identity provider server
# Can be set to NULL only if `che.oidc.oidcProvider` is used
che.oidc.auth_server_url=http://${CHE_HOST}:5050/auth
# Internal network service Url to OIDC identity provider server
che.oidc.auth_internal_server_url=NULL
# The number of seconds to tolerate for clock skew when verifying `exp` or `nbf` claims.
che.oidc.allowed_clock_skew_sec=3
# Username claim to be used as user display name when parsing JWT token
# if not defined the fallback value is 'preferred_username' in Keycloak installations and
# `name` in Dex installations.
che.oidc.username_claim=NULL
# Base URL of an alternate OIDC provider that provides
# a discovery endpoint as detailed in the following specification
# link:https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Obtaining OpenID Provider Configuration Information]
# Deprecated, use `che.oidc.auth_server_url` and `che.oidc.auth_internal_server_url` instead.
che.oidc.oidc_provider=NULL
### Keycloak configuration
# Url to keycloak identity provider server
# Can be set to NULL only if `che.keycloak.oidcProvider`
# is used
che.keycloak.auth_server_url=http://${CHE_HOST}:5050/auth
# Internal network service Url to keycloak identity provider server
che.keycloak.auth_internal_server_url=NULL
# Keycloak realm is used to authenticate users
# Can be set to NULL only if `che.keycloak.oidcProvider`
# is used
@ -117,9 +132,6 @@ che.keycloak.oso.endpoint=NULL
# URL to access Github OAuth tokens
che.keycloak.github.endpoint=NULL
# The number of seconds to tolerate for clock skew when verifying `exp` or `nbf` claims.
che.keycloak.allowed_clock_skew_sec=3
# Use the OIDC optional `nonce` feature to increase security.
che.keycloak.use_nonce=true
@ -130,21 +142,11 @@ che.keycloak.use_nonce=true
# if an alternate `oidc_provider` is used
che.keycloak.js_adapter_url=NULL
# Base URL of an alternate OIDC provider that provides
# a discovery endpoint as detailed in the following specification
# link:https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Obtaining OpenID Provider Configuration Information]
che.keycloak.oidc_provider=NULL
# Set to true when using an alternate OIDC provider that
# only supports fixed redirect Urls
# This property is ignored when `che.keycloak.oidc_provider` is NULL
che.keycloak.use_fixed_redirect_urls=false
# Username claim to be used as user display name
# when parsing JWT token
# if not defined the fallback value is 'preferred_username'
che.keycloak.username_claim=NULL
# Configuration of OAuth Authentication Service that can be used in "embedded" or "delegated" mode.
# If set to "embedded", then the service work as a wrapper to Che's OAuthAuthenticator ( as in Single User mode).
# If set to "delegated", then the service will use Keycloak IdentityProvider mechanism.

View File

@ -41,3 +41,9 @@ che.infra.kubernetes.trusted_ca.dest_configmap=che.infra.openshift.trusted_ca_bu
che.infra.kubernetes.trusted_ca.mount_path=che.infra.openshift.trusted_ca_bundles_mount_path
che.infra.openshift.trusted_ca.dest_configmap_labels=che.infra.openshift.trusted_ca_bundles_config_map_labels
che.integration.bitbucket.server_endpoints=bitbucket.server.endpoints
che.oidc.auth_server_url=che.keycloak.auth_server_url
che.oidc.auth_internal_server_url=che.keycloak.auth_internal_server_url
che.oidc.allowed_clock_skew_sec=che.keycloak.allowed_clock_skew_sec
che.oidc.username_claim=che.keycloak.username_claim
che.oidc.oidc_provider=che.keycloak.oidc_provider

View File

@ -220,6 +220,11 @@
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-server-mock</artifactId>

View File

@ -31,6 +31,7 @@ public class CheServerKubernetesClientFactory extends KubernetesClientFactory {
@Inject
public CheServerKubernetesClientFactory(
KubernetesClientConfigFactory configBuilder,
@Nullable @Named("che.infra.kubernetes.master_url") String masterUrl,
@Nullable @Named("che.infra.kubernetes.trust_certs") Boolean doTrustCerts,
@Named("che.infra.kubernetes.client.http.async_requests.max") int maxConcurrentRequests,
@ -41,6 +42,7 @@ public class CheServerKubernetesClientFactory extends KubernetesClientFactory {
int connectionPoolKeepAlive,
EventListener eventListener) {
super(
configBuilder,
masterUrl,
doTrustCerts,
maxConcurrentRequests,

View File

@ -14,6 +14,7 @@ package org.eclipse.che.workspace.infrastructure.kubernetes;
import static com.google.common.base.Strings.isNullOrEmpty;
import static io.fabric8.kubernetes.client.utils.Utils.isNotNullOrEmpty;
import io.fabric8.kubernetes.client.BaseKubernetesClient;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.ConfigBuilder;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
@ -55,10 +56,13 @@ public class KubernetesClientFactory {
* Default Kubernetes {@link Config} that will be the base configuration to create per-workspace
* configurations.
*/
private Config defaultConfig;
private final Config defaultConfig;
protected final KubernetesClientConfigFactory configBuilder;
@Inject
public KubernetesClientFactory(
KubernetesClientConfigFactory configBuilder,
@Nullable @Named("che.infra.kubernetes.master_url") String masterUrl,
@Nullable @Named("che.infra.kubernetes.trust_certs") Boolean doTrustCerts,
@Named("che.infra.kubernetes.client.http.async_requests.max") int maxConcurrentRequests,
@ -68,6 +72,7 @@ public class KubernetesClientFactory {
@Named("che.infra.kubernetes.client.http.connection_pool.keep_alive_min")
int connectionPoolKeepAlive,
EventListener eventListener) {
this.configBuilder = configBuilder;
this.defaultConfig = buildDefaultConfig(masterUrl, doTrustCerts);
OkHttpClient temporary = HttpClientUtils.createHttpClient(defaultConfig);
OkHttpClient.Builder builder = temporary.newBuilder();
@ -166,7 +171,12 @@ public class KubernetesClientFactory {
* infromation
*/
public OkHttpClient getAuthenticatedHttpClient() throws InfrastructureException {
return create(getDefaultConfig()).getHttpClient();
if (!configBuilder.isPersonalized()) {
throw new InfrastructureException(
"Not able to construct impersonating Kubernetes API client.");
}
// Ensure to get OkHttpClient with all necessary interceptors.
return create(buildConfig(getDefaultConfig(), null)).getHttpClient();
}
/**
@ -200,7 +210,7 @@ public class KubernetesClientFactory {
*/
protected Config buildConfig(Config config, @Nullable String workspaceId)
throws InfrastructureException {
return config;
return configBuilder.buildConfig(config, workspaceId);
}
protected Interceptor buildKubernetesInterceptor(Config config) {
@ -234,7 +244,7 @@ public class KubernetesClientFactory {
* authenticate with the credentials (user/password or Oauth token) contained in the {@code
* config} parameter.
*/
private DefaultKubernetesClient create(Config config) {
protected BaseKubernetesClient<?> create(Config config) {
OkHttpClient clientHttpClient =
httpClient.newBuilder().authenticator(Authenticator.NONE).build();
OkHttpClient.Builder builder = clientHttpClient.newBuilder();

View File

@ -48,9 +48,13 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.devfile.KubernetesDev
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironmentFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.RemoveNamespaceOnWorkspaceRemove;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.CredentialsSecretConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.PreferencesConfigMapConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.UserPermissionConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.UserPreferencesConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.UserProfileConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.WorkspaceServiceAccountConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.CommonPVCStrategy;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.PerWorkspacePVCStrategy;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.UniqueWorkspacePVCStrategy;
@ -101,8 +105,15 @@ public class KubernetesInfraModule extends AbstractModule {
workspaceAttributeValidators.addBinding().to(K8sInfraNamespaceWsAttributeValidator.class);
workspaceAttributeValidators.addBinding().to(AsyncStorageModeValidator.class);
// order matters here!
// We first need to grant permissions to user, only then we can run other configurators with
// user's client.
Multibinder<NamespaceConfigurator> namespaceConfigurators =
Multibinder.newSetBinder(binder(), NamespaceConfigurator.class);
namespaceConfigurators.addBinding().to(UserPermissionConfigurator.class);
namespaceConfigurators.addBinding().to(CredentialsSecretConfigurator.class);
namespaceConfigurators.addBinding().to(PreferencesConfigMapConfigurator.class);
namespaceConfigurators.addBinding().to(WorkspaceServiceAccountConfigurator.class);
namespaceConfigurators.addBinding().to(UserProfileConfigurator.class);
namespaceConfigurators.addBinding().to(UserPreferencesConfigurator.class);

View File

@ -16,7 +16,6 @@ import java.net.URI;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Named;
import org.eclipse.che.api.core.ValidationException;
import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.InternalInfrastructureException;
@ -47,8 +46,7 @@ public class KubernetesRuntimeContext<T extends KubernetesEnvironment> extends R
KubernetesRuntimeStateCache runtimeStatuses,
@Assisted T kubernetesEnvironment,
@Assisted RuntimeIdentity identity,
@Assisted RuntimeInfrastructure infrastructure)
throws ValidationException, InfrastructureException {
@Assisted RuntimeInfrastructure infrastructure) {
super(kubernetesEnvironment, identity, infrastructure);
this.namespaceFactory = namespaceFactory;
this.runtimeFactory = runtimeFactory;

View File

@ -145,7 +145,7 @@ public class KubernetesNamespace {
*/
void prepare(boolean canCreate, Map<String, String> labels, Map<String, String> annotations)
throws InfrastructureException {
KubernetesClient client = clientFactory.create(workspaceId);
KubernetesClient client = cheSAClientFactory.create(workspaceId);
Namespace namespace = get(name, client);
if (namespace == null) {

View File

@ -20,24 +20,17 @@ import static java.util.Collections.singletonList;
import static org.eclipse.che.api.workspace.shared.Constants.WORKSPACE_INFRASTRUCTURE_NAMESPACE_ATTRIBUTE;
import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.DEFAULT_ATTRIBUTE;
import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.PHASE_ATTRIBUTE;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.CREDENTIALS_SECRET_NAME;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.PREFERENCES_CONFIGMAP_NAME;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.NamespaceNameValidator.METADATA_NAME_MAX_LENGTH;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Sets;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Namespace;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.client.KubernetesClientException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -68,6 +61,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesCl
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.api.server.impls.KubernetesNamespaceMetaImpl;
import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSharedPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -103,25 +97,23 @@ public class KubernetesNamespaceFactory {
protected final Map<String, String> namespaceLabels;
protected final Map<String, String> namespaceAnnotations;
private final String serviceAccountName;
private final Set<String> clusterRoleNames;
private final KubernetesClientFactory clientFactory;
private final KubernetesClientFactory cheClientFactory;
private final boolean namespaceCreationAllowed;
private final UserManager userManager;
private final PreferenceManager preferenceManager;
protected final Set<NamespaceConfigurator> namespaceConfigurators;
protected final KubernetesSharedPool sharedPool;
@Inject
public KubernetesNamespaceFactory(
@Nullable @Named("che.infra.kubernetes.service_account_name") String serviceAccountName,
@Nullable @Named("che.infra.kubernetes.workspace_sa_cluster_roles") String clusterRoleNames,
@Nullable @Named("che.infra.kubernetes.namespace.default") String defaultNamespaceName,
@Named("che.infra.kubernetes.namespace.creation_allowed") boolean namespaceCreationAllowed,
@Named("che.infra.kubernetes.namespace.label") boolean labelNamespaces,
@Named("che.infra.kubernetes.namespace.annotate") boolean annotateNamespaces,
@Named("che.infra.kubernetes.namespace.labels") String namespaceLabels,
@Named("che.infra.kubernetes.namespace.annotations") String namespaceAnnotations,
Set<NamespaceConfigurator> namespaceConfigurators,
KubernetesClientFactory clientFactory,
CheServerKubernetesClientFactory cheClientFactory,
UserManager userManager,
@ -130,7 +122,6 @@ public class KubernetesNamespaceFactory {
throws ConfigurationException {
this.namespaceCreationAllowed = namespaceCreationAllowed;
this.userManager = userManager;
this.serviceAccountName = serviceAccountName;
this.clientFactory = clientFactory;
this.cheClientFactory = cheClientFactory;
this.defaultNamespaceName = defaultNamespaceName;
@ -138,6 +129,7 @@ public class KubernetesNamespaceFactory {
this.sharedPool = sharedPool;
this.labelNamespaces = labelNamespaces;
this.annotateNamespaces = annotateNamespaces;
this.namespaceConfigurators = ImmutableSet.copyOf(namespaceConfigurators);
//noinspection UnstableApiUsage
Splitter.MapSplitter csvMapSplitter = Splitter.on(",").withKeyValueSeparator("=");
@ -162,14 +154,6 @@ public class KubernetesNamespaceFactory {
+ " The current value is: `%s`.",
Joiner.on(" or ").join(REQUIRED_NAMESPACE_NAME_PLACEHOLDERS), defaultNamespaceName));
}
if (!isNullOrEmpty(clusterRoleNames)) {
this.clusterRoleNames =
Sets.newHashSet(
Splitter.on(",").trimResults().omitEmptyStrings().split(clusterRoleNames));
} else {
this.clusterRoleNames = Collections.emptySet();
}
}
/**
@ -260,7 +244,7 @@ public class KubernetesNamespaceFactory {
public Optional<KubernetesNamespaceMeta> fetchNamespace(String name)
throws InfrastructureException {
try {
Namespace namespace = clientFactory.create().namespaces().withName(name).get();
Namespace namespace = cheClientFactory.create().namespaces().withName(name).get();
if (namespace == null) {
return Optional.empty();
} else {
@ -336,8 +320,10 @@ public class KubernetesNamespaceFactory {
public KubernetesNamespace getOrCreate(RuntimeIdentity identity) throws InfrastructureException {
KubernetesNamespace namespace = get(identity);
var subject = EnvironmentContext.getCurrent().getSubject();
NamespaceResolutionContext resolutionCtx =
new NamespaceResolutionContext(EnvironmentContext.getCurrent().getSubject());
new NamespaceResolutionContext(
identity.getWorkspaceId(), subject.getUserId(), subject.getUserName());
Map<String, String> namespaceAnnotationsEvaluated =
evaluateAnnotationPlaceholders(resolutionCtx);
@ -346,44 +332,7 @@ public class KubernetesNamespaceFactory {
labelNamespaces ? namespaceLabels : emptyMap(),
annotateNamespaces ? namespaceAnnotationsEvaluated : emptyMap());
if (namespace
.secrets()
.get()
.stream()
.noneMatch(s -> s.getMetadata().getName().equals(CREDENTIALS_SECRET_NAME))) {
Secret secret =
new SecretBuilder()
.withType("opaque")
.withNewMetadata()
.withName(CREDENTIALS_SECRET_NAME)
.endMetadata()
.build();
clientFactory
.create()
.secrets()
.inNamespace(identity.getInfrastructureNamespace())
.create(secret);
}
if (namespace.configMaps().get(PREFERENCES_CONFIGMAP_NAME).isEmpty()) {
ConfigMap configMap =
new ConfigMapBuilder()
.withNewMetadata()
.withName(PREFERENCES_CONFIGMAP_NAME)
.endMetadata()
.build();
clientFactory
.create()
.configMaps()
.inNamespace(identity.getInfrastructureNamespace())
.create(configMap);
}
if (!isNullOrEmpty(serviceAccountName)) {
KubernetesWorkspaceServiceAccount workspaceServiceAccount =
doCreateServiceAccount(namespace.getWorkspaceId(), namespace.getName());
workspaceServiceAccount.prepare();
}
configureNamespace(resolutionCtx, namespace.getName());
return namespace;
}
@ -590,7 +539,7 @@ public class KubernetesNamespaceFactory {
NamespaceResolutionContext namespaceCtx) throws InfrastructureException {
try {
List<Namespace> workspaceNamespaces =
clientFactory.create().namespaces().withLabels(namespaceLabels).list().getItems();
cheClientFactory.create().namespaces().withLabels(namespaceLabels).list().getItems();
if (!workspaceNamespaces.isEmpty()) {
Map<String, String> evaluatedAnnotations = evaluateAnnotationPlaceholders(namespaceCtx);
return workspaceNamespaces
@ -617,6 +566,14 @@ public class KubernetesNamespaceFactory {
}
}
protected void configureNamespace(
NamespaceResolutionContext namespaceResolutionContext, String namespaceName)
throws InfrastructureException {
for (NamespaceConfigurator configurator : namespaceConfigurators) {
configurator.configure(namespaceResolutionContext, namespaceName);
}
}
/**
* Evaluate placeholder in `che.infra.kubernetes.namespace.annotations` property with given {@link
* NamespaceResolutionContext}.
@ -657,31 +614,6 @@ public class KubernetesNamespaceFactory {
}
}
protected boolean checkNamespaceExists(String namespaceName) throws InfrastructureException {
try {
return clientFactory.create().namespaces().withName(namespaceName).get() != null;
} catch (KubernetesClientException e) {
if (e.getCode() == 403) {
// 403 means that the project does not exist
// or a user really is not permitted to access it which is Che Server misconfiguration
return false;
} else {
throw new InfrastructureException(
format(
"Error occurred while trying to fetch the namespace '%s'. Cause: %s",
namespaceName, e.getMessage()),
e);
}
}
}
protected String evalPlaceholders(String namespace, Subject currentUser, String workspaceId) {
return evalPlaceholders(
namespace,
new NamespaceResolutionContext(
workspaceId, currentUser.getUserId(), currentUser.getUserName()));
}
protected String evalPlaceholders(String namespace, NamespaceResolutionContext ctx) {
checkArgument(!isNullOrEmpty(namespace));
String evaluated = namespace;
@ -710,7 +642,7 @@ public class KubernetesNamespaceFactory {
preferences.put(NAMESPACE_TEMPLATE_ATTRIBUTE, defaultNamespaceName);
preferenceManager.update(owner, preferences);
} catch (ServerException e) {
LOG.error(e.getMessage(), e);
LOG.error("Failed storing namespace name in user properties.", e);
}
}
@ -743,6 +675,7 @@ public class KubernetesNamespaceFactory {
String normalizeNamespaceName(String namespaceName) {
namespaceName =
namespaceName
.toLowerCase()
.replaceAll("[^-a-zA-Z0-9]", "-") // replace invalid chars with '-'
.replaceAll("-+", "-") // replace multiple '-' with single ones
.replaceAll("^-|-$", ""); // trim dashes at beginning/end of the string
@ -755,19 +688,4 @@ public class KubernetesNamespaceFactory {
namespaceName.length(),
METADATA_NAME_MAX_LENGTH)); // limit length to METADATA_NAME_MAX_LENGTH
}
@VisibleForTesting
KubernetesWorkspaceServiceAccount doCreateServiceAccount(
String workspaceId, String namespaceName) {
return new KubernetesWorkspaceServiceAccount(
workspaceId, namespaceName, serviceAccountName, getClusterRoleNames(), clientFactory);
}
protected String getServiceAccountName() {
return serviceAccountName;
}
protected Set<String> getClusterRoleNames() {
return clusterRoleNames;
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.CREDENTIALS_SECRET_NAME;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
/**
* This {@link NamespaceConfigurator} ensures that Secret {@link
* org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount#CREDENTIALS_SECRET_NAME}
* is present in the Workspace namespace.
*/
@Singleton
public class CredentialsSecretConfigurator implements NamespaceConfigurator {
private final KubernetesClientFactory clientFactory;
@Inject
public CredentialsSecretConfigurator(KubernetesClientFactory clientFactory) {
this.clientFactory = clientFactory;
}
@Override
public void configure(NamespaceResolutionContext namespaceResolutionContext, String namespaceName)
throws InfrastructureException {
var client = clientFactory.create();
if (client.secrets().inNamespace(namespaceName).withName(CREDENTIALS_SECRET_NAME).get()
== null) {
Secret secret =
new SecretBuilder()
.withType("opaque")
.withNewMetadata()
.withName(CREDENTIALS_SECRET_NAME)
.endMetadata()
.build();
client.secrets().inNamespace(namespaceName).create(secret);
}
}
}

View File

@ -30,6 +30,6 @@ public interface NamespaceConfigurator {
* @param namespaceResolutionContext users namespace context
* @throws InfrastructureException when any error occurs
*/
public void configure(NamespaceResolutionContext namespaceResolutionContext)
void configure(NamespaceResolutionContext namespaceResolutionContext, String namespaceName)
throws InfrastructureException;
}

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.PREFERENCES_CONFIGMAP_NAME;
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import javax.inject.Inject;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
/**
* This {@link NamespaceConfigurator} ensures that ConfigMap {@link
* org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount#PREFERENCES_CONFIGMAP_NAME}
* is present in the Workspace namespace.
*/
public class PreferencesConfigMapConfigurator implements NamespaceConfigurator {
private final KubernetesClientFactory clientFactory;
@Inject
public PreferencesConfigMapConfigurator(KubernetesClientFactory clientFactory) {
this.clientFactory = clientFactory;
}
@Override
public void configure(NamespaceResolutionContext namespaceResolutionContext, String namespaceName)
throws InfrastructureException {
var client = clientFactory.create();
if (client.configMaps().inNamespace(namespaceName).withName(PREFERENCES_CONFIGMAP_NAME).get()
== null) {
ConfigMap configMap =
new ConfigMapBuilder()
.withNewMetadata()
.withName(PREFERENCES_CONFIGMAP_NAME)
.endMetadata()
.build();
client.configMaps().inNamespace(namespaceName).create(configMap);
}
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator;
import static com.google.common.base.Strings.isNullOrEmpty;
import com.google.common.base.Splitter;
import com.google.common.collect.Sets;
import io.fabric8.kubernetes.api.model.rbac.RoleBindingBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import java.util.Collections;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
/**
* This {@link NamespaceConfigurator} ensures that User has assigned configured ClusterRoles from
* `che.infra.kubernetes.user_cluster_roles` property. These are assigned with RoleBindings in
* user's namespace.
*/
@Singleton
public class UserPermissionConfigurator implements NamespaceConfigurator {
private final Set<String> userClusterRoles;
private final KubernetesClientFactory clientFactory;
@Inject
public UserPermissionConfigurator(
@Nullable @Named("che.infra.kubernetes.user_cluster_roles") String userClusterRoles,
CheServerKubernetesClientFactory cheClientFactory) {
this.clientFactory = cheClientFactory;
if (!isNullOrEmpty(userClusterRoles)) {
this.userClusterRoles =
Sets.newHashSet(
Splitter.on(",").trimResults().omitEmptyStrings().split(userClusterRoles));
} else {
this.userClusterRoles = Collections.emptySet();
}
}
@Override
public void configure(NamespaceResolutionContext namespaceResolutionContext, String namespaceName)
throws InfrastructureException {
if (!userClusterRoles.isEmpty()) {
bindRoles(
clientFactory.create(),
namespaceName,
namespaceResolutionContext.getUserName(),
userClusterRoles);
}
}
private void bindRoles(
KubernetesClient client, String namespaceName, String username, Set<String> clusterRoles) {
for (String clusterRole : clusterRoles) {
client
.rbac()
.roleBindings()
.inNamespace(namespaceName)
.createOrReplace(
new RoleBindingBuilder()
.withNewMetadata()
.withName(clusterRole)
.endMetadata()
.addToSubjects(
new io.fabric8.kubernetes.api.model.rbac.Subject(
"rbac.authorization.k8s.io", "User", username, namespaceName))
.withNewRoleRef()
.withApiGroup("rbac.authorization.k8s.io")
.withKind("ClusterRole")
.withName(clusterRole)
.endRoleRef()
.build());
}
}
}

View File

@ -23,6 +23,7 @@ import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.model.user.User;
@ -31,7 +32,6 @@ import org.eclipse.che.api.user.server.UserManager;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory;
/**
* Creates {@link Secret} with user preferences. This serves as a way for DevWorkspaces to acquire
@ -39,39 +39,36 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesN
*
* @author Pavol Baran
*/
@Singleton
public class UserPreferencesConfigurator implements NamespaceConfigurator {
private static final String USER_PREFERENCES_SECRET_NAME = "user-preferences";
private static final String USER_PREFERENCES_SECRET_MOUNT_PATH = "/config/user/preferences";
private static final int PREFERENCE_NAME_MAX_LENGTH = 253;
private final KubernetesNamespaceFactory namespaceFactory;
private final KubernetesClientFactory clientFactory;
private final UserManager userManager;
private final PreferenceManager preferenceManager;
@Inject
public UserPreferencesConfigurator(
KubernetesNamespaceFactory namespaceFactory,
KubernetesClientFactory clientFactory,
UserManager userManager,
PreferenceManager preferenceManager) {
this.namespaceFactory = namespaceFactory;
this.clientFactory = clientFactory;
this.userManager = userManager;
this.preferenceManager = preferenceManager;
}
@Override
public void configure(NamespaceResolutionContext namespaceResolutionContext)
public void configure(NamespaceResolutionContext namespaceResolutionContext, String namespaceName)
throws InfrastructureException {
Secret userPreferencesSecret = preparePreferencesSecret(namespaceResolutionContext);
String namespace = namespaceFactory.evaluateNamespaceName(namespaceResolutionContext);
try {
clientFactory
.create()
.secrets()
.inNamespace(namespace)
.inNamespace(namespaceName)
.createOrReplace(userPreferencesSecret);
} catch (KubernetesClientException e) {
throw new InfrastructureException(

View File

@ -22,6 +22,7 @@ import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.model.user.User;
@ -29,7 +30,6 @@ import org.eclipse.che.api.user.server.UserManager;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory;
/**
* Creates {@link Secret} with user profile information such as his id, name and email. This serves
@ -37,31 +37,30 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesN
*
* @author Pavol Baran
*/
@Singleton
public class UserProfileConfigurator implements NamespaceConfigurator {
private static final String USER_PROFILE_SECRET_NAME = "user-profile";
private static final String USER_PROFILE_SECRET_MOUNT_PATH = "/config/user/profile";
private final KubernetesNamespaceFactory namespaceFactory;
private final KubernetesClientFactory clientFactory;
private final UserManager userManager;
@Inject
public UserProfileConfigurator(
KubernetesNamespaceFactory namespaceFactory,
KubernetesClientFactory clientFactory,
UserManager userManager) {
this.namespaceFactory = namespaceFactory;
public UserProfileConfigurator(KubernetesClientFactory clientFactory, UserManager userManager) {
this.clientFactory = clientFactory;
this.userManager = userManager;
}
@Override
public void configure(NamespaceResolutionContext namespaceResolutionContext)
public void configure(NamespaceResolutionContext namespaceResolutionContext, String namespaceName)
throws InfrastructureException {
Secret userProfileSecret = prepareProfileSecret(namespaceResolutionContext);
String namespace = namespaceFactory.evaluateNamespaceName(namespaceResolutionContext);
try {
clientFactory.create().secrets().inNamespace(namespace).createOrReplace(userProfileSecret);
clientFactory
.create()
.secrets()
.inNamespace(namespaceName)
.createOrReplace(userProfileSecret);
} catch (KubernetesClientException e) {
throw new InfrastructureException(
"Error occurred while trying to create user profile secret.", e);

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator;
import static com.google.common.base.Strings.isNullOrEmpty;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.Sets;
import java.util.Collections;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesWorkspaceServiceAccount;
/**
* This {@link NamespaceConfigurator} ensures that workspace ServiceAccount with proper ClusterRole
* is set in Workspace namespace.
*/
@Singleton
public class WorkspaceServiceAccountConfigurator implements NamespaceConfigurator {
private final KubernetesClientFactory clientFactory;
private final String serviceAccountName;
private final Set<String> clusterRoleNames;
@Inject
public WorkspaceServiceAccountConfigurator(
@Nullable @Named("che.infra.kubernetes.service_account_name") String serviceAccountName,
@Nullable @Named("che.infra.kubernetes.workspace_sa_cluster_roles") String clusterRoleNames,
KubernetesClientFactory clientFactory) {
this.clientFactory = clientFactory;
this.serviceAccountName = serviceAccountName;
if (!isNullOrEmpty(clusterRoleNames)) {
this.clusterRoleNames =
Sets.newHashSet(
Splitter.on(",").trimResults().omitEmptyStrings().split(clusterRoleNames));
} else {
this.clusterRoleNames = Collections.emptySet();
}
}
@Override
public void configure(NamespaceResolutionContext namespaceResolutionContext, String namespaceName)
throws InfrastructureException {
if (!isNullOrEmpty(serviceAccountName)) {
KubernetesWorkspaceServiceAccount workspaceServiceAccount =
doCreateServiceAccount(namespaceResolutionContext.getWorkspaceId(), namespaceName);
workspaceServiceAccount.prepare();
}
}
@VisibleForTesting
public KubernetesWorkspaceServiceAccount doCreateServiceAccount(
String workspaceId, String namespaceName) {
return new KubernetesWorkspaceServiceAccount(
workspaceId, namespaceName, serviceAccountName, clusterRoleNames, clientFactory);
}
}

View File

@ -12,7 +12,6 @@
package org.eclipse.che.workspace.infrastructure.kubernetes.provision;
import io.fabric8.kubernetes.api.model.Namespace;
import java.util.Set;
import javax.inject.Inject;
import org.eclipse.che.api.workspace.server.model.impl.RuntimeIdentityImpl;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
@ -30,17 +29,13 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurato
*/
public class NamespaceProvisioner {
private final KubernetesNamespaceFactory namespaceFactory;
private final Set<NamespaceConfigurator> namespaceConfigurators;
@Inject
public NamespaceProvisioner(
KubernetesNamespaceFactory namespaceFactory,
Set<NamespaceConfigurator> namespaceConfigurators) {
public NamespaceProvisioner(KubernetesNamespaceFactory namespaceFactory) {
this.namespaceFactory = namespaceFactory;
this.namespaceConfigurators = namespaceConfigurators;
}
/** Tests for this method are in KubernetesFactoryTest. */
/** Tests for this method are in KubernetesNamespaceFactoryTest. */
public KubernetesNamespaceMeta provision(NamespaceResolutionContext namespaceResolutionContext)
throws InfrastructureException {
KubernetesNamespace namespace =
@ -51,21 +46,9 @@ public class NamespaceProvisioner {
namespaceResolutionContext.getUserId(),
namespaceFactory.evaluateNamespaceName(namespaceResolutionContext)));
KubernetesNamespaceMeta namespaceMeta =
namespaceFactory
.fetchNamespace(namespace.getName())
.orElseThrow(
() ->
new InfrastructureException(
"Not able to find namespace " + namespace.getName()));
configureNamespace(namespaceResolutionContext);
return namespaceMeta;
}
private void configureNamespace(NamespaceResolutionContext namespaceResolutionContext)
throws InfrastructureException {
for (NamespaceConfigurator configurator : namespaceConfigurators) {
configurator.configure(namespaceResolutionContext);
}
return namespaceFactory
.fetchNamespace(namespace.getName())
.orElseThrow(
() -> new InfrastructureException("Not able to find namespace " + namespace.getName()));
}
}

View File

@ -20,6 +20,7 @@ import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.Kub
import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.PHASE_ATTRIBUTE;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.CREDENTIALS_SECRET_NAME;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.PREFERENCES_CONFIGMAP_NAME;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.SECRETS_ROLE_NAME;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory.NAMESPACE_TEMPLATE_ATTRIBUTE;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
@ -51,6 +52,7 @@ import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.ServiceAccountList;
import io.fabric8.kubernetes.api.model.Status;
import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBuilder;
import io.fabric8.kubernetes.api.model.rbac.PolicyRule;
import io.fabric8.kubernetes.api.model.rbac.Role;
import io.fabric8.kubernetes.api.model.rbac.RoleBindingList;
import io.fabric8.kubernetes.api.model.rbac.RoleList;
@ -88,6 +90,10 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesCl
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.api.server.impls.KubernetesNamespaceMetaImpl;
import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.CredentialsSecretConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.PreferencesConfigMapConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.WorkspaceServiceAccountConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.provision.NamespaceProvisioner;
import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSharedPool;
import org.mockito.ArgumentCaptor;
@ -145,6 +151,7 @@ public class KubernetesNamespaceFactoryTest {
serverMock.before();
k8sClient = spy(serverMock.getClient());
lenient().when(clientFactory.create()).thenReturn(k8sClient);
lenient().when(cheClientFactory.create()).thenReturn(k8sClient);
lenient().when(k8sClient.namespaces()).thenReturn(namespaceOperation);
lenient().when(namespaceOperation.withName(any())).thenReturn(namespaceResource);
@ -171,14 +178,13 @@ public class KubernetesNamespaceFactoryTest {
throws Exception {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -199,14 +205,13 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -220,14 +225,13 @@ public class KubernetesNamespaceFactoryTest {
public void shouldNormaliseNamespaceWhenUserNameStartsWithKube() {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"che-<userid>",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -246,14 +250,13 @@ public class KubernetesNamespaceFactoryTest {
throws Exception {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -269,14 +272,13 @@ public class KubernetesNamespaceFactoryTest {
public void shouldThrowExceptionIfNoDefaultNamespaceIsConfigured() {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
null,
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -320,14 +322,13 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -364,14 +365,13 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -395,14 +395,13 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -429,14 +428,13 @@ public class KubernetesNamespaceFactoryTest {
.build());
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -458,14 +456,13 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -489,28 +486,27 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
Set.of(new CredentialsSecretConfigurator(clientFactory)),
clientFactory,
cheClientFactory,
userManager,
preferenceManager,
pool));
KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class);
KubernetesSecrets secrets = mock(KubernetesSecrets.class);
when(toReturnNamespace.secrets()).thenReturn(secrets);
when(toReturnNamespace.configMaps()).thenReturn(mock(KubernetesConfigsMaps.class));
when(secrets.get()).thenReturn(Collections.emptyList());
when(toReturnNamespace.getName()).thenReturn("namespaceName");
doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any());
MixedOperation mixedOperation = mock(MixedOperation.class);
lenient().when(k8sClient.secrets()).thenReturn(mixedOperation);
lenient().when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation);
when(k8sClient.secrets()).thenReturn(mixedOperation);
when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation);
when(namespaceResource.get()).thenReturn(null);
when(cheClientFactory.create()).thenReturn(k8sClient);
when(clientFactory.create()).thenReturn(k8sClient);
// when
RuntimeIdentity identity =
@ -531,28 +527,25 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
Set.of(new PreferencesConfigMapConfigurator(clientFactory)),
clientFactory,
cheClientFactory,
userManager,
preferenceManager,
pool));
KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class);
KubernetesConfigsMaps configsMaps = mock(KubernetesConfigsMaps.class);
when(toReturnNamespace.secrets()).thenReturn(mock(KubernetesSecrets.class));
when(toReturnNamespace.configMaps()).thenReturn(configsMaps);
when(configsMaps.get(eq(PREFERENCES_CONFIGMAP_NAME))).thenReturn(empty());
when(toReturnNamespace.getName()).thenReturn("namespaceName");
doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any());
MixedOperation mixedOperation = mock(MixedOperation.class);
lenient().when(k8sClient.configMaps()).thenReturn(mixedOperation);
lenient().when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation);
when(k8sClient.configMaps()).thenReturn(mixedOperation);
when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation);
when(namespaceResource.get()).thenReturn(null);
// when
RuntimeIdentity identity =
@ -567,19 +560,60 @@ public class KubernetesNamespaceFactoryTest {
}
@Test
public void shouldNotCreateCredentialsSecretIfExists() throws Exception {
public void testAllConfiguratorsAreCalledWhenCreatingNamespace() throws InfrastructureException {
// given
String namespaceName = "testNamespaceName";
NamespaceConfigurator configurator1 = Mockito.mock(NamespaceConfigurator.class);
NamespaceConfigurator configurator2 = Mockito.mock(NamespaceConfigurator.class);
Set<NamespaceConfigurator> namespaceConfigurators = Set.of(configurator1, configurator2);
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
namespaceConfigurators,
clientFactory,
cheClientFactory,
userManager,
preferenceManager,
pool));
EnvironmentContext.getCurrent().setSubject(new SubjectImpl("jondoe", "123", null, false));
KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class);
when(toReturnNamespace.getName()).thenReturn(namespaceName);
RuntimeIdentity identity = new RuntimeIdentityImpl("workspace123", null, USER_ID, "old-che");
doReturn(toReturnNamespace).when(namespaceFactory).get(identity);
// when
KubernetesNamespace namespace = namespaceFactory.getOrCreate(identity);
// then
NamespaceResolutionContext resolutionCtx =
new NamespaceResolutionContext("workspace123", "123", "jondoe");
verify(configurator1).configure(resolutionCtx, namespaceName);
verify(configurator2).configure(resolutionCtx, namespaceName);
assertEquals(namespace, toReturnNamespace);
}
@Test
public void shouldNotCreateCredentialsSecretIfExists() throws Exception {
// given
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -607,14 +641,13 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -643,14 +676,13 @@ public class KubernetesNamespaceFactoryTest {
public void shouldThrowExceptionWhenFailedToGetInfoAboutDefaultNamespace() throws Exception {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -669,14 +701,13 @@ public class KubernetesNamespaceFactoryTest {
public void shouldThrowExceptionWhenFailedToGetNamespaces() throws Exception {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -700,14 +731,13 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -723,7 +753,6 @@ public class KubernetesNamespaceFactoryTest {
// then
assertEquals(toReturnNamespace, namespace);
verify(namespaceFactory, never()).doCreateServiceAccount(any(), any());
verify(toReturnNamespace).prepare(eq(false), any(), any());
}
@ -733,14 +762,13 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
false,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -757,7 +785,6 @@ public class KubernetesNamespaceFactoryTest {
// then
assertEquals(toReturnNamespace, namespace);
verify(namespaceFactory, never()).doCreateServiceAccount(any(), any());
verify(toReturnNamespace).prepare(eq(false), any(), any());
}
@ -765,17 +792,18 @@ public class KubernetesNamespaceFactoryTest {
public void shouldPrepareWorkspaceServiceAccountIfItIsConfiguredAndNamespaceIsNotPredefined()
throws Exception {
// given
var serviceAccountCfg =
spy(new WorkspaceServiceAccountConfigurator("serviceAccount", "", clientFactory));
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"serviceAccount",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
Set.of(serviceAccountCfg),
clientFactory,
cheClientFactory,
userManager,
@ -783,13 +811,12 @@ public class KubernetesNamespaceFactoryTest {
pool));
KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class);
prepareNamespace(toReturnNamespace);
when(toReturnNamespace.getWorkspaceId()).thenReturn("workspace123");
when(toReturnNamespace.getName()).thenReturn("workspace123");
doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any());
KubernetesWorkspaceServiceAccount serviceAccount =
mock(KubernetesWorkspaceServiceAccount.class);
doReturn(serviceAccount).when(namespaceFactory).doCreateServiceAccount(any(), any());
doReturn(serviceAccount).when(serviceAccountCfg).doCreateServiceAccount(any(), any());
// when
RuntimeIdentity identity =
@ -797,24 +824,25 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory.getOrCreate(identity);
// then
verify(namespaceFactory).doCreateServiceAccount("workspace123", "workspace123");
verify(serviceAccountCfg).doCreateServiceAccount("workspace123", "workspace123");
verify(serviceAccount).prepare();
}
@Test
public void shouldBindToAllConfiguredClusterRoles() throws Exception {
// given
var serviceAccountConfigurator =
new WorkspaceServiceAccountConfigurator("serviceAccount", "cr2, cr3", clientFactory);
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"serviceAccount",
"cr2, cr3",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
Set.of(serviceAccountConfigurator),
clientFactory,
cheClientFactory,
userManager,
@ -822,10 +850,10 @@ public class KubernetesNamespaceFactoryTest {
pool));
KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class);
prepareNamespace(toReturnNamespace);
when(toReturnNamespace.getWorkspaceId()).thenReturn("workspace123");
when(toReturnNamespace.getName()).thenReturn("workspace123");
doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any());
when(k8sClient.supportsApiPath(eq("/apis/metrics.k8s.io"))).thenReturn(true);
when(cheClientFactory.create()).thenReturn(k8sClient);
when(clientFactory.create(any())).thenReturn(k8sClient);
// pre-create the cluster roles
@ -848,7 +876,6 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory.getOrCreate(identity);
// then
verify(namespaceFactory).doCreateServiceAccount("workspace123", "workspace123");
ServiceAccountList sas = k8sClient.serviceAccounts().inNamespace("workspace123").list();
assertEquals(sas.getItems().size(), 1);
@ -881,19 +908,20 @@ public class KubernetesNamespaceFactoryTest {
}
@Test
public void shouldCreateExecAndViewRolesAndBindings() throws Exception {
public void shouldCreateAndBindCredentialsSecretRole() throws Exception {
// given
var serviceAccountConfigurator =
new WorkspaceServiceAccountConfigurator("serviceAccount", "cr2, cr3", clientFactory);
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"serviceAccount",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
Set.of(serviceAccountConfigurator),
clientFactory,
cheClientFactory,
userManager,
@ -901,11 +929,70 @@ public class KubernetesNamespaceFactoryTest {
pool));
KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class);
prepareNamespace(toReturnNamespace);
when(toReturnNamespace.getWorkspaceId()).thenReturn("workspace123");
when(toReturnNamespace.getName()).thenReturn("workspace123");
doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any());
when(k8sClient.supportsApiPath(eq("/apis/metrics.k8s.io"))).thenReturn(true);
when(clientFactory.create(any())).thenReturn(k8sClient);
when(cheClientFactory.create()).thenReturn(k8sClient);
// when
RuntimeIdentity identity =
new RuntimeIdentityImpl("workspace123", null, USER_ID, "workspace123");
namespaceFactory.getOrCreate(identity);
// then
Optional<Role> roleOptional =
k8sClient
.rbac()
.roles()
.inNamespace("workspace123")
.list()
.getItems()
.stream()
.filter(r -> r.getMetadata().getName().equals(SECRETS_ROLE_NAME))
.findAny();
assertTrue(roleOptional.isPresent());
PolicyRule rule = roleOptional.get().getRules().get(0);
assertEquals(rule.getResources(), singletonList("secrets"));
assertEquals(rule.getResourceNames(), singletonList(CREDENTIALS_SECRET_NAME));
assertEquals(rule.getApiGroups(), singletonList(""));
assertEquals(rule.getVerbs(), Arrays.asList("get", "patch"));
assertTrue(
k8sClient
.rbac()
.roleBindings()
.inNamespace("workspace123")
.list()
.getItems()
.stream()
.anyMatch(rb -> rb.getMetadata().getName().equals("serviceAccount-secrets")));
}
@Test
public void shouldCreateExecAndViewRolesAndBindings() throws Exception {
// given
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
Set.of(
new WorkspaceServiceAccountConfigurator("serviceAccount", "", clientFactory)),
clientFactory,
cheClientFactory,
userManager,
preferenceManager,
pool));
KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class);
prepareNamespace(toReturnNamespace);
when(toReturnNamespace.getName()).thenReturn("workspace123");
doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any());
when(k8sClient.supportsApiPath(eq("/apis/metrics.k8s.io"))).thenReturn(true);
when(clientFactory.create(any())).thenReturn(k8sClient);
when(cheClientFactory.create()).thenReturn(k8sClient);
// when
RuntimeIdentity identity =
@ -913,7 +1000,6 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory.getOrCreate(identity);
// then
verify(namespaceFactory).doCreateServiceAccount("workspace123", "workspace123");
ServiceAccountList sas = k8sClient.serviceAccounts().inNamespace("workspace123").list();
assertEquals(sas.getItems().size(), 1);
@ -951,62 +1037,19 @@ public class KubernetesNamespaceFactoryTest {
"serviceAccount-secrets"));
}
@Test
public void testNullClusterRolesResultsInEmptySet() {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
null,
"che-<userid>",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
clientFactory,
cheClientFactory,
userManager,
preferenceManager,
pool);
assertTrue(namespaceFactory.getClusterRoleNames().isEmpty());
}
@Test
public void testClusterRolesProperlyParsed() {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
" one,two, three ,,five ",
"che-<userid>",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
clientFactory,
cheClientFactory,
userManager,
preferenceManager,
pool);
Set<String> expected = Sets.newHashSet("one", "two", "three", "five");
assertTrue(namespaceFactory.getClusterRoleNames().containsAll(expected));
assertTrue(expected.containsAll(namespaceFactory.getClusterRoleNames()));
}
@Test
public void
testEvalNamespaceUsesNamespaceDefaultIfWorkspaceDoesntRecordNamespaceAndLegacyNamespaceDoesntExist()
throws Exception {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"che-<userid>",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -1025,14 +1068,13 @@ public class KubernetesNamespaceFactoryTest {
public void testEvalNamespaceUsesNamespaceFromUserPreferencesIfExist() throws Exception {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"che-<userid>",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -1056,14 +1098,13 @@ public class KubernetesNamespaceFactoryTest {
throws Exception {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"che-<userid>-<username>",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -1088,14 +1129,13 @@ public class KubernetesNamespaceFactoryTest {
throws Exception {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"che-<userid>-<username>",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -1120,14 +1160,13 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"",
"",
"che-<username>",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -1148,14 +1187,13 @@ public class KubernetesNamespaceFactoryTest {
throws Exception {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"che-<userid>",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -1178,14 +1216,13 @@ public class KubernetesNamespaceFactoryTest {
public void testEvalNamespaceTreatsWorkspaceRecordedNamespaceLiterally() throws Exception {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"che-<userid>",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -1229,14 +1266,13 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -1256,14 +1292,13 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
false,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -1296,14 +1331,13 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"",
"",
"<username>-cha-cha-cha",
false,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -1335,14 +1369,13 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"",
"",
"<username>-cha-cha-cha",
false,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -1386,14 +1419,13 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
"try_placeholder_here=<username>",
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -1412,14 +1444,13 @@ public class KubernetesNamespaceFactoryTest {
namespaceFactory =
spy(
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
"try_placeholder_here=<username>",
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -1444,14 +1475,13 @@ public class KubernetesNamespaceFactoryTest {
public void normalizeTest(String raw, String expected) {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -1464,14 +1494,13 @@ public class KubernetesNamespaceFactoryTest {
public void normalizeLengthTest() {
namespaceFactory =
new KubernetesNamespaceFactory(
"",
"",
"che-<userid>",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
emptySet(),
clientFactory,
cheClientFactory,
userManager,
@ -1551,14 +1580,15 @@ public class KubernetesNamespaceFactoryTest {
private void prepareNamespace(KubernetesNamespace namespace) throws InfrastructureException {
KubernetesSecrets secrets = mock(KubernetesSecrets.class);
lenient().when(namespace.secrets()).thenReturn(secrets);
KubernetesConfigsMaps configmaps = mock(KubernetesConfigsMaps.class);
when(namespace.secrets()).thenReturn(secrets);
when(namespace.configMaps()).thenReturn(configmaps);
lenient().when(namespace.secrets()).thenReturn(secrets);
lenient().when(namespace.configMaps()).thenReturn(configmaps);
Secret secretMock = mock(Secret.class);
ObjectMeta objectMeta = mock(ObjectMeta.class);
when(objectMeta.getName()).thenReturn(CREDENTIALS_SECRET_NAME);
when(secretMock.getMetadata()).thenReturn(objectMeta);
when(secrets.get()).thenReturn(Collections.singletonList(secretMock));
lenient().when(objectMeta.getName()).thenReturn(CREDENTIALS_SECRET_NAME);
lenient().when(secretMock.getMetadata()).thenReturn(objectMeta);
lenient().when(secrets.get()).thenReturn(Collections.singletonList(secretMock));
}
private Namespace createNamespace(String name, String phase) {
@ -1574,6 +1604,6 @@ public class KubernetesNamespaceFactoryTest {
private KubernetesNamespaceMeta testProvisioning(NamespaceResolutionContext context)
throws InfrastructureException {
return new NamespaceProvisioner(namespaceFactory, emptySet()).provision(context);
return new NamespaceProvisioner(namespaceFactory).provision(context);
}
}

View File

@ -84,6 +84,7 @@ public class KubernetesNamespaceTest {
@BeforeMethod
public void setUp() throws Exception {
lenient().when(clientFactory.create(anyString())).thenReturn(kubernetesClient);
lenient().when(cheClientFactory.create(anyString())).thenReturn(kubernetesClient);
lenient().doReturn(namespaceOperation).when(kubernetesClient).namespaces();

View File

@ -0,0 +1,104 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.CREDENTIALS_SECRET_NAME;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
import java.util.Map;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(MockitoTestNGListener.class)
public class CredentialsSecretConfiguratorTest {
private NamespaceConfigurator configurator;
@Mock private KubernetesClientFactory clientFactory;
private KubernetesServer serverMock;
private NamespaceResolutionContext namespaceResolutionContext;
private final String TEST_NAMESPACE_NAME = "namespace123";
private final String TEST_WORKSPACE_ID = "workspace123";
private final String TEST_USER_ID = "user123";
private final String TEST_USERNAME = "jondoe";
@BeforeMethod
public void setUp() throws InfrastructureException {
configurator = new CredentialsSecretConfigurator(clientFactory);
serverMock = new KubernetesServer(true, true);
serverMock.before();
KubernetesClient client = spy(serverMock.getClient());
when(clientFactory.create()).thenReturn(client);
namespaceResolutionContext =
new NamespaceResolutionContext(TEST_WORKSPACE_ID, TEST_USER_ID, TEST_USERNAME);
}
@Test
public void createCredentialsSecretWhenDoesNotExist()
throws InfrastructureException, InterruptedException {
// given - clean env
// when
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
// then create a secret
Assert.assertEquals(serverMock.getLastRequest().getMethod(), "POST");
Assert.assertNotNull(
serverMock
.getClient()
.secrets()
.inNamespace(TEST_NAMESPACE_NAME)
.withName(CREDENTIALS_SECRET_NAME)
.get());
}
@Test
public void doNothingWhenSecretAlreadyExists()
throws InfrastructureException, InterruptedException {
// given - secret already exists
serverMock
.getClient()
.secrets()
.inNamespace(TEST_NAMESPACE_NAME)
.create(
new SecretBuilder()
.withNewMetadata()
.withName(CREDENTIALS_SECRET_NAME)
.withAnnotations(Map.of("already", "created"))
.endMetadata()
.build());
// when
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
// then - don't create the secret
Assert.assertEquals(serverMock.getLastRequest().getMethod(), "GET");
var secrets =
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).list().getItems();
Assert.assertEquals(secrets.size(), 1);
Assert.assertEquals(secrets.get(0).getMetadata().getAnnotations().get("already"), "created");
}
}

View File

@ -0,0 +1,105 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.PREFERENCES_CONFIGMAP_NAME;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
import java.util.Map;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(MockitoTestNGListener.class)
public class PreferencesConfigMapConfiguratorTest {
private NamespaceConfigurator configurator;
@Mock private KubernetesClientFactory clientFactory;
private KubernetesServer serverMock;
private NamespaceResolutionContext namespaceResolutionContext;
private final String TEST_NAMESPACE_NAME = "namespace123";
private final String TEST_WORKSPACE_ID = "workspace123";
private final String TEST_USER_ID = "user123";
private final String TEST_USERNAME = "jondoe";
@BeforeMethod
public void setUp() throws InfrastructureException {
configurator = new PreferencesConfigMapConfigurator(clientFactory);
serverMock = new KubernetesServer(true, true);
serverMock.before();
KubernetesClient client = spy(serverMock.getClient());
when(clientFactory.create()).thenReturn(client);
namespaceResolutionContext =
new NamespaceResolutionContext(TEST_WORKSPACE_ID, TEST_USER_ID, TEST_USERNAME);
}
@Test
public void createConfigmapWhenDoesntExist()
throws InfrastructureException, InterruptedException {
// given - clean env
// when
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
// then configmap created
Assert.assertEquals(serverMock.getLastRequest().getMethod(), "POST");
Assert.assertNotNull(
serverMock
.getClient()
.configMaps()
.inNamespace(TEST_NAMESPACE_NAME)
.withName(PREFERENCES_CONFIGMAP_NAME)
.get());
verify(clientFactory, times(1)).create();
}
@Test
public void doNothingWhenConfigmapExists() throws InfrastructureException, InterruptedException {
// given - configmap already exists
serverMock
.getClient()
.configMaps()
.inNamespace(TEST_NAMESPACE_NAME)
.create(
new ConfigMapBuilder()
.withNewMetadata()
.withName(PREFERENCES_CONFIGMAP_NAME)
.withAnnotations(Map.of("already", "created"))
.endMetadata()
.build());
// when
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
// then - don't create the configmap
Assert.assertEquals(serverMock.getLastRequest().getMethod(), "GET");
var configmaps =
serverMock.getClient().configMaps().inNamespace(TEST_NAMESPACE_NAME).list().getItems();
Assert.assertEquals(configmaps.size(), 1);
Assert.assertEquals(configmaps.get(0).getMetadata().getAnnotations().get("already"), "created");
}
}

View File

@ -0,0 +1,162 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import io.fabric8.kubernetes.api.model.rbac.RoleBindingBuilder;
import io.fabric8.kubernetes.api.model.rbac.Subject;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(MockitoTestNGListener.class)
public class UserPermissionConfiguratorTest {
private NamespaceConfigurator configurator;
@Mock private CheServerKubernetesClientFactory clientFactory;
private KubernetesClient client;
private KubernetesServer serverMock;
private NamespaceResolutionContext namespaceResolutionContext;
private final String TEST_NAMESPACE_NAME = "namespace123";
private final String TEST_WORKSPACE_ID = "workspace123";
private final String TEST_USER_ID = "user123";
private final String TEST_USERNAME = "jondoe";
private final String TEST_CLUSTER_ROLES = "cr1,cr2";
@BeforeMethod
public void setUp() throws InfrastructureException {
configurator = new UserPermissionConfigurator(TEST_CLUSTER_ROLES, clientFactory);
serverMock = new KubernetesServer(true, true);
serverMock.before();
client = spy(serverMock.getClient());
lenient().when(clientFactory.create()).thenReturn(client);
namespaceResolutionContext =
new NamespaceResolutionContext(TEST_WORKSPACE_ID, TEST_USER_ID, TEST_USERNAME);
}
@Test
public void doNothingWhenNoClusterRolesSet()
throws InfrastructureException, InterruptedException {
// given - no cluster roles set
configurator = new UserPermissionConfigurator("", clientFactory);
// when
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
// then - do nothing
Assert.assertNull(serverMock.getLastRequest());
verify(clientFactory, never()).create();
}
@Test
public void bindAllClusterRolesWhenEmptyEnv()
throws InfrastructureException, InterruptedException {
// given - clean env
// when
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
// then - create all role bindings
var roleBindings =
serverMock.getClient().rbac().roleBindings().inNamespace(TEST_NAMESPACE_NAME);
Assert.assertEquals(roleBindings.list().getItems().size(), 2);
var cr1 = roleBindings.withName("cr1").get();
Assert.assertNotNull(cr1);
Assert.assertEquals(cr1.getSubjects().get(0).getName(), TEST_USERNAME);
Assert.assertEquals(cr1.getSubjects().get(0).getNamespace(), TEST_NAMESPACE_NAME);
Assert.assertEquals(cr1.getRoleRef().getName(), "cr1");
var cr2 = roleBindings.withName("cr2").get();
Assert.assertNotNull(cr2);
Assert.assertEquals(cr2.getSubjects().get(0).getName(), TEST_USERNAME);
Assert.assertEquals(cr2.getSubjects().get(0).getNamespace(), TEST_NAMESPACE_NAME);
Assert.assertEquals(cr2.getRoleRef().getName(), "cr2");
verify(client, times(2)).rbac();
}
@Test
public void replaceExistingBindingsWithSameName() throws InfrastructureException {
// given - cr1 binding already exists
client
.rbac()
.roleBindings()
.inNamespace(TEST_NAMESPACE_NAME)
.create(
new RoleBindingBuilder()
.withNewMetadata()
.withName("cr1")
.endMetadata()
.withSubjects(new Subject("blabol", "blabol", "blabol", "blabol"))
.withNewRoleRef()
.withName("blabol")
.endRoleRef()
.build());
// when
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
// then
var roleBindings = client.rbac().roleBindings().inNamespace(TEST_NAMESPACE_NAME);
Assert.assertEquals(roleBindings.list().getItems().size(), 2);
var cr1 = roleBindings.withName("cr1").get();
Assert.assertEquals(cr1.getRoleRef().getName(), "cr1");
Assert.assertEquals(cr1.getSubjects().size(), 1);
Assert.assertEquals(cr1.getSubjects().get(0).getName(), TEST_USERNAME);
Assert.assertEquals(cr1.getSubjects().get(0).getNamespace(), TEST_NAMESPACE_NAME);
}
@Test
public void keepOtherClusterRoles() throws InfrastructureException {
// given - some other binding in place
client
.rbac()
.roleBindings()
.inNamespace(TEST_NAMESPACE_NAME)
.create(
new RoleBindingBuilder()
.withNewMetadata()
.withName("othercr")
.endMetadata()
.withSubjects(new Subject("blabol", "blabol", "blabol", "blabol"))
.withNewRoleRef()
.withName("blabol")
.endRoleRef()
.build());
// when
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
// then
var roleBindings = client.rbac().roleBindings().inNamespace(TEST_NAMESPACE_NAME);
Assert.assertEquals(roleBindings.list().getItems().size(), 3);
}
}

View File

@ -86,7 +86,7 @@ public class UserPreferencesConfiguratorTest {
@Test
public void shouldCreatePreferencesSecret() throws InfrastructureException {
userPreferencesConfigurator.configure(context);
userPreferencesConfigurator.configure(context, USER_NAMESPACE);
List<Secret> secrets =
kubernetesServer.getClient().secrets().inNamespace(USER_NAMESPACE).list().getItems();
assertEquals(secrets.size(), 1);
@ -99,7 +99,7 @@ public class UserPreferencesConfiguratorTest {
"Preferences of user with id:" + USER_ID + " cannot be retrieved.")
public void shouldNotCreateSecretOnException() throws ServerException, InfrastructureException {
when(preferenceManager.find(USER_ID)).thenThrow(new ServerException("test exception"));
userPreferencesConfigurator.configure(context);
userPreferencesConfigurator.configure(context, USER_NAMESPACE);
fail("InfrastructureException should have been thrown.");
}

View File

@ -11,7 +11,6 @@
*/
package org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.fail;
@ -26,7 +25,6 @@ import org.eclipse.che.api.user.server.model.impl.UserImpl;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
@ -47,7 +45,6 @@ public class UserProfileConfiguratorTest {
private static final String USER_EMAIL = "user-email";
private static final String USER_NAMESPACE = "user-namespace";
@Mock private KubernetesNamespaceFactory namespaceFactory;
@Mock private KubernetesClientFactory clientFactory;
@Mock private UserManager userManager;
@ -63,7 +60,6 @@ public class UserProfileConfiguratorTest {
kubernetesServer.before();
when(userManager.getById(USER_ID)).thenReturn(new UserImpl(USER_ID, USER_EMAIL, USER_NAME));
when(namespaceFactory.evaluateNamespaceName(any())).thenReturn(USER_NAMESPACE);
when(clientFactory.create()).thenReturn(kubernetesServer.getClient());
}
@ -74,7 +70,7 @@ public class UserProfileConfiguratorTest {
@Test
public void shouldCreateProfileSecret() throws InfrastructureException {
userProfileConfigurator.configure(context);
userProfileConfigurator.configure(context, USER_NAMESPACE);
List<Secret> secrets =
kubernetesServer.getClient().secrets().inNamespace(USER_NAMESPACE).list().getItems();
assertEquals(secrets.size(), 1);
@ -87,7 +83,7 @@ public class UserProfileConfiguratorTest {
public void shouldNotCreateSecretOnException()
throws NotFoundException, ServerException, InfrastructureException {
when(userManager.getById(USER_ID)).thenThrow(new ServerException("test exception"));
userProfileConfigurator.configure(context);
userProfileConfigurator.configure(context, USER_NAMESPACE);
fail("InfrastructureException should have been thrown.");
}
}

View File

@ -0,0 +1,137 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator;
import static java.util.stream.Collectors.joining;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.spy;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesWorkspaceServiceAccount;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(MockitoTestNGListener.class)
public class WorkspaceServiceAccountConfiguratorTest {
private WorkspaceServiceAccountConfigurator configurator;
@Mock private CheServerKubernetesClientFactory clientFactory;
private KubernetesClient client;
private KubernetesServer serverMock;
private NamespaceResolutionContext namespaceResolutionContext;
@Mock private KubernetesWorkspaceServiceAccount kubeWSA;
private final String TEST_NAMESPACE_NAME = "namespace123";
private final String TEST_WORKSPACE_ID = "workspace123";
private final String TEST_USER_ID = "user123";
private final String TEST_USERNAME = "jondoe";
private final String TEST_SERVICE_ACCOUNT = "serviceAccount123";
private final String TEST_CLUSTER_ROLES = "cr1, cr2";
@BeforeMethod
public void setUp() throws InfrastructureException {
configurator =
spy(
new WorkspaceServiceAccountConfigurator(
TEST_SERVICE_ACCOUNT, TEST_CLUSTER_ROLES, clientFactory));
// when(configurator.doCreateServiceAccount(TEST_WORKSPACE_ID,
// TEST_NAMESPACE_NAME)).thenReturn(kubeWSA);
serverMock = new KubernetesServer(true, true);
serverMock.before();
client = spy(serverMock.getClient());
lenient().when(clientFactory.create(TEST_WORKSPACE_ID)).thenReturn(client);
namespaceResolutionContext =
new NamespaceResolutionContext(TEST_WORKSPACE_ID, TEST_USER_ID, TEST_USERNAME);
}
@Test
public void createWorkspaceServiceAccountWithBindings()
throws InfrastructureException, InterruptedException {
// given - cluster roles exists in cluster
configurator =
new WorkspaceServiceAccountConfigurator(
TEST_SERVICE_ACCOUNT, TEST_CLUSTER_ROLES, clientFactory);
client
.rbac()
.clusterRoles()
.create(new ClusterRoleBuilder().withNewMetadata().withName("cr1").endMetadata().build());
client
.rbac()
.clusterRoles()
.create(new ClusterRoleBuilder().withNewMetadata().withName("cr2").endMetadata().build());
// when
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
// then - create service account with all the bindings
var serviceAccount =
client
.serviceAccounts()
.inNamespace(TEST_NAMESPACE_NAME)
.withName(TEST_SERVICE_ACCOUNT)
.get();
assertNotNull(serviceAccount);
var roleBindings =
client.rbac().roleBindings().inNamespace(TEST_NAMESPACE_NAME).list().getItems();
assertEquals(
roleBindings.size(),
6,
roleBindings
.stream()
.map(r -> r.getMetadata().getName())
.collect(joining(", "))); // exec, secrets, configmaps, view bindings + cr1, cr2
}
@Test
public void dontCreateBindingsWhenClusterRolesDontExists() throws InfrastructureException {
// given - cluster roles exists in cluster
configurator =
new WorkspaceServiceAccountConfigurator(
TEST_SERVICE_ACCOUNT, TEST_CLUSTER_ROLES, clientFactory);
// when
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
// then - create service account with default bindings
var serviceAccount =
client
.serviceAccounts()
.inNamespace(TEST_NAMESPACE_NAME)
.withName(TEST_SERVICE_ACCOUNT)
.get();
assertNotNull(serviceAccount);
var roleBindings =
client.rbac().roleBindings().inNamespace(TEST_NAMESPACE_NAME).list().getItems();
assertEquals(
roleBindings.size(),
4,
roleBindings
.stream()
.map(r -> r.getMetadata().getName())
.collect(joining(", "))); // exec, secrets, configmaps, view bindings
}
}

View File

@ -134,6 +134,10 @@
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-keycloak-shared</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-oidc</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
@ -148,6 +152,22 @@
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>openshift-server-mock</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>junit-jupiter-api</artifactId>
<groupId>org.junit.jupiter</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-dto</artifactId>

View File

@ -57,8 +57,6 @@ public class OpenShiftClientFactory extends KubernetesClientFactory {
private static final String BEFORE_TOKEN = "access_token=";
private static final String AFTER_TOKEN = "&expires";
private final KubernetesClientConfigFactory configBuilder;
@Inject
public OpenShiftClientFactory(
KubernetesClientConfigFactory configBuilder,
@ -72,6 +70,7 @@ public class OpenShiftClientFactory extends KubernetesClientFactory {
int connectionPoolKeepAlive,
EventListener eventListener) {
super(
configBuilder,
masterUrl,
doTrustCerts,
maxConcurrentRequests,
@ -79,7 +78,6 @@ public class OpenShiftClientFactory extends KubernetesClientFactory {
maxIdleConnections,
connectionPoolKeepAlive,
eventListener);
this.configBuilder = configBuilder;
}
/**
@ -96,7 +94,7 @@ public class OpenShiftClientFactory extends KubernetesClientFactory {
*/
public OpenShiftClient createOC(String workspaceId) throws InfrastructureException {
Config configForWorkspace = buildConfig(getDefaultConfig(), workspaceId);
return createOC(configForWorkspace);
return create(configForWorkspace);
}
/**
@ -114,23 +112,13 @@ public class OpenShiftClientFactory extends KubernetesClientFactory {
* @throws InfrastructureException if any error occurs on client instance creation.
*/
public OpenShiftClient createOC() throws InfrastructureException {
return createOC(buildConfig(getDefaultConfig(), null));
return create(buildConfig(getDefaultConfig(), null));
}
public OpenShiftClient createAuthenticatedClient(String token) {
Config config = getDefaultConfig();
config.setOauthToken(token);
return createOC(config);
}
@Override
public OkHttpClient getAuthenticatedHttpClient() throws InfrastructureException {
if (!configBuilder.isPersonalized()) {
throw new InfrastructureException(
"Not able to construct impersonating openshift API client.");
}
// Ensure to get OkHttpClient with all necessary interceptors.
return createOC(buildConfig(getDefaultConfig(), null)).getHttpClient();
return create(config);
}
@Override
@ -147,19 +135,6 @@ public class OpenShiftClientFactory extends KubernetesClientFactory {
return configBuilder.build();
}
/**
* Builds the Openshift {@link Config} object based on a provided {@link Config} object and an
* optional workspace ID.
*
* <p>This method overrides the one in the Kubernetes infrastructure to introduce an additional
* extension level by delegating to an {@link KubernetesClientConfigFactory}
*/
@Override
protected Config buildConfig(Config config, @Nullable String workspaceId)
throws InfrastructureException {
return configBuilder.buildConfig(config, workspaceId);
}
@Override
protected Interceptor buildKubernetesInterceptor(Config config) {
final String oauthToken;
@ -223,7 +198,7 @@ public class OpenShiftClientFactory extends KubernetesClientFactory {
};
}
private DefaultOpenShiftClient createOC(Config config) {
protected DefaultOpenShiftClient create(Config config) {
return new UnclosableOpenShiftClient(
clientForConfig(config), config, this::initializeRequestTracing);
}

View File

@ -53,7 +53,9 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.devfile.KubernetesDev
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironmentFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.CredentialsSecretConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.PreferencesConfigMapConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.UserPreferencesConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.UserProfileConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.pvc.CommonPVCStrategy;
@ -94,6 +96,8 @@ import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftE
import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironmentFactory;
import org.eclipse.che.workspace.infrastructure.openshift.project.OpenShiftProjectFactory;
import org.eclipse.che.workspace.infrastructure.openshift.project.RemoveProjectOnWorkspaceRemove;
import org.eclipse.che.workspace.infrastructure.openshift.project.configurator.OpenShiftStopWorkspaceRoleConfigurator;
import org.eclipse.che.workspace.infrastructure.openshift.project.configurator.OpenShiftWorkspaceServiceAccountConfigurator;
import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftPreviewUrlCommandProvisioner;
import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenshiftTrustedCAProvisioner;
import org.eclipse.che.workspace.infrastructure.openshift.provision.RouteTlsProvisioner;
@ -117,6 +121,10 @@ public class OpenShiftInfraModule extends AbstractModule {
Multibinder.newSetBinder(binder(), NamespaceConfigurator.class);
namespaceConfigurators.addBinding().to(UserProfileConfigurator.class);
namespaceConfigurators.addBinding().to(UserPreferencesConfigurator.class);
namespaceConfigurators.addBinding().to(CredentialsSecretConfigurator.class);
namespaceConfigurators.addBinding().to(PreferencesConfigMapConfigurator.class);
namespaceConfigurators.addBinding().to(OpenShiftWorkspaceServiceAccountConfigurator.class);
namespaceConfigurators.addBinding().to(OpenShiftStopWorkspaceRoleConfigurator.class);
bind(KubernetesNamespaceService.class);

View File

@ -11,9 +11,9 @@
*/
package org.eclipse.che.workspace.infrastructure.openshift.multiuser.oauth;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.AUTH_SERVER_URL_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.CLIENT_ID_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING;
import static org.eclipse.che.multiuser.oidc.OIDCInfoProvider.AUTH_SERVER_URL_SETTING;
import com.google.inject.Provider;
import io.fabric8.kubernetes.client.Config;

View File

@ -16,15 +16,6 @@ import static com.google.common.base.MoreObjects.firstNonNull;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.openshift.client.OpenShiftClient;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -46,9 +37,6 @@ import org.slf4j.LoggerFactory;
/**
* This filter uses given token directly. It's used for native OpenShift user authentication.
* Requests without token or with invalid token are rejected.
*
* <p>{@link OpenshiftTokenInitializationFilter#UNAUTHORIZED_ENDPOINT_PATHS} is list of
* unauthenticated paths, that are allowed without token.
*/
@Singleton
public class OpenshiftTokenInitializationFilter
@ -57,9 +45,6 @@ public class OpenshiftTokenInitializationFilter
private static final Logger LOG =
LoggerFactory.getLogger(OpenshiftTokenInitializationFilter.class);
private static final List<String> UNAUTHORIZED_ENDPOINT_PATHS =
Collections.singletonList("/system/state");
private final PermissionChecker permissionChecker;
private final OpenShiftClientFactory clientFactory;
@ -121,38 +106,4 @@ public class OpenshiftTokenInitializationFilter
// we can use fake email, but probably we will need to find better solution.
return userMeta.getName() + "@che";
}
/**
* If request path is in {@link OpenshiftTokenInitializationFilter#UNAUTHORIZED_ENDPOINT_PATHS},
* the request is allowed. All other requests are rejected with error code 401.
*/
@Override
protected void handleMissingToken(
ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// if request path is in unauthorized endpoints, continue
if (request instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String path = httpRequest.getServletPath();
if (UNAUTHORIZED_ENDPOINT_PATHS.contains(path)) {
LOG.debug("Allowing request to '{}' without authorization header.", path);
chain.doFilter(request, response);
return;
}
}
LOG.error("Rejecting the request due to missing/expired token in Authorization header.");
sendError(response, 401, "Authorization token is missing or expired");
}
@Override
public void init(FilterConfig filterConfig) {
LOG.trace("OpenshiftTokenInitializationFilter#init({})", filterConfig);
}
@Override
public void destroy() {
LOG.trace("OpenshiftTokenInitializationFilter#destroy()");
}
}

View File

@ -50,7 +50,6 @@ public class OpenShiftProject extends KubernetesNamespace {
private final OpenShiftRoutes routes;
private final OpenShiftClientFactory clientFactory;
private final KubernetesClientFactory cheClientFactory;
private final CheServerOpenshiftClientFactory cheServerOpenshiftClientFactory;
@VisibleForTesting
@ -78,7 +77,6 @@ public class OpenShiftProject extends KubernetesNamespace {
ingresses,
secrets,
configMaps);
this.cheClientFactory = cheClientFactory;
this.clientFactory = clientFactory;
this.routes = routes;
this.cheServerOpenshiftClientFactory = cheServerOpenshiftClientFactory;
@ -93,7 +91,6 @@ public class OpenShiftProject extends KubernetesNamespace {
String workspaceId) {
super(clientFactory, cheClientFactory, executor, name, workspaceId);
this.clientFactory = clientFactory;
this.cheClientFactory = cheClientFactory;
this.routes = new OpenShiftRoutes(name, workspaceId, clientFactory);
this.cheServerOpenshiftClientFactory = cheServerOpenshiftClientFactory;
}

View File

@ -16,23 +16,18 @@ import static java.lang.String.format;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.PHASE_ATTRIBUTE;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.CREDENTIALS_SECRET_NAME;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.PREFERENCES_CONFIGMAP_NAME;
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.openshift.api.model.Project;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.inject.Named;
import org.eclipse.che.api.core.model.workspace.Workspace;
@ -47,11 +42,11 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesCl
import org.eclipse.che.workspace.infrastructure.kubernetes.api.server.impls.KubernetesNamespaceMetaImpl;
import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSharedPool;
import org.eclipse.che.workspace.infrastructure.openshift.CheServerOpenshiftClientFactory;
import org.eclipse.che.workspace.infrastructure.openshift.Constants;
import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory;
import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftStopWorkspaceRoleProvisioner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -67,14 +62,11 @@ public class OpenShiftProjectFactory extends KubernetesNamespaceFactory {
private final boolean initWithCheServerSa;
private final OpenShiftClientFactory clientFactory;
private final CheServerOpenshiftClientFactory cheOpenShiftClientFactory;
private final OpenShiftStopWorkspaceRoleProvisioner stopWorkspaceRoleProvisioner;
private final String oAuthIdentityProvider;
@Inject
public OpenShiftProjectFactory(
@Nullable @Named("che.infra.kubernetes.service_account_name") String serviceAccountName,
@Nullable @Named("che.infra.kubernetes.workspace_sa_cluster_roles") String clusterRoleNames,
@Nullable @Named("che.infra.kubernetes.namespace.default") String defaultNamespaceName,
@Named("che.infra.kubernetes.namespace.creation_allowed") boolean namespaceCreationAllowed,
@Named("che.infra.kubernetes.namespace.label") boolean labelProjects,
@ -82,24 +74,23 @@ public class OpenShiftProjectFactory extends KubernetesNamespaceFactory {
@Named("che.infra.kubernetes.namespace.labels") String projectLabels,
@Named("che.infra.kubernetes.namespace.annotations") String projectAnnotations,
@Named("che.infra.openshift.project.init_with_server_sa") boolean initWithCheServerSa,
Set<NamespaceConfigurator> namespaceConfigurators,
OpenShiftClientFactory clientFactory,
CheServerKubernetesClientFactory cheClientFactory,
CheServerOpenshiftClientFactory cheOpenShiftClientFactory,
OpenShiftStopWorkspaceRoleProvisioner stopWorkspaceRoleProvisioner,
UserManager userManager,
PreferenceManager preferenceManager,
KubernetesSharedPool sharedPool,
@Nullable @Named("che.infra.openshift.oauth_identity_provider")
String oAuthIdentityProvider) {
super(
serviceAccountName,
clusterRoleNames,
defaultNamespaceName,
namespaceCreationAllowed,
labelProjects,
annotateProjects,
projectLabels,
projectAnnotations,
namespaceConfigurators,
clientFactory,
cheClientFactory,
userManager,
@ -108,15 +99,16 @@ public class OpenShiftProjectFactory extends KubernetesNamespaceFactory {
this.initWithCheServerSa = initWithCheServerSa;
this.clientFactory = clientFactory;
this.cheOpenShiftClientFactory = cheOpenShiftClientFactory;
this.stopWorkspaceRoleProvisioner = stopWorkspaceRoleProvisioner;
this.oAuthIdentityProvider = oAuthIdentityProvider;
}
public OpenShiftProject getOrCreate(RuntimeIdentity identity) throws InfrastructureException {
OpenShiftProject osProject = get(identity);
var subject = EnvironmentContext.getCurrent().getSubject();
NamespaceResolutionContext resolutionCtx =
new NamespaceResolutionContext(EnvironmentContext.getCurrent().getSubject());
new NamespaceResolutionContext(
identity.getWorkspaceId(), subject.getUserId(), subject.getUserName());
Map<String, String> namespaceAnnotationsEvaluated =
evaluateAnnotationPlaceholders(resolutionCtx);
@ -126,50 +118,8 @@ public class OpenShiftProjectFactory extends KubernetesNamespaceFactory {
labelNamespaces ? namespaceLabels : emptyMap(),
annotateNamespaces ? namespaceAnnotationsEvaluated : emptyMap());
// create credentials secret
if (osProject
.secrets()
.get()
.stream()
.noneMatch(s -> s.getMetadata().getName().equals(CREDENTIALS_SECRET_NAME))) {
Secret secret =
new SecretBuilder()
.withType("opaque")
.withNewMetadata()
.withName(CREDENTIALS_SECRET_NAME)
.endMetadata()
.build();
clientFactory
.createOC()
.secrets()
.inNamespace(identity.getInfrastructureNamespace())
.create(secret);
}
configureNamespace(resolutionCtx, osProject.getName());
// create preferences configmap
if (osProject.configMaps().get(PREFERENCES_CONFIGMAP_NAME).isEmpty()) {
ConfigMap configMap =
new ConfigMapBuilder()
.withNewMetadata()
.withName(PREFERENCES_CONFIGMAP_NAME)
.endMetadata()
.build();
clientFactory
.createOC()
.configMaps()
.inNamespace(identity.getInfrastructureNamespace())
.create(configMap);
}
if (!isNullOrEmpty(getServiceAccountName())) {
OpenShiftWorkspaceServiceAccount osWorkspaceServiceAccount =
doCreateServiceAccount(osProject.getWorkspaceId(), osProject.getName());
osWorkspaceServiceAccount.prepare();
}
if (!isNullOrEmpty(oAuthIdentityProvider)) {
stopWorkspaceRoleProvisioner.provision(osProject.getName());
}
return osProject;
}
@ -190,11 +140,6 @@ public class OpenShiftProjectFactory extends KubernetesNamespaceFactory {
}
}
@Override
protected boolean checkNamespaceExists(String namespaceName) throws InfrastructureException {
return fetchNamespaceObject(namespaceName).isPresent();
}
/**
* Creates a kubernetes namespace for the specified workspace.
*
@ -218,12 +163,6 @@ public class OpenShiftProjectFactory extends KubernetesNamespaceFactory {
workspaceId);
}
@VisibleForTesting
OpenShiftWorkspaceServiceAccount doCreateServiceAccount(String workspaceId, String projectName) {
return new OpenShiftWorkspaceServiceAccount(
workspaceId, projectName, getServiceAccountName(), getClusterRoleNames(), clientFactory);
}
@Override
public Optional<KubernetesNamespaceMeta> fetchNamespace(String name)
throws InfrastructureException {

View File

@ -33,10 +33,10 @@ import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory
* @see
* org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesWorkspaceServiceAccount
*/
class OpenShiftWorkspaceServiceAccount
public class OpenShiftWorkspaceServiceAccount
extends AbstractWorkspaceServiceAccount<OpenShiftClient, Role, RoleBinding> {
OpenShiftWorkspaceServiceAccount(
public OpenShiftWorkspaceServiceAccount(
String workspaceId,
String projectName,
String serviceAccountName,

View File

@ -9,7 +9,9 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.openshift.provision;
package org.eclipse.che.workspace.infrastructure.openshift.project.configurator;
import static com.google.common.base.Strings.isNullOrEmpty;
import io.fabric8.kubernetes.api.model.ObjectReferenceBuilder;
import io.fabric8.openshift.api.model.PolicyRuleBuilder;
@ -20,8 +22,12 @@ import io.fabric8.openshift.api.model.RoleBuilder;
import io.fabric8.openshift.client.OpenShiftClient;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.CheInstallationLocation;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -32,27 +38,37 @@ import org.slf4j.LoggerFactory;
*
* @author Tom George
*/
public class OpenShiftStopWorkspaceRoleProvisioner {
@Singleton
public class OpenShiftStopWorkspaceRoleConfigurator implements NamespaceConfigurator {
private final OpenShiftClientFactory clientFactory;
private final String installationLocation;
private final boolean stopWorkspaceRoleEnabled;
private final String oAuthIdentityProvider;
private static final Logger LOG =
LoggerFactory.getLogger(OpenShiftStopWorkspaceRoleProvisioner.class);
LoggerFactory.getLogger(OpenShiftStopWorkspaceRoleConfigurator.class);
@Inject
public OpenShiftStopWorkspaceRoleProvisioner(
public OpenShiftStopWorkspaceRoleConfigurator(
OpenShiftClientFactory clientFactory,
CheInstallationLocation installationLocation,
@Named("che.workspace.stop.role.enabled") boolean stopWorkspaceRoleEnabled)
@Named("che.workspace.stop.role.enabled") boolean stopWorkspaceRoleEnabled,
@Nullable @Named("che.infra.openshift.oauth_identity_provider") String oAuthIdentityProvider)
throws InfrastructureException {
this.clientFactory = clientFactory;
this.installationLocation = installationLocation.getInstallationLocationNamespace();
this.stopWorkspaceRoleEnabled = stopWorkspaceRoleEnabled;
this.oAuthIdentityProvider = oAuthIdentityProvider;
}
public void provision(String projectName) throws InfrastructureException {
@Override
public void configure(NamespaceResolutionContext namespaceResolutionContext, String projectName)
throws InfrastructureException {
if (isNullOrEmpty(oAuthIdentityProvider)) {
return;
}
if (stopWorkspaceRoleEnabled && installationLocation != null) {
OpenShiftClient osClient = clientFactory.createOC();
String stopWorkspacesRoleName = "workspace-stop";

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.openshift.project.configurator;
import static com.google.common.base.Strings.isNullOrEmpty;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.collect.Sets;
import java.util.Collections;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory;
import org.eclipse.che.workspace.infrastructure.openshift.project.OpenShiftWorkspaceServiceAccount;
/**
* This {@link NamespaceConfigurator} ensures that workspace ServiceAccount with proper ClusterRole
* is set in Workspace project.
*/
@Singleton
public class OpenShiftWorkspaceServiceAccountConfigurator implements NamespaceConfigurator {
private final OpenShiftClientFactory clientFactory;
private final String serviceAccountName;
private final Set<String> clusterRoleNames;
@Inject
public OpenShiftWorkspaceServiceAccountConfigurator(
@Nullable @Named("che.infra.kubernetes.service_account_name") String serviceAccountName,
@Nullable @Named("che.infra.kubernetes.workspace_sa_cluster_roles") String clusterRoleNames,
OpenShiftClientFactory clientFactory) {
this.clientFactory = clientFactory;
this.serviceAccountName = serviceAccountName;
if (!isNullOrEmpty(clusterRoleNames)) {
this.clusterRoleNames =
Sets.newHashSet(
Splitter.on(",").trimResults().omitEmptyStrings().split(clusterRoleNames));
} else {
this.clusterRoleNames = Collections.emptySet();
}
}
@Override
public void configure(NamespaceResolutionContext namespaceResolutionContext, String namespaceName)
throws InfrastructureException {
if (!isNullOrEmpty(serviceAccountName)) {
OpenShiftWorkspaceServiceAccount osWorkspaceServiceAccount =
createServiceAccount(namespaceResolutionContext.getWorkspaceId(), namespaceName);
osWorkspaceServiceAccount.prepare();
}
}
@VisibleForTesting
public OpenShiftWorkspaceServiceAccount createServiceAccount(String wsId, String namespaceName) {
return new OpenShiftWorkspaceServiceAccount(
wsId, namespaceName, serviceAccountName, clusterRoleNames, clientFactory);
}
}

View File

@ -11,9 +11,9 @@
*/
package org.eclipse.che.workspace.infrastructure.openshift.multiuser.oauth;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.AUTH_SERVER_URL_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.CLIENT_ID_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING;
import static org.eclipse.che.multiuser.oidc.OIDCInfoProvider.AUTH_SERVER_URL_SETTING;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;

View File

@ -11,8 +11,6 @@
*/
package org.eclipse.che.workspace.infrastructure.openshift.multiuser.oauth;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.*;
@ -21,11 +19,6 @@ import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.openshift.api.model.User;
import io.fabric8.openshift.client.OpenShiftClient;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.ServerException;
@ -55,10 +48,6 @@ public class OpenshiftTokenInitializationFilterTest {
@Mock private User openshiftUser;
@Mock private ObjectMeta openshiftUserMeta;
@Mock private HttpServletRequest servletRequest;
@Mock private HttpServletResponse servletResponse;
@Mock private FilterChain filterChain;
private static final String TOKEN = "touken";
private static final String USER_UID = "almost-certainly-unique-id";
private static final String USERNAME = "test_username";
@ -111,7 +100,7 @@ public class OpenshiftTokenInitializationFilterTest {
@Test
public void extractSubjectCreatesSubjectWithCurrentlyAuthenticatedUser()
throws InfrastructureException, ServerException, ConflictException {
throws ServerException, ConflictException {
when(openShiftClientFactory.createAuthenticatedClient(TOKEN)).thenReturn(openShiftClient);
when(openShiftClient.currentUser()).thenReturn(openshiftUser);
when(openshiftUser.getMetadata()).thenReturn(openshiftUserMeta);
@ -128,27 +117,6 @@ public class OpenshiftTokenInitializationFilterTest {
assertEquals(subject.getUserName(), USERNAME);
}
@Test
public void handleMissingTokenShouldAllowUnauthorizedEndpoint()
throws ServletException, IOException {
when(servletRequest.getServletPath()).thenReturn("/system/state");
openshiftTokenInitializationFilter.handleMissingToken(
servletRequest, servletResponse, filterChain);
verify(filterChain).doFilter(servletRequest, servletResponse);
}
@Test
public void handleMissingTokenShouldRejectRequest() throws ServletException, IOException {
when(servletRequest.getServletPath()).thenReturn("blabol");
openshiftTokenInitializationFilter.handleMissingToken(
servletRequest, servletResponse, filterChain);
verify(servletResponse).sendError(eq(401), anyString());
}
@Test
public void invalidTokenShouldBeHandledAsMissing() throws Exception {
when(openShiftClientFactory.createAuthenticatedClient(TOKEN)).thenReturn(openShiftClient);

View File

@ -13,8 +13,8 @@ package org.eclipse.che.workspace.infrastructure.openshift.project;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.emptySet;
import static java.util.Collections.singletonList;
import static java.util.Optional.empty;
import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.DEFAULT_ATTRIBUTE;
import static org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta.PHASE_ATTRIBUTE;
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.AbstractWorkspaceServiceAccount.CREDENTIALS_SECRET_NAME;
@ -32,7 +32,6 @@ import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
@ -58,7 +57,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.eclipse.che.api.core.ValidationException;
import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity;
import org.eclipse.che.api.user.server.PreferenceManager;
@ -76,12 +75,16 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesCl
import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesConfigsMaps;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesSecrets;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.CredentialsSecretConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.PreferencesConfigMapConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSharedPool;
import org.eclipse.che.workspace.infrastructure.openshift.CheServerOpenshiftClientFactory;
import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory;
import org.eclipse.che.workspace.infrastructure.openshift.provision.OpenShiftStopWorkspaceRoleProvisioner;
import org.eclipse.che.workspace.infrastructure.openshift.project.configurator.OpenShiftWorkspaceServiceAccountConfigurator;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
@ -110,7 +113,6 @@ public class OpenShiftProjectFactoryTest {
@Mock private OpenShiftClientFactory clientFactory;
@Mock private CheServerKubernetesClientFactory cheClientFactory;
@Mock private CheServerOpenshiftClientFactory cheServerOpenshiftClientFactory;
@Mock private OpenShiftStopWorkspaceRoleProvisioner stopWorkspaceRoleProvisioner;
@Mock private WorkspaceManager workspaceManager;
@Mock private UserManager userManager;
@Mock private PreferenceManager preferenceManager;
@ -131,6 +133,7 @@ public class OpenShiftProjectFactoryTest {
@BeforeMethod
public void setUp() throws Exception {
lenient().when(clientFactory.createOC()).thenReturn(osClient);
lenient().when(clientFactory.create()).thenReturn(osClient);
lenient().when(osClient.projects()).thenReturn(projectOperation);
lenient()
@ -162,8 +165,6 @@ public class OpenShiftProjectFactoryTest {
projectFactory =
new OpenShiftProjectFactory(
"",
null,
"<username>-che",
true,
true,
@ -171,10 +172,10 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
emptySet(),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
@ -195,8 +196,6 @@ public class OpenShiftProjectFactoryTest {
System.out.println("2--------");
projectFactory =
new OpenShiftProjectFactory(
"",
null,
"<username>-che",
true,
true,
@ -204,10 +203,10 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
emptySet(),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
@ -228,8 +227,6 @@ public class OpenShiftProjectFactoryTest {
throws Exception {
projectFactory =
new OpenShiftProjectFactory(
"",
null,
null,
true,
true,
@ -237,10 +234,10 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
emptySet(),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
@ -266,8 +263,6 @@ public class OpenShiftProjectFactoryTest {
projectFactory =
new OpenShiftProjectFactory(
"",
"",
"<userid>-che",
true,
true,
@ -275,10 +270,10 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
emptySet(),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
@ -305,8 +300,6 @@ public class OpenShiftProjectFactoryTest {
projectFactory =
new OpenShiftProjectFactory(
"",
"",
"<userid>-che",
true,
true,
@ -314,10 +307,10 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
emptySet(),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
@ -340,8 +333,6 @@ public class OpenShiftProjectFactoryTest {
projectFactory =
new OpenShiftProjectFactory(
"",
"",
"<userid>-che",
true,
true,
@ -349,10 +340,10 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
emptySet(),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
@ -385,8 +376,6 @@ public class OpenShiftProjectFactoryTest {
projectFactory =
new OpenShiftProjectFactory(
"",
null,
"<username>-che",
true,
true,
@ -394,10 +383,10 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
emptySet(),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
@ -424,8 +413,6 @@ public class OpenShiftProjectFactoryTest {
projectFactory =
new OpenShiftProjectFactory(
"",
null,
"<username>-che",
true,
true,
@ -433,10 +420,10 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
emptySet(),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
@ -463,8 +450,6 @@ public class OpenShiftProjectFactoryTest {
projectFactory =
new OpenShiftProjectFactory(
"",
null,
"<username>-che",
true,
true,
@ -472,10 +457,10 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
emptySet(),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
@ -492,8 +477,6 @@ public class OpenShiftProjectFactoryTest {
throwOnTryToGetProjectsList(new KubernetesClientException("connection refused"));
projectFactory =
new OpenShiftProjectFactory(
"",
null,
"<username>-che",
true,
true,
@ -501,10 +484,10 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
emptySet(),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
@ -526,8 +509,6 @@ public class OpenShiftProjectFactoryTest {
projectFactory =
spy(
new OpenShiftProjectFactory(
"",
null,
"<userid>-che",
true,
true,
@ -535,10 +516,10 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
emptySet(),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
@ -554,7 +535,6 @@ public class OpenShiftProjectFactoryTest {
// then
assertEquals(toReturnProject, project);
verify(projectFactory, never()).doCreateServiceAccount(any(), any());
verify(toReturnProject).prepare(eq(false), eq(false), any(), any());
}
@ -564,8 +544,6 @@ public class OpenShiftProjectFactoryTest {
projectFactory =
spy(
new OpenShiftProjectFactory(
"",
null,
"<userid>-che",
true,
true,
@ -573,30 +551,28 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
Set.of(new CredentialsSecretConfigurator(clientFactory)),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
NO_OAUTH_IDENTITY_PROVIDER));
OpenShiftProject toReturnProject = mock(OpenShiftProject.class);
doReturn(toReturnProject).when(projectFactory).doCreateProjectAccess(any(), any());
when(toReturnProject.getName()).thenReturn("namespace123");
NonNamespaceOperation namespaceOperation = mock(NonNamespaceOperation.class);
MixedOperation mixedOperation = mock(MixedOperation.class);
KubernetesSecrets secrets = mock(KubernetesSecrets.class);
KubernetesConfigsMaps configsMaps = mock(KubernetesConfigsMaps.class);
when(toReturnProject.secrets()).thenReturn(secrets);
when(toReturnProject.configMaps()).thenReturn(configsMaps);
when(secrets.get()).thenReturn(Collections.emptyList());
when(configsMaps.get(anyString())).thenReturn(Optional.of(mock(ConfigMap.class)));
lenient().when(osClient.secrets()).thenReturn(mixedOperation);
lenient().when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation);
when(osClient.secrets()).thenReturn(mixedOperation);
when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation);
Resource<Secret> nullSecret = mock(Resource.class);
when(namespaceOperation.withName(CREDENTIALS_SECRET_NAME)).thenReturn(nullSecret);
when(nullSecret.get()).thenReturn(null);
// when
RuntimeIdentity identity =
new RuntimeIdentityImpl("workspace123", null, USER_ID, "workspace123");
new RuntimeIdentityImpl("workspace123", null, USER_ID, "namespace123");
projectFactory.getOrCreate(identity);
// then
@ -613,8 +589,6 @@ public class OpenShiftProjectFactoryTest {
projectFactory =
spy(
new OpenShiftProjectFactory(
"",
null,
"<userid>-che",
true,
true,
@ -622,31 +596,23 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
Set.of(new PreferencesConfigMapConfigurator(clientFactory)),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
NO_OAUTH_IDENTITY_PROVIDER));
OpenShiftProject toReturnProject = mock(OpenShiftProject.class);
doReturn(toReturnProject).when(projectFactory).doCreateProjectAccess(any(), any());
when(toReturnProject.getName()).thenReturn("namespace123");
NonNamespaceOperation namespaceOperation = mock(NonNamespaceOperation.class);
MixedOperation mixedOperation = mock(MixedOperation.class);
KubernetesSecrets secrets = mock(KubernetesSecrets.class);
Secret secret = mock(Secret.class);
ObjectMeta objectMeta = mock(ObjectMeta.class);
when(secret.getMetadata()).thenReturn(objectMeta);
when(objectMeta.getName()).thenReturn(CREDENTIALS_SECRET_NAME);
when(toReturnProject.secrets()).thenReturn(secrets);
when(secrets.get()).thenReturn(singletonList(secret));
lenient().when(osClient.secrets()).thenReturn(mixedOperation);
KubernetesConfigsMaps configsMaps = mock(KubernetesConfigsMaps.class);
when(toReturnProject.configMaps()).thenReturn(configsMaps);
when(configsMaps.get(eq(PREFERENCES_CONFIGMAP_NAME))).thenReturn(empty());
lenient().when(osClient.configMaps()).thenReturn(mixedOperation);
lenient().when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation);
when(osClient.configMaps()).thenReturn(mixedOperation);
when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation);
Resource<ConfigMap> nullCm = mock(Resource.class);
when(namespaceOperation.withName(PREFERENCES_CONFIGMAP_NAME)).thenReturn(nullCm);
// when
RuntimeIdentity identity =
@ -666,8 +632,6 @@ public class OpenShiftProjectFactoryTest {
projectFactory =
spy(
new OpenShiftProjectFactory(
"",
null,
"<userid>-che",
true,
true,
@ -675,10 +639,10 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
Set.of(new CredentialsSecretConfigurator(clientFactory)),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
@ -686,10 +650,14 @@ public class OpenShiftProjectFactoryTest {
OpenShiftProject toReturnProject = mock(OpenShiftProject.class);
prepareProject(toReturnProject);
doReturn(toReturnProject).when(projectFactory).doCreateProjectAccess(any(), any());
when(toReturnProject.getName()).thenReturn("namespace123");
NonNamespaceOperation namespaceOperation = mock(NonNamespaceOperation.class);
MixedOperation mixedOperation = mock(MixedOperation.class);
lenient().when(osClient.secrets()).thenReturn(mixedOperation);
lenient().when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation);
when(osClient.secrets()).thenReturn(mixedOperation);
when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation);
Resource<Secret> secretResource = mock(Resource.class);
when(namespaceOperation.withName(CREDENTIALS_SECRET_NAME)).thenReturn(secretResource);
when(secretResource.get()).thenReturn(mock(Secret.class));
// when
RuntimeIdentity identity =
@ -706,8 +674,6 @@ public class OpenShiftProjectFactoryTest {
projectFactory =
spy(
new OpenShiftProjectFactory(
"",
null,
"<userid>-che",
true,
true,
@ -715,10 +681,10 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
Set.of(new PreferencesConfigMapConfigurator(clientFactory)),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
@ -726,10 +692,14 @@ public class OpenShiftProjectFactoryTest {
OpenShiftProject toReturnProject = mock(OpenShiftProject.class);
prepareProject(toReturnProject);
doReturn(toReturnProject).when(projectFactory).doCreateProjectAccess(any(), any());
when(toReturnProject.getName()).thenReturn("namespace123");
NonNamespaceOperation namespaceOperation = mock(NonNamespaceOperation.class);
MixedOperation mixedOperation = mock(MixedOperation.class);
lenient().when(osClient.configMaps()).thenReturn(mixedOperation);
lenient().when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation);
when(osClient.configMaps()).thenReturn(mixedOperation);
when(mixedOperation.inNamespace(anyString())).thenReturn(namespaceOperation);
Resource<ConfigMap> cmResource = mock(Resource.class);
when(namespaceOperation.withName(PREFERENCES_CONFIGMAP_NAME)).thenReturn(cmResource);
when(cmResource.get()).thenReturn(mock(ConfigMap.class));
// when
RuntimeIdentity identity =
@ -740,56 +710,13 @@ public class OpenShiftProjectFactoryTest {
verify(namespaceOperation, never()).create(any());
}
@Test
public void shouldPrepareWorkspaceServiceAccountIfItIsConfiguredAndProjectIsNotPredefined()
throws Exception {
// given
projectFactory =
spy(
new OpenShiftProjectFactory(
"serviceAccount",
null,
"<userid>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
NO_OAUTH_IDENTITY_PROVIDER));
OpenShiftProject toReturnProject = mock(OpenShiftProject.class);
prepareProject(toReturnProject);
when(toReturnProject.getWorkspaceId()).thenReturn("workspace123");
when(toReturnProject.getName()).thenReturn("workspace123");
doReturn(toReturnProject).when(projectFactory).doCreateProjectAccess(any(), any());
OpenShiftWorkspaceServiceAccount serviceAccount = mock(OpenShiftWorkspaceServiceAccount.class);
doReturn(serviceAccount).when(projectFactory).doCreateServiceAccount(any(), any());
// when
RuntimeIdentity identity =
new RuntimeIdentityImpl("workspace123", null, USER_ID, "workspace123");
projectFactory.getOrCreate(identity);
// then
verify(projectFactory).doCreateServiceAccount("workspace123", "workspace123");
verify(serviceAccount).prepare();
}
@Test
public void shouldCallStopWorkspaceRoleProvisionWhenIdentityProviderIsDefined() throws Exception {
var saConf =
spy(new OpenShiftWorkspaceServiceAccountConfigurator("serviceAccount", "", clientFactory));
projectFactory =
spy(
new OpenShiftProjectFactory(
"serviceAccount",
null,
"<userid>-che",
true,
true,
@ -797,22 +724,21 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
Set.of(saConf),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
OAUTH_IDENTITY_PROVIDER));
OpenShiftProject toReturnProject = mock(OpenShiftProject.class);
when(toReturnProject.getWorkspaceId()).thenReturn("workspace123");
when(toReturnProject.getName()).thenReturn("workspace123");
prepareProject(toReturnProject);
doReturn(toReturnProject).when(projectFactory).doCreateProjectAccess(any(), any());
OpenShiftWorkspaceServiceAccount serviceAccount = mock(OpenShiftWorkspaceServiceAccount.class);
doReturn(serviceAccount).when(projectFactory).doCreateServiceAccount(any(), any());
doReturn(serviceAccount).when(saConf).createServiceAccount("workspace123", "workspace123");
// when
RuntimeIdentity identity =
@ -820,52 +746,7 @@ public class OpenShiftProjectFactoryTest {
projectFactory.getOrCreate(identity);
// then
verify(projectFactory).doCreateServiceAccount("workspace123", "workspace123");
verify(serviceAccount).prepare();
verify(stopWorkspaceRoleProvisioner, times(1)).provision("workspace123");
}
@Test
public void shouldNotCallStopWorkspaceRoleProvisionWhenIdentityProviderIsDefined()
throws Exception {
projectFactory =
spy(
new OpenShiftProjectFactory(
"serviceAccount",
null,
"<userid>-che",
true,
true,
true,
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
NO_OAUTH_IDENTITY_PROVIDER));
OpenShiftProject toReturnProject = mock(OpenShiftProject.class);
prepareProject(toReturnProject);
when(toReturnProject.getWorkspaceId()).thenReturn("workspace123");
when(toReturnProject.getName()).thenReturn("workspace123");
doReturn(toReturnProject).when(projectFactory).doCreateProjectAccess(any(), any());
OpenShiftWorkspaceServiceAccount serviceAccount = mock(OpenShiftWorkspaceServiceAccount.class);
doReturn(serviceAccount).when(projectFactory).doCreateServiceAccount(any(), any());
// when
RuntimeIdentity identity =
new RuntimeIdentityImpl("workspace123", null, USER_ID, "workspace123");
projectFactory.getOrCreate(identity);
// then
verify(projectFactory).doCreateServiceAccount("workspace123", "workspace123");
verify(serviceAccount).prepare();
verify(stopWorkspaceRoleProvisioner, times(0)).provision("workspace123");
}
@Test
@ -886,8 +767,6 @@ public class OpenShiftProjectFactoryTest {
projectFactory =
new OpenShiftProjectFactory(
"",
"",
"<userid>-che",
true,
true,
@ -895,10 +774,10 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
NAMESPACE_ANNOTATIONS,
true,
emptySet(),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
@ -921,8 +800,6 @@ public class OpenShiftProjectFactoryTest {
projectFactory =
new OpenShiftProjectFactory(
"",
null,
"<userid>-che",
true,
true,
@ -930,10 +807,10 @@ public class OpenShiftProjectFactoryTest {
"try_placeholder_here=<username>",
NAMESPACE_ANNOTATIONS,
true,
emptySet(),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
@ -946,13 +823,10 @@ public class OpenShiftProjectFactoryTest {
@Test
public void testUsernamePlaceholderInAnnotationsIsEvaluated() throws InfrastructureException {
// given
projectFactory =
spy(
new OpenShiftProjectFactory(
"",
null,
"<userid>-che",
true,
true,
@ -960,10 +834,10 @@ public class OpenShiftProjectFactoryTest {
NAMESPACE_LABELS,
"try_placeholder_here=<username>",
true,
emptySet(),
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
stopWorkspaceRoleProvisioner,
userManager,
preferenceManager,
pool,
@ -983,6 +857,51 @@ public class OpenShiftProjectFactoryTest {
.prepare(eq(false), eq(false), any(), eq(Map.of("try_placeholder_here", "jondoe")));
}
@Test
public void testAllConfiguratorsAreCalledWhenCreatingProject() throws InfrastructureException {
// given
String projectName = "testprojectname";
NamespaceConfigurator configurator1 = Mockito.mock(NamespaceConfigurator.class);
NamespaceConfigurator configurator2 = Mockito.mock(NamespaceConfigurator.class);
Set<NamespaceConfigurator> namespaceConfigurators = Set.of(configurator1, configurator2);
projectFactory =
spy(
new OpenShiftProjectFactory(
"<username>-che",
true,
true,
true,
NAMESPACE_LABELS,
"try_placeholder_here=<username>",
true,
namespaceConfigurators,
clientFactory,
cheClientFactory,
cheServerOpenshiftClientFactory,
userManager,
preferenceManager,
pool,
NO_OAUTH_IDENTITY_PROVIDER));
EnvironmentContext.getCurrent().setSubject(new SubjectImpl("jondoe", "123", null, false));
OpenShiftProject toReturnProject = mock(OpenShiftProject.class);
when(toReturnProject.getName()).thenReturn(projectName);
RuntimeIdentity identity = new RuntimeIdentityImpl("workspace123", null, USER_ID, "old-che");
doReturn(toReturnProject).when(projectFactory).get(identity);
// when
OpenShiftProject project = projectFactory.getOrCreate(identity);
// then
NamespaceResolutionContext resolutionCtx =
new NamespaceResolutionContext("workspace123", "123", "jondoe");
verify(configurator1).configure(resolutionCtx, projectName);
verify(configurator2).configure(resolutionCtx, projectName);
assertEquals(project, toReturnProject);
}
private void prepareNamespaceToBeFoundByName(String name, Project project) throws Exception {
@SuppressWarnings("unchecked")
Resource<Project> getProjectByNameOperation = mock(Resource.class);
@ -1010,15 +929,13 @@ public class OpenShiftProjectFactoryTest {
private void prepareProject(OpenShiftProject project) throws InfrastructureException {
KubernetesSecrets secrets = mock(KubernetesSecrets.class);
lenient().when(project.secrets()).thenReturn(secrets);
KubernetesConfigsMaps configsMaps = mock(KubernetesConfigsMaps.class);
when(project.secrets()).thenReturn(secrets);
when(project.configMaps()).thenReturn(configsMaps);
when(configsMaps.get(anyString())).thenReturn(Optional.of(mock(ConfigMap.class)));
Secret secretMock = mock(Secret.class);
ObjectMeta objectMeta = mock(ObjectMeta.class);
when(objectMeta.getName()).thenReturn(CREDENTIALS_SECRET_NAME);
when(secretMock.getMetadata()).thenReturn(objectMeta);
when(secrets.get()).thenReturn(singletonList(secretMock));
lenient().when(objectMeta.getName()).thenReturn(CREDENTIALS_SECRET_NAME);
lenient().when(secretMock.getMetadata()).thenReturn(objectMeta);
lenient().when(secrets.get()).thenReturn(Collections.singletonList(secretMock));
}
private void throwOnTryToGetProjectsList(Throwable e) throws Exception {

View File

@ -9,7 +9,7 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.openshift.provision;
package org.eclipse.che.workspace.infrastructure.openshift.project.configurator;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyString;
@ -17,6 +17,7 @@ import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import io.fabric8.kubernetes.api.model.ObjectReferenceBuilder;
@ -42,15 +43,16 @@ import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
/**
* Test for {@link OpenShiftStopWorkspaceRoleProvisioner}
* Test for {@link
* org.eclipse.che.workspace.infrastructure.openshift.project.configurator.OpenShiftStopWorkspaceRoleConfigurator}
*
* <p>#author Tom George
*/
@Listeners(MockitoTestNGListener.class)
public class OpenShiftStopWorkspaceRoleProvisionerTest {
public class OpenShiftStopWorkspaceRoleConfiguratorTest {
@Mock private CheInstallationLocation cheInstallationLocation;
private OpenShiftStopWorkspaceRoleProvisioner stopWorkspaceRoleProvisioner;
private OpenShiftStopWorkspaceRoleConfigurator stopWorkspaceRoleProvisioner;
@Mock private OpenShiftClientFactory clientFactory;
@Mock private OpenShiftClient osClient;
@ -123,7 +125,8 @@ public class OpenShiftStopWorkspaceRoleProvisionerTest {
public void setUp() throws Exception {
lenient().when(cheInstallationLocation.getInstallationLocationNamespace()).thenReturn("che");
stopWorkspaceRoleProvisioner =
new OpenShiftStopWorkspaceRoleProvisioner(clientFactory, cheInstallationLocation, true);
new OpenShiftStopWorkspaceRoleConfigurator(
clientFactory, cheInstallationLocation, true, "yes");
lenient().when(clientFactory.createOC()).thenReturn(osClient);
lenient().when(osClient.roles()).thenReturn(mixedRoleOperation);
lenient().when(osClient.roleBindings()).thenReturn(mixedRoleBindingOperation);
@ -160,7 +163,7 @@ public class OpenShiftStopWorkspaceRoleProvisionerTest {
@Test
public void shouldCreateRoleAndRoleBindingWhenRoleDoesNotYetExist()
throws InfrastructureException {
stopWorkspaceRoleProvisioner.provision("developer-che");
stopWorkspaceRoleProvisioner.configure(null, "developer-che");
verify(osClient, times(2)).roles();
verify(osClient.roles(), times(2)).inNamespace("developer-che");
verify(osClient.roles().inNamespace("developer-che")).withName("workspace-stop");
@ -174,7 +177,7 @@ public class OpenShiftStopWorkspaceRoleProvisionerTest {
@Test
public void shouldCreateRoleBindingWhenRoleAlreadyExists() throws InfrastructureException {
lenient().when(roleResource.get()).thenReturn(expectedRole);
stopWorkspaceRoleProvisioner.provision("developer-che");
stopWorkspaceRoleProvisioner.configure(null, "developer-che");
verify(osClient, times(1)).roles();
verify(osClient).roleBindings();
verify(osClient.roleBindings()).inNamespace("developer-che");
@ -185,9 +188,10 @@ public class OpenShiftStopWorkspaceRoleProvisionerTest {
@Test
public void shouldNotCreateRoleBindingWhenStopWorkspaceRolePropertyIsDisabled()
throws InfrastructureException {
OpenShiftStopWorkspaceRoleProvisioner disabledStopWorkspaceRoleProvisioner =
new OpenShiftStopWorkspaceRoleProvisioner(clientFactory, cheInstallationLocation, false);
disabledStopWorkspaceRoleProvisioner.provision("developer-che");
OpenShiftStopWorkspaceRoleConfigurator disabledStopWorkspaceRoleProvisioner =
new OpenShiftStopWorkspaceRoleConfigurator(
clientFactory, cheInstallationLocation, false, "yes");
disabledStopWorkspaceRoleProvisioner.configure(null, "developer-che");
verify(osClient, never()).roles();
verify(osClient, never()).roleBindings();
verify(osClient.roleBindings(), never()).inNamespace("developer-che");
@ -197,12 +201,26 @@ public class OpenShiftStopWorkspaceRoleProvisionerTest {
public void shouldNotCreateRoleBindingWhenInstallationLocationIsNull()
throws InfrastructureException {
lenient().when(cheInstallationLocation.getInstallationLocationNamespace()).thenReturn(null);
OpenShiftStopWorkspaceRoleProvisioner
OpenShiftStopWorkspaceRoleConfigurator
stopWorkspaceRoleProvisionerWithoutValidInstallationLocation =
new OpenShiftStopWorkspaceRoleProvisioner(clientFactory, cheInstallationLocation, true);
stopWorkspaceRoleProvisionerWithoutValidInstallationLocation.provision("developer-che");
new OpenShiftStopWorkspaceRoleConfigurator(
clientFactory, cheInstallationLocation, true, "yes");
stopWorkspaceRoleProvisionerWithoutValidInstallationLocation.configure(null, "developer-che");
verify(osClient, never()).roles();
verify(osClient, never()).roleBindings();
verify(osClient.roleBindings(), never()).inNamespace("developer-che");
}
@Test
public void shouldNotCallStopWorkspaceRoleProvisionWhenIdentityProviderIsDefined()
throws Exception {
when(cheInstallationLocation.getInstallationLocationNamespace()).thenReturn("something");
OpenShiftStopWorkspaceRoleConfigurator configurator =
new OpenShiftStopWorkspaceRoleConfigurator(
clientFactory, cheInstallationLocation, true, null);
configurator.configure(null, "something");
verify(clientFactory, times(0)).createOC();
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.openshift.project.configurator;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.testng.Assert.*;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory;
import org.eclipse.che.workspace.infrastructure.openshift.project.OpenShiftWorkspaceServiceAccount;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(MockitoTestNGListener.class)
public class OpenShiftWorkspaceServiceAccountConfiguratorTest {
private final String SA_NAME = "test-serviceaccout";
private final String CLUSTER_ROLES = "role1, role2";
private final String WS_ID = "ws123";
private final String USER_ID = "user123";
private final String USERNAME = "user-che";
private final String NS_NAME = "namespace-che";
private NamespaceResolutionContext nsContext;
@Mock private OpenShiftClientFactory clientFactory;
private OpenShiftWorkspaceServiceAccountConfigurator saConfigurator;
@BeforeMethod
public void setUp() {
nsContext = new NamespaceResolutionContext(WS_ID, USER_ID, USERNAME);
}
@Test
public void testPreparesServiceAccount() throws InfrastructureException {
saConfigurator =
spy(
new OpenShiftWorkspaceServiceAccountConfigurator(
SA_NAME, CLUSTER_ROLES, clientFactory));
OpenShiftWorkspaceServiceAccount serviceAccount = mock(OpenShiftWorkspaceServiceAccount.class);
doReturn(serviceAccount).when(saConfigurator).createServiceAccount(WS_ID, NS_NAME);
saConfigurator.configure(nsContext, NS_NAME);
verify(serviceAccount).prepare();
}
@Test
public void testDoNothingWhenServiceAccountNotSet() throws InfrastructureException {
saConfigurator =
spy(new OpenShiftWorkspaceServiceAccountConfigurator(null, CLUSTER_ROLES, clientFactory));
saConfigurator.configure(nsContext, NS_NAME);
verify(saConfigurator, times(0)).createServiceAccount(any(), any());
}
}

View File

@ -23,6 +23,8 @@ import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
@ -43,6 +45,9 @@ import org.slf4j.LoggerFactory;
* <li>Set subject for current request into {@link EnvironmentContext}
* </ul>
*
* <p>{@link MultiUserEnvironmentInitializationFilter#UNAUTHORIZED_ENDPOINT_PATHS} is list of
* unauthenticated paths, that are allowed without token.
*
* @param <T> the type of intermediary type used for conversion from a string token to a Subject
* @author Max Shaposhnyk (mshaposh@redhat.com)
*/
@ -51,6 +56,9 @@ public abstract class MultiUserEnvironmentInitializationFilter<T> implements Fil
private static final Logger LOG =
LoggerFactory.getLogger(MultiUserEnvironmentInitializationFilter.class);
private static final List<String> UNAUTHORIZED_ENDPOINT_PATHS =
Collections.singletonList("/system/state");
private final SessionStore sessionStore;
private final RequestTokenExtractor tokenExtractor;
@ -197,9 +205,23 @@ public abstract class MultiUserEnvironmentInitializationFilter<T> implements Fil
* @throws IOException inherited from {@link FilterChain#doFilter}
* @throws ServletException inherited from {@link FilterChain#doFilter}
*/
protected abstract void handleMissingToken(
protected void handleMissingToken(
ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
throws IOException, ServletException {
// if request path is in unauthorized endpoints, continue
if (request instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String path = httpRequest.getServletPath();
if (UNAUTHORIZED_ENDPOINT_PATHS.contains(path)) {
LOG.debug("Allowing request to '{}' without authorization header.", path);
chain.doFilter(request, response);
return;
}
}
LOG.error("Rejecting the request due to missing/expired token in Authorization header.");
sendError(response, 401, "Authorization token is missing or expired");
}
/**
* Sends appropriate error status code and message into response.

View File

@ -25,9 +25,11 @@ import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Optional;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
@ -83,6 +85,7 @@ public class MultiUserEnvironmentInitializationFilterTest {
// then
verify(tokenExtractor).getToken(eq(request));
verify(filter).handleMissingToken(eq(request), eq(response), eq(chain));
verify(request).getServletPath();
verifyNoMoreInteractions(request);
verify(filter, never()).getUserId(any());
verify(filter, never()).extractSubject(anyString(), any());
@ -100,6 +103,7 @@ public class MultiUserEnvironmentInitializationFilterTest {
// then
verify(tokenExtractor).getToken(eq(request));
verify(filter).handleMissingToken(eq(request), eq(response), eq(chain));
verify(request).getServletPath();
verifyNoMoreInteractions(request);
verify(filter, never()).getUserId(any());
verify(filter, never()).extractSubject(anyString(), any());
@ -168,4 +172,23 @@ public class MultiUserEnvironmentInitializationFilterTest {
// then
verify(context).setSubject(eq(subject));
}
@Test
public void handleMissingTokenShouldAllowUnauthorizedEndpoint()
throws ServletException, IOException {
when(request.getServletPath()).thenReturn("/system/state");
filter.handleMissingToken(request, response, chain);
verify(chain).doFilter(request, response);
}
@Test
public void handleMissingTokenShouldRejectRequest() throws ServletException, IOException {
when(request.getServletPath()).thenReturn("blabol");
filter.handleMissingToken(request, response, chain);
verify(response).sendError(eq(401), anyString());
}
}

View File

@ -30,14 +30,6 @@
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
@ -102,10 +94,6 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-inject</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-lang</artifactId>
@ -130,6 +118,10 @@
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-machine-authentication-shared</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-oidc</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-personal-account</artifactId>

View File

@ -13,6 +13,7 @@ package org.eclipse.che.multiuser.keycloak.server;
import static com.google.common.base.Strings.isNullOrEmpty;
import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static org.eclipse.che.multiuser.oidc.OIDCInfoProvider.OIDC_USERNAME_CLAIM_SETTING;
import com.google.common.base.Splitter;
import io.jsonwebtoken.Claims;
@ -43,7 +44,6 @@ import org.eclipse.che.multiuser.api.authentication.commons.filter.MultiUserEnvi
import org.eclipse.che.multiuser.api.authentication.commons.token.RequestTokenExtractor;
import org.eclipse.che.multiuser.api.permission.server.AuthorizedSubject;
import org.eclipse.che.multiuser.api.permission.server.PermissionChecker;
import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -123,8 +123,7 @@ public class KeycloakEnvironmentInitializationFilter
try {
String username =
claims.get(
keycloakSettings.get().get(KeycloakConstants.USERNAME_CLAIM_SETTING), String.class);
claims.get(keycloakSettings.get().get(OIDC_USERNAME_CLAIM_SETTING), String.class);
if (username == null) { // fallback to unique id promised by spec
// https://openid.net/specs/openid-connect-basic-1_0.html#ClaimStability
username = claims.getIssuer() + ":" + claims.getSubject();

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.keycloak.server;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING;
import javax.inject.Inject;
import javax.inject.Named;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.multiuser.oidc.OIDCInfoProvider;
/**
* KeycloakOIDCInfoProvider retrieves OpenID Connect (OIDC) configuration for well-known endpoint.
* These information is useful to provide access to the Keycloak api.
*/
public class KeycloakOIDCInfoProvider extends OIDCInfoProvider {
public final String realm;
@Inject
public KeycloakOIDCInfoProvider(
@Nullable @Named(AUTH_SERVER_URL_SETTING) String serverURL,
@Nullable @Named(AUTH_SERVER_URL_INTERNAL_SETTING) String serverInternalURL,
@Nullable @Named(OIDC_PROVIDER_SETTING) String oidcProviderUrl,
@Nullable @Named(REALM_SETTING) String realm) {
super(serverURL, serverInternalURL, oidcProviderUrl);
this.realm = realm;
}
@Override
protected String constructServerAuthUrl(String serverAuthUrl) {
return serverAuthUrl + "/realms/" + realm;
}
protected void validate() {
if (oidcProviderUrl == null && realm == null) {
throw new RuntimeException("The '" + REALM_SETTING + "' property must be set");
}
super.validate();
}
}

View File

@ -21,6 +21,7 @@ import javax.inject.Singleton;
import org.eclipse.che.api.core.ApiException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.rest.HttpJsonRequestFactory;
import org.eclipse.che.multiuser.oidc.OIDCInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

View File

@ -52,6 +52,7 @@ import org.eclipse.che.commons.lang.Pair;
import org.eclipse.che.dto.server.DtoFactory;
import org.eclipse.che.multiuser.keycloak.shared.dto.KeycloakErrorResponse;
import org.eclipse.che.multiuser.keycloak.shared.dto.KeycloakTokenResponse;
import org.eclipse.che.multiuser.oidc.OIDCInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -75,7 +76,7 @@ public class KeycloakServiceClient {
Pattern.compile("<div id=\"kc-error-message\">(\\s*)<p class=\"instruction\">(.+?)</p>");
private static final Gson gson = new Gson();
private JwtParser jwtParser;
private final JwtParser jwtParser;
@Inject
public KeycloakServiceClient(

View File

@ -11,7 +11,6 @@
*/
package org.eclipse.che.multiuser.keycloak.server;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.AUTH_SERVER_URL_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.CLIENT_ID_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.FIXED_REDIRECT_URL_FOR_DASHBOARD;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.FIXED_REDIRECT_URL_FOR_IDE;
@ -19,16 +18,17 @@ import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.GITHUB
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.JS_ADAPTER_URL_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.JWKS_ENDPOINT_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.LOGOUT_ENDPOINT_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.OIDC_PROVIDER_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.OSO_ENDPOINT_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.PASSWORD_ENDPOINT_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.PROFILE_ENDPOINT_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.TOKEN_ENDPOINT_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USERINFO_ENDPOINT_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USERNAME_CLAIM_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USE_FIXED_REDIRECT_URLS_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USE_NONCE_SETTING;
import static org.eclipse.che.multiuser.oidc.OIDCInfoProvider.AUTH_SERVER_URL_SETTING;
import static org.eclipse.che.multiuser.oidc.OIDCInfoProvider.OIDC_PROVIDER_SETTING;
import static org.eclipse.che.multiuser.oidc.OIDCInfoProvider.OIDC_USERNAME_CLAIM_SETTING;
import com.google.common.collect.Maps;
import java.util.Collections;
@ -37,6 +37,7 @@ import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.multiuser.oidc.OIDCInfo;
/** @author Max Shaposhnik (mshaposh@redhat.com) */
@Singleton
@ -54,7 +55,7 @@ public class KeycloakSettings {
@Nullable @Named(REALM_SETTING) String realm,
@Named(CLIENT_ID_SETTING) String clientId,
@Nullable @Named(OIDC_PROVIDER_SETTING) String oidcProviderUrl,
@Nullable @Named(USERNAME_CLAIM_SETTING) String usernameClaim,
@Nullable @Named(OIDC_USERNAME_CLAIM_SETTING) String usernameClaim,
@Named(USE_NONCE_SETTING) boolean useNonce,
@Nullable @Named(OSO_ENDPOINT_SETTING) String osoEndpoint,
@Nullable @Named(GITHUB_ENDPOINT_SETTING) String gitHubEndpoint,
@ -64,7 +65,8 @@ public class KeycloakSettings {
Map<String, String> settings = Maps.newHashMap();
settings.put(
USERNAME_CLAIM_SETTING, usernameClaim == null ? DEFAULT_USERNAME_CLAIM : usernameClaim);
OIDC_USERNAME_CLAIM_SETTING,
usernameClaim == null ? DEFAULT_USERNAME_CLAIM : usernameClaim);
settings.put(CLIENT_ID_SETTING, clientId);
settings.put(REALM_SETTING, realm);
@ -80,9 +82,8 @@ public class KeycloakSettings {
serverURL + "/realms/" + realm + "/protocol/openid-connect/token");
}
if (oidcInfo.getEndSessionPublicEndpoint() != null) {
settings.put(LOGOUT_ENDPOINT_SETTING, oidcInfo.getEndSessionPublicEndpoint());
}
oidcInfo.getEndSessionPublicEndpoint().ifPresent(e -> settings.put(LOGOUT_ENDPOINT_SETTING, e));
if (oidcInfo.getTokenPublicEndpoint() != null) {
settings.put(TOKEN_ENDPOINT_SETTING, oidcInfo.getTokenPublicEndpoint());
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
@ -13,30 +13,20 @@ package org.eclipse.che.multiuser.keycloak.server;
import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.MACHINE_TOKEN_KIND;
import com.auth0.jwk.JwkException;
import com.auth0.jwk.JwkProvider;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.SigningKeyResolverAdapter;
import java.security.Key;
import java.security.PublicKey;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.eclipse.che.multiuser.oidc.OIDCSigningKeyResolver;
/** Resolves signing key based on id from JWT header */
@Singleton
public class KeycloakSigningKeyResolver extends SigningKeyResolverAdapter {
private final JwkProvider jwkProvider;
private static final Logger LOG = LoggerFactory.getLogger(KeycloakSigningKeyResolver.class);
public class KeycloakSigningKeyResolver extends OIDCSigningKeyResolver {
@Inject
KeycloakSigningKeyResolver(JwkProvider jwkProvider) {
this.jwkProvider = jwkProvider;
super(jwkProvider);
}
@Override
@ -54,19 +44,4 @@ public class KeycloakSigningKeyResolver extends SigningKeyResolverAdapter {
}
return getJwtPublicKey(header);
}
private synchronized PublicKey getJwtPublicKey(JwsHeader<?> header) {
String kid = header.getKeyId();
if (header.getKeyId() == null) {
LOG.warn(
"'kid' is missing in the JWT token header. This is not possible to validate the token with OIDC provider keys");
throw new JwtException("'kid' is missing in the JWT token header.");
}
try {
return jwkProvider.get(kid).getPublicKey();
} catch (JwkException e) {
throw new JwtException(
"Error during the retrieval of the public key during JWT token validation", e);
}
}
}

View File

@ -14,18 +14,20 @@ package org.eclipse.che.multiuser.keycloak.server.deploy;
import com.auth0.jwk.JwkProvider;
import com.google.inject.AbstractModule;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.SigningKeyResolver;
import org.eclipse.che.api.core.rest.HttpJsonRequestFactory;
import org.eclipse.che.api.user.server.TokenValidator;
import org.eclipse.che.api.user.server.spi.ProfileDao;
import org.eclipse.che.multiuser.api.account.personal.PersonalAccountUserManager;
import org.eclipse.che.multiuser.keycloak.server.KeycloakConfigurationService;
import org.eclipse.che.multiuser.keycloak.server.KeycloakJwkProvider;
import org.eclipse.che.multiuser.keycloak.server.KeycloakJwtParserProvider;
import org.eclipse.che.multiuser.keycloak.server.KeycloakOIDCInfoProvider;
import org.eclipse.che.multiuser.keycloak.server.KeycloakSigningKeyResolver;
import org.eclipse.che.multiuser.keycloak.server.KeycloakTokenValidator;
import org.eclipse.che.multiuser.keycloak.server.KeycloakUserManager;
import org.eclipse.che.multiuser.keycloak.server.OIDCInfo;
import org.eclipse.che.multiuser.keycloak.server.OIDCInfoProvider;
import org.eclipse.che.multiuser.keycloak.server.dao.KeycloakProfileDao;
import org.eclipse.che.multiuser.oidc.OIDCInfo;
import org.eclipse.che.multiuser.oidc.OIDCJwkProvider;
import org.eclipse.che.multiuser.oidc.OIDCJwtParserProvider;
import org.eclipse.che.security.oauth.OAuthAPI;
public class KeycloakModule extends AbstractModule {
@ -38,9 +40,10 @@ public class KeycloakModule extends AbstractModule {
bind(KeycloakConfigurationService.class);
bind(ProfileDao.class).to(KeycloakProfileDao.class);
bind(JwkProvider.class).toProvider(KeycloakJwkProvider.class);
bind(JwtParser.class).toProvider(KeycloakJwtParserProvider.class);
bind(OIDCInfo.class).toProvider(OIDCInfoProvider.class).asEagerSingleton();
bind(JwkProvider.class).toProvider(OIDCJwkProvider.class);
bind(SigningKeyResolver.class).to(KeycloakSigningKeyResolver.class);
bind(JwtParser.class).toProvider(OIDCJwtParserProvider.class);
bind(OIDCInfo.class).toProvider(KeycloakOIDCInfoProvider.class).asEagerSingleton();
bind(PersonalAccountUserManager.class).to(KeycloakUserManager.class);
bind(OAuthAPI.class).toProvider(OAuthAPIProvider.class);

View File

@ -12,7 +12,7 @@
package org.eclipse.che.multiuser.keycloak.server;
import static org.eclipse.che.multiuser.api.authentication.commons.Constants.CHE_SUBJECT_ATTRIBUTE;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USERNAME_CLAIM_SETTING;
import static org.eclipse.che.multiuser.oidc.OIDCInfoProvider.OIDC_USERNAME_CLAIM_SETTING;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
@ -50,7 +50,6 @@ import org.eclipse.che.multiuser.api.authentication.commons.SessionStore;
import org.eclipse.che.multiuser.api.authentication.commons.token.RequestTokenExtractor;
import org.eclipse.che.multiuser.api.permission.server.AuthorizedSubject;
import org.eclipse.che.multiuser.api.permission.server.PermissionChecker;
import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants;
import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
@ -119,7 +118,7 @@ public class KeycloakEnvironmentInitializationFilterTest {
DefaultJws<Claims> jws = new DefaultJws<>(new DefaultJwsHeader(), claims, "");
when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn("token");
when(jwtParser.parseClaimsJws(anyString())).thenReturn(jws);
keycloakSettingsMap.put(USERNAME_CLAIM_SETTING, "preferred_username");
keycloakSettingsMap.put(OIDC_USERNAME_CLAIM_SETTING, "preferred_username");
when(userManager.getOrCreateUser(anyString(), anyString(), anyString()))
.thenReturn(mock(UserImpl.class, RETURNS_DEEP_STUBS));
filter =
@ -149,7 +148,7 @@ public class KeycloakEnvironmentInitializationFilterTest {
DefaultJws<Claims> jws = new DefaultJws<>(new DefaultJwsHeader(), claims, "");
when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn("token");
when(jwtParser.parseClaimsJws(anyString())).thenReturn(jws);
keycloakSettingsMap.put(USERNAME_CLAIM_SETTING, "preferred_username");
keycloakSettingsMap.put(OIDC_USERNAME_CLAIM_SETTING, "preferred_username");
when(userManager.getOrCreateUser(anyString(), anyString(), anyString()))
.thenReturn(mock(UserImpl.class, RETURNS_DEEP_STUBS));
filter =
@ -210,7 +209,7 @@ public class KeycloakEnvironmentInitializationFilterTest {
Claims claims = new DefaultClaims(claimParams).setSubject("id");
DefaultJws<Claims> jws = new DefaultJws<>(new DefaultJwsHeader(), claims, "");
UserImpl user = new UserImpl("id", "test@test.com", "username");
keycloakSettingsMap.put(KeycloakConstants.USERNAME_CLAIM_SETTING, "preferred_username");
keycloakSettingsMap.put(OIDC_USERNAME_CLAIM_SETTING, "preferred_username");
// given
when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn("token");
when(jwtParser.parseClaimsJws(anyString())).thenReturn(jws);

View File

@ -49,6 +49,7 @@ import org.eclipse.che.api.core.rest.Service;
import org.eclipse.che.dto.server.DtoFactory;
import org.eclipse.che.multiuser.keycloak.shared.dto.KeycloakErrorResponse;
import org.eclipse.che.multiuser.keycloak.shared.dto.KeycloakTokenResponse;
import org.eclipse.che.multiuser.oidc.OIDCInfo;
import org.everrest.assured.EverrestJetty;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;

View File

@ -12,7 +12,6 @@
package org.eclipse.che.multiuser.keycloak.server;
import static org.eclipse.che.multiuser.keycloak.server.KeycloakSettings.DEFAULT_USERNAME_CLAIM;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.AUTH_SERVER_URL_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.CLIENT_ID_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.FIXED_REDIRECT_URL_FOR_DASHBOARD;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.FIXED_REDIRECT_URL_FOR_IDE;
@ -20,20 +19,23 @@ import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.GITHUB
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.JS_ADAPTER_URL_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.JWKS_ENDPOINT_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.LOGOUT_ENDPOINT_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.OIDC_PROVIDER_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.OSO_ENDPOINT_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.PASSWORD_ENDPOINT_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.PROFILE_ENDPOINT_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.TOKEN_ENDPOINT_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USERINFO_ENDPOINT_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USERNAME_CLAIM_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.USE_NONCE_SETTING;
import static org.eclipse.che.multiuser.oidc.OIDCInfoProvider.AUTH_SERVER_URL_SETTING;
import static org.eclipse.che.multiuser.oidc.OIDCInfoProvider.OIDC_PROVIDER_SETTING;
import static org.eclipse.che.multiuser.oidc.OIDCInfoProvider.OIDC_USERNAME_CLAIM_SETTING;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
import java.util.Map;
import java.util.Optional;
import org.eclipse.che.multiuser.oidc.OIDCInfo;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.Listeners;
@ -185,7 +187,8 @@ public class KeycloakSettingsTest {
public void shouldBeUsedConfigurationFromExternalOIDCProviderWithoutFixedRedirectLinks() {
final String SERVER_AUTH_URL = "https://external-keycloak-che.apps-crc.testing/auth";
when(oidcInfo.getEndSessionPublicEndpoint()).thenReturn(SERVER_AUTH_URL + LOGOUT_URL_PATH);
when(oidcInfo.getEndSessionPublicEndpoint())
.thenReturn(Optional.of(SERVER_AUTH_URL + LOGOUT_URL_PATH));
when(oidcInfo.getJwksPublicUri()).thenReturn(SERVER_AUTH_URL + JWKS_ENDPOINT_PATH);
when(oidcInfo.getUserInfoPublicEndpoint()).thenReturn(SERVER_AUTH_URL + USER_INFO_PATH);
when(oidcInfo.getTokenPublicEndpoint()).thenReturn(SERVER_AUTH_URL + TOKEN_URL_PATH);
@ -206,7 +209,7 @@ public class KeycloakSettingsTest {
oidcInfo);
Map<String, String> publicSettings = settings.get();
assertEquals(publicSettings.get(USERNAME_CLAIM_SETTING), DEFAULT_USERNAME_CLAIM);
assertEquals(publicSettings.get(OIDC_USERNAME_CLAIM_SETTING), DEFAULT_USERNAME_CLAIM);
assertEquals(publicSettings.get(CLIENT_ID_SETTING), CLIENT_ID);
assertEquals(publicSettings.get(REALM_SETTING), CHE_REALM);
assertNull(publicSettings.get(AUTH_SERVER_URL_SETTING));
@ -229,7 +232,8 @@ public class KeycloakSettingsTest {
public void shouldBeUsedConfigurationFromExternalAuthServer() {
final String SERVER_AUTH_URL = "https://keycloak-che.apps-crc.testing/auth";
when(oidcInfo.getEndSessionPublicEndpoint()).thenReturn(SERVER_AUTH_URL + LOGOUT_URL_PATH);
when(oidcInfo.getEndSessionPublicEndpoint())
.thenReturn(Optional.of(SERVER_AUTH_URL + LOGOUT_URL_PATH));
when(oidcInfo.getJwksPublicUri()).thenReturn(SERVER_AUTH_URL + JWKS_ENDPOINT_PATH);
when(oidcInfo.getUserInfoPublicEndpoint()).thenReturn(SERVER_AUTH_URL + USER_INFO_PATH);
when(oidcInfo.getTokenPublicEndpoint()).thenReturn(SERVER_AUTH_URL + TOKEN_URL_PATH);
@ -250,7 +254,7 @@ public class KeycloakSettingsTest {
oidcInfo);
Map<String, String> publicSettings = settings.get();
assertEquals(publicSettings.get(USERNAME_CLAIM_SETTING), DEFAULT_USERNAME_CLAIM);
assertEquals(publicSettings.get(OIDC_USERNAME_CLAIM_SETTING), DEFAULT_USERNAME_CLAIM);
assertEquals(publicSettings.get(CLIENT_ID_SETTING), CLIENT_ID);
assertEquals(publicSettings.get(REALM_SETTING), CHE_REALM);
assertEquals(publicSettings.get(AUTH_SERVER_URL_SETTING), SERVER_AUTH_URL);

View File

@ -21,6 +21,7 @@ import static org.testng.Assert.assertNull;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import org.eclipse.che.multiuser.oidc.OIDCInfo;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
@ -86,9 +87,8 @@ public class OIDCInfoProviderTest {
.willReturn(
aResponse().withHeader("Content-Type", "text/html").withBody("broken json")));
OIDCInfoProvider oidcInfoProvider = new OIDCInfoProvider();
oidcInfoProvider.oidcProviderUrl = serverUrl;
oidcInfoProvider.realm = CHE_REALM;
KeycloakOIDCInfoProvider oidcInfoProvider =
new KeycloakOIDCInfoProvider(null, null, serverUrl, CHE_REALM);
oidcInfoProvider.get();
}
@ -100,9 +100,8 @@ public class OIDCInfoProviderTest {
.willReturn(
aResponse().withHeader("Content-Type", "text/html").withBody(openIdConfig)));
OIDCInfoProvider oidcInfoProvider = new OIDCInfoProvider();
oidcInfoProvider.serverURL = serverUrl;
oidcInfoProvider.realm = CHE_REALM;
KeycloakOIDCInfoProvider oidcInfoProvider =
new KeycloakOIDCInfoProvider(serverUrl, null, null, CHE_REALM);
OIDCInfo oidcInfo = oidcInfoProvider.get();
assertEquals(
@ -110,7 +109,7 @@ public class OIDCInfoProviderTest {
oidcInfo.getTokenPublicEndpoint());
assertEquals(
serverUrl + "/realms/" + CHE_REALM + "/protocol/openid-connect/logout",
oidcInfo.getEndSessionPublicEndpoint());
oidcInfo.getEndSessionPublicEndpoint().get());
assertNull(oidcInfo.getUserInfoInternalEndpoint());
assertNull(oidcInfo.getJwksInternalUri());
}
@ -150,10 +149,8 @@ public class OIDCInfoProviderTest {
.withHeader("Content-Type", "text/html")
.withBody(OPEN_ID_CONF_TEMPLATE)));
OIDCInfoProvider oidcInfoProvider = new OIDCInfoProvider();
oidcInfoProvider.serverURL = serverPublicUrl;
oidcInfoProvider.serverInternalURL = serverUrl;
oidcInfoProvider.realm = CHE_REALM;
KeycloakOIDCInfoProvider oidcInfoProvider =
new KeycloakOIDCInfoProvider(serverPublicUrl, serverUrl, null, CHE_REALM);
OIDCInfo oidcInfo = oidcInfoProvider.get();
assertEquals(
@ -161,7 +158,7 @@ public class OIDCInfoProviderTest {
oidcInfo.getTokenPublicEndpoint());
assertEquals(
serverPublicUrl + "/realms/" + CHE_REALM + "/protocol/openid-connect/logout",
oidcInfo.getEndSessionPublicEndpoint());
oidcInfo.getEndSessionPublicEndpoint().get());
assertEquals(
serverPublicUrl + "/realms/" + CHE_REALM + "/protocol/openid-connect/userinfo",
oidcInfo.getUserInfoPublicEndpoint());
@ -215,10 +212,8 @@ public class OIDCInfoProviderTest {
.withHeader("Content-Type", "text/html")
.withBody(OPEN_ID_CONF_TEMPLATE)));
OIDCInfoProvider oidcInfoProvider = new OIDCInfoProvider();
oidcInfoProvider.serverURL = serverPublicUrl;
oidcInfoProvider.serverInternalURL = serverInternalUrl;
oidcInfoProvider.realm = CHE_REALM;
KeycloakOIDCInfoProvider oidcInfoProvider =
new KeycloakOIDCInfoProvider(serverPublicUrl, serverInternalUrl, null, CHE_REALM);
OIDCInfo oidcInfo = oidcInfoProvider.get();
assertEquals(
@ -226,7 +221,7 @@ public class OIDCInfoProviderTest {
oidcInfo.getTokenPublicEndpoint());
assertEquals(
serverPublicUrl + "/realms/" + CHE_REALM + "/protocol/openid-connect/logout",
oidcInfo.getEndSessionPublicEndpoint());
oidcInfo.getEndSessionPublicEndpoint().get());
assertEquals(
serverPublicUrl + "/realms/" + CHE_REALM + "/protocol/openid-connect/userinfo",
oidcInfo.getUserInfoPublicEndpoint());
@ -253,11 +248,8 @@ public class OIDCInfoProviderTest {
.willReturn(
aResponse().withHeader("Content-Type", "text/html").withBody(openIdConfig)));
OIDCInfoProvider oidcInfoProvider = new OIDCInfoProvider();
oidcInfoProvider.serverURL = TEST_URL;
oidcInfoProvider.serverInternalURL = TEST_URL;
oidcInfoProvider.oidcProviderUrl = OIDCProviderUrl;
oidcInfoProvider.realm = CHE_REALM;
KeycloakOIDCInfoProvider oidcInfoProvider =
new KeycloakOIDCInfoProvider(TEST_URL, TEST_URL, OIDCProviderUrl, CHE_REALM);
OIDCInfo oidcInfo = oidcInfoProvider.get();
assertEquals(
@ -265,7 +257,7 @@ public class OIDCInfoProviderTest {
oidcInfo.getTokenPublicEndpoint());
assertEquals(
serverUrl + "/realms/" + CHE_REALM + "/protocol/openid-connect/logout",
oidcInfo.getEndSessionPublicEndpoint());
oidcInfo.getEndSessionPublicEndpoint().get());
assertEquals(
serverUrl + "/realms/" + CHE_REALM + "/protocol/openid-connect/userinfo",
oidcInfo.getUserInfoInternalEndpoint());
@ -278,17 +270,17 @@ public class OIDCInfoProviderTest {
expectedExceptions = RuntimeException.class,
expectedExceptionsMessageRegExp = "Either the '.*' or '.*' or '.*' property should be set")
public void shouldThrowErrorWhenAuthServerWasNotSet() {
OIDCInfoProvider oidcInfoProvider = new OIDCInfoProvider();
oidcInfoProvider.realm = CHE_REALM;
KeycloakOIDCInfoProvider oidcInfoProvider =
new KeycloakOIDCInfoProvider(null, null, null, CHE_REALM);
oidcInfoProvider.get();
}
@Test(
expectedExceptions = RuntimeException.class,
expectedExceptionsMessageRegExp = "The '.*' property should be set")
expectedExceptionsMessageRegExp = "The '.*' property must be set")
public void shouldThrowErrorWhenRealmPropertyWasNotSet() {
OIDCInfoProvider oidcInfoProvider = new OIDCInfoProvider();
oidcInfoProvider.serverURL = TEST_URL;
KeycloakOIDCInfoProvider oidcInfoProvider =
new KeycloakOIDCInfoProvider(null, null, null, null);
oidcInfoProvider.get();
}
}

View File

@ -15,22 +15,13 @@ package org.eclipse.che.multiuser.keycloak.shared;
public class KeycloakConstants {
private static final String KEYCLOAK_SETTING_PREFIX = "che.keycloak.";
private static final String KEYCLOAK_SETTINGS_ENDPOINT_PATH = "/keycloak/settings";
public static final String AUTH_SERVER_URL_SETTING = KEYCLOAK_SETTING_PREFIX + "auth_server_url";
public static final String AUTH_SERVER_URL_INTERNAL_SETTING =
KEYCLOAK_SETTING_PREFIX + "auth_internal_server_url";
public static final String REALM_SETTING = KEYCLOAK_SETTING_PREFIX + "realm";
public static final String CLIENT_ID_SETTING = KEYCLOAK_SETTING_PREFIX + "client_id";
public static final String OIDC_PROVIDER_SETTING = KEYCLOAK_SETTING_PREFIX + "oidc_provider";
public static final String USERNAME_CLAIM_SETTING = KEYCLOAK_SETTING_PREFIX + "username_claim";
public static final String USE_NONCE_SETTING = KEYCLOAK_SETTING_PREFIX + "use_nonce";
public static final String USE_FIXED_REDIRECT_URLS_SETTING =
KEYCLOAK_SETTING_PREFIX + "use_fixed_redirect_urls";
public static final String JS_ADAPTER_URL_SETTING = KEYCLOAK_SETTING_PREFIX + "js_adapter_url";
public static final String ALLOWED_CLOCK_SKEW_SEC =
KEYCLOAK_SETTING_PREFIX + "allowed_clock_skew_sec";
public static final String OSO_ENDPOINT_SETTING = KEYCLOAK_SETTING_PREFIX + "oso.endpoint";
public static final String PROFILE_ENDPOINT_SETTING =
@ -48,8 +39,4 @@ public class KeycloakConstants {
KEYCLOAK_SETTING_PREFIX + "redirect_url.dashboard";
public static final String FIXED_REDIRECT_URL_FOR_IDE =
KEYCLOAK_SETTING_PREFIX + "redirect_url.ide";
public static String getEndpoint(String apiEndpoint) {
return apiEndpoint + KEYCLOAK_SETTINGS_ENDPOINT_PATH;
}
}

View File

@ -79,6 +79,10 @@
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-keycloak-shared</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-oidc</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>

View File

@ -11,7 +11,9 @@
*/
package org.eclipse.che.multiuser.keycloak.server;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.*;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING;
import static org.eclipse.che.multiuser.oidc.OIDCInfoProvider.AUTH_SERVER_URL_INTERNAL_SETTING;
import static org.eclipse.che.multiuser.oidc.OIDCInfoProvider.AUTH_SERVER_URL_SETTING;
import com.google.common.base.Strings;
import com.google.gson.JsonSyntaxException;
@ -30,6 +32,7 @@ import org.eclipse.che.api.user.server.event.BeforeUserRemovedEvent;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.core.db.cascade.CascadeEventSubscriber;
import org.eclipse.che.inject.ConfigurationException;
import org.eclipse.che.multiuser.oidc.OIDCInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

View File

@ -25,6 +25,7 @@ import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.rest.DefaultHttpJsonRequest;
import org.eclipse.che.api.core.rest.HttpJsonRequestFactory;
import org.eclipse.che.multiuser.oidc.OIDCInfo;
import org.mockito.Mock;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

104
multiuser/oidc/pom.xml Normal file
View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2012-2021 Red Hat, Inc.
This program and the accompanying materials are made
available under the terms of the Eclipse Public License 2.0
which is available at https://www.eclipse.org/legal/epl-2.0/
SPDX-License-Identifier: EPL-2.0
Contributors:
Red Hat, Inc. - initial API and implementation
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>che-multiuser-parent</artifactId>
<groupId>org.eclipse.che.multiuser</groupId>
<version>7.40.0-SNAPSHOT</version>
</parent>
<artifactId>che-multiuser-oidc</artifactId>
<packaging>jar</packaging>
<name>Che Multiuser :: OIDC</name>
<dependencies>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-core</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-model</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-user</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-inject</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-api-authentication-commons</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-api-authorization</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-testng</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -9,9 +9,12 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.keycloak.server;
package org.eclipse.che.multiuser.oidc;
/** OIDCInfo - POJO object to store information about Keycloak api. */
import java.util.Optional;
import java.util.StringJoiner;
/** OIDCInfo - POJO object to store information about OIDC api. */
public class OIDCInfo {
private final String tokenPublicEndpoint;
@ -38,7 +41,6 @@ public class OIDCInfo {
this.userInfoInternalEndpoint = userInfoInternalEndpoint;
this.jwksPublicUri = jwksPublicUri;
this.jwksInternalUri = jwksInternalUri;
this.authServerURL = authServerURL;
this.authServerPublicURL = authServerPublicURL;
}
@ -48,11 +50,6 @@ public class OIDCInfo {
return tokenPublicEndpoint;
}
/** @return public log out url. */
public String getEndSessionPublicEndpoint() {
return endSessionPublicEndpoint;
}
/** @return public url to get user profile information. */
public String getUserInfoPublicEndpoint() {
return userInfoPublicEndpoint;
@ -85,4 +82,21 @@ public class OIDCInfo {
public String getAuthServerPublicURL() {
return authServerPublicURL;
}
@Override
public String toString() {
return new StringJoiner(", ", OIDCInfo.class.getSimpleName() + "[", "]")
.add("tokenPublicEndpoint='" + tokenPublicEndpoint + "'")
.add("userInfoPublicEndpoint='" + userInfoPublicEndpoint + "'")
.add("userInfoInternalEndpoint='" + userInfoInternalEndpoint + "'")
.add("jwksPublicUri='" + jwksPublicUri + "'")
.add("jwksInternalUri='" + jwksInternalUri + "'")
.add("authServerURL='" + authServerURL + "'")
.add("authServerPublicURL='" + authServerPublicURL + "'")
.toString();
}
public Optional<String> getEndSessionPublicEndpoint() {
return Optional.ofNullable(endSessionPublicEndpoint);
}
}

View File

@ -9,13 +9,9 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.keycloak.server;
package org.eclipse.che.multiuser.oidc;
import static com.google.common.base.MoreObjects.firstNonNull;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.AUTH_SERVER_URL_INTERNAL_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.AUTH_SERVER_URL_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.OIDC_PROVIDER_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
@ -34,32 +30,35 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* OIDCInfoProvider retrieves OpenID Connect (OIDC) configuration for well-known endpoint. These
* information is useful to provide access to the Keycloak api.
* OIDCInfoProvider retrieves OpenID Connect (OIDC) configuration for well-known endpoint. This
* information is useful to provide access to the OIDC api.
*/
public class OIDCInfoProvider implements Provider<OIDCInfo> {
private static final Logger LOG = LoggerFactory.getLogger(OIDCInfoProvider.class);
@Inject
@Nullable
@Named(AUTH_SERVER_URL_SETTING)
private static final String OIDC_SETTING_PREFIX = "che.oidc.";
public static final String AUTH_SERVER_URL_SETTING = OIDC_SETTING_PREFIX + "auth_server_url";
public static final String AUTH_SERVER_URL_INTERNAL_SETTING =
OIDC_SETTING_PREFIX + "auth_internal_server_url";
public static final String OIDC_PROVIDER_SETTING = OIDC_SETTING_PREFIX + "oidc_provider";
public static final String OIDC_USERNAME_CLAIM_SETTING = OIDC_SETTING_PREFIX + "username_claim";
public static final String OIDC_ALLOWED_CLOCK_SKEW_SEC =
OIDC_SETTING_PREFIX + "allowed_clock_skew_sec";
protected String serverURL;
@Inject
@Nullable
@Named(AUTH_SERVER_URL_INTERNAL_SETTING)
protected String serverInternalURL;
@Inject
@Nullable
@Named(OIDC_PROVIDER_SETTING)
protected String oidcProviderUrl;
@Inject
@Nullable
@Named(REALM_SETTING)
protected String realm;
public OIDCInfoProvider(
@Nullable @Named(AUTH_SERVER_URL_SETTING) String serverURL,
@Nullable @Named(AUTH_SERVER_URL_INTERNAL_SETTING) String serverInternalURL,
@Nullable @Named(OIDC_PROVIDER_SETTING) String oidcProviderUrl) {
this.serverURL = serverURL;
this.serverInternalURL = serverInternalURL;
this.oidcProviderUrl = oidcProviderUrl;
}
/** @return OIDCInfo with OIDC settings information. */
@Override
@ -84,7 +83,9 @@ public class OIDCInfoProvider implements Provider<OIDCInfo> {
String userInfoPublicEndpoint =
setPublicUrl((String) openIdConfiguration.get("userinfo_endpoint"));
String endSessionPublicEndpoint =
setPublicUrl((String) openIdConfiguration.get("end_session_endpoint"));
openIdConfiguration.containsKey("end_session_endpoint")
? setPublicUrl((String) openIdConfiguration.get("end_session_endpoint"))
: null;
String jwksPublicUri = setPublicUrl((String) openIdConfiguration.get("jwks_uri"));
String jwksInternalUri = setInternalUrl(jwksPublicUri);
String userInfoInternalEndpoint = setInternalUrl(userInfoPublicEndpoint);
@ -107,7 +108,7 @@ public class OIDCInfoProvider implements Provider<OIDCInfo> {
}
private String getWellKnownEndpoint(String serverAuthUrl) {
String wellKnownEndpoint = firstNonNull(oidcProviderUrl, serverAuthUrl + "/realms/" + realm);
String wellKnownEndpoint = firstNonNull(oidcProviderUrl, constructServerAuthUrl(serverAuthUrl));
if (!wellKnownEndpoint.endsWith("/")) {
wellKnownEndpoint = wellKnownEndpoint + "/";
}
@ -115,7 +116,11 @@ public class OIDCInfoProvider implements Provider<OIDCInfo> {
return wellKnownEndpoint;
}
private void validate() {
protected String constructServerAuthUrl(String serverAuthUrl) {
return serverAuthUrl;
}
protected void validate() {
if (serverURL == null && serverInternalURL == null && oidcProviderUrl == null) {
throw new RuntimeException(
"Either the '"
@ -126,10 +131,6 @@ public class OIDCInfoProvider implements Provider<OIDCInfo> {
+ OIDC_PROVIDER_SETTING
+ "' property should be set");
}
if (oidcProviderUrl == null && realm == null) {
throw new RuntimeException("The '" + REALM_SETTING + "' property should be set");
}
}
private String setInternalUrl(String endpointUrl) {

View File

@ -9,13 +9,12 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.keycloak.server;
import static com.google.common.base.Strings.isNullOrEmpty;
package org.eclipse.che.multiuser.oidc;
import com.auth0.jwk.GuavaCachedJwkProvider;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.UrlJwkProvider;
import com.google.common.base.Strings;
import java.net.MalformedURLException;
import java.net.URL;
import javax.inject.Inject;
@ -23,14 +22,14 @@ import javax.inject.Provider;
import org.eclipse.che.inject.ConfigurationException;
/** Constructs {@link UrlJwkProvider} based on Jwk endpoint from keycloak settings */
public class KeycloakJwkProvider implements Provider<JwkProvider> {
public class OIDCJwkProvider implements Provider<JwkProvider> {
private final JwkProvider jwkProvider;
@Inject
public KeycloakJwkProvider(OIDCInfo oidcInfo) throws MalformedURLException {
public OIDCJwkProvider(OIDCInfo oidcInfo) throws MalformedURLException {
final String jwksUrl =
isNullOrEmpty(oidcInfo.getJwksInternalUri())
Strings.isNullOrEmpty(oidcInfo.getJwksInternalUri())
? oidcInfo.getJwksPublicUri()
: oidcInfo.getJwksInternalUri();

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
@ -9,30 +9,32 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.keycloak.server;
package org.eclipse.che.multiuser.oidc;
import static org.eclipse.che.multiuser.oidc.OIDCInfoProvider.OIDC_ALLOWED_CLOCK_SKEW_SEC;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SigningKeyResolver;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants;
/** Provides instance of {@link JwtParser} */
@Singleton
public class KeycloakJwtParserProvider implements Provider<JwtParser> {
public class OIDCJwtParserProvider implements Provider<JwtParser> {
private final JwtParser jwtParser;
@Inject
public KeycloakJwtParserProvider(
@Named(KeycloakConstants.ALLOWED_CLOCK_SKEW_SEC) long allowedClockSkewSec,
KeycloakSigningKeyResolver keycloakSigningKeyResolver) {
public OIDCJwtParserProvider(
@Named(OIDC_ALLOWED_CLOCK_SKEW_SEC) long allowedClockSkewSec,
SigningKeyResolver signingKeyResolver) {
this.jwtParser =
Jwts.parser()
Jwts.parserBuilder()
.setSigningKeyResolver(signingKeyResolver)
.setAllowedClockSkewSeconds(allowedClockSkewSec)
.setSigningKeyResolver(keycloakSigningKeyResolver);
.build();
}
@Override

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.oidc;
import com.auth0.jwk.JwkException;
import com.auth0.jwk.JwkProvider;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.SigningKeyResolverAdapter;
import java.security.Key;
import java.security.PublicKey;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Resolves signing key based on id from JWT header */
@Singleton
public class OIDCSigningKeyResolver extends SigningKeyResolverAdapter {
private final JwkProvider jwkProvider;
private static final Logger LOG = LoggerFactory.getLogger(OIDCSigningKeyResolver.class);
@Inject
protected OIDCSigningKeyResolver(JwkProvider jwkProvider) {
this.jwkProvider = jwkProvider;
}
@Override
public Key resolveSigningKey(JwsHeader header, String plaintext) {
return getJwtPublicKey(header);
}
@Override
public Key resolveSigningKey(JwsHeader header, Claims claims) {
return getJwtPublicKey(header);
}
protected synchronized PublicKey getJwtPublicKey(JwsHeader<?> header) {
String kid = header.getKeyId();
if (kid == null) {
LOG.warn(
"'kid' is missing in the JWT token header. This is not possible to validate the token with OIDC provider keys");
throw new JwtException("'kid' is missing in the JWT token header.");
}
try {
return jwkProvider.get(kid).getPublicKey();
} catch (JwkException e) {
throw new JwtException(
"Error during the retrieval of the public key during JWT token validation", e);
}
}
}

View File

@ -0,0 +1,97 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.oidc.filter;
import static com.google.common.base.Strings.isNullOrEmpty;
import static org.eclipse.che.multiuser.oidc.OIDCInfoProvider.OIDC_USERNAME_CLAIM_SETTING;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtParser;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.model.user.User;
import org.eclipse.che.api.user.server.UserManager;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.commons.subject.SubjectImpl;
import org.eclipse.che.multiuser.api.authentication.commons.SessionStore;
import org.eclipse.che.multiuser.api.authentication.commons.filter.MultiUserEnvironmentInitializationFilter;
import org.eclipse.che.multiuser.api.authentication.commons.token.RequestTokenExtractor;
import org.eclipse.che.multiuser.api.permission.server.AuthorizedSubject;
import org.eclipse.che.multiuser.api.permission.server.PermissionChecker;
/**
* This filter uses given token directly. It's used for native Kubernetes user authentication.
* Requests without token or with invalid token are rejected.
*
* <p>It also makes sure that User is present in Che database. If not, it will create the User from
* JWT token claims. The username claim is configured with {@link
* org.eclipse.che.multiuser.oidc.OIDCInfoProvider#OIDC_USERNAME_CLAIM_SETTING}.
*/
@Singleton
public class OidcTokenInitializationFilter
extends MultiUserEnvironmentInitializationFilter<Jws<Claims>> {
private static final String EMAIL_CLAIM = "email";
private static final String NAME_CLAIM = "name";
protected static final String DEFAULT_USERNAME_CLAIM = NAME_CLAIM;
private final JwtParser jwtParser;
private final PermissionChecker permissionChecker;
private final UserManager userManager;
private final String usernameClaim;
@Inject
public OidcTokenInitializationFilter(
PermissionChecker permissionChecker,
JwtParser jwtParser,
SessionStore sessionStore,
RequestTokenExtractor tokenExtractor,
UserManager userManager,
@Nullable @Named(OIDC_USERNAME_CLAIM_SETTING) String usernameClaim) {
super(sessionStore, tokenExtractor);
this.permissionChecker = permissionChecker;
this.jwtParser = jwtParser;
this.userManager = userManager;
this.usernameClaim = isNullOrEmpty(usernameClaim) ? DEFAULT_USERNAME_CLAIM : usernameClaim;
}
@Override
protected Optional<Jws<Claims>> processToken(String token) {
return Optional.ofNullable(jwtParser.parseClaimsJws(token));
}
@Override
protected String getUserId(Jws<Claims> processedToken) {
return processedToken.getBody().getSubject();
}
@Override
protected Subject extractSubject(String token, Jws<Claims> processedToken) {
try {
Claims claims = processedToken.getBody();
User user =
userManager.getOrCreateUser(
claims.getSubject(),
claims.get(EMAIL_CLAIM, String.class),
claims.get(usernameClaim, String.class));
return new AuthorizedSubject(
new SubjectImpl(user.getName(), user.getId(), token, false), permissionChecker);
} catch (ServerException | ConflictException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,149 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.oidc.filter;
import static org.eclipse.che.multiuser.oidc.filter.OidcTokenInitializationFilter.DEFAULT_USERNAME_CLAIM;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtParser;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.model.user.User;
import org.eclipse.che.api.user.server.UserManager;
import org.eclipse.che.multiuser.api.authentication.commons.SessionStore;
import org.eclipse.che.multiuser.api.authentication.commons.token.RequestTokenExtractor;
import org.eclipse.che.multiuser.api.permission.server.PermissionChecker;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(MockitoTestNGListener.class)
public class OidcTokenInitializationFilterTest {
@Mock private PermissionChecker permissionsChecker;
@Mock private JwtParser jwtParser;
@Mock private SessionStore sessionStore;
@Mock private RequestTokenExtractor tokenExtractor;
@Mock private UserManager userManager;
private final String usernameClaim = "blabolClaim";
private final String TEST_USERNAME = "jondoe";
private final String TEST_USER_EMAIL = "jon@doe";
private final String TEST_USERID = "jon1337";
private final String TEST_TOKEN = "abcToken";
@Mock private Jws<Claims> jwsClaims;
@Mock private Claims claims;
OidcTokenInitializationFilter tokenInitializationFilter;
@BeforeMethod
public void setUp() {
tokenInitializationFilter =
new OidcTokenInitializationFilter(
permissionsChecker,
jwtParser,
sessionStore,
tokenExtractor,
userManager,
usernameClaim);
lenient().when(jwsClaims.getBody()).thenReturn(claims);
lenient().when(claims.getSubject()).thenReturn(TEST_USERID);
lenient().when(claims.get("email", String.class)).thenReturn(TEST_USER_EMAIL);
lenient().when(claims.get(usernameClaim, String.class)).thenReturn(TEST_USERNAME);
}
@Test
public void testProcessToken() {
when(jwtParser.parseClaimsJws(TEST_TOKEN)).thenReturn(jwsClaims);
var returnedClaims = tokenInitializationFilter.processToken(TEST_TOKEN).get();
assertEquals(returnedClaims, jwsClaims);
verify(jwtParser).parseClaimsJws(TEST_TOKEN);
}
@Test
public void testProcessEmptyToken() {
var returnedClaims = tokenInitializationFilter.processToken("");
assertTrue(returnedClaims.isEmpty());
}
@Test
public void testGetUserId() {
var userId = tokenInitializationFilter.getUserId(jwsClaims);
assertEquals(userId, TEST_USERID);
}
@Test
public void testExtractSubject() throws ServerException, ConflictException {
User createdUser = mock(User.class);
when(createdUser.getId()).thenReturn(TEST_USERID);
when(createdUser.getName()).thenReturn(TEST_USERNAME);
when(userManager.getOrCreateUser(TEST_USERID, TEST_USER_EMAIL, TEST_USERNAME))
.thenReturn(createdUser);
var subject = tokenInitializationFilter.extractSubject(TEST_TOKEN, jwsClaims);
assertEquals(subject.getUserId(), TEST_USERID);
assertEquals(subject.getUserName(), TEST_USERNAME);
assertEquals(subject.getToken(), TEST_TOKEN);
verify(userManager).getOrCreateUser(TEST_USERID, TEST_USER_EMAIL, TEST_USERNAME);
}
@Test(dataProvider = "usernameClaims")
public void testDefaultUsernameClaimWhenEmpty(String customUsernameClaim)
throws ServerException, ConflictException {
tokenInitializationFilter =
new OidcTokenInitializationFilter(
permissionsChecker,
jwtParser,
sessionStore,
tokenExtractor,
userManager,
customUsernameClaim);
User createdUser = mock(User.class);
when(createdUser.getId()).thenReturn(TEST_USERID);
when(createdUser.getName()).thenReturn(TEST_USERNAME);
when(userManager.getOrCreateUser(TEST_USERID, TEST_USER_EMAIL, TEST_USERNAME))
.thenReturn(createdUser);
when(claims.get(DEFAULT_USERNAME_CLAIM, String.class)).thenReturn(TEST_USERNAME);
var subject = tokenInitializationFilter.extractSubject(TEST_TOKEN, jwsClaims);
assertEquals(subject.getUserId(), TEST_USERID);
assertEquals(subject.getUserName(), TEST_USERNAME);
assertEquals(subject.getToken(), TEST_TOKEN);
verify(userManager).getOrCreateUser(TEST_USERID, TEST_USER_EMAIL, TEST_USERNAME);
verify(claims).get(DEFAULT_USERNAME_CLAIM, String.class);
verify(claims, never()).get(usernameClaim, String.class);
}
@DataProvider
public static Object[][] usernameClaims() {
return new Object[][] {{""}, {null}};
}
}

View File

@ -32,5 +32,6 @@
<module>machine-auth</module>
<module>personal-account</module>
<module>integration-tests</module>
<module>oidc</module>
</modules>
</project>

15
pom.xml
View File

@ -271,6 +271,11 @@
<artifactId>logging-interceptor</artifactId>
<version>${com.squareup.okhttp3.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>${com.squareup.okhttp3.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
@ -307,6 +312,11 @@
<artifactId>commons-lang</artifactId>
<version>${commons-lang.version}</version>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>openshift-server-mock</artifactId>
<version>${io.fabric8.kubernetes-client}</version>
</dependency>
<dependency>
<groupId>io.github.mweirauch</groupId>
<artifactId>micrometer-jvm-extras</artifactId>
@ -1115,6 +1125,11 @@
<artifactId>che-multiuser-machine-authentication-shared</artifactId>
<version>${che.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-oidc</artifactId>
<version>${che.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-permission-devfile</artifactId>

View File

@ -24,10 +24,6 @@
<name>Che Core :: API :: InfraProxy</name>
<description>Provides direct HTTP access to the underlying infrastructure web API.</description>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>

View File

@ -11,9 +11,7 @@
*/
package org.eclipse.che.api.infraproxy.server;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
@ -24,7 +22,6 @@ import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.IOException;
import java.io.InputStream;
@ -52,39 +49,17 @@ public class InfrastructureApiService extends Service {
private final boolean allowed;
private final RuntimeInfrastructure runtimeInfrastructure;
private final ObjectMapper mapper;
@Context private MediaType mediaType;
private static boolean determineAllowed(
String infra, String identityProvider, boolean allowedForKubernetes) {
if ("openshift".equals(infra)) {
return identityProvider != null && identityProvider.startsWith("openshift");
}
return allowedForKubernetes;
private static boolean determineAllowed(String identityProvider) {
return identityProvider != null;
}
@Inject
public InfrastructureApiService(
@Nullable @Named("che.infra.openshift.oauth_identity_provider") String identityProvider,
@Named("che.infra.kubernetes.enable_unsupported_k8s") boolean allowedForKubernetes,
RuntimeInfrastructure runtimeInfrastructure) {
this(
System.getenv("CHE_INFRASTRUCTURE_ACTIVE"),
allowedForKubernetes,
identityProvider,
runtimeInfrastructure);
}
@VisibleForTesting
InfrastructureApiService(
String infraName,
boolean allowedForKubernetes,
String identityProvider,
RuntimeInfrastructure infra) {
this.runtimeInfrastructure = infra;
this.mapper = new ObjectMapper();
this.allowed = determineAllowed(infraName, identityProvider, allowedForKubernetes);
this.runtimeInfrastructure = runtimeInfrastructure;
this.allowed = determineAllowed(identityProvider);
}
@GET

View File

@ -37,49 +37,13 @@ public class InfrastructureApiServiceTest {
@BeforeMethod
public void setup() throws Exception {
apiService =
new InfrastructureApiService("openshift", false, "openshift-identityProvider", infra);
}
@Test
public void testFailsAuthWhenNotAllowedForKubernetesAndNotOnOpenShift() throws Exception {
// given
apiService =
new InfrastructureApiService("not-openshift", false, "openshift-identityProvider", infra);
// when
Response response =
given()
.contentType("application/json; charset=utf-8")
.when()
.get("/unsupported/k8s/nazdar/");
// then
assertEquals(response.getStatusCode(), 403);
}
@Test
public void testFailsAuthWhenNotUsingOpenShiftIdentityProvider() throws Exception {
// given
apiService =
new InfrastructureApiService("openshift", false, "not-openshift-identityProvider", infra);
// when
Response response =
given()
.contentType("application/json; charset=utf-8")
.when()
.get("/unsupported/k8s/nazdar/");
// then
assertEquals(response.getStatusCode(), 403);
apiService = new InfrastructureApiService("openshift-identityProvider", infra);
}
@Test
public void testResolvesCallWhenAllowedForKubernetesOnKubernetes() throws Exception {
// given
apiService =
new InfrastructureApiService("kubernetes", true, "not-openshift-identityProvider", infra);
apiService = new InfrastructureApiService("not-openshift-identityProvider", infra);
when(infra.sendDirectInfrastructureRequest(any(), any(), any(), any()))
.thenReturn(
jakarta.ws.rs.core.Response.ok()
@ -98,24 +62,6 @@ public class InfrastructureApiServiceTest {
assertEquals(response.getContentType(), "application/json;charset=utf-8");
}
@Test
public void testFailsAuthWhenAllowedForKubernetesOnOpenshiftWithNonOpenshiftIdentityProvider()
throws Exception {
// given
apiService =
new InfrastructureApiService("openshift", true, "not-openshift-identityProvider", infra);
// when
Response response =
given()
.contentType("application/json; charset=utf-8")
.when()
.get("/unsupported/k8s/nazdar/");
// then
assertEquals(response.getStatusCode(), 403);
}
@Test
public void testGet() throws Exception {
// given