diff --git a/assembly-multiuser/assembly-ide-war/pom.xml b/assembly-multiuser/assembly-ide-war/pom.xml new file mode 100644 index 0000000000..a36524b432 --- /dev/null +++ b/assembly-multiuser/assembly-ide-war/pom.xml @@ -0,0 +1,272 @@ + + + + 4.0.0 + + che-assembly-parent + org.eclipse.che.assembly-multiuser + 5.19.0-SNAPSHOT + + assembly-ide-war + war + Che IDE Assembly Multiuser :: Compiling GWT Application + + ${project.build.directory}/generated-sources/gen + UTF-8 + + + + org.eclipse.che + assembly-ide-war + classes + + + org.eclipse.che + assembly-ide-war + war + + + org.eclipse.che.multiuser + che-multiuser-keycloak-ide + + + org.eclipse.che.multiuser + che-multiuser-machine-authentication-ide + + + + + + maven-resources-plugin + + + copy-web-resources + process-sources + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + analyze + + true + + + + + + + org.codehaus.mojo + exec-maven-plugin + + + gwt-xml + generate-sources + + java + + + org.eclipse.che.util.GwtXmlGenerator + + --rootDir=${generated.sources.directory} + --loggingEnabled=${gwt.log.enable} + + + + + extManager-client + generate-sources + + java + + + org.eclipse.che.util.ExtensionManagerGenerator + + --rootDir=${generated.sources.directory} + + + + + IDEInjector-client + generate-sources + + java + + + org.eclipse.che.util.IDEInjectorGenerator + + --rootDir=${generated.sources.directory} + + + + + DtoRegistry-client + generate-sources + + java + + + org.eclipse.che.util.DtoFactoryVisitorRegistryGenerator + + --rootDir=${generated.sources.directory} + + + + + + + org.eclipse.che.core + che-core-dyna-provider-generator-maven-plugin + ${che.version} + + + generate-sources + + generate + + + + + ${generated.sources.directory} + + + + + org.codehaus.mojo + gwt-maven-plugin + + + + compile + + + + + + + com.google.gwt + gwt-codeserver + ${com.google.gwt.version} + + + com.google.gwt + gwt-dev + ${com.google.gwt.version} + + + com.google.gwt + gwt-user + ${com.google.gwt.version} + + + + true + ${gwt.compiler.extraJvmArgs} + + org.eclipse.che.ide.IDE + + + + ${gwt.compiler.logLevel} + + + + org.apache.maven.plugins + maven-antrun-plugin + + + buildnumber + compile + + run + + + + revision = ${revision} + buildTime = ${timestamp} + version = + ${project.version} + + + + + + + org.apache.maven.plugins + maven-war-plugin + + + + org.eclipse.che + assembly-ide-war + war + + _app/IDE.html + _app/browserNotSupported.js + _app/favicon.ico + META-INF/context.xml + WEB-INF/rewrite.config + WEB-INF/web.xml + WEB-INF/classes/org/eclipse/che/*.class + + + + + %regex[WEB-INF\\lib\\(?!.*j2ee).*.jar] + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source + generate-sources + + add-source + + + + ${generated.sources.directory} + + + + + + + org.codehaus.mojo + buildnumber-maven-plugin + + + validate + + create + + + + + {0, date, yyyy-MM-dd HH:mm:ss} + revision + false + false + 16 + + + + + diff --git a/assembly-multiuser/assembly-main/pom.xml b/assembly-multiuser/assembly-main/pom.xml new file mode 100644 index 0000000000..23a370a018 --- /dev/null +++ b/assembly-multiuser/assembly-main/pom.xml @@ -0,0 +1,98 @@ + + + + 4.0.0 + + che-assembly-parent + org.eclipse.che.assembly-multiuser + 5.19.0-SNAPSHOT + + assembly-main + pom + Che IDE Assembly Multiuser :: Assemblies Tomcat + + + org.eclipse.che.assembly-multiuser + assembly-ide-war + war + + + org.eclipse.che.assembly-multiuser + assembly-wsagent-server + tar.gz + + + org.eclipse.che.assembly-multiuser + assembly-wsmaster-war + war + + + org.eclipse.che.dashboard + che-dashboard-war + war + + + org.postgresql + postgresql + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + make-assembly + package + + single + + + + + false + false + ${project.basedir}/src/assembly/assembly.xml + eclipse-che-${project.version} + posix + + + + org.apache.maven.plugins + maven-dependency-plugin + + + unpack-original-che-assembly + prepare-package + + unpack + + + + + org.eclipse.che + assembly-main + zip + true + ${project.build.directory}/dependency + + + + + + + + + diff --git a/assembly-multiuser/assembly-main/src/assembly/assembly.xml b/assembly-multiuser/assembly-main/src/assembly/assembly.xml new file mode 100644 index 0000000000..c5a5f8786e --- /dev/null +++ b/assembly-multiuser/assembly-main/src/assembly/assembly.xml @@ -0,0 +1,86 @@ + + + + tomcat-zip + + dir + zip + tar.gz + + + + false + false + tomcat/webapps + ROOT.war + + org.eclipse.che.assembly-multiuser:assembly-ide-war + + + + false + false + tomcat/webapps + wsmaster.war + + org.eclipse.che.assembly-multiuser:assembly-wsmaster-war + + + + false + false + lib + ws-agent.tar.gz + + org.eclipse.che.assembly-multiuser:assembly-wsagent-server + + + + false + false + tomcat/webapps + dashboard.war + + org.eclipse.che.dashboard:che-dashboard-war + + + + false + false + lib + + org.postgresql:postgresql + + + + + + ${project.build.directory}/dependency/eclipse-che-${che.version} + + + **/tomcat/webapps/ROOT.war + **/tomcat/webapps/wsmaster.war + **/lib/ws-agent.tar.gz + **/tomcat/webapps/dashboard.war + + + + ${project.build.directory}/keycloak/ + tomcat/lib + + + diff --git a/assembly-multiuser/assembly-wsagent-server/pom.xml b/assembly-multiuser/assembly-wsagent-server/pom.xml new file mode 100644 index 0000000000..829de0dc0d --- /dev/null +++ b/assembly-multiuser/assembly-wsagent-server/pom.xml @@ -0,0 +1,78 @@ + + + + 4.0.0 + + che-assembly-parent + org.eclipse.che.assembly-multiuser + 5.19.0-SNAPSHOT + + assembly-wsagent-server + pom + Che IDE Assembly Multiuser :: Agent Server + + + org.eclipse.che.assembly-multiuser + assembly-wsagent-war + war + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + make-assembly + package + + single + + + + + false + false + ${project.basedir}/src/assembly/assembly.xml + ext-server-${project.version} + + + + org.apache.maven.plugins + maven-dependency-plugin + + + unpack-tomcat + prepare-package + + unpack + + + + + org.eclipse.che + assembly-wsagent-server + zip + true + ${project.build.directory}/dependency + + + + + + + + + diff --git a/assembly-multiuser/assembly-wsagent-server/src/assembly/assembly.xml b/assembly-multiuser/assembly-wsagent-server/src/assembly/assembly.xml new file mode 100644 index 0000000000..fbf8e22305 --- /dev/null +++ b/assembly-multiuser/assembly-wsagent-server/src/assembly/assembly.xml @@ -0,0 +1,41 @@ + + + tomcat-zip + + zip + tar.gz + + false + + + false + false + webapps + ROOT.war + + org.eclipse.che.assembly-multiuser:assembly-wsagent-war + + + + + + ${project.build.directory}/dependency + + + **/webapps/ROOT.war + + + + diff --git a/assembly-multiuser/assembly-wsagent-war/pom.xml b/assembly-multiuser/assembly-wsagent-war/pom.xml new file mode 100644 index 0000000000..fae6479db8 --- /dev/null +++ b/assembly-multiuser/assembly-wsagent-war/pom.xml @@ -0,0 +1,92 @@ + + + + 4.0.0 + + che-assembly-parent + org.eclipse.che.assembly-multiuser + 5.19.0-SNAPSHOT + + assembly-wsagent-war + war + Che IDE Assembly Multiuser :: Agent War Packaging + + + com.google.inject + guice + + + org.eclipse.che + assembly-wsagent-war + war + + + org.eclipse.che.core + che-core-api-core + + + org.eclipse.che.core + che-core-commons-inject + + + org.eclipse.che.multiuser + che-multiuser-keycloak-server + + + org.eclipse.che.multiuser + che-multiuser-machine-authentication-agent + + + javax.servlet + javax.servlet-api + provided + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + analyze + + true + + + + + + org.apache.maven.plugins + maven-war-plugin + + true + WEB-INF/lib/*gwt*.jar, + WEB-INF/lib/gin-*.jar, + WEB-INF/lib/jsr305*.jar, + WEB-INF/classes/org/eclipse/che/wsagent/server/CheWsAgentServletModule.class + + + ${basedir}/src/main/webapp/WEB-INF + WEB-INF + + **/* + + + + + + + + diff --git a/assembly-multiuser/assembly-wsagent-war/src/main/java/org/eclipse/che/wsagent/server/AgentHttpJsonRequestFactory.java b/assembly-multiuser/assembly-wsagent-war/src/main/java/org/eclipse/che/wsagent/server/AgentHttpJsonRequestFactory.java new file mode 100644 index 0000000000..6ad9c2784d --- /dev/null +++ b/assembly-multiuser/assembly-wsagent-war/src/main/java/org/eclipse/che/wsagent/server/AgentHttpJsonRequestFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.wsagent.server; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import javax.validation.constraints.NotNull; +import org.eclipse.che.api.core.rest.DefaultHttpJsonRequestFactory; +import org.eclipse.che.api.core.rest.HttpJsonRequest; +import org.eclipse.che.api.core.rest.shared.dto.Link; + +/** + * Implementation of {@link org.eclipse.che.api.core.rest.HttpJsonRequestFactory} that add + * ```user.token``` as authorization header. Used to make request from ws-agent to ws-master. + */ +@Singleton +public class AgentHttpJsonRequestFactory extends DefaultHttpJsonRequestFactory { + + private final String TOKEN; + + @Inject + public AgentHttpJsonRequestFactory(@Named("user.token") String token) { + this.TOKEN = token; + } + + @Override + public HttpJsonRequest fromUrl(@NotNull String url) { + return super.fromUrl(url).setAuthorizationHeader(TOKEN); + } + + @Override + public HttpJsonRequest fromLink(@NotNull Link link) { + return super.fromLink(link).setAuthorizationHeader(TOKEN); + } +} diff --git a/assembly-multiuser/assembly-wsagent-war/src/main/java/org/eclipse/che/wsagent/server/WsAgentMachineAuthModule.java b/assembly-multiuser/assembly-wsagent-war/src/main/java/org/eclipse/che/wsagent/server/WsAgentMachineAuthModule.java new file mode 100644 index 0000000000..d7bf4dd10a --- /dev/null +++ b/assembly-multiuser/assembly-wsagent-war/src/main/java/org/eclipse/che/wsagent/server/WsAgentMachineAuthModule.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.wsagent.server; + +import com.google.inject.AbstractModule; +import org.eclipse.che.api.core.rest.HttpJsonRequestFactory; +import org.eclipse.che.commons.auth.token.ChainedTokenExtractor; +import org.eclipse.che.commons.auth.token.RequestTokenExtractor; +import org.eclipse.che.inject.DynaModule; + +/** Provide multi user specific implementation of ws-agent components. */ +@DynaModule +public class WsAgentMachineAuthModule extends AbstractModule { + @Override + protected void configure() { + bind(HttpJsonRequestFactory.class).to(AgentHttpJsonRequestFactory.class); + bind(RequestTokenExtractor.class).to(ChainedTokenExtractor.class); + } +} diff --git a/assembly-multiuser/assembly-wsagent-war/src/main/java/org/eclipse/che/wsagent/server/WsAgentMachineAuthServletModule.java b/assembly-multiuser/assembly-wsagent-war/src/main/java/org/eclipse/che/wsagent/server/WsAgentMachineAuthServletModule.java new file mode 100644 index 0000000000..d513067bed --- /dev/null +++ b/assembly-multiuser/assembly-wsagent-war/src/main/java/org/eclipse/che/wsagent/server/WsAgentMachineAuthServletModule.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.wsagent.server; + +import com.google.inject.servlet.ServletModule; +import org.eclipse.che.api.core.cors.CheCorsFilter; +import org.eclipse.che.inject.DynaModule; +import org.eclipse.che.multiuser.machine.authentication.agent.MachineLoginFilter; +import org.everrest.guice.servlet.GuiceEverrestServlet; + +/** Provide bindings of security && authentication filters necessary for multi-user Che */ +@DynaModule +public class WsAgentMachineAuthServletModule extends ServletModule { + @Override + protected void configureServlets() { + filter("/*").through(CheCorsFilter.class); + filter("/*").through(MachineLoginFilter.class); + serveRegex("^/api((?!(/(ws|eventbus)($|/.*)))/.*)").with(GuiceEverrestServlet.class); + } +} diff --git a/assembly-multiuser/assembly-wsagent-war/src/main/webapp/WEB-INF/web.xml b/assembly-multiuser/assembly-wsagent-war/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..550f99eb87 --- /dev/null +++ b/assembly-multiuser/assembly-wsagent-war/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,56 @@ + + + + + + org.everrest.websocket.context + /api + + + org.eclipse.che.websocket.endpoint + /ws + + + org.eclipse.che.eventbus.endpoint + /eventbus/ + + + + org.eclipse.che.inject.CheBootstrap + + + org.eclipse.che.everrest.ServerContainerInitializeListener + + + + org.everrest.websockets.WSConnectionTracker + + + + guiceFilter + com.google.inject.servlet.GuiceFilter + + + guiceFilter + /* + + + + 180 + + diff --git a/assembly-multiuser/assembly-wsmaster-war/pom.xml b/assembly-multiuser/assembly-wsmaster-war/pom.xml new file mode 100644 index 0000000000..320ea19bec --- /dev/null +++ b/assembly-multiuser/assembly-wsmaster-war/pom.xml @@ -0,0 +1,167 @@ + + + + 4.0.0 + + che-assembly-parent + org.eclipse.che.assembly-multiuser + 5.19.0-SNAPSHOT + + assembly-wsmaster-war + war + Che IDE Assembly Multiuser :: Compiling WS Master WAR + + + org.eclipse.che + assembly-wsmaster-war + war + + + org.eclipse.che.core + che-core-api-factory + + + org.eclipse.che.core + che-core-api-system + + + org.eclipse.che.core + che-core-api-workspace + + + org.eclipse.che.core + che-core-db + + + org.eclipse.che.core + che-core-db-vendor-postgresql + + + org.eclipse.che.multiuser + che-multiuser-api-authorization-impl + + + org.eclipse.che.multiuser + che-multiuser-api-organization + + + org.eclipse.che.multiuser + che-multiuser-api-resource + + + org.eclipse.che.multiuser + che-multiuser-keycloak-server + + + org.eclipse.che.multiuser + che-multiuser-keycloak-token-provider + + + org.eclipse.che.multiuser + che-multiuser-machine-authentication + + + org.eclipse.che.multiuser + che-multiuser-permission-factory + + + org.eclipse.che.multiuser + che-multiuser-permission-resource + + + org.eclipse.che.multiuser + che-multiuser-permission-system + + + org.eclipse.che.multiuser + che-multiuser-permission-user + + + org.eclipse.che.multiuser + che-multiuser-permission-workspace + + + org.eclipse.che.multiuser + che-multiuser-personal-account + + + org.eclipse.che.multiuser + che-multiuser-sql-schema + + + org.eclipse.che.plugin + che-plugin-activity-wsmaster + + + org.eclipse.che.plugin + che-plugin-docker-machine-auth + + + org.postgresql + postgresql + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + analyze + + true + + + + + + org.apache.maven.plugins + maven-war-plugin + + + + org.eclipse.che + assembly-wsmaster-war + war + + **/** + + + + + + ${basedir}/src/main/webapp/META-INF + META-INF + + **/* + + + + ${basedir}/src/main/webapp/WEB-INF + WEB-INF + + **/* + + + + WEB-INF/lib/wsmaster-local*.jar, + WEB-INF/lib/che-core-db-vendor-h2-*.jar, + WEB-INF/classes/org/eclipse/che/api/deploy/CheWsMasterModule.class, + WEB-INF/classes/org/eclipse/che/api/deploy/CheWsMasterServletModule.class + + + + + diff --git a/assembly-multiuser/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/MachineAuthModule.java b/assembly-multiuser/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/MachineAuthModule.java new file mode 100644 index 0000000000..079f2cfc6e --- /dev/null +++ b/assembly-multiuser/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/MachineAuthModule.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.api.deploy; + +import com.google.inject.AbstractModule; +import org.eclipse.che.api.workspace.server.WorkspaceServiceLinksInjector; +import org.eclipse.che.commons.auth.token.ChainedTokenExtractor; +import org.eclipse.che.commons.auth.token.RequestTokenExtractor; +import org.eclipse.che.inject.DynaModule; +import org.eclipse.che.multiuser.machine.authentication.server.interceptor.InterceptorModule; + +/** + * Machine authentication bindings. + * + * @author Max Shaposhnik (mshaposh@redhat.com) + */ +@DynaModule +public class MachineAuthModule extends AbstractModule { + + @Override + protected void configure() { + install(new InterceptorModule()); + bind(org.eclipse.che.api.agent.server.WsAgentHealthChecker.class) + .to(org.eclipse.che.multiuser.machine.authentication.server.AuthWsAgentHealthChecker.class); + bind( + org.eclipse.che.multiuser.machine.authentication.server.MachineTokenPermissionsFilter + .class); + bind(org.eclipse.che.multiuser.machine.authentication.server.MachineTokenService.class); + bind(org.eclipse.che.multiuser.machine.authentication.server.MachineTokenRegistry.class); + bind(org.eclipse.che.multiuser.machine.authentication.server.MachineSessionInvalidator.class); + bind(RequestTokenExtractor.class).to(ChainedTokenExtractor.class); + bind(WorkspaceServiceLinksInjector.class) + .to( + org.eclipse.che.multiuser.machine.authentication.server + .WorkspaceServiceAuthLinksInjector.class); + bind(org.eclipse.che.api.environment.server.MachineInstanceProvider.class) + .to(org.eclipse.che.plugin.docker.machine.AuthMachineProviderImpl.class); + } +} diff --git a/assembly-multiuser/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/MultiUserCheServletModule.java b/assembly-multiuser/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/MultiUserCheServletModule.java new file mode 100644 index 0000000000..ee54c5ef73 --- /dev/null +++ b/assembly-multiuser/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/MultiUserCheServletModule.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.api.deploy; + +import com.google.inject.servlet.ServletModule; +import org.eclipse.che.inject.DynaModule; +import org.eclipse.che.multiuser.keycloak.server.deploy.KeycloakServletModule; +import org.eclipse.che.multiuser.machine.authentication.server.MachineLoginFilter; + +/** + * Machine authentication bindings. + * + * @author Max Shaposhnik (mshaposh@redhat.com) + */ +@DynaModule +public class MultiUserCheServletModule extends ServletModule { + + @Override + protected void configureServlets() { + // Not contains '/websocket/' and not ends with '/ws' or '/eventbus' + filterRegex("^(?!.*/websocket/)(?!.*(/ws|/eventbus)$).*").through(MachineLoginFilter.class); + install(new KeycloakServletModule()); + } +} diff --git a/assembly-multiuser/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/MultiUserCheWsMasterModule.java b/assembly-multiuser/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/MultiUserCheWsMasterModule.java new file mode 100644 index 0000000000..b9f9da6c2b --- /dev/null +++ b/assembly-multiuser/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/MultiUserCheWsMasterModule.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.api.deploy; + +import com.google.inject.AbstractModule; +import javax.sql.DataSource; +import org.eclipse.che.api.user.server.jpa.JpaPreferenceDao; +import org.eclipse.che.api.user.server.jpa.JpaUserDao; +import org.eclipse.che.api.user.server.spi.PreferenceDao; +import org.eclipse.che.api.user.server.spi.UserDao; +import org.eclipse.che.inject.DynaModule; +import org.eclipse.che.multiuser.api.permission.server.PermissionChecker; +import org.eclipse.che.multiuser.api.permission.server.PermissionCheckerImpl; +import org.eclipse.che.multiuser.keycloak.server.deploy.KeycloakModule; +import org.eclipse.che.multiuser.organization.api.OrganizationApiModule; +import org.eclipse.che.multiuser.organization.api.OrganizationJpaModule; +import org.eclipse.che.multiuser.resource.api.ResourceModule; +import org.eclipse.che.security.PBKDF2PasswordEncryptor; +import org.eclipse.che.security.PasswordEncryptor; + +@DynaModule +public class MultiUserCheWsMasterModule extends AbstractModule { + + @Override + protected void configure() { + bind(DataSource.class).toProvider(org.eclipse.che.core.db.JndiDataSourceProvider.class); + install(new org.eclipse.che.multiuser.api.permission.server.jpa.SystemPermissionsJpaModule()); + install(new org.eclipse.che.multiuser.api.permission.server.PermissionsModule()); + install( + new org.eclipse.che.multiuser.permission.workspace.server.WorkspaceApiPermissionsModule()); + install( + new org.eclipse.che.multiuser.permission.workspace.server.jpa + .MultiuserWorkspaceJpaModule()); + + //Permission filters + bind(org.eclipse.che.multiuser.permission.system.SystemServicePermissionsFilter.class); + bind(org.eclipse.che.multiuser.permission.user.UserProfileServicePermissionsFilter.class); + bind(org.eclipse.che.multiuser.permission.user.UserServicePermissionsFilter.class); + bind(org.eclipse.che.multiuser.permission.factory.FactoryPermissionsFilter.class); + bind(org.eclipse.che.plugin.activity.ActivityPermissionsFilter.class); + bind( + org.eclipse.che.multiuser.permission.resource.filters.ResourceUsageServicePermissionsFilter + .class); + bind( + org.eclipse.che.multiuser.permission.resource.filters + .FreeResourcesLimitServicePermissionsFilter.class); + + install(new ResourceModule()); + install(new OrganizationApiModule()); + install(new OrganizationJpaModule()); + + install(new KeycloakModule()); + + //User and profile - use profile from keycloak and other stuff is JPA + bind(PasswordEncryptor.class).to(PBKDF2PasswordEncryptor.class); + bind(UserDao.class).to(JpaUserDao.class); + bind(PreferenceDao.class).to(JpaPreferenceDao.class); + bind(PermissionChecker.class).to(PermissionCheckerImpl.class); + } +} diff --git a/assembly-multiuser/assembly-wsmaster-war/src/main/resources/META-INF/persistence.xml b/assembly-multiuser/assembly-wsmaster-war/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000000..07271bdd44 --- /dev/null +++ b/assembly-multiuser/assembly-wsmaster-war/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,76 @@ + + + + org.eclipse.persistence.jpa.PersistenceProvider + java:/comp/env/jdbc/che + + org.eclipse.che.account.spi.AccountImpl + org.eclipse.che.api.user.server.model.impl.UserImpl + org.eclipse.che.api.user.server.model.impl.ProfileImpl + org.eclipse.che.api.user.server.jpa.PreferenceEntity + + org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl + org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl + org.eclipse.che.api.workspace.server.model.impl.ProjectConfigImpl + org.eclipse.che.api.workspace.server.model.impl.EnvironmentImpl + org.eclipse.che.api.workspace.server.model.impl.EnvironmentRecipeImpl + org.eclipse.che.api.workspace.server.model.impl.ExtendedMachineImpl + org.eclipse.che.api.workspace.server.model.impl.ProjectConfigImpl$Attribute + org.eclipse.che.api.workspace.server.model.impl.SourceStorageImpl + org.eclipse.che.api.workspace.server.model.impl.ServerConf2Impl + org.eclipse.che.api.workspace.server.model.impl.stack.StackImpl + + org.eclipse.che.api.machine.server.model.impl.CommandImpl + org.eclipse.che.api.machine.server.model.impl.MachineSourceImpl + org.eclipse.che.api.machine.server.model.impl.SnapshotImpl + org.eclipse.che.api.machine.server.recipe.RecipeImpl + + org.eclipse.che.api.factory.server.model.impl.FactoryImpl + org.eclipse.che.api.factory.server.model.impl.OnAppClosedImpl + org.eclipse.che.api.factory.server.model.impl.OnProjectsLoadedImpl + org.eclipse.che.api.factory.server.model.impl.OnAppLoadedImpl + org.eclipse.che.api.factory.server.model.impl.PoliciesImpl + org.eclipse.che.api.factory.server.model.impl.ActionImpl + org.eclipse.che.api.factory.server.model.impl.AuthorImpl + org.eclipse.che.api.factory.server.model.impl.ButtonAttributesImpl + org.eclipse.che.api.factory.server.model.impl.ButtonImpl + org.eclipse.che.api.factory.server.model.impl.IdeImpl + org.eclipse.che.api.factory.server.FactoryImage + + org.eclipse.che.api.ssh.server.model.impl.SshPairImpl + + org.eclipse.che.multiuser.api.permission.server.model.impl.SystemPermissionsImpl + org.eclipse.che.multiuser.api.permission.server.model.impl.AbstractPermissions + org.eclipse.che.multiuser.permission.workspace.server.model.impl.WorkerImpl + org.eclipse.che.multiuser.permission.workspace.server.stack.StackPermissionsImpl + org.eclipse.che.multiuser.permission.machine.recipe.RecipePermissionsImpl + + org.eclipse.che.multiuser.resource.spi.impl.FreeResourcesLimitImpl + org.eclipse.che.multiuser.resource.spi.impl.ResourceImpl + + org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl + org.eclipse.che.multiuser.organization.spi.impl.MemberImpl + org.eclipse.che.multiuser.organization.spi.impl.OrganizationDistributedResourcesImpl + + true + + + + + + + + diff --git a/assembly-multiuser/assembly-wsmaster-war/src/main/webapp/META-INF/context.xml b/assembly-multiuser/assembly-wsmaster-war/src/main/webapp/META-INF/context.xml new file mode 100644 index 0000000000..761998235a --- /dev/null +++ b/assembly-multiuser/assembly-wsmaster-war/src/main/webapp/META-INF/context.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/assembly-multiuser/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/codenvy/multiuser.properties b/assembly-multiuser/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/codenvy/multiuser.properties new file mode 100644 index 0000000000..fc9cb75b0f --- /dev/null +++ b/assembly-multiuser/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/codenvy/multiuser.properties @@ -0,0 +1,84 @@ +# +# Copyright (c) 2012-2017 Red Hat, Inc. +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# Contributors: +# Red Hat, Inc. - initial API and implementation +# + + +######################################################################################## +##### CHE SYSTEM ##### + +# System Super Privileged Mode +# Grants users with the manageSystem permission additional permissions for +# getByKey, getByNameSpace, stopWorkspaces, and getResourcesInformation. +# These are not given to admins by default and these permissions allow +# admins gain visibility to any workspace along with naming themselves +# with admin privileges to those workspaces. +che.system.super_privileged_mode=false + +# Grant system permission for 'che.admin.name' user. If the user already exists it'll happen on +# component startup, if not - during the first login when user is persisted in the database. +che.system.admin_name=admin + +######################################################################################## +##### WORKSPACE LIMITS ##### +# +# Workspaces are the fundamental runtime for users when doing development. You can set +# parameters that limit how workspaces are created and the resources that are consumed. + +# The maximum amount of RAM that a user can allocate to a workspace when they +# create a new workspace. The RAM slider is adjusted to this maximum value. +che.limits.workspace.env.ram=16gb + +# The length of time that a user is idle with their workspace when the system will +# suspend the workspace by snapshotting it and then stopping it. Idleness is the +# length of time that the user has not interacted with the workspace, meaning that +# one of our agents has not received interaction. Leaving a browser window open +# counts toward idleness. +che.limits.workspace.idle.timeout=-1 + +##### USERS' WORKSPACE LIMITS ##### + +# The total amount of RAM that a single user is allowed to allocate to running +# workspaces. A user can allocate this RAM to a single workspace or spread it +# across multiple workspaces. +che.limits.user.workspaces.ram=-1 + +# The maximum number of workspaces that a user is allowed to create. The user will +# be presented with an error message if they try to create additional workspaces. +# This applies to the total number of both running and stopped workspaces. Since +# each workspace is saved as a snapshot, placing a cap on this number is a way +# to limit the disk consumption for workspace storage. +che.limits.user.workspaces.count=-1 + +# The maximum number of running workspaces that a single user is allowed to have. +# If the user has reached this threshold and they try to start an additional +# workspace, they will be prompted with an error message. The user will need to +# stop a running workspace to activate another. +che.limits.user.workspaces.run.count=-1 + +##### ORGANIZATIONS' WORKSPACE LIMITS ##### + +# The total amount of RAM that a single organization (team) is allowed to allocate +# to running workspaces. An organization owner can allocate this RAM however they +# see fit across the team's workspaces. +che.limits.organization.workspaces.ram=-1 + +# The maximum number of workspaces that a organization is allowed to own. The +# organization will be presented an error message if they try to create +# additional workspaces. This applies to the total number of both running +# and stopped workspaces. Since each workspace is saved as a snapshot, placing a +# cap on this number limits the disk consumption for workspace storage. + +che.limits.organization.workspaces.count=-1 +# The maximum number of running workspaces that a single organization is allowed. +# If the organization has reached this threshold and they try to start an +# additional workspace, they will be prompted with an error message. The +# organization will need to stop a running workspace to activate another. +che.limits.organization.workspaces.run.count=-1 + diff --git a/assembly-multiuser/assembly-wsmaster-war/src/main/webapp/WEB-INF/web.xml b/assembly-multiuser/assembly-wsmaster-war/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..257fff75c3 --- /dev/null +++ b/assembly-multiuser/assembly-wsmaster-war/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,58 @@ + + + + + + org.everrest.websocket.context + /api + + + org.eclipse.che.websocket.endpoint + /ws + + + org.eclipse.che.eventbus.endpoint + /eventbus/ + + + + org.eclipse.che.inject.CheBootstrap + + + org.eclipse.che.everrest.ServerContainerInitializeListener + + + + org.everrest.websockets.WSConnectionTracker + + + + guiceFilter + com.google.inject.servlet.GuiceFilter + + + guiceFilter + /* + + + + jdbc/che + javax.sql.DataSource + + + diff --git a/assembly-multiuser/pom.xml b/assembly-multiuser/pom.xml new file mode 100644 index 0000000000..b834ae96dc --- /dev/null +++ b/assembly-multiuser/pom.xml @@ -0,0 +1,34 @@ + + + + 4.0.0 + + che-parent + org.eclipse.che + 5.19.0-SNAPSHOT + ../pom.xml + + org.eclipse.che.assembly-multiuser + che-assembly-parent + 5.19.0-SNAPSHOT + pom + Che IDE Assembly Multiuser :: Parent + + assembly-wsagent-war + assembly-wsagent-server + assembly-wsmaster-war + assembly-ide-war + assembly-main + + diff --git a/assembly/assembly-ide-war/pom.xml b/assembly/assembly-ide-war/pom.xml index e0e76ffb32..56fd9c7eba 100644 --- a/assembly/assembly-ide-war/pom.xml +++ b/assembly/assembly-ide-war/pom.xml @@ -422,6 +422,21 @@ + + add-resource + generate-resources + + add-resource + + + + + src/main/webapp + + + + + diff --git a/assembly/assembly-main/src/assembly/tomcat/conf/server.xml b/assembly/assembly-main/src/assembly/tomcat/conf/server.xml index 87994916aa..0400e24332 100644 --- a/assembly/assembly-main/src/assembly/tomcat/conf/server.xml +++ b/assembly/assembly-main/src/assembly/tomcat/conf/server.xml @@ -46,14 +46,6 @@ description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" pathname="conf/tomcat-users.xml" /--> - - - - + - + diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/web.xml b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/web.xml index 0e454ecb7c..257fff75c3 100644 --- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/web.xml +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/web.xml @@ -49,15 +49,10 @@ guiceFilter /* - - the user role - developer - - - jdbc/che - javax.sql.DataSource - Container - + + jdbc/che + javax.sql.DataSource + diff --git a/core/che-core-api-core/src/main/java/org/eclipse/che/commons/env/EnvironmentContext.java b/core/che-core-api-core/src/main/java/org/eclipse/che/commons/env/EnvironmentContext.java index b5d7e54b43..bebaf9a07f 100644 --- a/core/che-core-api-core/src/main/java/org/eclipse/che/commons/env/EnvironmentContext.java +++ b/core/che-core-api-core/src/main/java/org/eclipse/che/commons/env/EnvironmentContext.java @@ -24,12 +24,7 @@ public class EnvironmentContext { /** ThreadLocal keeper for EnvironmentContext. */ private static ThreadLocal current = - new ThreadLocal() { - @Override - protected EnvironmentContext initialValue() { - return new EnvironmentContext(); - } - }; + ThreadLocal.withInitial(EnvironmentContext::new); static { ThreadLocalPropagateContext.addThreadLocal(current); diff --git a/core/che-core-db-vendor-h2/pom.xml b/core/che-core-db-vendor-h2/pom.xml index 3600b07310..89853b27e9 100644 --- a/core/che-core-db-vendor-h2/pom.xml +++ b/core/che-core-db-vendor-h2/pom.xml @@ -21,6 +21,10 @@ che-core-db-vendor-h2 Che Core :: DB :: Vendor H2 + + com.google.guava + guava + javax.inject javax.inject diff --git a/core/che-core-db-vendor-h2/src/main/java/org/eclipse/che/core/db/h2/H2SQLJndiDataSourceFactory.java b/core/che-core-db-vendor-h2/src/main/java/org/eclipse/che/core/db/h2/H2SQLJndiDataSourceFactory.java new file mode 100644 index 0000000000..71ef661976 --- /dev/null +++ b/core/che-core-db-vendor-h2/src/main/java/org/eclipse/che/core/db/h2/H2SQLJndiDataSourceFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.core.db.h2; + +import static com.google.common.base.MoreObjects.firstNonNull; + +import org.eclipse.che.core.db.JNDIDataSourceFactory; + +/** + * Environment params based JNDI data source factory for H2SQL. + * + * @author Sergii Kabashniuk + */ +public class H2SQLJndiDataSourceFactory extends JNDIDataSourceFactory { + + private static final String DEFAULT_USERNAME = ""; + private static final String DEFAULT_PASSWORD = ""; + private static final String DEFAULT_URL = "jdbc:h2:che"; + private static final String DEFAULT_DRIVER__CLASS__NAME = "org.h2.Driver"; + private static final String DEFAULT_MAX__TOTAL = "8"; + private static final String DEFAULT_MAX__IDLE = "2"; + private static final String DEFAULT_MAX__WAIT__MILLIS = "-1"; + + public H2SQLJndiDataSourceFactory() throws Exception { + super( + firstNonNull(System.getenv("CHE_JDBC_USERNAME"), DEFAULT_USERNAME), + firstNonNull(System.getenv("CHE_JDBC_PASSWORD"), DEFAULT_PASSWORD), + firstNonNull(System.getenv("CHE_JDBC_URL"), DEFAULT_URL), + firstNonNull(System.getenv("CHE_JDBC_DRIVER__CLASS__NAME"), DEFAULT_DRIVER__CLASS__NAME), + firstNonNull(System.getenv("CHE_JDBC_MAX__TOTAL"), DEFAULT_MAX__TOTAL), + firstNonNull(System.getenv("CHE_JDBC_MAX__IDLE"), DEFAULT_MAX__IDLE), + firstNonNull(System.getenv("CHE_JDBC_MAX__WAIT__MILLIS"), DEFAULT_MAX__WAIT__MILLIS)); + } +} diff --git a/core/che-core-db-vendor-postgresql/pom.xml b/core/che-core-db-vendor-postgresql/pom.xml index 89e1bb9e57..e21952cd2b 100644 --- a/core/che-core-db-vendor-postgresql/pom.xml +++ b/core/che-core-db-vendor-postgresql/pom.xml @@ -21,6 +21,10 @@ che-core-db-vendor-postgresql Che Core :: DB :: Vendor PostgreSQL + + com.google.guava + guava + org.eclipse.che.core che-core-db diff --git a/core/che-core-db-vendor-postgresql/src/main/java/org/eclipse/che/core/db/postgresql/PostgreSQLJndiDataSourceFactory.java b/core/che-core-db-vendor-postgresql/src/main/java/org/eclipse/che/core/db/postgresql/PostgreSQLJndiDataSourceFactory.java new file mode 100644 index 0000000000..6b4ffe8a3e --- /dev/null +++ b/core/che-core-db-vendor-postgresql/src/main/java/org/eclipse/che/core/db/postgresql/PostgreSQLJndiDataSourceFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.core.db.postgresql; + +import static com.google.common.base.MoreObjects.firstNonNull; + +import org.eclipse.che.core.db.JNDIDataSourceFactory; + +/** + * Environment params based JNDI data source factory for Postgres. + * + * @author Sergii Kabashniuk + */ +public class PostgreSQLJndiDataSourceFactory extends JNDIDataSourceFactory { + + private static final String DEFAULT_USERNAME = "pgche"; + private static final String DEFAULT_PASSWORD = "pgchepassword"; + private static final String DEFAULT_URL = "jdbc:postgresql://postgres:5432/dbche"; + private static final String DEFAULT_DRIVER__CLASS__NAME = "org.postgresql.Driver"; + private static final String DEFAULT_MAX__TOTAL = "20"; + private static final String DEFAULT_MAX__IDLE = "2"; + private static final String DEFAULT_MAX__WAIT__MILLIS = "-1"; + + public PostgreSQLJndiDataSourceFactory() throws Exception { + super( + firstNonNull(System.getenv("CHE_JDBC_USERNAME"), DEFAULT_USERNAME), + firstNonNull(System.getenv("CHE_JDBC_PASSWORD"), DEFAULT_PASSWORD), + firstNonNull(System.getenv("CHE_JDBC_URL"), DEFAULT_URL), + firstNonNull(System.getenv("CHE_JDBC_DRIVER__CLASS__NAME"), DEFAULT_DRIVER__CLASS__NAME), + firstNonNull(System.getenv("CHE_JDBC_MAX__TOTAL"), DEFAULT_MAX__TOTAL), + firstNonNull(System.getenv("CHE_JDBC_MAX__IDLE"), DEFAULT_MAX__IDLE), + firstNonNull(System.getenv("CHE_JDBC_MAX__WAIT__MILLIS"), DEFAULT_MAX__WAIT__MILLIS)); + } +} diff --git a/core/che-core-db/pom.xml b/core/che-core-db/pom.xml index 8cb5fe38d3..8bfc329807 100644 --- a/core/che-core-db/pom.xml +++ b/core/che-core-db/pom.xml @@ -37,6 +37,10 @@ javax.inject javax.inject + + org.apache.tomcat + tomcat-dbcp + org.eclipse.che.core che-core-api-core diff --git a/core/che-core-db/src/main/java/org/eclipse/che/core/db/JNDIDataSourceFactory.java b/core/che-core-db/src/main/java/org/eclipse/che/core/db/JNDIDataSourceFactory.java new file mode 100644 index 0000000000..3d2dd0c4fd --- /dev/null +++ b/core/che-core-db/src/main/java/org/eclipse/che/core/db/JNDIDataSourceFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.core.db; + +import java.util.Hashtable; +import java.util.Properties; +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.spi.ObjectFactory; +import org.apache.tomcat.dbcp.dbcp2.BasicDataSource; +import org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abstract JNDI factory that constructs {@link BasicDataSource} objects from the given params. + * Should not be used directly and must be subclassed to provide instantiation params from needful + * source. + * + * @author Sergii Kabashniuk + */ +public abstract class JNDIDataSourceFactory implements ObjectFactory { + + private static final Logger LOG = LoggerFactory.getLogger(JNDIDataSourceFactory.class); + + private final BasicDataSource dataSource; + + public JNDIDataSourceFactory( + String userName, + String password, + String url, + String driverClassName, + String maxTotal, + String maxIdle, + String maxWaitMillis) + throws Exception { + Properties poolConfigurationProperties = new Properties(); + poolConfigurationProperties.setProperty("username", userName); + poolConfigurationProperties.setProperty("password", password); + poolConfigurationProperties.setProperty("url", url); + poolConfigurationProperties.setProperty("driverClassName", driverClassName); + poolConfigurationProperties.setProperty("maxTotal", maxTotal); + poolConfigurationProperties.setProperty("maxIdle", maxIdle); + poolConfigurationProperties.setProperty("maxWaitMillis", maxWaitMillis); + dataSource = BasicDataSourceFactory.createDataSource(poolConfigurationProperties); + } + + @Override + public Object getObjectInstance( + Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception { + LOG.info( + "This={} obj={} name={} Context={} environment={}", this, obj, name, nameCtx, environment); + return dataSource; + } +} diff --git a/core/commons/che-core-commons-auth/pom.xml b/core/commons/che-core-commons-auth/pom.xml new file mode 100644 index 0000000000..e437f36bcd --- /dev/null +++ b/core/commons/che-core-commons-auth/pom.xml @@ -0,0 +1,51 @@ + + + + 4.0.0 + + che-core-commons-parent + org.eclipse.che.core + 5.19.0-SNAPSHOT + + che-core-commons-auth + jar + Che Core :: Commons :: Auth + + + javax.inject + javax.inject + + + javax.ws.rs + javax.ws.rs-api + + + org.eclipse.che.core + che-core-api-core + + + org.eclipse.che.core + che-core-api-dto + + + org.slf4j + slf4j-api + + + javax.servlet + javax.servlet-api + provided + + + diff --git a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/api/auth/AuthenticationException.java b/core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/AuthenticationException.java similarity index 97% rename from wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/api/auth/AuthenticationException.java rename to core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/AuthenticationException.java index 503bb496c3..d1e1e6e993 100644 --- a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/api/auth/AuthenticationException.java +++ b/core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/AuthenticationException.java @@ -8,7 +8,7 @@ * Contributors: * Red Hat, Inc. - initial API and implementation */ -package org.eclipse.che.api.auth; +package org.eclipse.che.commons.auth; import org.eclipse.che.api.core.ApiException; diff --git a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/api/auth/AuthenticationExceptionMapper.java b/core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/AuthenticationExceptionMapper.java similarity index 97% rename from wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/api/auth/AuthenticationExceptionMapper.java rename to core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/AuthenticationExceptionMapper.java index 19356e6923..1f86c4e305 100644 --- a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/api/auth/AuthenticationExceptionMapper.java +++ b/core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/AuthenticationExceptionMapper.java @@ -8,7 +8,7 @@ * Contributors: * Red Hat, Inc. - initial API and implementation */ -package org.eclipse.che.api.auth; +package org.eclipse.che.commons.auth; import javax.inject.Singleton; import javax.ws.rs.core.MediaType; diff --git a/core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/token/ChainedTokenExtractor.java b/core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/token/ChainedTokenExtractor.java new file mode 100644 index 0000000000..553e88aebc --- /dev/null +++ b/core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/token/ChainedTokenExtractor.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.commons.auth.token; + +import javax.servlet.http.HttpServletRequest; + +/** + * Try to extract token from request in 3 steps. 1. From query parameter. 2. From header. 3. From + * cookie. + * + * @author Sergii Kabashniuk + */ +public class ChainedTokenExtractor implements RequestTokenExtractor { + + private final HeaderRequestTokenExtractor headerRequestTokenExtractor; + + private final QueryRequestTokenExtractor queryRequestTokenExtractor; + + public ChainedTokenExtractor() { + headerRequestTokenExtractor = new HeaderRequestTokenExtractor(); + queryRequestTokenExtractor = new QueryRequestTokenExtractor(); + } + + @Override + public String getToken(HttpServletRequest req) { + String token; + if ((token = queryRequestTokenExtractor.getToken(req)) == null) { + token = headerRequestTokenExtractor.getToken(req); + } + return token; + } +} diff --git a/core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/token/HeaderRequestTokenExtractor.java b/core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/token/HeaderRequestTokenExtractor.java new file mode 100644 index 0000000000..1c4ebc115a --- /dev/null +++ b/core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/token/HeaderRequestTokenExtractor.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.commons.auth.token; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.HttpHeaders; + +/** Extract sso token from request headers. */ +public class HeaderRequestTokenExtractor implements RequestTokenExtractor { + @Override + public String getToken(HttpServletRequest req) { + if (req.getHeader(HttpHeaders.AUTHORIZATION) == null) { + return null; + } + return req.getHeader(HttpHeaders.AUTHORIZATION).toLowerCase().startsWith("bearer") + ? req.getHeader(HttpHeaders.AUTHORIZATION).split(" ")[1] + : req.getHeader(HttpHeaders.AUTHORIZATION); + } +} diff --git a/core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/token/QueryRequestTokenExtractor.java b/core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/token/QueryRequestTokenExtractor.java new file mode 100644 index 0000000000..65bc508fdd --- /dev/null +++ b/core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/token/QueryRequestTokenExtractor.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.commons.auth.token; + +import javax.servlet.http.HttpServletRequest; + +/** @author Max Shaposhnik (mshaposh@redhat.com) */ +public class QueryRequestTokenExtractor implements RequestTokenExtractor { + @Override + public String getToken(HttpServletRequest req) { + String query = req.getQueryString(); + if (query != null) { + int start = query.indexOf("&token="); + if (start != -1 || query.startsWith("token=")) { + int end = query.indexOf('&', start + 7); + if (end == -1) { + end = query.length(); + } + if (end != start + 7) { + return query.substring(start + 7, end); + } + } + } + return null; + } +} diff --git a/core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/token/RequestTokenExtractor.java b/core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/token/RequestTokenExtractor.java new file mode 100644 index 0000000000..f23047fd2c --- /dev/null +++ b/core/commons/che-core-commons-auth/src/main/java/org/eclipse/che/commons/auth/token/RequestTokenExtractor.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.commons.auth.token; + +import javax.servlet.http.HttpServletRequest; + +/** Allows to extract sso token from request. */ +public interface RequestTokenExtractor { + /** + * Extract token from request. + * + * @param req - request object. + * @return - token if it was found, null otherwise. + */ + String getToken(HttpServletRequest req); +} diff --git a/core/commons/che-core-commons-mail/pom.xml b/core/commons/che-core-commons-mail/pom.xml new file mode 100644 index 0000000000..68838583cd --- /dev/null +++ b/core/commons/che-core-commons-mail/pom.xml @@ -0,0 +1,102 @@ + + + + 4.0.0 + + che-core-commons-parent + org.eclipse.che.core + 5.19.0-SNAPSHOT + + che-core-commons-mail + jar + Che Core :: Commons :: Mail sender + + + com.google.guava + guava + + + commons-io + commons-io + + + javax.annotation + javax.annotation-api + + + javax.inject + javax.inject + + + javax.mail + mail + + + org.eclipse.che.core + che-core-commons-annotations + + + org.eclipse.che.core + che-core-commons-inject + + + org.eclipse.che.core + che-core-commons-lang + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + test + + + com.github.kirviq + dumbster + test + + + org.everrest + everrest-assured + test + + + org.hamcrest + hamcrest-core + test + + + org.mockito + mockito-core + test + + + org.mockitong + mockitong + test + + + org.slf4j + jcl-over-slf4j + test + + + org.testng + testng + test + + + diff --git a/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/Attachment.java b/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/Attachment.java new file mode 100644 index 0000000000..52b4e977da --- /dev/null +++ b/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/Attachment.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.mail; + +import java.util.Objects; + +/** + * Describing e-mail attachment. + * + * @author Igor Vinokur + * @author Alexander Garagatyi + */ +public class Attachment { + private String content; + private String contentId; + private String fileName; + + /** Base-64 encoded string that represents attachment content. */ + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Attachment withContent(String content) { + this.content = content; + return this; + } + + public String getContentId() { + return contentId; + } + + public void setContentId(String contentId) { + this.contentId = contentId; + } + + public Attachment withContentId(String contentId) { + this.contentId = contentId; + return this; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public Attachment withFileName(String fileName) { + this.fileName = fileName; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Attachment)) return false; + Attachment that = (Attachment) o; + return Objects.equals(getContent(), that.getContent()) + && Objects.equals(getContentId(), that.getContentId()) + && Objects.equals(getFileName(), that.getFileName()); + } + + @Override + public int hashCode() { + return Objects.hash(getContent(), getContentId(), getFileName()); + } + + @Override + public String toString() { + return "Attachment{" + + "content='" + + content + + '\'' + + ", contentId='" + + contentId + + '\'' + + ", fileName='" + + fileName + + '\'' + + '}'; + } +} diff --git a/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/EmailBean.java b/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/EmailBean.java new file mode 100644 index 0000000000..dc2193ad89 --- /dev/null +++ b/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/EmailBean.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.mail; + +import java.util.List; +import java.util.Objects; +import org.eclipse.che.commons.annotation.Nullable; + +/** + * Describing e-mail properties. + * + * @author Igor Vinokur + * @author Alexander Garagatyi + */ +public class EmailBean { + private String from; + private String to; + private String replyTo; + private String mimeType; + private String body; + private String subject; + private List attachments; + + public String getFrom() { + return from; + } + + public void setFrom(String from) { + this.from = from; + } + + public EmailBean withFrom(String from) { + this.from = from; + return this; + } + + public String getTo() { + return to; + } + + public void setTo(String to) { + this.to = to; + } + + public EmailBean withTo(String to) { + this.to = to; + return this; + } + + public String getReplyTo() { + return replyTo; + } + + public void setReplyTo(String replyTo) { + this.replyTo = replyTo; + } + + public EmailBean withReplyTo(String replyTo) { + this.replyTo = replyTo; + return this; + } + + public String getMimeType() { + return mimeType; + } + + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + public EmailBean withMimeType(String mimeType) { + this.mimeType = mimeType; + return this; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public EmailBean withBody(String body) { + this.body = body; + return this; + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public EmailBean withSubject(String subject) { + this.subject = subject; + return this; + } + + @Nullable + public List getAttachments() { + return attachments; + } + + public void setAttachments(List attachments) { + this.attachments = attachments; + } + + public EmailBean withAttachments(List attachments) { + this.attachments = attachments; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof EmailBean)) return false; + EmailBean emailBean = (EmailBean) o; + return Objects.equals(getFrom(), emailBean.getFrom()) + && Objects.equals(getTo(), emailBean.getTo()) + && Objects.equals(getReplyTo(), emailBean.getReplyTo()) + && Objects.equals(getMimeType(), emailBean.getMimeType()) + && Objects.equals(getBody(), emailBean.getBody()) + && Objects.equals(getSubject(), emailBean.getSubject()) + && Objects.equals(getAttachments(), emailBean.getAttachments()); + } + + @Override + public int hashCode() { + return Objects.hash( + getFrom(), getTo(), getReplyTo(), getMimeType(), getBody(), getSubject(), getAttachments()); + } + + @Override + public String toString() { + return "EmailBean{" + + "from='" + + from + + '\'' + + ", to='" + + to + + '\'' + + ", replyTo='" + + replyTo + + '\'' + + ", mimeType='" + + mimeType + + '\'' + + ", body='" + + body + + '\'' + + ", subject='" + + subject + + '\'' + + ", attachments=" + + attachments + + '}'; + } +} diff --git a/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/MailSender.java b/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/MailSender.java new file mode 100644 index 0000000000..a6a374ccf9 --- /dev/null +++ b/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/MailSender.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.mail; + +import static java.util.concurrent.Executors.newFixedThreadPool; + +import com.google.common.io.Files; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.io.File; +import java.io.IOException; +import java.util.Base64; +import java.util.Date; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import javax.annotation.PreDestroy; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.mail.Message; +import javax.mail.Multipart; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import org.apache.commons.io.FileUtils; +import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides email sending capability + * + * @author Alexander Garagatyi + * @author Sergii Kabashniuk + */ +@Singleton +public class MailSender { + private static final Logger LOG = LoggerFactory.getLogger(MailSender.class); + + private final ExecutorService executor; + private final MailSessionProvider mailSessionProvider; + + @Inject + public MailSender(MailSessionProvider mailSessionProvider) { + this.mailSessionProvider = mailSessionProvider; + this.executor = + newFixedThreadPool( + 2 * Runtime.getRuntime().availableProcessors(), + new ThreadFactoryBuilder() + .setNameFormat("MailNotificationsPool-%d") + .setDaemon(false) + .setUncaughtExceptionHandler(LoggingUncaughtExceptionHandler.getInstance()) + .build()); + } + + public void sendAsync(EmailBean emailBean) { + executor.execute( + () -> { + try { + sendMail(emailBean); + } catch (Exception ex) { + LOG.warn( + "Failed to send email notification for {} with subject {}. Cause: '{}'", + emailBean.getTo(), + emailBean.getSubject(), + ex.getLocalizedMessage()); + } + }); + } + + public void sendMail(EmailBean emailBean) throws SendMailException { + File tempDir = null; + try { + MimeMessage message = new MimeMessage(mailSessionProvider.get()); + Multipart contentPart = new MimeMultipart(); + + MimeBodyPart bodyPart = new MimeBodyPart(); + bodyPart.setText(emailBean.getBody(), "UTF-8", getSubType(emailBean.getMimeType())); + contentPart.addBodyPart(bodyPart); + + if (emailBean.getAttachments() != null) { + tempDir = Files.createTempDir(); + for (Attachment attachment : emailBean.getAttachments()) { + // Create attachment file in temporary directory + byte[] attachmentContent = Base64.getDecoder().decode(attachment.getContent()); + File attachmentFile = new File(tempDir, attachment.getFileName()); + Files.write(attachmentContent, attachmentFile); + + // Attach the attachment file to email + MimeBodyPart attachmentPart = new MimeBodyPart(); + attachmentPart.attachFile(attachmentFile); + attachmentPart.setContentID("<" + attachment.getContentId() + ">"); + contentPart.addBodyPart(attachmentPart); + } + } + + message.setContent(contentPart); + message.setSubject(emailBean.getSubject(), "UTF-8"); + message.setFrom(new InternetAddress(emailBean.getFrom(), true)); + message.setSentDate(new Date()); + message.addRecipients(Message.RecipientType.TO, InternetAddress.parse(emailBean.getTo())); + + if (emailBean.getReplyTo() != null) { + message.setReplyTo(InternetAddress.parse(emailBean.getReplyTo())); + } + LOG.info( + "Sending from {} to {} with subject {}", + emailBean.getFrom(), + emailBean.getTo(), + emailBean.getSubject()); + + Transport.send(message); + LOG.debug("Mail sent"); + } catch (Exception e) { + LOG.error(e.getLocalizedMessage()); + throw new SendMailException(e.getLocalizedMessage(), e); + } finally { + if (tempDir != null) { + try { + FileUtils.deleteDirectory(tempDir); + } catch (IOException exception) { + LOG.error(exception.getMessage()); + } + } + } + } + + /** + * Get the specified MIME subtype from given primary MIME type. + * + *

It is needed for setText method in MimeBodyPar because it works only with text MimeTypes. + * setText method in MimeBodyPar already adds predefined "text/" to given subtype. + * + * @param mimeType primary MIME type + * @return MIME subtype + */ + private String getSubType(String mimeType) { + return mimeType.substring(mimeType.lastIndexOf("/") + 1); + } + + @PreDestroy + public void shutdown() throws InterruptedException { + executor.shutdown(); + try { + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { + executor.shutdownNow(); + if (!executor.awaitTermination(60, TimeUnit.SECONDS)) LOG.warn("Pool did not terminate"); + } + } catch (InterruptedException ie) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } +} diff --git a/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/MailSessionProvider.java b/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/MailSessionProvider.java new file mode 100644 index 0000000000..29121ae989 --- /dev/null +++ b/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/MailSessionProvider.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.mail; + +import com.google.common.annotations.VisibleForTesting; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Collectors; +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; +import javax.mail.Authenticator; +import javax.mail.PasswordAuthentication; +import javax.mail.Session; +import org.eclipse.che.inject.ConfigurationProperties; + +/** Provider of {@link Session} */ +@Singleton +public class MailSessionProvider implements Provider { + + private final Session session; + /** + * Configuration can be injected from container with help of {@lin ConfigurationProperties} class. + * In this case all properties that starts with 'che.mail.' will be used to create {@link + * Session}. First 4 letters 'che.' from property names will be removed. + */ + @Inject + public MailSessionProvider(ConfigurationProperties configurationProperties) { + + this( + configurationProperties + .getProperties("che.mail.*") + .entrySet() + .stream() + .collect(Collectors.toMap(e -> e.getKey().substring(4), Map.Entry::getValue))); + } + + @VisibleForTesting + MailSessionProvider(Map mailConfiguration) { + if (mailConfiguration != null && !mailConfiguration.isEmpty()) { + Properties props = new Properties(); + mailConfiguration.forEach(props::setProperty); + + if (Boolean.parseBoolean(props.getProperty("mail.smtp.auth"))) { + final String username = props.getProperty("mail.smtp.auth.username"); + final String password = props.getProperty("mail.smtp.auth.password"); + + // remove useless properties + props.remove("mail.smtp.auth.username"); + props.remove("mail.smtp.auth.password"); + + this.session = + Session.getInstance( + props, + new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } else { + this.session = Session.getInstance(props); + } + } else { + this.session = null; + } + } + + @Override + public Session get() { + if (session == null) { + throw new RuntimeException("SMTP is not configured"); + } + return session; + } +} diff --git a/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/SendMailException.java b/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/SendMailException.java new file mode 100644 index 0000000000..b44e384d16 --- /dev/null +++ b/core/commons/che-core-commons-mail/src/main/java/org/eclipse/che/mail/SendMailException.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.mail; + +/** Exception happened during mail sending * */ +public class SendMailException extends Exception { + + public SendMailException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core/commons/che-core-commons-mail/src/test/java/org/eclipse/che/mail/MailSenderTest.java b/core/commons/che-core-commons-mail/src/test/java/org/eclipse/che/mail/MailSenderTest.java new file mode 100644 index 0000000000..fb36c70bb3 --- /dev/null +++ b/core/commons/che-core-commons-mail/src/test/java/org/eclipse/che/mail/MailSenderTest.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2012-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.mail; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import com.dumbster.smtp.SimpleSmtpServer; +import com.dumbster.smtp.SmtpMessage; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.util.Base64; +import java.util.Collections; +import java.util.Map; +import org.testng.ITestContext; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class MailSenderTest { + private MailSender mailSender; + private SimpleSmtpServer server; + + public static void assertMail( + SimpleSmtpServer server, + String from, + String to, + String replyTo, + String subject, + String mimeType, + String body, + String attachmentContentID, + String attachmentFileName) { + assertEquals(server.getReceivedEmails().size(), 1); + SmtpMessage email = server.getReceivedEmails().iterator().next(); + + assertEquals(email.getHeaderValue("Subject"), subject); + assertEquals(email.getHeaderValue("From"), from); + assertEquals(email.getHeaderValue("Reply-To"), replyTo); + assertEquals(email.getHeaderValue("To"), to); + assertTrue(email.getBody().contains("Content-Type: " + mimeType)); + assertTrue(email.getBody().contains(body)); + if (attachmentFileName != null && attachmentContentID != null) { + assertTrue(email.getBody().contains("filename=" + attachmentFileName)); + assertTrue(email.getBody().contains("Content-ID: <" + attachmentContentID + ">")); + } + } + + @BeforeMethod + public void setup(ITestContext context) throws IOException { + server = SimpleSmtpServer.start(SimpleSmtpServer.AUTO_SMTP_PORT); + + Map mailConfiguration = + ImmutableMap.of( + "mail.smtp.host", + "localhost", + "mail.smtp.port", + server.getPort() + "", + "mail.transport.protocol", + "smtp", + " mail.smtp.auth", + "false"); + + mailSender = new MailSender(new MailSessionProvider(mailConfiguration)); + } + + @AfterMethod + public void stop() { + server.stop(); + } + + @Test + public void shouldBeAbleToSendMessage() throws SendMailException { + EmailBean emailBean = + new EmailBean() + .withFrom("noreply@cloud-ide.com") + .withTo("dev-test@cloud-ide.com") + .withReplyTo("dev-test@cloud-ide.com") + .withSubject("Subject") + .withMimeType("text/html") + .withBody("hello user"); + mailSender.sendMail(emailBean); + assertMail( + server, + "noreply@cloud-ide.com", + "dev-test@cloud-ide.com", + "dev-test@cloud-ide.com", + "Subject", + "text/html", + "hello user", + null, + null); + } + + @Test + public void shouldBeAbleToSendMessageWithFormattedFields() throws SendMailException { + EmailBean emailBean = + new EmailBean() + .withFrom("Exo IDE ") + .withTo("dev-test@cloud-ide.com") + .withReplyTo("Developers to reply ") + .withSubject("Subject") + .withMimeType("text/html") + .withBody("hello user"); + mailSender.sendMail(emailBean); + assertMail( + server, + "Exo IDE ", + "dev-test@cloud-ide.com", + "Developers to reply ", + "Subject", + "text/html", + "hello user", + null, + null); + } + + @Test + public void shouldBeAbleToSendMessageToFewEmails() throws SendMailException { + EmailBean emailBean = + new EmailBean() + .withFrom("noreply@cloud-ide.com") + .withTo("dev-test@cloud-ide.com, dev-test1@cloud-ide.com, dev-test2@cloud-ide.com") + .withReplyTo("dev-test@cloud-ide.com") + .withSubject("Subject") + .withMimeType("text/html") + .withBody("hello user"); + mailSender.sendMail(emailBean); + + assertMail( + server, + "noreply@cloud-ide.com", + "dev-test@cloud-ide.com, dev-test1@cloud-ide.com, dev-test2@cloud-ide.com", + "dev-test@cloud-ide.com", + "Subject", + "text/html", + "hello user", + null, + null); + } + + @Test + public void shouldBeAbleToSendMessageWithAttachment() throws SendMailException { + EmailBean emailBean = + new EmailBean() + .withFrom("noreply@cloud-ide.com") + .withTo("dev-test@cloud-ide.com") + .withReplyTo("dev-test@cloud-ide.com") + .withSubject("Subject") + .withMimeType("text/html") + .withBody("hello user"); + + Attachment attachment = + new Attachment() + .withContentId("attachmentId") + .withFileName("attachment.txt") + .withContent(Base64.getEncoder().encodeToString("attachmentContent".getBytes(UTF_8))); + + emailBean.setAttachments(Collections.singletonList(attachment)); + + mailSender.sendMail(emailBean); + assertMail( + server, + "noreply@cloud-ide.com", + "dev-test@cloud-ide.com", + "dev-test@cloud-ide.com", + "Subject", + "text/html", + "hello user", + "attachmentId", + "attachment.txt"); + } +} diff --git a/core/commons/che-core-commons-mail/src/test/resources/logback-test.xml b/core/commons/che-core-commons-mail/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..92391f09e8 --- /dev/null +++ b/core/commons/che-core-commons-mail/src/test/resources/logback-test.xml @@ -0,0 +1,26 @@ + + + + + + + %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n + + + + + + + + diff --git a/core/commons/pom.xml b/core/commons/pom.xml index 74a31883e1..873b2babfc 100644 --- a/core/commons/pom.xml +++ b/core/commons/pom.xml @@ -24,6 +24,7 @@ Che Core :: Commons :: Parent che-core-commons-annotations + che-core-commons-auth che-core-commons-lang che-core-commons-inject che-core-commons-json @@ -31,5 +32,6 @@ che-core-commons-schedule che-core-commons-test che-core-commons-j2ee + che-core-commons-mail diff --git a/dashboard/bower.json b/dashboard/bower.json index 5268c24fb8..9ba94a659f 100644 --- a/dashboard/bower.json +++ b/dashboard/bower.json @@ -14,6 +14,7 @@ "angular-charts": "0.2.6", "angular-cookies": "1.4.8", "angular-dropdowns": "1.0.0", + "angular-gravatar": "0.2.4", "angular-filter": "0.5.4", "angular-material": "1.0.1", "angular-moment": "0.9.0", diff --git a/dashboard/gulp/proxy.js b/dashboard/gulp/proxy.js index ea7dc1d0ab..6b0ef4aa23 100644 --- a/dashboard/gulp/proxy.js +++ b/dashboard/gulp/proxy.js @@ -22,10 +22,9 @@ var serverOptions = { var options = minimist(process.argv.slice(2), serverOptions); -var patterns = ['/api', '/ext', '/ws', '/datasource', '/java-ca', '/im', '/che', '/admin']; - -var proxies = [] +var patterns = ['/api', '/ext', '/ws', '/datasource', '/java-ca', '/im', '/che', '/admin', '/wsmaster']; +var proxies = []; patterns.forEach(function(pattern) { var proxyOptions = url.parse(options.server + pattern); @@ -37,6 +36,8 @@ patterns.forEach(function(pattern) { proxyOptions.route = '/admin'; } else if (pattern === '/ext') { proxyOptions.route = '/ext'; + } else if (pattern === '/wsmaster') { + proxyOptions.route = '/wsmaster'; } else { proxyOptions.route = '/api'; } diff --git a/dashboard/src/app/admin/admin-config.ts b/dashboard/src/app/admin/admin-config.ts index b089d0114e..e06893708f 100644 --- a/dashboard/src/app/admin/admin-config.ts +++ b/dashboard/src/app/admin/admin-config.ts @@ -11,15 +11,16 @@ 'use strict'; import {AdminsPluginsConfig} from './plugins/plugins-config'; +import {AdminsUserManagementConfig} from './user-management/user-management-config'; /** * @author Florent Benoit */ export class AdminsConfig { - constructor(register) { + constructor(register: che.IRegisterService) { new AdminsPluginsConfig(register); - + new AdminsUserManagementConfig(register); } } diff --git a/dashboard/src/app/admin/user-management/account-profile/account-profile.directive.ts b/dashboard/src/app/admin/user-management/account-profile/account-profile.directive.ts new file mode 100644 index 0000000000..181e6356a9 --- /dev/null +++ b/dashboard/src/app/admin/user-management/account-profile/account-profile.directive.ts @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2015-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +'use strict'; + +interface IAccountProfileScope extends ng.IScope { + profileAttributes: { + phone?: string; + country?: string; + employer?: string; + jobtitle?: string; + lastName?: string; + firstName?: string; + }; + profileInformationForm: ng.IFormController; + countries?: Array<{ 'name': string, 'code': string }>; + jobs?: Array<{ 'name': string }>; +} + +/** + * @ngdoc directive + * @name account.profile.directive:accountProfile + * @restrict E + * @element + * + * @description + * ` for displaying account profile. + * + * @usage + * + * + * @author Florent Benoit + */ +export class AccountProfile implements ng.IDirective { + restrict = 'E'; + templateUrl = 'app/account/account-profile/account-profile.html'; + replace = true; + scope = { + profileAttributes: '=profileAttributes', + profileInformationForm: '=?profileInformationForm' + }; + + jsonCountries: string; + jsonJobs: string; + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor(jsonCountries: string, jsonJobs: string) { + this.jsonCountries = jsonCountries; + this.jsonJobs = jsonJobs; + } + + link($scope: IAccountProfileScope) { + $scope.countries = angular.fromJson(this.jsonCountries); + $scope.jobs = angular.fromJson(this.jsonJobs); + } +} diff --git a/dashboard/src/app/admin/user-management/account-profile/account-profile.html b/dashboard/src/app/admin/user-management/account-profile/account-profile.html new file mode 100644 index 0000000000..573bfb10cd --- /dev/null +++ b/dashboard/src/app/admin/user-management/account-profile/account-profile.html @@ -0,0 +1,67 @@ +

diff --git a/dashboard/src/app/admin/user-management/account-profile/account-profile.styl b/dashboard/src/app/admin/user-management/account-profile/account-profile.styl new file mode 100644 index 0000000000..2ba6ba3599 --- /dev/null +++ b/dashboard/src/app/admin/user-management/account-profile/account-profile.styl @@ -0,0 +1,8 @@ +.account-profile + padding 0 14px + + .che-input-desktop + margin-top -1px + + .che-select + margin-top -2px diff --git a/dashboard/src/app/admin/user-management/add-user/add-user.controller.ts b/dashboard/src/app/admin/user-management/add-user/add-user.controller.ts new file mode 100644 index 0000000000..c1fa184fd0 --- /dev/null +++ b/dashboard/src/app/admin/user-management/add-user/add-user.controller.ts @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2015-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +'use strict'; +import {AdminsUserManagementCtrl} from '../user-management.controller'; + +/** + * This class is handling the controller for the add user + * @author Oleksii Orel + */ +export class AdminsAddUserController { + private $mdDialog: ng.material.IDialogService; + private lodash: any; + private cheNotification: any; + private cheUser: any; + private callbackController: AdminsUserManagementCtrl; + private newUserName: string; + private newUserEmail: string; + private newUserPassword: string; + private organizations: Array; + private organization: string; + private cheOrganization: che.api.ICheOrganization; + private chePermissions: che.api.IChePermissions; + private organizationRoles: che.resource.ICheOrganizationRoles; + + /** + * Default constructor. + * @ngInject for Dependency injection + */ + constructor($mdDialog: ng.material.IDialogService, + cheUser: any, + cheNotification: any, + lodash: any, + cheOrganization: che.api.ICheOrganization, + chePermissions: che.api.IChePermissions, + resourcesService: che.service.IResourcesService) { + this.$mdDialog = $mdDialog; + this.lodash = lodash; + this.cheUser = cheUser; + this.cheNotification = cheNotification; + this.cheOrganization = cheOrganization; + this.chePermissions = chePermissions; + this.organizationRoles = resourcesService.getOrganizationRoles(); + + this.organizations = []; + + this.cheOrganization.fetchOrganizations().then(() => { + let organizations = this.cheOrganization.getOrganizations(); + let rootOrganizations = organizations.filter((organization: any) => { + return !organization.parent; + }); + this.organizations = lodash.pluck(rootOrganizations, 'name'); + if (this.organizations.length > 0) { + this.organization = this.organizations[0]; + } + }); + } + + /** + * Callback of the cancel button of the dialog. + */ + abort(): void { + this.$mdDialog.hide(); + } + + /** + * Callback of the add button of the dialog(create new user). + */ + createUser(): void { + let promise = this.cheUser.createUser(this.newUserName, this.newUserEmail, this.newUserPassword); + + promise.then((data: any) => { + if (this.organization) { + this.addUserToOrganization(data.id); + } else { + this.finish(); + } + }, (error: any) => { + this.cheNotification.showError(error.data.message ? error.data.message : 'Failed to create user.'); + }); + } + + /** + * Finish user creation. + */ + private finish(): void { + this.$mdDialog.hide(); + this.callbackController.updateUsers(); + this.cheNotification.showInfo('User successfully created.'); + } + + /** + * Adds user to chosen organization. + * + * @param userId + */ + private addUserToOrganization(userId: string): void { + let organizations = this.cheOrganization.getOrganizations(); + let organization = this.lodash.find(organizations, (organization: any) => { + return organization.name === this.organization; + }); + + let actions = this.organizationRoles.MEMBER.actions; + let permissions = { + instanceId: organization.id, + userId: userId, + domainId: 'organization', + actions: actions + }; + this.chePermissions.storePermissions(permissions).then(() => { + this.finish(); + }, (error: any) => { + this.cheNotification.showError(error.data.message ? error.data.message : 'Failed to add user to organization' + this.organization + '.'); + }); + } +} diff --git a/dashboard/src/app/admin/user-management/add-user/add-user.html b/dashboard/src/app/admin/user-management/add-user/add-user.html new file mode 100644 index 0000000000..6989bfcdf4 --- /dev/null +++ b/dashboard/src/app/admin/user-management/add-user/add-user.html @@ -0,0 +1,95 @@ + + + +
+ +
User login to be less than 100 characters long.
+
+ +
Enter a valid email address.
+
User email has to be less than 100 characters long
+
+
+ + +
+ + +
User password should contain both letters and digits
+
User password should contain at least 8 characters.
+
User password has to be less than 100 characters long.
+
+
+
+ Minimum 8 characters, both letters and digits. +
+
+
+
+
+ +
Confirm password has to be less than 100 characters long.
+
+ Passwords do not match. +
+
+ +
+
+ + + + +
+
+
diff --git a/dashboard/src/app/admin/user-management/add-user/add-user.styl b/dashboard/src/app/admin/user-management/add-user/add-user.styl new file mode 100644 index 0000000000..7383dc934c --- /dev/null +++ b/dashboard/src/app/admin/user-management/add-user/add-user.styl @@ -0,0 +1,48 @@ +.admins-add-user-form + min-width 520px + + .form-input-fields + margin 20px 0px + + che-filter-selector + margin-bottom 20px + line-height 33px + + .che-input-box-desktop + width 520px + max-width 520px + + label + max-width 23% !important + width 23% !important + margin-top 10px + + input + width 400px + float right + +.admins-add-user-form .che-input-desktop-label + margin-top 1px + +.admins-add-user-form .password-prompt, +.admins-add-user-form .che-input-desktop-label + min-width 125px + +.admins-add-user-form .password-prompt + font-size 12px + color $disabled-color + margin-bottom 20px + +.admins-add-user-form .password-prompt p + margin-top 5px + margin-bottom 5px + +.admins-add-user-form .password-prompt + .password-prompt-text + min-width 125px + + .pass-strength + width 100% + + & > div + margin-bottom 10px diff --git a/dashboard/src/app/admin/user-management/user-details/user-details.controller.ts b/dashboard/src/app/admin/user-management/user-details/user-details.controller.ts new file mode 100644 index 0000000000..253bd10124 --- /dev/null +++ b/dashboard/src/app/admin/user-management/user-details/user-details.controller.ts @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2015-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +'use strict'; + +enum Tab {Profile, Organization} + +interface IScope extends ng.IScope { + profileInformationForm: ng.IFormController; +} + +interface IProfileAttributes { + firstName?: string; + lastName?: string; + phone?: string; + country?: string; + employer?: string; + jobtitle?: string; +} + +const MAX_ITEMS = 12; + +/** + * Controller for user details. + * + * @author Oleksii Orel + */ +export class AdminUserDetailsController { + tab: Object = Tab; + + /** + * Angular Location service. + */ + private $location: ng.ILocationService; + /** + * User profile service. + */ + private cheProfile: any; + /** + * Notification service. + */ + private cheNotification: any; + /** + * Index of the selected tab. + */ + private selectedTabIndex: number = 0; + /** + * User profile. + */ + private profile: che.IProfile; + /** + * Profile attributes. + */ + private profileAttributes: IProfileAttributes; + /** + * Loading state of the page. + */ + private isLoading: boolean; + /** + * User ID. + */ + private userId: string; + /** + * User Name. + */ + private userName: string; + + private cheOrganization: che.api.ICheOrganization; + + private userOrganizations: Array; + /** + * User's page info. + */ + private pageInfo: che.IPageInfo; + + /** + * Default constructor that is using resource injection + * @ngInject for Dependency injection + */ + constructor(cheProfile: any, $location: ng.ILocationService, $timeout: ng.ITimeoutService, $scope: ng.IScope, cheNotification: any, cheOrganization: che.api.ICheOrganization, initData: {userId; userName}) { + this.cheOrganization = cheOrganization; + this.$location = $location; + this.cheProfile = cheProfile; + this.cheNotification = cheNotification; + this.userId = initData.userId; + this.userName = initData.userName; + + this.updateSelectedTab(this.$location.search().tab); + let deRegistrationFn = $scope.$watch(() => { + return $location.search().tab; + }, (tab: string) => { + if (!angular.isUndefined(tab)) { + this.updateSelectedTab(tab); + } + }, true); + + let timeoutPromise: ng.IPromise; + $scope.$watch(() => { + return angular.isUndefined(this.profileAttributes) || this.profileAttributes; + }, () => { + if (!this.profileAttributes || !($scope).profileInformationForm || ($scope).profileInformationForm.$invalid) { + return; + } + if (timeoutPromise) { + $timeout.cancel(timeoutPromise); + } + timeoutPromise = $timeout(() => { + this.setProfileAttributes(); + }, 500); + }, true); + + $scope.$on('$destroy', () => { + deRegistrationFn(); + if (timeoutPromise) { + $timeout.cancel(timeoutPromise); + } + }); + + this.updateData(); + } + + /** + * Update user's data. + */ + updateData(): void { + this.isLoading = true; + this.cheProfile.fetchProfileById(this.userId).then(() => { + this.profile = this.cheProfile.getProfileById(this.userId); + this.profileAttributes = angular.copy(this.profile.attributes); + this.fetchOrganizations(); + }, (error: any) => { + this.isLoading = false; + this.cheNotification.showError(error && error.data && error.data.message !== null ? error.data.message : 'Failed to retrieve user\'s profile.'); + }); + } + + /** + * Request the list of the user's organizations (first page). + * + * @returns {ng.IPromise} + */ + fetchOrganizations(): void { + this.isLoading = true; + this.cheOrganization.fetchUserOrganizations(this.userId, MAX_ITEMS).then((userOrganizations: Array) => { + this.userOrganizations = userOrganizations; + }, (error: any) => { + this.cheNotification.showError(error && error.data && error.data.message !== null ? error.data.message : 'Failed to retrieve organizations.'); + }).finally(() => { + this.isLoading = false; + this.pageInfo = this.cheOrganization.getUserOrganizationPageInfo(this.userId); + }); + } + + /** + * Returns the array of user's organizations. + * + * @returns {Array} + */ + getUserOrganizations(): Array { + return this.userOrganizations; + } + + /** + * Returns the the user's page info. + * + * @returns {che.IPageInfo} + */ + getPagesInfo(): che.IPageInfo { + return this.pageInfo; + } + + /** + * Request the list of the user's organizations for a page depends on page key('first', 'prev', 'next', 'last'). + * @param key {string} + */ + fetchOrganizationPageObjects(key: string): void { + this.isLoading = true; + this.cheOrganization.fetchUserOrganizationPageObjects(this.userId, key).then((userOrganizations: Array) => { + this.userOrganizations = userOrganizations; + }).finally(() => { + this.isLoading = false; + }); + } + + /** + * Check if profile attributes have changed + * @returns {boolean} + */ + isAttributesChanged(): boolean { + return !angular.equals(this.profile.attributes, this.profileAttributes); + } + + /** + * Set profile attributes + */ + setProfileAttributes(): void { + if (angular.equals(this.profile.attributes, this.profileAttributes)) { + return; + } + let promise = this.cheProfile.setAttributes(this.profileAttributes, this.userId); + + promise.then(() => { + this.cheNotification.showInfo('Profile successfully updated.'); + this.updateData(); + }, (error: any) => { + this.profileAttributes = angular.copy(this.profile.attributes); + this.cheNotification.showError(error.data.message ? error.data.message : 'Profile update failed.'); + }); + } + + /** + * Update selected tab index by search part of URL. + * + * @param {string} tab + */ + updateSelectedTab(tab: string): void { + this.selectedTabIndex = parseInt(this.tab[tab], 10); + } + + /** + * Changes search part of URL. + * + * @param {number} tabIndex + */ + onSelectTab(tabIndex?: number): void { + let param: { tab?: string } = {}; + if (!angular.isUndefined(tabIndex)) { + param.tab = Tab[tabIndex]; + } + if (angular.isUndefined(this.$location.search().tab)) { + this.$location.replace(); + } + this.$location.search(param); + } + +} diff --git a/dashboard/src/app/admin/user-management/user-details/user-details.html b/dashboard/src/app/admin/user-management/user-details/user-details.html new file mode 100644 index 0000000000..8cd58b10dc --- /dev/null +++ b/dashboard/src/app/admin/user-management/user-details/user-details.html @@ -0,0 +1,45 @@ + + + + + + + + + + Profile + + +
+ +
+ +
+
+ + + + + Organizations + + +
+ +
+ + +
+
+ +
+
diff --git a/dashboard/src/app/admin/user-management/user-management-config.ts b/dashboard/src/app/admin/user-management/user-management-config.ts new file mode 100644 index 0000000000..0375f9528e --- /dev/null +++ b/dashboard/src/app/admin/user-management/user-management-config.ts @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2015-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +'use strict'; + +import {AdminsAddUserController} from './add-user/add-user.controller'; +import {AdminsUserManagementCtrl} from './user-management.controller'; +import {AdminUserDetailsController} from './user-details/user-details.controller'; +import {AccountProfile} from './account-profile/account-profile.directive'; + +export class AdminsUserManagementConfig { + + constructor(register: che.IRegisterService) { + register.controller('AdminUserDetailsController', AdminUserDetailsController); + register.controller('AdminsAddUserController', AdminsAddUserController); + register.controller('AdminsUserManagementCtrl', AdminsUserManagementCtrl); + register.directive('accountProfile', AccountProfile); + + const userDetailLocationProvider = { + title: 'User Details', + reloadOnSearch: false, + templateUrl: 'app/admin/user-management/user-details/user-details.html', + controller: 'AdminUserDetailsController', + controllerAs: 'adminUserDetailsController', + resolve: { + initData: ['$q', 'cheUser', '$route', 'chePermissions', ($q: ng.IQService, cheUser: any, $route: any, chePermissions: che.api.IChePermissions) => { + const userId = $route.current.params.userId; + let defer = $q.defer(); + chePermissions.fetchSystemPermissions().finally(() => { + cheUser.fetchUserId(userId).then((user: che.IUser) => { + if (!chePermissions.getUserServices().hasAdminUserService) { + defer.reject(); + } + defer.resolve({userId: userId, userName: user.name}); + }, (error: any) => { + defer.reject(error); + }); + }); + return defer.promise; + }] + } + }; + + // configure routes + register.app.config(($routeProvider: che.route.IRouteProvider) => { + $routeProvider.accessWhen('/admin/usermanagement', { + title: 'Users', + templateUrl: 'app/admin/user-management/user-management.html', + controller: 'AdminsUserManagementCtrl', + controllerAs: 'adminsUserManagementCtrl', + resolve: { + check: ['$q', 'chePermissions', ($q: ng.IQService, chePermissions: che.api.IChePermissions) => { + let defer = $q.defer(); + chePermissions.fetchSystemPermissions().finally(() => { + if (chePermissions.getUserServices().hasUserService) { + defer.resolve(); + } else { + defer.reject(); + } + }); + return defer.promise; + }] + } + }) + .accessWhen('/admin/userdetails/:userId', userDetailLocationProvider); + }); + + } +} diff --git a/dashboard/src/app/admin/user-management/user-management.controller.ts b/dashboard/src/app/admin/user-management/user-management.controller.ts new file mode 100644 index 0000000000..304bd2a3d1 --- /dev/null +++ b/dashboard/src/app/admin/user-management/user-management.controller.ts @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2015-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +'use strict'; + +const MAX_ITEMS = 12; + +/** + * This class is handling the controller for the admins user management + * @author Oleksii Orel + */ +export class AdminsUserManagementCtrl { + $q: ng.IQService; + $log: ng.ILogService; + $mdDialog: ng.material.IDialogService; + $location: ng.ILocationService; + cheUser: any; + cheNotification: any; + pagesInfo: any; + users: Array; + usersMap: Map; + userFilter: {name: string}; + userOrderBy: string; + isLoading: boolean; + + private confirmDialogService: any; + private cheOrganization: che.api.ICheOrganization; + private userOrganizationCount: {[userId: string]: number} = {}; + private cheListHelper: che.widget.ICheListHelper; + + /** + * Default constructor. + * @ngInject for Dependency injection + */ + constructor($q: ng.IQService, + $rootScope: che.IRootScopeService, + $log: ng.ILogService, + $mdDialog: ng.material.IDialogService, + cheUser: any, + $location: ng.ILocationService, + cheNotification: any, + confirmDialogService: any, + cheOrganization: che.api.ICheOrganization, + $scope: ng.IScope, + cheListHelperFactory: che.widget.ICheListHelperFactory) { + this.$q = $q; + this.$log = $log; + this.$mdDialog = $mdDialog; + this.$location = $location; + this.cheUser = cheUser; + this.cheOrganization = cheOrganization; + this.cheNotification = cheNotification; + this.confirmDialogService = confirmDialogService; + + $rootScope.showIDE = false; + + this.isLoading = false; + + this.users = []; + this.usersMap = this.cheUser.getUsersMap(); + + this.userOrderBy = 'name'; + this.userFilter = {name: ''}; + + const helperId = 'user-management'; + this.cheListHelper = cheListHelperFactory.getHelper(helperId); + $scope.$on('$destroy', () => { + cheListHelperFactory.removeHelper(helperId); + }); + + if (this.usersMap && this.usersMap.size > 1) { + this.updateUsers(); + } else { + this.isLoading = true; + this.cheUser.fetchUsers(MAX_ITEMS, 0).then(() => { + this.isLoading = false; + this.updateUsers(); + }, (error: any) => { + this.isLoading = false; + if (error && error.status !== 304) { + this.cheNotification.showError(error.data && error.data.message ? error.data.message : 'Failed to retrieve the list of users.'); + } + }); + } + + this.pagesInfo = this.cheUser.getPagesInfo(); + } + + /** + * Callback when name is changed. + * + * @param str {string} a string to filter user names. + */ + onSearchChanged(str: string): void { + this.userFilter.name = str; + this.cheListHelper.applyFilter('name', this.userFilter); + } + + /** + * Redirect to user details + * @param userId {string} + * @param tab {string} + */ + redirectToUserDetails(userId: string, tab?: string): void { + this.$location.path('/admin/userdetails/' + userId).search(!tab ? {} : {tab: tab}); + } + + /** + * Update user's organizations count + * @param userId {string} + */ + updateUserOrganizationsCount(userId: string): void { + this.cheOrganization.fetchUserOrganizations(userId, 1).then((userOrganizations: Array) => { + if (!angular.isArray(userOrganizations) || userOrganizations.length === 0) { + return; + } + this.userOrganizationCount[userId] = this.cheOrganization.getUserOrganizationPageInfo(userId).countPages; + }); + } + + /** + * User clicked on the - action to remove the user. Show the dialog + * @param event {MouseEvent} - the $event + * @param user {any} - the selected user + */ + removeUser(event: MouseEvent, user: any): void { + let content = 'Are you sure you want to remove \'' + user.email + '\'?'; + let promise = this.confirmDialogService.showConfirmDialog('Remove user', content, 'Delete', 'Cancel'); + + promise.then(() => { + this.isLoading = true; + let promise = this.cheUser.deleteUserById(user.id); + promise.then(() => { + this.isLoading = false; + this.updateUsers(); + }, (error: any) => { + this.isLoading = false; + this.cheNotification.showError(error.data && error.data.message ? error.data.message : 'Delete user failed.'); + }); + }); + } + + /** + * Delete all selected users + */ + deleteSelectedUsers(): void { + const selectedUsers = this.cheListHelper.getSelectedItems(), + selectedUserIds = selectedUsers.map((user: che.IUser) => { + return user.id; + }); + + const queueLength = selectedUserIds.length; + if (!queueLength) { + this.cheNotification.showError('No such user.'); + return; + } + + const confirmationPromise = this.showDeleteUsersConfirmation(queueLength); + confirmationPromise.then(() => { + const numberToDelete = queueLength; + const deleteUserPromises = []; + let isError = false; + let currentUserId; + + selectedUserIds.forEach((userId: string) => { + currentUserId = userId; + this.cheListHelper.itemsSelectionStatus[userId] = false; + + let promise = this.cheUser.deleteUserById(userId); + promise.catch((error: any) => { + isError = true; + this.$log.error('Cannot delete user: ', error); + }); + deleteUserPromises.push(promise); + }); + + this.$q.all(deleteUserPromises).finally(() => { + this.isLoading = true; + + const promise = this.cheUser.fetchUsersPage(this.pagesInfo.currentPageNumber); + promise.then(() => { + this.isLoading = false; + this.updateUsers(); + }, (error: any) => { + this.isLoading = false; + this.$log.error(error); + }); + + if (isError) { + this.cheNotification.showError('Delete failed.'); + } else { + if (numberToDelete === 1) { + this.cheNotification.showInfo('Selected user has been removed.'); + } else { + this.cheNotification.showInfo('Selected users have been removed.'); + } + } + }); + }); + } + + /** + * Show confirmation popup before delete + * @param numberToDelete {number} + * @returns {angular.IPromise} + */ + showDeleteUsersConfirmation(numberToDelete: number): angular.IPromise { + let content = 'Are you sure you want to remove ' + numberToDelete + ' selected '; + if (numberToDelete > 1) { + content += 'users?'; + } else { + content += 'user?'; + } + + return this.confirmDialogService.showConfirmDialog('Remove users', content, 'Delete', 'Cancel'); + } + + /** + * Update users array + */ + updateUsers(): void { + // update users array + this.users.length = 0; + this.usersMap.forEach((user: any) => { + this.users.push(user); + }); + + this.cheListHelper.setList(this.users, 'id'); + } + + /** + * Ask for loading the users page in asynchronous way + * @param pageKey {string} - the key of page + */ + fetchUsersPage(pageKey: string): void { + this.isLoading = true; + let promise = this.cheUser.fetchUsersPage(pageKey); + + promise.then(() => { + this.isLoading = false; + this.updateUsers(); + }, (error: any) => { + this.isLoading = false; + if (error.status === 304) { + this.updateUsers(); + } else { + this.cheNotification.showError(error.data && error.data.message ? error.data.message : 'Update information failed.'); + } + }); + } + + /** + * Returns true if the next page is exist. + * @returns {boolean} + */ + hasNextPage(): boolean { + return this.pagesInfo.currentPageNumber < this.pagesInfo.countOfPages; + } + + /** + * Returns true if the previous page is exist. + * @returns {boolean} + */ + hasPreviousPage(): boolean { + return this.pagesInfo.currentPageNumber > 1; + } + + /** + * Returns true if we have more then one page. + * @returns {boolean} + */ + isPagination(): boolean { + return this.pagesInfo.countOfPages > 1; + } + + /** + * Add a new user. Show the dialog + * @param event {MouseEvent} - the $event + */ + showAddUserDialog(event: MouseEvent): void { + this.$mdDialog.show({ + targetEvent: event, + bindToController: true, + clickOutsideToClose: true, + controller: 'AdminsAddUserController', + controllerAs: 'adminsAddUserController', + locals: {callbackController: this}, + templateUrl: 'app/admin/user-management/add-user/add-user.html' + }); + } +} diff --git a/dashboard/src/app/admin/user-management/user-management.html b/dashboard/src/app/admin/user-management/user-management.html new file mode 100644 index 0000000000..49329e16f1 --- /dev/null +++ b/dashboard/src/app/admin/user-management/user-management.html @@ -0,0 +1,147 @@ + + + +
+ +
+
+ +
+
+
+ +
+
+
+ + + + +
+
+
+ + +
+
+ +
+
+
+ Email + + {{user.email}} +
+
+ Login + {{user.name}} +
+
+ Organizations + {{adminsUserManagementCtrl.userOrganizationCount[user.id] ? adminsUserManagementCtrl.userOrganizationCount[user.id] : '-'}} +
+
+ Actions + +
+ +
+
+
+
+
+
+
+ + << + + + < + + + {{adminsUserManagementCtrl.pagesInfo.currentPageNumber}} + + + > + + + >> + +
+
+
+ + No users found. + + There are no users. +
+
+
diff --git a/dashboard/src/app/admin/user-management/user-management.styl b/dashboard/src/app/admin/user-management/user-management.styl new file mode 100644 index 0000000000..1d30dab23a --- /dev/null +++ b/dashboard/src/app/admin/user-management/user-management.styl @@ -0,0 +1,17 @@ +.admins-user-management + .user-email, + .user-description + max-width 100% + .user-description + color $label-info-color + .user-face + che-developers-face() + height 16px + width 16px + margin-right 5px + + .che-list-item + outline none + user-select none + * + outline inherit diff --git a/dashboard/src/app/index.module.ts b/dashboard/src/app/index.module.ts index 94c048f8be..5b65788aad 100755 --- a/dashboard/src/app/index.module.ts +++ b/dashboard/src/app/index.module.ts @@ -36,12 +36,81 @@ import IdeIFrameSvc from './ide/ide-iframe/ide-iframe.service'; import {CheIdeFetcher} from '../components/ide-fetcher/che-ide-fetcher.service'; import {RouteHistory} from '../components/routing/route-history.service'; import {CheUIElementsInjectorService} from '../components/service/injector/che-ui-elements-injector.service'; -import {WorkspaceDetailsService} from './workspaces/workspace-details/workspace-details.service'; +import {OrganizationsConfig} from './organizations/organizations-config'; +import {TeamsConfig} from './teams/teams-config'; // init module const initModule = angular.module('userDashboard', ['ngAnimate', 'ngCookies', 'ngTouch', 'ngSanitize', 'ngResource', 'ngRoute', 'angular-websocket', 'ui.bootstrap', 'ui.codemirror', 'ngMaterial', 'ngMessages', 'angularMoment', 'angular.filter', - 'ngDropdowns', 'ngLodash', 'angularCharts', 'uuid4', 'angularFileUpload']); + 'ngDropdowns', 'ngLodash', 'angularCharts', 'uuid4', 'angularFileUpload', 'ui.gravatar']); + +declare const Keycloak: Function; +function buildKeycloakConfig(keycloakSettings: any) { + return { + url: keycloakSettings['che.keycloak.auth_server_url'], + realm: keycloakSettings['che.keycloak.realm'], + clientId: keycloakSettings['che.keycloak.client_id'] + }; +} +interface IResolveFn { + (value: T | PromiseLike): Promise; +} +interface IRejectFn { + (reason: any): Promise; +} +function keycloakLoad(keycloakSettings: any) { + return new Promise((resolve: IResolveFn, reject: IRejectFn) => { + const script = document.createElement('script'); + script.async = true; + script.src = keycloakSettings['che.keycloak.auth_server_url'] + '/js/keycloak.js'; + script.addEventListener('load', resolve); + script.addEventListener('error', () => reject('Error loading script.')); + script.addEventListener('abort', () => reject('Script loading aborted.')); + document.head.appendChild(script); + }); +} +function keycloakInit(keycloakConfig: any) { + return new Promise((resolve: IResolveFn, reject: IRejectFn) => { + const keycloak = Keycloak(keycloakConfig); + keycloak.init({ + onLoad: 'login-required', checkLoginIframe: false + }).success(() => { + resolve(keycloak); + }).error((error: any) => { + reject(error); + }); + }); +} + +const keycloakAuth = { + isPresent: false, + keycloak: null, + config: null +}; +initModule.constant('keycloakAuth', keycloakAuth); + +const promise = new Promise((resolve: IResolveFn, reject: IRejectFn) => { + angular.element.get('/api/keycloak/settings').then(resolve, reject); +}); +promise.then((keycloakSettings: any) => { + keycloakAuth.config = buildKeycloakConfig(keycloakSettings); + + // load Keycloak + return keycloakLoad(keycloakSettings).then(() => { + // init Keycloak + return keycloakInit(keycloakAuth.config); + }).then((keycloak: any) => { + keycloakAuth.isPresent = true; + keycloakAuth.keycloak = keycloak; + /* tslint:disable */ + window['_keycloak'] = keycloak; + /* tslint:enable */ + }); +}).catch((error: any) => { + console.error('Keycloak initialization failed with error: ', error); +}).then(() => { + angular.bootstrap(document.body, ['userDashboard'], {strictDi: true}); // manually bootstrap Angular +}); // add a global resolve flag on all routes (user needs to be resolved first) initModule.config(['$routeProvider', ($routeProvider: che.route.IRouteProvider) => { @@ -92,7 +161,7 @@ initModule.config(['$routeProvider', ($routeProvider: che.route.IRouteProvider) const DEV = false; // configs -initModule.config(['$routeProvider', ($routeProvider: che.route.IRouteProvider) => { +initModule.config(['$routeProvider', ($routeProvider) => { // config routes (add demo page) if (DEV) { $routeProvider.accessWhen('/demo-components', { @@ -113,13 +182,11 @@ initModule.config(['$routeProvider', ($routeProvider: che.route.IRouteProvider) * Setup route redirect module */ initModule.run(['$rootScope', '$location', '$routeParams', 'routingRedirect', '$timeout', 'ideIFrameSvc', 'cheIdeFetcher', 'routeHistory', 'cheUIElementsInjectorService', 'workspaceDetailsService', - ($rootScope: che.IRootScopeService, $location: ng.ILocationService, $routeParams: ng.route.IRouteParamsService, routingRedirect: RoutingRedirect, $timeout: ng.ITimeoutService, ideIFrameSvc: IdeIFrameSvc, cheIdeFetcher: CheIdeFetcher, routeHistory: RouteHistory, cheUIElementsInjectorService: CheUIElementsInjectorService, workspaceDetailsService: WorkspaceDetailsService) => { + ($rootScope: che.IRootScopeService, $location: ng.ILocationService, $routeParams: ng.route.IRouteParamsService, routingRedirect: RoutingRedirect, $timeout: ng.ITimeoutService, ideIFrameSvc: IdeIFrameSvc, cheIdeFetcher: CheIdeFetcher, routeHistory: RouteHistory, cheUIElementsInjectorService: CheUIElementsInjectorService) => { $rootScope.hideLoader = false; $rootScope.waitingLoaded = false; $rootScope.showIDE = false; - workspaceDetailsService.addPage('SSH', '', 'icon-ic_vpn_key_24px'); - // here only to create instances of these components /* tslint:disable */ cheIdeFetcher; @@ -172,45 +239,6 @@ initModule.run(['$rootScope', '$location', '$routeParams', 'routingRedirect', '$ } ]); -// add interceptors -initModule.factory('ETagInterceptor', ($window: ng.IWindowService, $cookies: ng.cookies.ICookiesService, $q: ng.IQService) => { - - const etagMap = {}; - - return { - request: (config: any) => { - // add IfNoneMatch request on the che api if there is an existing eTag - if ('GET' === config.method) { - if (config.url.indexOf('/api') === 0) { - const eTagURI = etagMap[config.url]; - if (eTagURI) { - config.headers = config.headers || {}; - angular.extend(config.headers, {'If-None-Match': eTagURI}); - } - } - } - return config || $q.when(config); - }, - response: (response: any) => { - - // if response is ok, keep ETag - if ('GET' === response.config.method) { - if (response.status === 200) { - const responseEtag = response.headers().etag; - if (responseEtag) { - if (response.config.url.indexOf('/api') === 0) { - - etagMap[response.config.url] = responseEtag; - } - } - } - - } - return response || $q.when(response); - } - }; -}); - initModule.config(($mdThemingProvider: ng.material.IThemingProvider, jsonColors: any) => { const cheColors = angular.fromJson(jsonColors); @@ -357,11 +385,6 @@ initModule.constant('userDashboardConfig', { developmentMode: DEV }); -initModule.config(['$routeProvider', '$httpProvider', ($routeProvider: che.route.IRouteProvider, $httpProvider: ng.IHttpProvider) => { - // add the ETag interceptor for Che API - $httpProvider.interceptors.push('ETagInterceptor'); -}]); - const instanceRegister = new Register(initModule); if (DEV) { @@ -386,4 +409,6 @@ new WorkspacesConfig(instanceRegister); new DashboardConfig(instanceRegister); new StacksConfig(instanceRegister); new FactoryConfig(instanceRegister); +new OrganizationsConfig(instanceRegister); +new TeamsConfig(instanceRegister); /* tslint:enable */ diff --git a/dashboard/src/app/navbar/navbar.controller.ts b/dashboard/src/app/navbar/navbar.controller.ts index 47767dcd69..e0ba472285 100644 --- a/dashboard/src/app/navbar/navbar.controller.ts +++ b/dashboard/src/app/navbar/navbar.controller.ts @@ -10,9 +10,10 @@ */ 'use strict'; import {CheAPI} from '../../components/api/che-api.factory'; +import {CheKeycloak, keycloakUserInfo} from '../../components/api/che-keycloak.factory'; export class CheNavBarController { - menuItemUrl = { + private menuItemUrl = { dashboard: '#/', workspaces: '#/workspaces', administration: '#/administration', @@ -20,9 +21,26 @@ export class CheNavBarController { plugins: '#/admin/plugins', factories: '#/factories', account: '#/account', - stacks: '#/stacks' + stacks: '#/stacks', + organizations: '#/organizations', + usermanagement: '#/admin/usermanagement' }; + accountItems = [ + { + name: 'Go to Profile', + onclick: () => { + this.gotoProfile(); + } + }, + { + name: 'Logout', + onclick: () => { + this.logout(); + } + } + ]; + private $mdSidenav: ng.material.ISidenavService; private $scope: ng.IScope; private $window: ng.IWindowService; @@ -30,21 +48,39 @@ export class CheNavBarController { private $route: ng.route.IRouteService; private cheAPI: CheAPI; private profile: che.IProfile; + private chePermissions: che.api.IChePermissions; + private userServices: che.IUserServices; + private hasPersonalAccount: boolean; + private organizations: Array; + private cheKeycloak: CheKeycloak; + private userInfo: keycloakUserInfo; /** * Default constructor * @ngInject for Dependency injection */ - constructor($mdSidenav: ng.material.ISidenavService, $scope: ng.IScope, $location: ng.ILocationService, $route: ng.route.IRouteService, cheAPI: CheAPI, $window: ng.IWindowService) { + constructor($mdSidenav: ng.material.ISidenavService, + $scope: ng.IScope, + $location: ng.ILocationService, + $route: ng.route.IRouteService, + cheAPI: CheAPI, + $window: ng.IWindowService, + chePermissions: che.api.IChePermissions, + cheKeycloak: CheKeycloak) { this.$mdSidenav = $mdSidenav; this.$scope = $scope; this.$location = $location; this.$route = $route; this.cheAPI = cheAPI; this.$window = $window; + this.chePermissions = chePermissions; + this.cheKeycloak = cheKeycloak; + this.userInfo = null; this.profile = cheAPI.getProfile().getProfile(); + this.userServices = this.chePermissions.getUserServices(); + // highlight navbar menu item $scope.$on('$locationChangeStart', () => { let path = '#' + $location.path(); @@ -53,6 +89,34 @@ export class CheNavBarController { cheAPI.getWorkspace().fetchWorkspaces(); cheAPI.getFactory().fetchFactories(); + + if (this.cheKeycloak.isPresent()) { + this.cheKeycloak.fetchUserInfo().then((userInfo: keycloakUserInfo) => { + this.userInfo = userInfo; + }); + } + + if (this.chePermissions.getSystemPermissions()) { + this.updateData(); + } else { + this.chePermissions.fetchSystemPermissions().finally(() => { + this.updateData(); + }); + } + } + + /** + * Update data. + */ + updateData(): void { + const organization = this.cheAPI.getOrganization(); + organization.fetchOrganizations().then(() => { + this.organizations = organization.getOrganizations(); + const user = this.cheAPI.getUser().getUser(); + organization.fetchOrganizationByName(user.name).finally(() => { + this.hasPersonalAccount = angular.isDefined(organization.getOrganizationByName(user.name)); + }); + }); } reload(): void { @@ -66,15 +130,63 @@ export class CheNavBarController { this.$mdSidenav('left').toggle(); } + /** + * Returns number of workspaces. + * + * @return {number} + */ getWorkspacesNumber(): number { return this.cheAPI.getWorkspace().getWorkspaces().length; } + /** + * Returns number of factories. + * + * @return {number} + */ getFactoriesNumber(): number { return this.cheAPI.getFactory().getPageFactories().length; } + /** + * Returns number of all organizations. + * + * @return {number} + */ + getOrganizationsNumber(): number { + if (!this.organizations) { + return 0; + } + + return this.organizations.length; + } + openLinkInNewTab(url: string): void { this.$window.open(url, '_blank'); } + + /** + * Returns true if Keycloak is present. + * + * @returns {boolean} + */ + isKeycloakPresent(): boolean { + return this.cheKeycloak.isPresent(); + } + + /** + * Opens user profile in new browser page. + */ + gotoProfile(): void { + const url = this.cheKeycloak.getProfileUrl(); + this.$window.open(url); + } + + /** + * Logout. + */ + logout(): void { + this.cheKeycloak.logout(); + } + } diff --git a/dashboard/src/app/navbar/navbar.directive.ts b/dashboard/src/app/navbar/navbar.directive.ts index 0baa7dbb76..338666e395 100644 --- a/dashboard/src/app/navbar/navbar.directive.ts +++ b/dashboard/src/app/navbar/navbar.directive.ts @@ -30,7 +30,7 @@ export class CheNavBar { this.replace = false; this.templateUrl = 'app/navbar/navbar.html'; this.controller = 'CheNavBarController'; - this.controllerAs = 'navbarCtrl'; + this.controllerAs = 'navbarController'; } } diff --git a/dashboard/src/app/navbar/navbar.html b/dashboard/src/app/navbar/navbar.html index a4b4922d1f..9376750635 100644 --- a/dashboard/src/app/navbar/navbar.html +++ b/dashboard/src/app/navbar/navbar.html @@ -12,8 +12,9 @@ -->