diff --git a/assembly/assembly-wsmaster-war/pom.xml b/assembly/assembly-wsmaster-war/pom.xml index 93702343c0..fc31271294 100644 --- a/assembly/assembly-wsmaster-war/pom.xml +++ b/assembly/assembly-wsmaster-war/pom.xml @@ -35,6 +35,10 @@ ch.qos.logback logback-classic + + com.auth0 + jwks-rsa + com.google.guava guava @@ -275,6 +279,10 @@ org.eclipse.che.multiuser che-multiuser-machine-authentication + + org.eclipse.che.multiuser + che-multiuser-oidc + org.eclipse.che.multiuser che-multiuser-permission-devfile @@ -454,6 +462,7 @@ com.google.guava:guava org.everrest:everrest-core io.jsonwebtoken:jjwt-jackson + io.jsonwebtoken:jjwt-impl diff --git a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java index 649aacaac8..19cb76512d 100644 --- a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java +++ b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java @@ -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()); diff --git a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterServletModule.java b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterServletModule.java index ffdcf0776a..027c7dddd7 100644 --- a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterServletModule.java +++ b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterServletModule.java @@ -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."); } diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties index 5b3228fc0b..47f9ada732 100644 --- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties @@ -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 diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/multiuser.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/multiuser.properties index b88cff9ffb..e7735cab73 100644 --- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/multiuser.properties +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/multiuser.properties @@ -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. diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che_aliases.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che_aliases.properties index 4b82be1fdc..21ca4cb661 100644 --- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che_aliases.properties +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che_aliases.properties @@ -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 diff --git a/infrastructures/kubernetes/pom.xml b/infrastructures/kubernetes/pom.xml index 2c9acb7559..3b404fb72d 100644 --- a/infrastructures/kubernetes/pom.xml +++ b/infrastructures/kubernetes/pom.xml @@ -220,6 +220,11 @@ h2 test + + com.squareup.okhttp3 + mockwebserver + test + io.fabric8 kubernetes-server-mock diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/CheServerKubernetesClientFactory.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/CheServerKubernetesClientFactory.java index 298ae34f9e..c532471e85 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/CheServerKubernetesClientFactory.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/CheServerKubernetesClientFactory.java @@ -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, diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesClientFactory.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesClientFactory.java index 13b93fd2c1..6554c3aaa7 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesClientFactory.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesClientFactory.java @@ -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(); diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java index f520c54734..c773d9bce7 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java @@ -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 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); diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesRuntimeContext.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesRuntimeContext.java index b14815bf6f..964ec4c6cb 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesRuntimeContext.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesRuntimeContext.java @@ -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 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; diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespace.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespace.java index 19de4ac729..29d269ff67 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespace.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespace.java @@ -145,7 +145,7 @@ public class KubernetesNamespace { */ void prepare(boolean canCreate, Map labels, Map annotations) throws InfrastructureException { - KubernetesClient client = clientFactory.create(workspaceId); + KubernetesClient client = cheSAClientFactory.create(workspaceId); Namespace namespace = get(name, client); if (namespace == null) { diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java index 79acb00ccf..01402f7b42 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java @@ -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 namespaceLabels; protected final Map namespaceAnnotations; - private final String serviceAccountName; - private final Set clusterRoleNames; private final KubernetesClientFactory clientFactory; private final KubernetesClientFactory cheClientFactory; private final boolean namespaceCreationAllowed; private final UserManager userManager; private final PreferenceManager preferenceManager; + protected final Set 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 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 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 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 workspaceNamespaces = - clientFactory.create().namespaces().withLabels(namespaceLabels).list().getItems(); + cheClientFactory.create().namespaces().withLabels(namespaceLabels).list().getItems(); if (!workspaceNamespaces.isEmpty()) { Map 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 getClusterRoleNames() { - return clusterRoleNames; - } } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/CredentialsSecretConfigurator.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/CredentialsSecretConfigurator.java new file mode 100644 index 0000000000..8e32f49076 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/CredentialsSecretConfigurator.java @@ -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); + } + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/NamespaceConfigurator.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/NamespaceConfigurator.java index e42e9fb1d4..d91e90eab5 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/NamespaceConfigurator.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/NamespaceConfigurator.java @@ -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; } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/PreferencesConfigMapConfigurator.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/PreferencesConfigMapConfigurator.java new file mode 100644 index 0000000000..bc4b08eeb6 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/PreferencesConfigMapConfigurator.java @@ -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); + } + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPermissionConfigurator.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPermissionConfigurator.java new file mode 100644 index 0000000000..2889a3a696 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPermissionConfigurator.java @@ -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 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 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()); + } + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPreferencesConfigurator.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPreferencesConfigurator.java index fb69ac251b..593a8feb13 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPreferencesConfigurator.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPreferencesConfigurator.java @@ -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( diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserProfileConfigurator.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserProfileConfigurator.java index ecfe5a2845..8f94e84161 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserProfileConfigurator.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserProfileConfigurator.java @@ -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); diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/WorkspaceServiceAccountConfigurator.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/WorkspaceServiceAccountConfigurator.java new file mode 100644 index 0000000000..1c9e7f2ccf --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/WorkspaceServiceAccountConfigurator.java @@ -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 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); + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/NamespaceProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/NamespaceProvisioner.java index eb96ea011a..b9388893bf 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/NamespaceProvisioner.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/provision/NamespaceProvisioner.java @@ -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 namespaceConfigurators; @Inject - public NamespaceProvisioner( - KubernetesNamespaceFactory namespaceFactory, - Set 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())); } } diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java index ab379d0a33..e09f22415d 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java @@ -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( - "", - "", "-che", true, true, true, NAMESPACE_LABELS, NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -199,14 +205,13 @@ public class KubernetesNamespaceFactoryTest { namespaceFactory = new KubernetesNamespaceFactory( - "", - "", "-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-", true, true, true, NAMESPACE_LABELS, NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -246,14 +250,13 @@ public class KubernetesNamespaceFactoryTest { throws Exception { namespaceFactory = new KubernetesNamespaceFactory( - "", - "", "-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( - "", - "", "-che", true, true, true, NAMESPACE_LABELS, NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -364,14 +365,13 @@ public class KubernetesNamespaceFactoryTest { namespaceFactory = new KubernetesNamespaceFactory( - "", - "", "-che", true, true, true, NAMESPACE_LABELS, NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -395,14 +395,13 @@ public class KubernetesNamespaceFactoryTest { namespaceFactory = new KubernetesNamespaceFactory( - "", - "", "-che", true, true, true, NAMESPACE_LABELS, NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -429,14 +428,13 @@ public class KubernetesNamespaceFactoryTest { .build()); namespaceFactory = new KubernetesNamespaceFactory( - "", - "", "-che", true, true, true, NAMESPACE_LABELS, NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -458,14 +456,13 @@ public class KubernetesNamespaceFactoryTest { namespaceFactory = new KubernetesNamespaceFactory( - "", - "", "-che", true, true, true, NAMESPACE_LABELS, NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -489,28 +486,27 @@ public class KubernetesNamespaceFactoryTest { namespaceFactory = spy( new KubernetesNamespaceFactory( - "", - "", "-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( - "", - "", "-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 namespaceConfigurators = Set.of(configurator1, configurator2); + namespaceFactory = spy( new KubernetesNamespaceFactory( - "", - "", "-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( + "-che", + true, + true, + true, + NAMESPACE_LABELS, + NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -607,14 +641,13 @@ public class KubernetesNamespaceFactoryTest { namespaceFactory = spy( new KubernetesNamespaceFactory( - "", - "", "-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( - "", - "", "-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( - "", - "", "-che", true, true, true, NAMESPACE_LABELS, NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -700,14 +731,13 @@ public class KubernetesNamespaceFactoryTest { namespaceFactory = spy( new KubernetesNamespaceFactory( - "", - "", "-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( - "", - "", "-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", - "", "-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", "-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", - "", "-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 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( + "-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-", - 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-", - true, - true, - true, - NAMESPACE_LABELS, - NAMESPACE_ANNOTATIONS, - clientFactory, - cheClientFactory, - userManager, - preferenceManager, - pool); - Set 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-", 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-", true, true, true, NAMESPACE_LABELS, NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -1056,14 +1098,13 @@ public class KubernetesNamespaceFactoryTest { throws Exception { namespaceFactory = new KubernetesNamespaceFactory( - "", - "", "che--", true, true, true, NAMESPACE_LABELS, NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -1088,14 +1129,13 @@ public class KubernetesNamespaceFactoryTest { throws Exception { namespaceFactory = new KubernetesNamespaceFactory( - "", - "", "che--", true, true, true, NAMESPACE_LABELS, NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -1120,14 +1160,13 @@ public class KubernetesNamespaceFactoryTest { namespaceFactory = spy( new KubernetesNamespaceFactory( - "", - "", "che-", true, true, true, NAMESPACE_LABELS, NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -1148,14 +1187,13 @@ public class KubernetesNamespaceFactoryTest { throws Exception { namespaceFactory = new KubernetesNamespaceFactory( - "", - "", "che-", 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-", true, true, true, NAMESPACE_LABELS, NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -1229,14 +1266,13 @@ public class KubernetesNamespaceFactoryTest { namespaceFactory = new KubernetesNamespaceFactory( - "", - "", "-che", true, true, true, NAMESPACE_LABELS, NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -1256,14 +1292,13 @@ public class KubernetesNamespaceFactoryTest { namespaceFactory = spy( new KubernetesNamespaceFactory( - "", - "", "-che", false, true, true, NAMESPACE_LABELS, NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -1296,14 +1331,13 @@ public class KubernetesNamespaceFactoryTest { namespaceFactory = spy( new KubernetesNamespaceFactory( - "", - "", "-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( - "", - "", "-cha-cha-cha", false, true, true, NAMESPACE_LABELS, NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -1386,14 +1419,13 @@ public class KubernetesNamespaceFactoryTest { namespaceFactory = new KubernetesNamespaceFactory( - "", - "", "-che", true, true, true, "try_placeholder_here=", NAMESPACE_ANNOTATIONS, + emptySet(), clientFactory, cheClientFactory, userManager, @@ -1412,14 +1444,13 @@ public class KubernetesNamespaceFactoryTest { namespaceFactory = spy( new KubernetesNamespaceFactory( - "", - "", "-che", true, true, true, NAMESPACE_LABELS, "try_placeholder_here=", + emptySet(), clientFactory, cheClientFactory, userManager, @@ -1444,14 +1475,13 @@ public class KubernetesNamespaceFactoryTest { public void normalizeTest(String raw, String expected) { namespaceFactory = new KubernetesNamespaceFactory( - "", - "", "-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-", 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); } } diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceTest.java index 964bf2ce3b..c31ffb0586 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceTest.java @@ -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(); diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/CredentialsSecretConfiguratorTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/CredentialsSecretConfiguratorTest.java new file mode 100644 index 0000000000..bc286fd66e --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/CredentialsSecretConfiguratorTest.java @@ -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"); + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/PreferencesConfigMapConfiguratorTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/PreferencesConfigMapConfiguratorTest.java new file mode 100644 index 0000000000..a1aa2cc40d --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/PreferencesConfigMapConfiguratorTest.java @@ -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"); + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPermissionConfiguratorTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPermissionConfiguratorTest.java new file mode 100644 index 0000000000..8e6ec82bca --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPermissionConfiguratorTest.java @@ -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); + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPreferencesConfiguratorTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPreferencesConfiguratorTest.java index 8bf7f86989..e66b945183 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPreferencesConfiguratorTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserPreferencesConfiguratorTest.java @@ -86,7 +86,7 @@ public class UserPreferencesConfiguratorTest { @Test public void shouldCreatePreferencesSecret() throws InfrastructureException { - userPreferencesConfigurator.configure(context); + userPreferencesConfigurator.configure(context, USER_NAMESPACE); List 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."); } diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserProfileConfiguratorTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserProfileConfiguratorTest.java index 33d175742f..07d4ff1c79 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserProfileConfiguratorTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/UserProfileConfiguratorTest.java @@ -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 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."); } } diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/WorkspaceServiceAccountConfiguratorTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/WorkspaceServiceAccountConfiguratorTest.java new file mode 100644 index 0000000000..eaf57870af --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/WorkspaceServiceAccountConfiguratorTest.java @@ -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 + } +} diff --git a/infrastructures/openshift/pom.xml b/infrastructures/openshift/pom.xml index dfeae777d1..e571cc4e5f 100644 --- a/infrastructures/openshift/pom.xml +++ b/infrastructures/openshift/pom.xml @@ -134,6 +134,10 @@ org.eclipse.che.multiuser che-multiuser-keycloak-shared + + org.eclipse.che.multiuser + che-multiuser-oidc + org.slf4j slf4j-api @@ -148,6 +152,22 @@ logback-classic test + + com.squareup.okhttp3 + mockwebserver + test + + + io.fabric8 + openshift-server-mock + test + + + junit-jupiter-api + org.junit.jupiter + + + org.eclipse.che.core che-core-api-dto diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftClientFactory.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftClientFactory.java index 73805fc824..4ce931f2ab 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftClientFactory.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftClientFactory.java @@ -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. - * - *

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); } diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java index 0bfd6c80f5..17e6932c5e 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java @@ -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); diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/KeycloakProviderConfigFactory.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/KeycloakProviderConfigFactory.java index 4d957a38de..87813445c7 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/KeycloakProviderConfigFactory.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/KeycloakProviderConfigFactory.java @@ -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; diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/OpenshiftTokenInitializationFilter.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/OpenshiftTokenInitializationFilter.java index 9d4862df58..8aa42afefc 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/OpenshiftTokenInitializationFilter.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/OpenshiftTokenInitializationFilter.java @@ -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. - * - *

{@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 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()"); - } } diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProject.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProject.java index 9d202ade4d..9009ea35d3 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProject.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProject.java @@ -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; } diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java index 604c011dd8..673ccb3b81 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java @@ -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 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 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 fetchNamespace(String name) throws InfrastructureException { diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftWorkspaceServiceAccount.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftWorkspaceServiceAccount.java index 7870a2d299..2933fc0af0 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftWorkspaceServiceAccount.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftWorkspaceServiceAccount.java @@ -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 { - OpenShiftWorkspaceServiceAccount( + public OpenShiftWorkspaceServiceAccount( String workspaceId, String projectName, String serviceAccountName, diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftStopWorkspaceRoleProvisioner.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/configurator/OpenShiftStopWorkspaceRoleConfigurator.java similarity index 80% rename from infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftStopWorkspaceRoleProvisioner.java rename to infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/configurator/OpenShiftStopWorkspaceRoleConfigurator.java index 0db4b86d60..91d09ea080 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftStopWorkspaceRoleProvisioner.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/configurator/OpenShiftStopWorkspaceRoleConfigurator.java @@ -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"; diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/configurator/OpenShiftWorkspaceServiceAccountConfigurator.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/configurator/OpenShiftWorkspaceServiceAccountConfigurator.java new file mode 100644 index 0000000000..5c18666e20 --- /dev/null +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/configurator/OpenShiftWorkspaceServiceAccountConfigurator.java @@ -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 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); + } +} diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/KeycloakProviderConfigFactoryTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/KeycloakProviderConfigFactoryTest.java index 501c6bcf2c..d85784d030 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/KeycloakProviderConfigFactoryTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/KeycloakProviderConfigFactoryTest.java @@ -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; diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/OpenshiftTokenInitializationFilterTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/OpenshiftTokenInitializationFilterTest.java index a649dde5af..547c37f419 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/OpenshiftTokenInitializationFilterTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/OpenshiftTokenInitializationFilterTest.java @@ -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); diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java index e7dfa27233..5182d2c945 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java @@ -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, "-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, "-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( - "", - "", "-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( - "", - "", "-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( - "", - "", "-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, "-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, "-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, "-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, "-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, "-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, "-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 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, "-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 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, "-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 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, "-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 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, - "-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, "-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, - "-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( - "", - "", "-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, "-che", true, true, @@ -930,10 +807,10 @@ public class OpenShiftProjectFactoryTest { "try_placeholder_here=", 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, "-che", true, true, @@ -960,10 +834,10 @@ public class OpenShiftProjectFactoryTest { NAMESPACE_LABELS, "try_placeholder_here=", 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 namespaceConfigurators = Set.of(configurator1, configurator2); + + projectFactory = + spy( + new OpenShiftProjectFactory( + "-che", + true, + true, + true, + NAMESPACE_LABELS, + "try_placeholder_here=", + 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 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 { diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftStopWorkspaceRoleProvisionerTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/configurator/OpenShiftStopWorkspaceRoleConfiguratorTest.java similarity index 82% rename from infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftStopWorkspaceRoleProvisionerTest.java rename to infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/configurator/OpenShiftStopWorkspaceRoleConfiguratorTest.java index f9694ab164..e59f0888bc 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/provision/OpenShiftStopWorkspaceRoleProvisionerTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/configurator/OpenShiftStopWorkspaceRoleConfiguratorTest.java @@ -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} * *

#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(); + } } diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/configurator/OpenShiftWorkspaceServiceAccountConfiguratorTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/configurator/OpenShiftWorkspaceServiceAccountConfiguratorTest.java new file mode 100644 index 0000000000..fef482236b --- /dev/null +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/configurator/OpenShiftWorkspaceServiceAccountConfiguratorTest.java @@ -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()); + } +} diff --git a/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/filter/MultiUserEnvironmentInitializationFilter.java b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/filter/MultiUserEnvironmentInitializationFilter.java index c100be3417..85206f81c4 100644 --- a/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/filter/MultiUserEnvironmentInitializationFilter.java +++ b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/filter/MultiUserEnvironmentInitializationFilter.java @@ -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; *

  • Set subject for current request into {@link EnvironmentContext} * * + *

    {@link MultiUserEnvironmentInitializationFilter#UNAUTHORIZED_ENDPOINT_PATHS} is list of + * unauthenticated paths, that are allowed without token. + * * @param 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 implements Fil private static final Logger LOG = LoggerFactory.getLogger(MultiUserEnvironmentInitializationFilter.class); + private static final List UNAUTHORIZED_ENDPOINT_PATHS = + Collections.singletonList("/system/state"); + private final SessionStore sessionStore; private final RequestTokenExtractor tokenExtractor; @@ -197,9 +205,23 @@ public abstract class MultiUserEnvironmentInitializationFilter 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. diff --git a/multiuser/api/che-multiuser-api-authentication-commons/src/test/java/org/eclipse/che/multiuser/api/authentication/commons/filter/MultiUserEnvironmentInitializationFilterTest.java b/multiuser/api/che-multiuser-api-authentication-commons/src/test/java/org/eclipse/che/multiuser/api/authentication/commons/filter/MultiUserEnvironmentInitializationFilterTest.java index 62847c8e7c..10c94ddc94 100644 --- a/multiuser/api/che-multiuser-api-authentication-commons/src/test/java/org/eclipse/che/multiuser/api/authentication/commons/filter/MultiUserEnvironmentInitializationFilterTest.java +++ b/multiuser/api/che-multiuser-api-authentication-commons/src/test/java/org/eclipse/che/multiuser/api/authentication/commons/filter/MultiUserEnvironmentInitializationFilterTest.java @@ -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()); + } } diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/pom.xml b/multiuser/keycloak/che-multiuser-keycloak-server/pom.xml index efc65be2bf..e1dfe7620e 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/pom.xml +++ b/multiuser/keycloak/che-multiuser-keycloak-server/pom.xml @@ -30,14 +30,6 @@ com.auth0 jwks-rsa - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - com.google.code.gson gson @@ -102,10 +94,6 @@ org.eclipse.che.core che-core-commons-annotations - - org.eclipse.che.core - che-core-commons-inject - org.eclipse.che.core che-core-commons-lang @@ -130,6 +118,10 @@ org.eclipse.che.multiuser che-multiuser-machine-authentication-shared + + org.eclipse.che.multiuser + che-multiuser-oidc + org.eclipse.che.multiuser che-multiuser-personal-account diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitializationFilter.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitializationFilter.java index 84bf08d59f..92d8f324a6 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitializationFilter.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitializationFilter.java @@ -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(); diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakOIDCInfoProvider.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakOIDCInfoProvider.java new file mode 100644 index 0000000000..59c89deebf --- /dev/null +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakOIDCInfoProvider.java @@ -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(); + } +} diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakProfileRetriever.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakProfileRetriever.java index 05d54cf96c..7328022908 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakProfileRetriever.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakProfileRetriever.java @@ -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; diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakServiceClient.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakServiceClient.java index b645e53fe7..7daee72683 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakServiceClient.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakServiceClient.java @@ -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("

    (\\s*)

    (.+?)

    "); private static final Gson gson = new Gson(); - private JwtParser jwtParser; + private final JwtParser jwtParser; @Inject public KeycloakServiceClient( diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettings.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettings.java index c1031596bd..e805c98b2a 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettings.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettings.java @@ -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 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()); } diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSigningKeyResolver.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSigningKeyResolver.java index af02cd466e..aa32c9e61c 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSigningKeyResolver.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSigningKeyResolver.java @@ -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); - } - } } diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakModule.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakModule.java index 6e5beb3f2e..543aa3e132 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakModule.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakModule.java @@ -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); diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitializationFilterTest.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitializationFilterTest.java index 8ada9dd611..a1732ecf04 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitializationFilterTest.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitializationFilterTest.java @@ -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 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 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 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); diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakServiceClientTest.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakServiceClientTest.java index e0e76b4ee9..befe584e2f 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakServiceClientTest.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakServiceClientTest.java @@ -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; diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettingsTest.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettingsTest.java index 962055b5b7..6138eae411 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettingsTest.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettingsTest.java @@ -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 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 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); diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/OIDCInfoProviderTest.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/OIDCInfoProviderTest.java index 815c7a4266..d81238932f 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/OIDCInfoProviderTest.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/OIDCInfoProviderTest.java @@ -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(); } } diff --git a/multiuser/keycloak/che-multiuser-keycloak-shared/src/main/java/org/eclipse/che/multiuser/keycloak/shared/KeycloakConstants.java b/multiuser/keycloak/che-multiuser-keycloak-shared/src/main/java/org/eclipse/che/multiuser/keycloak/shared/KeycloakConstants.java index e9b9313ebe..e098abde70 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-shared/src/main/java/org/eclipse/che/multiuser/keycloak/shared/KeycloakConstants.java +++ b/multiuser/keycloak/che-multiuser-keycloak-shared/src/main/java/org/eclipse/che/multiuser/keycloak/shared/KeycloakConstants.java @@ -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; - } } diff --git a/multiuser/keycloak/che-multiuser-keycloak-user-remover/pom.xml b/multiuser/keycloak/che-multiuser-keycloak-user-remover/pom.xml index 7dba7744c8..dfa9b91b39 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-user-remover/pom.xml +++ b/multiuser/keycloak/che-multiuser-keycloak-user-remover/pom.xml @@ -79,6 +79,10 @@ org.eclipse.che.multiuser che-multiuser-keycloak-shared + + org.eclipse.che.multiuser + che-multiuser-oidc + org.slf4j slf4j-api diff --git a/multiuser/keycloak/che-multiuser-keycloak-user-remover/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakUserRemover.java b/multiuser/keycloak/che-multiuser-keycloak-user-remover/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakUserRemover.java index 732f692f48..16207a0844 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-user-remover/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakUserRemover.java +++ b/multiuser/keycloak/che-multiuser-keycloak-user-remover/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakUserRemover.java @@ -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; diff --git a/multiuser/keycloak/che-multiuser-keycloak-user-remover/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakUserRemoverTest.java b/multiuser/keycloak/che-multiuser-keycloak-user-remover/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakUserRemoverTest.java index f308c77610..f96876e2c1 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-user-remover/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakUserRemoverTest.java +++ b/multiuser/keycloak/che-multiuser-keycloak-user-remover/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakUserRemoverTest.java @@ -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; diff --git a/multiuser/oidc/pom.xml b/multiuser/oidc/pom.xml new file mode 100644 index 0000000000..0d6e3dc4f9 --- /dev/null +++ b/multiuser/oidc/pom.xml @@ -0,0 +1,104 @@ + + + + 4.0.0 + + che-multiuser-parent + org.eclipse.che.multiuser + 7.40.0-SNAPSHOT + + che-multiuser-oidc + jar + Che Multiuser :: OIDC + + + com.auth0 + jwks-rsa + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.google.guava + guava + + + io.jsonwebtoken + jjwt-api + + + org.eclipse.che.core + che-core-api-core + + + org.eclipse.che.core + che-core-api-model + + + org.eclipse.che.core + che-core-api-user + + + org.eclipse.che.core + che-core-commons-annotations + + + org.eclipse.che.core + che-core-commons-inject + + + org.eclipse.che.multiuser + che-multiuser-api-authentication-commons + + + org.eclipse.che.multiuser + che-multiuser-api-authorization + + + org.slf4j + slf4j-api + + + jakarta.inject + jakarta.inject-api + provided + + + jakarta.servlet + jakarta.servlet-api + provided + + + org.mockito + mockito-core + test + + + org.mockito + mockito-testng + test + + + org.testng + testng + test + + + diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/OIDCInfo.java b/multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/OIDCInfo.java similarity index 74% rename from multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/OIDCInfo.java rename to multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/OIDCInfo.java index eb76cd805a..fefa1c0f47 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/OIDCInfo.java +++ b/multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/OIDCInfo.java @@ -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 getEndSessionPublicEndpoint() { + return Optional.ofNullable(endSessionPublicEndpoint); + } } diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/OIDCInfoProvider.java b/multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/OIDCInfoProvider.java similarity index 73% rename from multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/OIDCInfoProvider.java rename to multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/OIDCInfoProvider.java index ad3ab74253..cb15930715 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/OIDCInfoProvider.java +++ b/multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/OIDCInfoProvider.java @@ -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 { - 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 { 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 { } 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 { 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 { + 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) { diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakJwkProvider.java b/multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/OIDCJwkProvider.java similarity index 79% rename from multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakJwkProvider.java rename to multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/OIDCJwkProvider.java index bcc0091728..0481b8b8e9 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakJwkProvider.java +++ b/multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/OIDCJwkProvider.java @@ -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 { +public class OIDCJwkProvider implements Provider { 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(); diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakJwtParserProvider.java b/multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/OIDCJwtParserProvider.java similarity index 58% rename from multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakJwtParserProvider.java rename to multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/OIDCJwtParserProvider.java index 5ba4737638..01ff5ac515 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakJwtParserProvider.java +++ b/multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/OIDCJwtParserProvider.java @@ -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 { - +public class OIDCJwtParserProvider implements Provider { 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 diff --git a/multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/OIDCSigningKeyResolver.java b/multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/OIDCSigningKeyResolver.java new file mode 100644 index 0000000000..acdd0a5363 --- /dev/null +++ b/multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/OIDCSigningKeyResolver.java @@ -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); + } + } +} diff --git a/multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/filter/OidcTokenInitializationFilter.java b/multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/filter/OidcTokenInitializationFilter.java new file mode 100644 index 0000000000..256011ec9b --- /dev/null +++ b/multiuser/oidc/src/main/java/org/eclipse/che/multiuser/oidc/filter/OidcTokenInitializationFilter.java @@ -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. + * + *

    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> { + 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> processToken(String token) { + return Optional.ofNullable(jwtParser.parseClaimsJws(token)); + } + + @Override + protected String getUserId(Jws processedToken) { + return processedToken.getBody().getSubject(); + } + + @Override + protected Subject extractSubject(String token, Jws 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); + } + } +} diff --git a/multiuser/oidc/src/test/java/org/eclipse/che/multiuser/oidc/filter/OidcTokenInitializationFilterTest.java b/multiuser/oidc/src/test/java/org/eclipse/che/multiuser/oidc/filter/OidcTokenInitializationFilterTest.java new file mode 100644 index 0000000000..57f5fd76a5 --- /dev/null +++ b/multiuser/oidc/src/test/java/org/eclipse/che/multiuser/oidc/filter/OidcTokenInitializationFilterTest.java @@ -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 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}}; + } +} diff --git a/multiuser/pom.xml b/multiuser/pom.xml index 349789dcde..c308cac5f7 100644 --- a/multiuser/pom.xml +++ b/multiuser/pom.xml @@ -32,5 +32,6 @@ machine-auth personal-account integration-tests + oidc diff --git a/pom.xml b/pom.xml index e45aaaa06a..bd4f8710a2 100644 --- a/pom.xml +++ b/pom.xml @@ -271,6 +271,11 @@ logging-interceptor ${com.squareup.okhttp3.version} + + com.squareup.okhttp3 + mockwebserver + ${com.squareup.okhttp3.version} + com.squareup.okhttp3 okhttp @@ -307,6 +312,11 @@ commons-lang ${commons-lang.version} + + io.fabric8 + openshift-server-mock + ${io.fabric8.kubernetes-client} + io.github.mweirauch micrometer-jvm-extras @@ -1115,6 +1125,11 @@ che-multiuser-machine-authentication-shared ${che.version} + + org.eclipse.che.multiuser + che-multiuser-oidc + ${che.version} + org.eclipse.che.multiuser che-multiuser-permission-devfile diff --git a/wsmaster/che-core-api-infraproxy/pom.xml b/wsmaster/che-core-api-infraproxy/pom.xml index 7b716e9e72..17113e33e8 100644 --- a/wsmaster/che-core-api-infraproxy/pom.xml +++ b/wsmaster/che-core-api-infraproxy/pom.xml @@ -24,10 +24,6 @@ Che Core :: API :: InfraProxy Provides direct HTTP access to the underlying infrastructure web API. - - com.fasterxml.jackson.core - jackson-databind - com.google.guava guava diff --git a/wsmaster/che-core-api-infraproxy/src/main/java/org/eclipse/che/api/infraproxy/server/InfrastructureApiService.java b/wsmaster/che-core-api-infraproxy/src/main/java/org/eclipse/che/api/infraproxy/server/InfrastructureApiService.java index f4d365dd65..42686e99c5 100644 --- a/wsmaster/che-core-api-infraproxy/src/main/java/org/eclipse/che/api/infraproxy/server/InfrastructureApiService.java +++ b/wsmaster/che-core-api-infraproxy/src/main/java/org/eclipse/che/api/infraproxy/server/InfrastructureApiService.java @@ -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 diff --git a/wsmaster/che-core-api-infraproxy/src/test/java/org/eclipse/che/api/infraproxy/server/InfrastructureApiServiceTest.java b/wsmaster/che-core-api-infraproxy/src/test/java/org/eclipse/che/api/infraproxy/server/InfrastructureApiServiceTest.java index 9d55f8cf92..15d710e43c 100644 --- a/wsmaster/che-core-api-infraproxy/src/test/java/org/eclipse/che/api/infraproxy/server/InfrastructureApiServiceTest.java +++ b/wsmaster/che-core-api-infraproxy/src/test/java/org/eclipse/che/api/infraproxy/server/InfrastructureApiServiceTest.java @@ -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