diff --git a/pom.xml b/pom.xml
index 8d994b17..7e598fb3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -178,12 +178,10 @@
1.3.2
- com.github.pagehelper
- pagehelper-spring-boot-starter
- 1.2.10
-
-
-
+ com.github.pagehelper
+ pagehelper-spring-boot-starter
+ 1.2.10
+
@@ -226,6 +224,13 @@
spring-security-jwt
1.0.9.RELEASE
+
+
+
+ org.eclipse.rap
+ org.eclipse.rap.rwt
+ 3.5.0
+
diff --git a/src/main/java/ch/ethz/seb/sebserver/SEBServer.java b/src/main/java/ch/ethz/seb/sebserver/SEBServer.java
index 225c13d9..a07982bf 100644
--- a/src/main/java/ch/ethz/seb/sebserver/SEBServer.java
+++ b/src/main/java/ch/ethz/seb/sebserver/SEBServer.java
@@ -14,12 +14,12 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
import org.springframework.context.annotation.Configuration;
-@Configuration
@SpringBootApplication(exclude = {
// OAuth2ResourceServerAutoConfiguration.class,
UserDetailsServiceAutoConfiguration.class,
DataSourceAutoConfiguration.class
})
+@Configuration
public class SEBServer {
public static void main(final String[] args) {
diff --git a/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java b/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java
index d85e7114..aa43138e 100644
--- a/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java
+++ b/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java
@@ -8,10 +8,27 @@
package ch.ethz.seb.sebserver;
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@@ -21,7 +38,12 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@Configuration
@WebServiceProfile
@GuiProfile
-public class WebSecurityConfig {
+@RestController
+@Order(6)
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements ErrorController {
+
+ @Value("${sebserver.webservice.api.redirect.unauthorized}")
+ private String unauthorizedRedirect;
/** Spring bean name of user password encoder */
public static final String USER_PASSWORD_ENCODER_BEAN_NAME = "userPasswordEncoder";
@@ -40,4 +62,54 @@ public class WebSecurityConfig {
return new BCryptPasswordEncoder(4);
}
+ @Override
+ public void configure(final WebSecurity web) {
+ web
+ .ignoring()
+ .antMatchers("/error");
+ }
+
+ @Override
+ public void configure(final HttpSecurity http) throws Exception {
+ http
+ .sessionManagement()
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ .and()
+ .antMatcher("/**")
+ .authorizeRequests()
+ .anyRequest()
+ .authenticated()
+ .and()
+ .exceptionHandling()
+ .authenticationEntryPoint(
+ new AuthenticationEntryPoint() {
+
+ @Override
+ public void commence(
+ final HttpServletRequest request,
+ final HttpServletResponse response,
+ final AuthenticationException authException) throws IOException, ServletException {
+
+ response.sendRedirect(WebSecurityConfig.this.unauthorizedRedirect);
+ }
+ })
+ .and()
+ .formLogin().disable()
+ .httpBasic().disable()
+ .logout().disable()
+ .headers().frameOptions().disable()
+ .and()
+ .csrf().disable();
+ }
+
+ @RequestMapping("/error")
+ public void handleError(final HttpServletResponse response) throws IOException {
+ response.sendRedirect(this.unauthorizedRedirect);
+ }
+
+ @Override
+ public String getErrorPath() {
+ return "/error";
+ }
+
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/profile/GuiProfile.java b/src/main/java/ch/ethz/seb/sebserver/gbl/profile/GuiProfile.java
index 4a3162c2..609fe2a5 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gbl/profile/GuiProfile.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gbl/profile/GuiProfile.java
@@ -21,6 +21,6 @@ import org.springframework.context.annotation.Profile;
* but for all vertical profiles like dev, prod and test */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
-@Profile({ "dev-gui", "prod-gui", "test" })
+@Profile({ "dev-gui", "prod-gui" })
public @interface GuiProfile {
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/GuiTestController.java b/src/main/java/ch/ethz/seb/sebserver/gui/GuiTestController.java
deleted file mode 100644
index 3284ef86..00000000
--- a/src/main/java/ch/ethz/seb/sebserver/gui/GuiTestController.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-package ch.ethz.seb.sebserver.gui;
-
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestMethod;
-import org.springframework.web.bind.annotation.RestController;
-
-import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
-
-@RestController
-@RequestMapping("/gui")
-@GuiProfile
-public class GuiTestController {
-
- public GuiTestController() {
- System.out.println("************** TestController GUI");
- }
-
- @RequestMapping(value = "/", method = RequestMethod.GET)
- public String helloFromGuiService() {
- return "Hello From GUI";
- }
-
-}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/GuiWebsecurityConfig.java b/src/main/java/ch/ethz/seb/sebserver/gui/GuiWebsecurityConfig.java
index 6f00b3be..3e60e612 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gui/GuiWebsecurityConfig.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/GuiWebsecurityConfig.java
@@ -8,12 +8,12 @@
package ch.ethz.seb.sebserver.gui;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
-import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -22,12 +22,15 @@ import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
@Configuration
@GuiProfile
-@Order(3)
+@Order(4)
public class GuiWebsecurityConfig extends WebSecurityConfigurerAdapter {
+ @Value("${sebserver.gui.entrypoint}")
+ private String guiEndpointPath;
+
/** Gui-service related public URLS from spring web security perspective */
public static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
- new AntPathRequestMatcher("/gui/**"),
+ new AntPathRequestMatcher("/gui"),
// RAP/RWT resources has to be accessible
new AntPathRequestMatcher("/rwt-resources/**"),
// project specific static resources
@@ -43,22 +46,35 @@ public class GuiWebsecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(final HttpSecurity http) throws Exception {
System.out.println("**************** GuiWebConfig: ");
- //@formatter:off
- http
- .sessionManagement()
- .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
- .and()
- .antMatcher("/gui/**")
- .authorizeRequests()
- .anyRequest()
- .authenticated()
- .and()
- .formLogin().disable()
- .httpBasic().disable()
- .logout().disable()
- .headers().frameOptions().disable()
- .and()
- .csrf().disable();
+// //@formatter:off
+// http
+// .sessionManagement()
+// .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+// .and()
+// .antMatcher("/**"/*this.guiEndpointPath + "/**"*/)
+// .authorizeRequests()
+// .anyRequest()
+// .authenticated()
+// .and()
+// .exceptionHandling()
+// .authenticationEntryPoint(
+// new AuthenticationEntryPoint() {
+//
+// @Override
+// public void commence(final HttpServletRequest request, final HttpServletResponse response,
+// final AuthenticationException authException) throws IOException, ServletException {
+// response.sendRedirect("/gui");
+// }
+//
+// })
+// .and()
+// .formLogin().disable()
+// .httpBasic().disable()
+// .logout().disable()
+// .headers().frameOptions().disable()
+// .and()
+// .csrf().disable();
//@formatter:on
}
+
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/RAPConfiguration.java b/src/main/java/ch/ethz/seb/sebserver/gui/RAPConfiguration.java
new file mode 100644
index 00000000..d4c3d444
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/RAPConfiguration.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package ch.ethz.seb.sebserver.gui;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpSession;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.application.AbstractEntryPoint;
+import org.eclipse.rap.rwt.application.Application;
+import org.eclipse.rap.rwt.application.ApplicationConfiguration;
+import org.eclipse.rap.rwt.application.EntryPoint;
+import org.eclipse.rap.rwt.application.EntryPointFactory;
+import org.eclipse.rap.rwt.client.WebClient;
+import org.eclipse.rap.rwt.client.service.StartupParameters;
+import org.eclipse.swt.widgets.Composite;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.context.support.WebApplicationContextUtils;
+
+import ch.ethz.seb.sebserver.gbl.model.Entity;
+import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder;
+import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext;
+
+public class RAPConfiguration implements ApplicationConfiguration {
+
+ private static final Logger log = LoggerFactory.getLogger(RAPConfiguration.class);
+
+ @Override
+ public void configure(final Application application) {
+ final Map properties = new HashMap<>();
+ properties.put(WebClient.PAGE_TITLE, "SEB Server");
+ properties.put(WebClient.BODY_HTML, "Loading Application");
+// properties.put(WebClient.FAVICON, "icons/favicon.png");
+
+ application.addEntryPoint("/gui", RAPSpringEntryPointFactory, properties);
+
+ try {
+ // TODO get file path from properties
+ application.addStyleSheet(RWT.DEFAULT_THEME_ID, "static/css/sebserver.css");
+ } catch (final Exception e) {
+ log.error("Error during CSS parsing. Please check the custom CSS files for errors.", e);
+ }
+ }
+
+ public static interface EntryPointService {
+
+ void loadLoginPage(final Composite parent);
+
+ void loadMainPage(final Composite parent);
+ }
+
+ private static final EntryPointFactory RAPSpringEntryPointFactory = new EntryPointFactory() {
+
+ @Override
+ public EntryPoint create() {
+ return new AbstractEntryPoint() {
+
+ private static final long serialVersionUID = -1299125117752916270L;
+
+ @Override
+ protected void createContents(final Composite parent) {
+ final HttpSession httpSession = RWT
+ .getUISession(parent.getDisplay())
+ .getHttpSession();
+
+ log.debug("Create new GUI entrypoint. HttpSession: " + httpSession);
+ if (httpSession == null) {
+ log.error("HttpSession not available from RWT.getUISession().getHttpSession()");
+ throw new IllegalStateException(
+ "HttpSession not available from RWT.getUISession().getHttpSession()");
+ }
+
+ final WebApplicationContext webApplicationContext = getWebApplicationContext(httpSession);
+
+ final EntryPointService entryPointService = webApplicationContext
+ .getBean(EntryPointService.class);
+
+ if (isAuthenticated(httpSession, webApplicationContext)) {
+ entryPointService.loadMainPage(parent);
+ } else {
+ entryPointService.loadLoginPage(parent);
+ }
+ }
+ };
+ }
+
+ private boolean isAuthenticated(
+ final HttpSession httpSession,
+ final WebApplicationContext webApplicationContext) {
+
+ // NOTE: if the user comes from a specified institutional login url (redirect from server) the institutionId is get from
+ // request and put to the session attributes. The institutionId can later be used for institution specific login page
+ // look and feel as well as for sending the institutionId within the login credentials to give the authorization service
+ // some restriction to search the user. This is especially useful if the user is external registered and verified
+ // with LDAP or AAI SAML
+ final StartupParameters reqParams = RWT.getClient().getService(StartupParameters.class);
+ final String institutionId = reqParams.getParameter(Entity.FILTER_ATTR_INSTITUTION);
+ if (StringUtils.isNotBlank(institutionId)) {
+ httpSession.setAttribute(Entity.FILTER_ATTR_INSTITUTION, institutionId);
+ } else {
+ httpSession.removeAttribute(Entity.FILTER_ATTR_INSTITUTION);
+ }
+
+ final AuthorizationContextHolder authorizationContextHolder = webApplicationContext
+ .getBean(AuthorizationContextHolder.class);
+ final SEBServerAuthorizationContext authorizationContext = authorizationContextHolder
+ .getAuthorizationContext(httpSession);
+ return authorizationContext.isValid() && authorizationContext.isLoggedIn();
+ }
+
+ private WebApplicationContext getWebApplicationContext(final HttpSession httpSession) {
+ try {
+ final ServletContext servletContext = httpSession.getServletContext();
+
+ log.debug("Initialize Spring-Context on Servlet-Context: " + servletContext);
+
+ final WebApplicationContext cc = WebApplicationContextUtils.getRequiredWebApplicationContext(
+ servletContext);
+
+ if (cc == null) {
+ log.error("Failed to initialize Spring-Context on Servlet-Context: " + servletContext);
+ throw new RuntimeException(
+ "Failed to initialize Spring-Context on Servlet-Context: " + servletContext);
+ }
+ return cc;
+ } catch (final Exception e) {
+ log.error("Failed to initialize Spring-Context on HttpSession: " + httpSession);
+ throw new RuntimeException("Failed to initialize Spring-Context on HttpSession: " + httpSession);
+ }
+ }
+
+ };
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/RAPSpringConfig.java b/src/main/java/ch/ethz/seb/sebserver/gui/RAPSpringConfig.java
new file mode 100644
index 00000000..dca20f24
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/RAPSpringConfig.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package ch.ethz.seb.sebserver.gui;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletContextListener;
+import javax.servlet.ServletException;
+
+import org.eclipse.rap.rwt.engine.RWTServlet;
+import org.eclipse.rap.rwt.engine.RWTServletContextListener;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.web.servlet.ServletContextInitializer;
+import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
+import org.springframework.boot.web.servlet.ServletRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
+
+@Configuration
+@GuiProfile
+public class RAPSpringConfig {
+
+ @Value("${sebserver.gui.entrypoint}")
+ private String entrypoint;
+
+ @Bean
+ public ServletContextInitializer initializer() {
+ return new ServletContextInitializer() {
+
+ @Override
+ public void onStartup(final ServletContext servletContext) throws ServletException {
+ servletContext.setInitParameter(
+ "org.eclipse.rap.applicationConfiguration",
+ RAPConfiguration.class.getName());
+ }
+ };
+ }
+
+ @Bean
+ public ServletListenerRegistrationBean listenerRegistrationBean() {
+ final ServletListenerRegistrationBean bean =
+ new ServletListenerRegistrationBean<>();
+ bean.setListener(new RWTServletContextListener());
+ return bean;
+ }
+
+ @Bean
+ public ServletRegistrationBean servletRegistrationBean() {
+ return new ServletRegistrationBean<>(new RWTServlet(), this.entrypoint + "/*");
+ }
+
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/BuildErrorCall.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/BuildErrorCall.java
new file mode 100644
index 00000000..10e1c42e
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/BuildErrorCall.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package ch.ethz.seb.sebserver.gui.service.remote.webservice.api;
+
+import ch.ethz.seb.sebserver.gbl.util.Result;
+
+public class BuildErrorCall extends RestCall {
+
+ private final Throwable error;
+
+ protected BuildErrorCall(final Throwable error) {
+ super(null, null, null, null);
+ this.error = error;
+ }
+
+ @Override
+ protected Result exchange(final RestCallBuilder builder) {
+ return Result.ofError(this.error);
+ }
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java
new file mode 100644
index 00000000..93f43b82
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package ch.ethz.seb.sebserver.gui.service.remote.webservice.api;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestClientResponseException;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import ch.ethz.seb.sebserver.gbl.JSONMapper;
+import ch.ethz.seb.sebserver.gbl.model.APIMessage;
+import ch.ethz.seb.sebserver.gbl.model.Page;
+import ch.ethz.seb.sebserver.gbl.util.Result;
+import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService.SortOrder;
+
+public abstract class RestCall {
+
+ private static final Logger log = LoggerFactory.getLogger(RestCall.class);
+
+ private RestService restService;
+ private JSONMapper jsonMapper;
+ protected final TypeReference typeRef;
+ protected final HttpMethod httpMethod;
+ protected final MediaType contentType;
+ protected final String path;
+
+ protected RestCall(
+ final TypeReference typeRef,
+ final HttpMethod httpMethod,
+ final MediaType contentType,
+ final String path) {
+
+ this.typeRef = typeRef;
+ this.httpMethod = httpMethod;
+ this.contentType = contentType;
+ this.path = path;
+
+ }
+
+ void init(final RestService restService, final JSONMapper jsonMapper) {
+ this.restService = restService;
+ this.jsonMapper = jsonMapper;
+ }
+
+ protected Result exchange(final RestCallBuilder builder) {
+ try {
+ final ResponseEntity responseEntity = RestCall.this.restService
+ .getWebserviceAPIRestTemplate()
+ .exchange(
+ builder.buildURI(),
+ this.httpMethod,
+ builder.buildRequestEntity(),
+ String.class,
+ builder.uriVariables);
+
+ if (responseEntity.getStatusCode() == HttpStatus.OK) {
+
+ return Result.of(RestCall.this.jsonMapper.readValue(
+ responseEntity.getBody(),
+ RestCall.this.typeRef));
+
+ } else {
+
+ final RestCallError restCallError =
+ new RestCallError("Response Entity: " + responseEntity.toString());
+ restCallError.errors.addAll(RestCall.this.jsonMapper.readValue(
+ responseEntity.getBody(),
+ new TypeReference>() {
+ }));
+ return Result.ofError(restCallError);
+ }
+
+ } catch (final Throwable t) {
+ final RestCallError restCallError = new RestCallError("Unexpected error while rest call", t);
+ try {
+ final String responseBody = ((RestClientResponseException) t).getResponseBodyAsString();
+ restCallError.errors.addAll(RestCall.this.jsonMapper.readValue(
+ responseBody,
+ new TypeReference>() {
+ }));
+ } catch (final Exception e) {
+ log.error("Unable to handle rest call error: ", e);
+ }
+
+ return Result.ofError(restCallError);
+ }
+ }
+
+ public final class RestCallBuilder {
+
+ private final HttpHeaders httpHeaders = new HttpHeaders();
+ private String body = null;
+ private final MultiValueMap queryParams = new LinkedMultiValueMap<>();
+ private final Map uriVariables = new HashMap<>();
+
+ RestCallBuilder() {
+ this.httpHeaders.set(
+ HttpHeaders.CONTENT_TYPE,
+ RestCall.this.contentType.getType());
+ }
+
+ public RestCallBuilder withHeaders(final HttpHeaders headers) {
+ this.httpHeaders.addAll(headers);
+ return this;
+ }
+
+ public RestCallBuilder withHeader(final String name, final String value) {
+ this.httpHeaders.set(name, value);
+ return this;
+ }
+
+ public RestCallBuilder withBody(final Object body) {
+ if (body instanceof String) {
+ this.body = String.valueOf(body);
+ }
+
+ try {
+ this.body = RestCall.this.jsonMapper.writeValueAsString(body);
+ } catch (final JsonProcessingException e) {
+ log.error("Error while trying to parse body json object: " + body);
+ }
+
+ return this;
+ }
+
+ public RestCallBuilder withURIVariable(final String name, final String value) {
+ this.uriVariables.put(name, value);
+ return this;
+ }
+
+ public RestCallBuilder withQueryParam(final String name, final String value) {
+ this.queryParams.put(name, Arrays.asList(value));
+ return this;
+ }
+
+ public RestCallBuilder withPaging(final int pageNumber, final int pageSize) {
+ this.queryParams.put(Page.ATTR_PAGE_NUMBER, Arrays.asList(String.valueOf(pageNumber)));
+ this.queryParams.put(Page.ATTR_PAGE_SIZE, Arrays.asList(String.valueOf(pageSize)));
+ return this;
+ }
+
+ public RestCallBuilder withSorting(final String column, final SortOrder order) {
+ this.queryParams.put(Page.ATTR_SORT, Arrays.asList(order.prefix + column));
+ return this;
+ }
+
+ public final Result exchange() {
+ return RestCall.this.exchange(this);
+ }
+
+ String buildURI() {
+ return RestCall.this.restService.getWebserviceURIBuilder()
+ .path(RestCall.this.path)
+ .queryParams(this.queryParams)
+ .toUriString();
+ }
+
+ HttpEntity> buildRequestEntity() {
+ if (this.body != null) {
+ return new HttpEntity<>(this.body, this.httpHeaders);
+ } else {
+ return new HttpEntity<>(this.httpHeaders);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCallError.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCallError.java
new file mode 100644
index 00000000..59a532fa
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCallError.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package ch.ethz.seb.sebserver.gui.service.remote.webservice.api;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import ch.ethz.seb.sebserver.gbl.model.APIMessage;
+
+public class RestCallError extends RuntimeException {
+
+ private static final long serialVersionUID = -5201349295667957490L;
+
+ final List errors = new ArrayList<>();
+
+ public RestCallError(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+
+ public RestCallError(final String message) {
+ super(message);
+ }
+
+ public List getErrorMessages() {
+ return this.errors;
+ }
+
+ public boolean hasErrorMessages() {
+ return !this.errors.isEmpty();
+ }
+
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestService.java
new file mode 100644
index 00000000..bab4862a
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestService.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package ch.ethz.seb.sebserver.gui.service.remote.webservice.api;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import ch.ethz.seb.sebserver.gbl.JSONMapper;
+import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
+import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder;
+import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.WebserviceURIBuilderSupplier;
+
+@Lazy
+@Service
+@GuiProfile
+public class RestService {
+
+ private static final Logger log = LoggerFactory.getLogger(RestService.class);
+
+ private final AuthorizationContextHolder authorizationContextHolder;
+ private final WebserviceURIBuilderSupplier webserviceURIBuilderSupplier;
+ private final JSONMapper jsonMapper;
+
+ public RestService(
+ final AuthorizationContextHolder authorizationContextHolder,
+ final WebserviceURIBuilderSupplier webserviceURIBuilderSupplier,
+ final JSONMapper jsonMapper) {
+
+ this.authorizationContextHolder = authorizationContextHolder;
+ this.webserviceURIBuilderSupplier = webserviceURIBuilderSupplier;
+ this.jsonMapper = jsonMapper;
+ }
+
+ public RestTemplate getWebserviceAPIRestTemplate() {
+ return this.authorizationContextHolder
+ .getAuthorizationContext()
+ .getRestTemplate();
+ }
+
+ public UriComponentsBuilder getWebserviceURIBuilder() {
+ return this.webserviceURIBuilderSupplier.getBuilder();
+ }
+
+ public RestCall getRestCall(final Class extends RestCall> type) {
+ try {
+ final RestCall restCall = type.getDeclaredConstructor().newInstance();
+ restCall.init(this, this.jsonMapper);
+ return restCall;
+ } catch (final Exception e) {
+ log.error("Error while trying to create RestCall of type: {}", type, e);
+ return new BuildErrorCall<>(e);
+ }
+ }
+
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/AuthorizationContextHolder.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/AuthorizationContextHolder.java
new file mode 100644
index 00000000..5ffec53c
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/AuthorizationContextHolder.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package ch.ethz.seb.sebserver.gui.service.remote.webservice.auth;
+
+import javax.servlet.http.HttpSession;
+
+import org.eclipse.rap.rwt.RWT;
+
+public interface AuthorizationContextHolder {
+
+ SEBServerAuthorizationContext getAuthorizationContext(HttpSession session);
+
+ // TODO error handling!?
+ default SEBServerAuthorizationContext getAuthorizationContext() {
+ return getAuthorizationContext(RWT.getUISession().getHttpSession());
+ }
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/CurrentUser.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/CurrentUser.java
new file mode 100644
index 00000000..db856eaf
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/CurrentUser.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package ch.ethz.seb.sebserver.gui.service.remote.webservice.auth;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Scope;
+import org.springframework.context.annotation.ScopedProxyMode;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.WebApplicationContext;
+
+import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
+import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
+
+@Component
+@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
+@GuiProfile
+public class CurrentUser {
+
+ private static final Logger log = LoggerFactory.getLogger(CurrentUser.class);
+
+ private final AuthorizationContextHolder authorizationContextHolder;
+ private SEBServerAuthorizationContext authContext = null;
+
+ public CurrentUser(final AuthorizationContextHolder authorizationContextHolder) {
+ this.authorizationContextHolder = authorizationContextHolder;
+ }
+
+ public UserInfo get() {
+ if (isAvailable()) {
+ return this.authContext.getLoggedInUser();
+ }
+
+ log.warn("Current user requested but no user is currently logged in");
+
+ return null;
+ }
+
+ public boolean isAvailable() {
+ updateContext();
+ return this.authContext != null && this.authContext.isLoggedIn();
+ }
+
+ private void updateContext() {
+ if (this.authContext == null || !this.authContext.isValid()) {
+ this.authContext = this.authorizationContextHolder.getAuthorizationContext();
+ }
+ }
+
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/OAuth2AuthorizationContextHolder.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/OAuth2AuthorizationContextHolder.java
new file mode 100644
index 00000000..6a28585e
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/OAuth2AuthorizationContextHolder.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package ch.ethz.seb.sebserver.gui.service.remote.webservice.auth;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import javax.servlet.http.HttpSession;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
+import org.springframework.security.oauth2.client.OAuth2RestTemplate;
+import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException;
+import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
+import org.springframework.security.oauth2.client.token.AccessTokenRequest;
+import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest;
+import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails;
+import org.springframework.security.oauth2.common.OAuth2AccessToken;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RequestCallback;
+import org.springframework.web.client.ResponseExtractor;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+
+import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
+import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
+import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
+
+@Lazy
+@Component
+@GuiProfile
+public class OAuth2AuthorizationContextHolder implements AuthorizationContextHolder {
+
+ private static final Logger log = LoggerFactory.getLogger(OAuth2AuthorizationContextHolder.class);
+
+ private static final String CONTEXT_HOLDER_ATTRIBUTE = "CONTEXT_HOLDER_ATTRIBUTE";
+ private static final String OAUTH_TOKEN_URI_PATH = "oauth/token"; // TODO to config properties?
+ private static final String OAUTH_REVOKE_TOKEN_URI_PATH = "/oauth/revoke-token"; // TODO to config properties?
+ private static final String CURRENT_USER_URI_PATH = "/user/me"; // TODO to config properties?
+
+ private final String guiClientId;
+ private final String guiClientSecret;
+ private final WebserviceURIBuilderSupplier webserviceURIBuilderSupplier;
+
+ @Autowired
+ public OAuth2AuthorizationContextHolder(
+ @Value("${sebserver.gui.webservice.clientId}") final String guiClientId,
+ @Value("${sebserver.gui.webservice.clientSecret}") final String guiClientSecret,
+ final WebserviceURIBuilderSupplier webserviceURIBuilderSupplier) {
+
+ this.guiClientId = guiClientId;
+ this.guiClientSecret = guiClientSecret;
+ this.webserviceURIBuilderSupplier = webserviceURIBuilderSupplier;
+ }
+
+ @Override
+ public SEBServerAuthorizationContext getAuthorizationContext(final HttpSession session) {
+ log.debug("Trying to get OAuth2AuthorizationContext from HttpSession: {}", session.getId());
+
+ OAuth2AuthorizationContext context =
+ (OAuth2AuthorizationContext) session.getAttribute(CONTEXT_HOLDER_ATTRIBUTE);
+
+ if (context == null || !context.valid) {
+ log.debug(
+ "OAuth2AuthorizationContext for HttpSession: {} is not present or is invalid. "
+ + "Create new OAuth2AuthorizationContext for this session",
+ session.getId());
+
+ context = new OAuth2AuthorizationContext(
+ this.guiClientId,
+ this.guiClientSecret,
+ this.webserviceURIBuilderSupplier);
+ session.setAttribute(CONTEXT_HOLDER_ATTRIBUTE, context);
+ }
+
+ return context;
+ }
+
+ private static final class DisposableOAuth2RestTemplate extends OAuth2RestTemplate {
+
+ private boolean enabled = true;
+
+ public DisposableOAuth2RestTemplate(final OAuth2ProtectedResourceDetails resource) {
+ super(
+ resource,
+ new DefaultOAuth2ClientContext(new DefaultAccessTokenRequest()) {
+
+ private static final long serialVersionUID = 3921115327670719271L;
+
+ @Override
+ public AccessTokenRequest getAccessTokenRequest() {
+ final AccessTokenRequest accessTokenRequest = super.getAccessTokenRequest();
+ accessTokenRequest.set("Institution", "testInstitution");
+ return accessTokenRequest;
+ }
+ });
+ }
+
+ @Override
+ protected T doExecute(
+ final URI url,
+ final HttpMethod method,
+ final RequestCallback requestCallback,
+ final ResponseExtractor responseExtractor) throws RestClientException {
+
+ if (this.enabled) {
+ return super.doExecute(url, method, requestCallback, responseExtractor);
+ } else {
+ throw new IllegalStateException(
+ "Error: Forbidden execution call on disabled DisposableOAuth2RestTemplate");
+ }
+ }
+ }
+
+ private static final class OAuth2AuthorizationContext implements SEBServerAuthorizationContext {
+
+ private static final String GRANT_TYPE = "password";
+ private static final List SCOPES = Collections.unmodifiableList(
+ Arrays.asList("web-service-api-read", "web-service-api-write"));
+
+ private boolean valid = true;
+
+ private final ResourceOwnerPasswordResourceDetails resource;
+ private final DisposableOAuth2RestTemplate restTemplate;
+ private final String revokeTokenURI;
+ private final String currentUserURI;
+
+ private UserInfo loggedInUser = null;
+
+ OAuth2AuthorizationContext(
+ final String guiClientId,
+ final String guiClientSecret,
+ final WebserviceURIBuilderSupplier webserviceURIBuilderSupplier) {
+
+ this.resource = new ResourceOwnerPasswordResourceDetails();
+ this.resource.setAccessTokenUri(
+ webserviceURIBuilderSupplier
+ .getBuilder()
+ .path(OAUTH_TOKEN_URI_PATH)
+ .toUriString() /* restCallBuilder.withPath(OAUTH_TOKEN_URI_PATH) */);
+ this.resource.setClientId(guiClientId);
+ this.resource.setClientSecret(guiClientSecret);
+ this.resource.setGrantType(GRANT_TYPE);
+ this.resource.setScope(SCOPES);
+
+ this.restTemplate = new DisposableOAuth2RestTemplate(this.resource);
+
+ this.revokeTokenURI = webserviceURIBuilderSupplier
+ .getBuilder()
+ .path(OAUTH_REVOKE_TOKEN_URI_PATH)
+ .toUriString(); //restCallBuilder.withPath(OAUTH_REVOKE_TOKEN_URI_PATH);
+ this.currentUserURI = webserviceURIBuilderSupplier
+ .getBuilder()
+ .path(CURRENT_USER_URI_PATH)
+ .toUriString(); //restCallBuilder.withPath(CURRENT_USER_URI_PATH);
+ }
+
+ @Override
+ public boolean isValid() {
+ return this.valid;
+ }
+
+ @Override
+ public boolean isLoggedIn() {
+ final OAuth2AccessToken accessToken = this.restTemplate.getOAuth2ClientContext().getAccessToken();
+ return accessToken != null && !StringUtils.isEmpty(accessToken.toString());
+ }
+
+ @Override
+ public boolean login(final String username, final String password) {
+ if (!this.valid || this.isLoggedIn()) {
+ return false;
+ }
+
+ this.resource.setUsername(username);
+ this.resource.setPassword(password);
+
+ log.debug("Trying to login for user: {}", username);
+
+ try {
+ final OAuth2AccessToken accessToken = this.restTemplate.getAccessToken();
+ log.debug("Got token for user: {} : {}", username, accessToken);
+ this.loggedInUser = getLoggedInUser();
+ return true;
+ } catch (final OAuth2AccessDeniedException | AccessDeniedException e) {
+ log.info("Access Denied for user: {}", username);
+ return false;
+ }
+ }
+
+ @Override
+ public boolean logout() {
+ // set this context invalid to force creation of a new context on next request
+ this.valid = false;
+ this.loggedInUser = null;
+ if (this.restTemplate.getAccessToken() != null) {
+ // delete the access-token (and refresh-token) on authentication server side
+ this.restTemplate.delete(this.revokeTokenURI);
+ // delete the access-token within the RestTemplate
+ this.restTemplate.getOAuth2ClientContext().setAccessToken(null);
+ }
+ // mark the RestTemplate as disposed
+ this.restTemplate.enabled = false;
+
+ return true;
+ }
+
+ @Override
+ public RestTemplate getRestTemplate() {
+ return this.restTemplate;
+ }
+
+ @Override
+ public UserInfo getLoggedInUser() {
+ if (this.loggedInUser != null) {
+ return this.loggedInUser;
+ }
+
+ log.debug("Request logged in User from SEBserver web-service API");
+
+ try {
+ if (isValid() && isLoggedIn()) {
+ final ResponseEntity response =
+ this.restTemplate.getForEntity(this.currentUserURI, UserInfo.class);
+ this.loggedInUser = response.getBody();
+ return this.loggedInUser;
+ } else {
+ throw new IllegalStateException("Logged in User requested on invalid or not logged in ");
+ }
+ } catch (final AccessDeniedException | OAuth2AccessDeniedException ade) {
+ log.error("Acccess denied while trying to request logged in User from API", ade);
+ throw ade;
+ } catch (final Exception e) {
+ log.error("Unexpected error while trying to request logged in User from API", e);
+ throw new RuntimeException("Unexpected error while trying to request logged in User from API", e);
+ }
+ }
+
+ @Override
+ public boolean hasRole(final UserRole role) {
+ if (!isValid() || !isLoggedIn()) {
+ return false;
+ }
+
+ return getLoggedInUser().roles
+ .contains(role.name());
+ }
+ }
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/SEBServerAuthorizationContext.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/SEBServerAuthorizationContext.java
new file mode 100644
index 00000000..ede1a752
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/SEBServerAuthorizationContext.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package ch.ethz.seb.sebserver.gui.service.remote.webservice.auth;
+
+import org.springframework.web.client.RestTemplate;
+
+import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
+import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
+
+public interface SEBServerAuthorizationContext {
+
+ boolean isValid();
+
+ boolean isLoggedIn();
+
+ boolean login(String username, String password);
+
+ boolean logout();
+
+ UserInfo getLoggedInUser();
+
+ public boolean hasRole(UserRole role);
+
+ RestTemplate getRestTemplate();
+
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIBuilderSupplier.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIBuilderSupplier.java
new file mode 100644
index 00000000..a0ff9ae1
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIBuilderSupplier.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package ch.ethz.seb.sebserver.gui.service.remote.webservice.auth;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
+
+@Lazy
+@Component
+@GuiProfile
+public class WebserviceURIBuilderSupplier {
+
+ private final UriComponentsBuilder webserviceURIBuilder;
+
+ public WebserviceURIBuilderSupplier(
+ @Value("${sebserver.gui.webservice.protocol}") final String webserviceProtocol,
+ @Value("${sebserver.gui.webservice.address}") final String webserviceServerAdress,
+ @Value("${sebserver.gui.webservice.portol}") final String webserviceServerPort,
+ @Value("${sebserver.gui.webservice.apipath}") final String webserviceAPIPath) {
+
+ this.webserviceURIBuilder = UriComponentsBuilder
+ .fromHttpUrl(webserviceProtocol + "://" + webserviceServerAdress)
+ .port(webserviceServerPort)
+ .path(webserviceAPIPath);
+ }
+
+ public UriComponentsBuilder getBuilder() {
+ return this.webserviceURIBuilder.cloneBuilder();
+ }
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/ClientSessionWebSecurityConfig.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/ClientSessionWebSecurityConfig.java
index ee5450d5..46758a3b 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/ClientSessionWebSecurityConfig.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/ClientSessionWebSecurityConfig.java
@@ -64,7 +64,7 @@ import ch.ethz.seb.sebserver.webservice.weblayer.oauth.WebResourceServerConfigur
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
-@Order(4)
+@Order(5)
public class ClientSessionWebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final Logger log = LoggerFactory.getLogger(ClientSessionWebSecurityConfig.class);
@@ -86,6 +86,8 @@ public class ClientSessionWebSecurityConfig extends WebSecurityConfigurerAdapter
private String adminAPIEndpoint;
@Value("${sebserver.webservice.api.exam.endpoint}")
private String examAPIEndpoint;
+ @Value("${sebserver.webservice.api.redirect.unauthorized}")
+ private String unauthorizedRedirect;
@Bean
public AccessTokenConverter accessTokenConverter() {
@@ -116,13 +118,28 @@ public class ClientSessionWebSecurityConfig extends WebSecurityConfigurerAdapter
.passwordEncoder(this.userPasswordEncoder);
}
+ @Override
+ public void configure(final HttpSecurity http) throws Exception {
+ http
+ .sessionManagement()
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ .and()
+ .formLogin().disable()
+ .httpBasic().disable()
+ .logout().disable()
+ .headers().frameOptions().disable()
+ .and()
+ .csrf().disable();
+ }
+
@Bean
protected ResourceServerConfiguration sebServerAdminAPIResources() throws Exception {
return new AdminAPIResourceServerConfiguration(
this.tokenStore,
this.webServiceClientDetails,
authenticationManagerBean(),
- this.adminAPIEndpoint);
+ this.adminAPIEndpoint,
+ this.unauthorizedRedirect);
}
@Bean
@@ -134,28 +151,6 @@ public class ClientSessionWebSecurityConfig extends WebSecurityConfigurerAdapter
this.examAPIEndpoint);
}
- @Override
- public void configure(final HttpSecurity http) throws Exception {
- http
- .sessionManagement()
- .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
- .and()
- .antMatcher("/**")
- .authorizeRequests()
- .anyRequest()
- .authenticated()
- .and()
- .exceptionHandling()
- .authenticationEntryPoint(new LoginRedirectOnUnauthorized())
- .and()
- .formLogin().disable()
- .httpBasic().disable()
- .logout().disable()
- .headers().frameOptions().disable()
- .and()
- .csrf().disable();
- }
-
// NOTE: We need two different class types here to support Spring configuration for different
// ResourceServerConfiguration. There is a class type now for the Admin API as well as for the Exam API
private static final class AdminAPIResourceServerConfiguration extends WebResourceServerConfiguration {
@@ -164,17 +159,18 @@ public class ClientSessionWebSecurityConfig extends WebSecurityConfigurerAdapter
final TokenStore tokenStore,
final WebClientDetailsService webServiceClientDetails,
final AuthenticationManager authenticationManager,
- final String apiEndpoint) {
+ final String apiEndpoint,
+ final String redirect) {
super(
tokenStore,
webServiceClientDetails,
authenticationManager,
- new LoginRedirectOnUnauthorized(),
+ new LoginRedirectOnUnauthorized(redirect),
ADMIN_API_RESOURCE_ID,
apiEndpoint,
true,
- 1);
+ 2);
}
}
@@ -202,12 +198,18 @@ public class ClientSessionWebSecurityConfig extends WebSecurityConfigurerAdapter
EXAM_API_RESOURCE_ID,
apiEndpoint,
true,
- 2);
+ 3);
}
}
private static class LoginRedirectOnUnauthorized implements AuthenticationEntryPoint {
+ private final String redirect;
+
+ protected LoginRedirectOnUnauthorized(final String redirect) {
+ this.redirect = redirect;
+ }
+
@Override
public void commence(
final HttpServletRequest request,
@@ -216,8 +218,8 @@ public class ClientSessionWebSecurityConfig extends WebSecurityConfigurerAdapter
log.warn("Unauthorized Request: {} : Redirect to login after unauthorized request",
request.getRequestURI());
- // TODO define login redirect
- response.sendRedirect("/gui/");
+
+ response.sendRedirect(this.redirect);
}
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/WebServiceUserDetails.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/WebServiceUserDetails.java
index 56311b5b..fc8cacd4 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/WebServiceUserDetails.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/WebServiceUserDetails.java
@@ -14,10 +14,12 @@ import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
+import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
@Lazy
@Component
+@WebServiceProfile
public class WebServiceUserDetails implements UserDetailsService {
private final UserDAO userDAO;
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java
index e7b9facc..61adffd0 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java
@@ -41,6 +41,7 @@ public class WebClientDetailsService implements ClientDetailsService {
@Qualifier(WebSecurityConfig.CLIENT_PASSWORD_ENCODER_BEAN_NAME)
private PasswordEncoder clientPasswordEncoder;
+ // TODO inject a collection of BaseClientDetails here to allow multiple admin client configurations
public WebClientDetailsService(final AdminAPIClientDetails adminClientDetails) {
this.adminClientDetails = adminClientDetails;
}
diff --git a/src/main/resources/config/application-dev-gui.properties b/src/main/resources/config/application-dev-gui.properties
index 524f811a..91c249da 100644
--- a/src/main/resources/config/application-dev-gui.properties
+++ b/src/main/resources/config/application-dev-gui.properties
@@ -5,5 +5,12 @@ server.servlet.context-path=/
server.servlet.session.cookie.http-only=true
server.servlet.session.tracking-modes=cookie
+sebserver.gui.entrypoint=/gui
+sebserver.gui.webservice.protocol=http
+sebserver.gui.webservice.address=localhost
+sebserver.gui.webservice.port=8080
+sebserver.gui.webservice.apipath=/admin-api/v1
+
+
sebserver.gui.theme=css/sebserver.css
-sebserver.gui.date.displayformat=EEEE, dd MMMM yyyy - HH:mm
\ No newline at end of file
+sebserver.gui.date.displayformat=EEEE, dd MMMM yyyy - HH:mm
diff --git a/src/main/resources/config/application-dev-ws.properties b/src/main/resources/config/application-dev-ws.properties
index b36a16f3..2f962d72 100644
--- a/src/main/resources/config/application-dev-ws.properties
+++ b/src/main/resources/config/application-dev-ws.properties
@@ -9,11 +9,12 @@ spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.platform=dev
spring.datasource.hikari.max-lifetime=600000
-sebserver.webservice.api.admin.endpoint=/admin-api/v1/**
+sebserver.webservice.api.admin.endpoint=/admin-api/v1
sebserver.webservice.api.admin.accessTokenValiditySeconds=1800
sebserver.webservice.api.admin.refreshTokenValiditySeconds=-1
-sebserver.webservice.api.exam.endpoint=/exam-api/v1/**
+sebserver.webservice.api.exam.endpoint=/exam-api/v1
sebserver.webservice.api.exam.accessTokenValiditySeconds=1800
sebserver.webservice.api.exam.refreshTokenValiditySeconds=-1
sebserver.webservice.api.pagination.maxPageSize=500
+
diff --git a/src/main/resources/config/application-dev.properties b/src/main/resources/config/application-dev.properties
index 9c64b17f..7f4217b7 100644
--- a/src/main/resources/config/application-dev.properties
+++ b/src/main/resources/config/application-dev.properties
@@ -4,7 +4,7 @@ server.address=localhost
server.port=8080
server.servlet.context-path=/
-
+sebserver.webservice.api.redirect.unauthorized=http://localhost:8080/gui
diff --git a/src/main/resources/static/css/sebserver.css b/src/main/resources/static/css/sebserver.css
new file mode 100644
index 00000000..0758e2fe
--- /dev/null
+++ b/src/main/resources/static/css/sebserver.css
@@ -0,0 +1,681 @@
+* {
+ color: #000000;
+ font: normal 12px Arial, Helvetica, sans-serif;
+ background-image: none;
+ background-color: #FFFFFF;
+ padding: 0;
+}
+
+*:disabled {
+ color: #CFCFCF;
+}
+
+/* Label default theme */
+Label {
+ font: 12px Verdana, "Lucida Sans", Arial, Helvetica, sans-serif;
+ color: #4a4a4a;
+ background-color: transparent;
+ background-image: none;
+ background-repeat: repeat;
+ background-position: left top;
+ border: none;
+ border-radius: 0;
+ text-decoration: none;
+ cursor: default;
+ opacity: 1;
+ text-shadow: none;
+}
+
+Label.h1 {
+ font: 25px Arial, Helvetica, sans-serif;
+ height: 28px;
+ padding: 0px 12px 12px 12px;
+ color: #1f407a;
+}
+
+Label.h2 {
+ font: 19px Arial, Helvetica, sans-serif;
+ height: 22px;
+ padding: 0 0 10px 0;
+ color: #1f407a;
+}
+
+Label.h3 {
+ font: bold 14px Arial, Helvetica, sans-serif;
+ height: 20px;
+ padding: 0;
+ color: #1f407a;
+}
+
+Label:hover.imageButton {
+ background-color: #82BE1E;
+ background-gradient-color: #82BE1E;
+ background-image: gradient( linear, left top, left bottom, from( #82BE1E ), to( #82BE1E ) );
+}
+
+Composite.bordered {
+ border: 2px;
+}
+
+Composite.header {
+ background-color: #000000;
+ color: #FFFFFF;
+}
+
+Composite.logo {
+ background-color: #1F407A;
+}
+
+Composite.bgLogo {
+ background-color: #1F407A;
+ background-image: url(static/images/ethz_logo_white.png);
+ background-repeat: no-repeat;
+ background-position: left center;
+}
+
+Composite.bgContent {
+ background-color: #EAECEE;
+ background-image: url(static/images/blueBackground.png);
+ background-repeat: repeat-x;
+}
+
+Composite.content {
+ background-color: #FFFFFF;
+ margin: 0 0 0 0;
+}
+
+Composite.selectionPane {
+ background-color: #D3D9DB;
+}
+
+Composite.bgFooter {
+ background-color: #EAECEE;
+}
+
+Composite.footer {
+ background-color: #1F407A;
+}
+
+Composite.login {
+ background-color: #EAECEE;
+ margin: 20px 0 0 0;
+ padding: 15px 8px 8px 8px;
+ border: 1px solid #bdbdbd;
+ border-radius: 2px;
+}
+
+*.header {
+ font: bold 12px Arial, Helvetica, sans-serif;
+ color: #FFFFFF;
+ background-color: transparent;
+}
+
+*.footer {
+ font: bold 12px Arial, Helvetica, sans-serif;
+ color: #FFFFFF;
+ background-color: transparent;
+}
+
+
+/* Text default */
+Text {
+ font: 12px Verdana, "Lucida Sans", Arial, Helvetica, sans-serif;
+ border: none;
+ border-radius: 0;
+ padding: 3px 10px 3px 10px;
+ color: #4a4a4a;
+ background-repeat: repeat;
+ background-position: left top;
+ background-color: #ffffff;
+ background-image: none;
+ text-shadow: none;
+ box-shadow: none;
+}
+
+Text.error {
+ font: 12px Verdana, "Lucida Sans", Arial, Helvetica, sans-serif;
+ border: none;
+ border-radius: 0;
+ padding: 3px 10px 3px 10px;
+ color: #4a4a4a;
+ background-repeat: repeat;
+ background-position: left top;
+ background-color: #ff0000;
+ background-image: none;
+ text-shadow: none;
+ box-shadow: none;
+ opacity: 0.5;
+}
+
+Text[MULTI] {
+ padding: 5px 10px 5px 10px;
+}
+
+Text[BORDER], Text[MULTI][BORDER] {
+ border: 1px solid #aaaaaa;
+ border-radius: 0;
+ box-shadow: none;
+}
+
+Text[BORDER]:focused, Text[MULTI][BORDER]:focused {
+ border: 1px solid #4f7cb1;
+ box-shadow: none;
+}
+
+Text[BORDER]:disabled, Text[MULTI][BORDER]:disabled, Text[BORDER]:read-only,
+ Text[MULTI][BORDER]:read-only {
+ box-shadow: none;
+}
+
+/* Combo default theme */
+Combo, Combo[BORDER] {
+ font: 12px Verdana, "Lucida Sans", Arial, Helvetica, sans-serif;
+ color: #4a4a4a;
+ background-color: #ffffff;
+ border: 1px solid #aaaaaa;
+ border-radius: 0 2px 2px 0;
+ background-image: none;
+ text-shadow: none;
+ box-shadow: none;
+}
+
+Combo:focused, Combo[BORDER]:focused {
+ text-shadow: none;
+ box-shadow: none;
+}
+
+Combo:disabled, Combo[BORDER]:disabled {
+ text-shadow: none;
+ box-shadow: none;
+}
+
+Combo-Button {
+ cursor: default;
+ background-color: #ffffff;
+ background-image: gradient(linear, left top, left bottom, from(#ffffff), to(#ffffff));
+ border: none;
+ width: 30px;
+}
+
+Combo-Field {
+ padding: 3px 10px 3px 10px;
+}
+
+
+/* Message titlebar */
+Shell.message {
+ animation: none;
+ border: 1px solid #bdbdbd;
+ background-color: #ffffff;
+ background-image: none;
+ padding: 0px;
+ opacity: 1;
+ box-shadow: none;
+ width: 400px;
+}
+
+Shell-Titlebar.message {
+ background-color: #1f407a;
+ background-gradient-color: #1f407a;
+ color: white;
+ background-image: gradient( linear, left top, left bottom, from( #0069B4 ), to( #0069B4 ) );
+ padding: 2px 5px 2px;
+ margin: 0px;
+ height: 22px;
+ font: 14px Verdana, "Lucida Sans", Arial, Helvetica, sans-serif;
+ border: none;
+ border-radius: 1px 1px 0px 0px;
+ text-shadow: none;
+}
+
+Button {
+ font: 12px Arial, Helvetica, sans-serif;
+ padding: 5px 6px 5px 6px;
+}
+
+/* Push Buttons */
+Button[PUSH],
+Button[PUSH]:default {
+ font: bold 12px Arial, Helvetica, sans-serif;
+ background-color: #0069B4;
+ background-gradient-color: #0069B4;
+ background-image: gradient( linear, left top, left bottom, from( #0069B4 ), to( #0069B4 ) );
+ color: #fff;
+ border: none;
+ border-radius: 0px;
+ padding: 6px 15px;
+ text-shadow: none;
+}
+
+Button[PUSH]:pressed {
+ background-color: #444;
+ color: #fff;
+ background-gradient-color: #444;
+ background-image: gradient( linear, left top, left bottom, from( #444 ), to( #444 ) );
+}
+
+Button[PUSH]:hover {
+ background-color: #82BE1E;
+ background-gradient-color: #82BE1E;
+ background-image: gradient( linear, left top, left bottom, from( #82BE1E ), to( #82BE1E ) );
+ color: #444;
+ cursor: pointer;
+}
+
+Button[PUSH]:disabled {
+ background-color: transparent;
+ border: 1px solid #EAECEE;
+ color: #c0c0c0;
+ background-repeat: no-repeat;
+ background-position: right;
+}
+
+Button-FocusIndicator[PUSH] {
+ background-color: transparent;
+}
+
+/* Push Buttons header */
+Button[PUSH].header,
+Button[PUSH]:default.header {
+ font: bold 12px Arial, Helvetica, sans-serif;
+ background-color: #595959;
+ background-gradient-color: #595959;
+ background-image: gradient( linear, left top, left bottom, from( #595959 ), to( #595959 ) );
+ color: #fff;
+ border: none;
+ border-radius: 0px;
+ padding: 6px 15px;
+ text-shadow: none;
+}
+
+Button[PUSH]:pressed.header {
+ background-color: #444;
+ color: #fff;
+ background-gradient-color: #444;
+ background-image: gradient( linear, left top, left bottom, from( #444 ), to( #444 ) );
+}
+
+Button[PUSH]:hover.header {
+ background-color: #82BE1E;
+ background-gradient-color: #82BE1E;
+ background-image: gradient( linear, left top, left bottom, from( #82BE1E ), to( #82BE1E ) );
+ color: #444;
+ cursor: pointer;
+}
+
+
+/* Sash default */
+Sash {
+ background-image: none;
+ background-color: transparent;
+ background-color: #EAECEE;
+}
+
+Sash:hover {
+ background-color: #444444;
+}
+
+
+/*Standard Einstellungen fuer Trees*/
+Tree {
+ font: bold 14px Arial, Helvetica, sans-serif;
+ background-color: transparent;
+ border: none;
+ color: #1f407a;
+ margin: 0px 0px 0px 0px;
+ padding: 0px 0px 0px 0px;
+}
+
+Tree[BORDER] {
+ border: 1px solid #eceeef;
+}
+
+TreeItem {
+ font: bold 14px Arial, Helvetica, sans-serif;
+ color: #1f407a;
+ background-color: transparent;
+ text-decoration: none;
+ text-shadow: none;
+ background-image: none;
+ margin: 20px 20px 20px 20px;
+ padding: 20px 20px 20px 20px;
+}
+
+TreeItem:linesvisible:even {
+ background-color: #f3f3f4;
+}
+
+Tree-RowOverlay {
+ background-color: transparent;
+ color: inherit;
+ background-image: none;
+}
+
+Tree-RowOverlay:hover {
+ background-color: #82be1e;
+ color: #1F407A;
+}
+
+Tree-RowOverlay:selected {
+ background-color: #D3D9DB;
+ color: #1F407A;
+}
+
+Tree-RowOverlay:selected:unfocused {
+ background-color: #D3D9DB;
+ color: #1f407a;
+}
+
+Tree-RowOverlay:selected:hover {
+ background-color: #82be1e;
+ color: #000000;
+}
+
+Tree.actions {
+ font: 12px Verdana, "Lucida Sans", Arial, Helvetica, sans-serif;
+ color: #4a4a4a;
+ background-color: transparent;
+ border: none;
+ margin: 0 0 0 0;
+}
+
+Tree[BORDER].actions {
+ border: 1px solid #eceeef;
+}
+
+TreeItem.actions {
+ font: 12px Verdana, "Lucida Sans", Arial, Helvetica, sans-serif;
+ color: #4a4a4a;
+ background-color: transparent;
+ text-decoration: none;
+ text-shadow: none;
+ background-image: none;
+ margin: 0 0 0 0;
+}
+
+Tree-RowOverlay:hover.actions {
+ background-color: #82be1e;
+ color: #4a4a4a;
+}
+
+Tree-RowOverlay:selected.actions {
+ background-color: #595959;
+ color: #4a4a4a;
+}
+
+/* TabFolder default theme */
+
+TabFolder {
+ font: 12px Verdana, "Lucida Sans", Arial, Helvetica, sans-serif;
+ color: #4a4a4a;
+ border: none;
+}
+
+TabFolder-ContentContainer {
+ border: none;
+ border-top: 1px solid #bdbdbd;
+}
+
+TabItem {
+ font: 12px Verdana, "Lucida Sans", Arial, Helvetica, sans-serif;
+ color: #4a4a4a;
+ background-color: #FFFFFF;
+ text-decoration: none;
+ text-shadow: none;
+ background-image: none;
+ margin: 1px 0px 0px -1px;
+ border: 1px solid #bdbdbd;
+ border-bottom: none;
+ border-left: none;
+}
+
+TabItem:selected {
+ background-color: #D3D9DB;
+ background-gradient-color: #D3D9DB;
+ background-image: gradient( linear, left top, left bottom, from( #D3D9DB ), to( #D3D9DB ) );
+ color: #4a4a4a;
+ margin: 0px 0px 0px 0px;
+ border: 1px solid #bdbdbd;
+ border-bottom: none;
+ border-left: none;
+}
+
+TabItem:hover {
+ background-color: #82be1e;
+ background-gradient-color: #82be1e;
+ background-image: gradient( linear, left top, left bottom, from( #82be1e ), to( #82be1e ) );
+ color: #4a4a4a;
+ margin: 1px 0px 0px -1px;
+ border-left: none;
+}
+
+TabItem:selected:hover {
+ background-color: #82be1e;
+ background-gradient-color: #82be1e;
+ background-image: gradient( linear, left top, left bottom, from( #82be1e ), to( #82be1e ) );
+ color: #4a4a4a;
+ margin: 0px 0px 0px 0px;
+ border-left: none;
+}
+
+TabItem:first {
+ font: 12px Verdana, "Lucida Sans", Arial, Helvetica, sans-serif;
+ color: #4a4a4a;
+ background-color: #FFFFFF;
+ text-decoration: none;
+ text-shadow: none;
+ background-image: none;
+ margin: 1px 0px 0px -1px;
+ border: 1px solid #bdbdbd;
+ border-bottom: none;
+}
+
+TabItem:first:hover {
+ background-color: #82be1e;
+ background-gradient-color: #82be1e;
+ background-image: gradient( linear, left top, left bottom, from( #82be1e ), to( #82be1e ) );
+ color: #4a4a4a;
+ margin: 1px 0px 0px -1px;
+ border-left: none;
+}
+
+TabItem:selected:first {
+ background-color: #D3D9DB;
+ color: #4a4a4a;
+ margin: 0px 0px 0px 0px;
+ border-left: 1px solid #bdbdbd;
+}
+
+TabItem:selected:hover:first {
+ background-color: #82be1e;
+ background-gradient-color: #82be1e;
+ background-image: gradient( linear, left top, left bottom, from( #82be1e ), to( #82be1e ) );
+ color: #4a4a4a;
+ margin: 0px 0px 0px 0px;
+ border-left: 1px solid #bdbdbd;
+}
+
+
+/* ScrollBar default theme */
+ScrollBar {
+ background-color: #c0c0c0;
+ background-image: none;
+ border: none;
+ border-radius: 0;
+ width: 10px;
+}
+
+ScrollBar-Thumb {
+ background-color: #c0c0c0;
+ border: 1px solid #bdbdbd;
+ border-radius: 15px;
+ /*background-image: url( themes/images/scrollbar/scrollbar-background.png );*/
+ min-height: 20px;
+}
+
+ScrollBar-UpButton, ScrollBar-DownButton {
+ background-color: transparent;
+ border: none;
+ border-radius: 0;
+ cursor: default;
+}
+/*
+ScrollBar-UpButton[HORIZONTAL] {
+ background-image: url( themes/images/scrollbar/right.png );
+}
+
+ScrollBar-DownButton[HORIZONTAL] {
+ background-image: url( themes/images/scrollbar/left.png );
+}
+
+ScrollBar-UpButton[VERTICAL] {
+ background-image: url( themes/images/scrollbar/down.png )
+}
+
+ScrollBar-DownButton[VERTICAL] {
+ background-image: url( themes/images/scrollbar/up.png );
+}
+*/
+
+Widget-ToolTip {
+ padding: 1px 3px 2px 3px;
+ background-color: #82be1e;
+ border: 1px solid #3C5A0F;
+ border-radius: 2px 2px 2px 2px;
+ color: #4a4a4a;
+ opacity: 1;
+ animation: fadeIn 200ms linear, fadeOut 600ms ease-out;
+ box-shadow: 3px 4px 2px rgba(0, 0, 0, 0.3);
+ text-align: center;
+}
+
+Widget-ToolTip-Pointer {
+ background-image: none;
+}
+
+
+
+
+
+
+
+
+
+
+/* Table default theme */
+Table {
+ font: 12px Verdana, "Lucida Sans", Arial, Helvetica, sans-serif;
+ background-color: #ffffff;
+ background-image: none;
+ color: #4a4a4a;
+ border: none;
+}
+
+Table[BORDER] {
+ border: 1px solid #bdbdbd;
+}
+
+TableColumn {
+ font: 12px Verdana, "Lucida Sans", Arial, Helvetica, sans-serif;
+ background-color: #595959;
+ background-gradient-color: #595959;
+ background-image: gradient( linear, left top, left bottom, from( #595959 ), to( #595959 ) );
+ padding: 4px 3px 4px 3px;
+
+ color: #FFFFFF;
+ border-bottom: 1px solid #bdbdbd;
+ text-shadow: none;
+}
+
+TableColumn:hover {
+ background-color: #595959;
+ background-gradient-color: #595959;
+ background-image: gradient( linear, left top, left bottom, from( #595959 ), to( #595959 ) );
+}
+
+TableItem, TableItem:linesvisible:even:rowtemplate {
+ background-color: transparent;
+ color: inherit;
+ text-decoration: none;
+ text-shadow: none;
+ background-image: none;
+}
+
+TableItem:linesvisible:even {
+ background-color: #ffffff;
+ color: inherit;
+}
+
+Table-RowOverlay {
+ background-color: transparent;
+ color: inherit;
+ background-image: none;
+}
+
+Table-RowOverlay:hover {
+ color: #4a4a4a;
+ background-color: #b5b5b5;
+ background-image: gradient(linear, left top, left bottom, from(#b5b5b5), to(#b5b5b5));
+}
+
+Table-RowOverlay:selected {
+ color: #4a4a4a;
+ background-color: #b5b5b5;
+ background-image: gradient(linear, left top, left bottom, from(#b5b5b5),to(#b5b5b5));
+}
+
+Table-RowOverlay:selected:unfocused {
+ color: #4a4a4a;
+ background-color: #b5b5b5;
+ background-image: gradient(linear, left top, left bottom, from(#b5b5b5),to(#b5b5b5));
+}
+
+Table-RowOverlay:linesvisible:even:hover {
+ color: #4a4a4a;
+ background-color: #b5b5b5;
+ background-image: gradient(linear, left top, left bottom, from(#b5b5b5),to(#d5d5d5));
+}
+
+Table-RowOverlay:linesvisible:even:selected {
+ color: #4a4a4a;
+ background-color: #b5b5b5;
+ background-image: gradient(linear, left top, left bottom, from(#b5b5b5),to(#b5b5b5));
+}
+
+Table-RowOverlay:linesvisible:even:selected:unfocused {
+ background-color: #b5b5b5;
+ background-image: gradient(linear, left top, left bottom, from(#b5b5b5),to(#b5b5b5));
+ color: #4a4a4a;
+}
+
+TableColumn-SortIndicator {
+ background-image: none;
+}
+/*
+TableColumn-SortIndicator:up {
+ background-image: url( themes/images/column/sort-indicator-up.png );
+}
+
+TableColumn-SortIndicator:down {
+ background-image: url( themes/images/column/sort-indicator-down.png );
+}
+*/
+
+Table-Cell {
+ spacing: 3px;
+ padding: 5px 3px 5px 3px;
+}
+
+Table-GridLine, Table-GridLine:vertical:rowtemplate {
+ color: transparent;
+}
+
+Table-GridLine:vertical, Table-GridLine:header, Table-GridLine:horizontal:rowtemplate {
+ color: transparent;
+}
+
+
+
+
+
+
diff --git a/src/main/resources/static/images/blueBackground.png b/src/main/resources/static/images/blueBackground.png
new file mode 100644
index 00000000..41c6fa20
Binary files /dev/null and b/src/main/resources/static/images/blueBackground.png differ
diff --git a/src/main/resources/static/images/deleteAction.png b/src/main/resources/static/images/deleteAction.png
new file mode 100644
index 00000000..3ddfaf8a
Binary files /dev/null and b/src/main/resources/static/images/deleteAction.png differ
diff --git a/src/main/resources/static/images/ethz_logo_white.png b/src/main/resources/static/images/ethz_logo_white.png
new file mode 100644
index 00000000..f0c70ee9
Binary files /dev/null and b/src/main/resources/static/images/ethz_logo_white.png differ
diff --git a/src/main/resources/static/images/maximize.png b/src/main/resources/static/images/maximize.png
new file mode 100644
index 00000000..f7f38d9d
Binary files /dev/null and b/src/main/resources/static/images/maximize.png differ
diff --git a/src/main/resources/static/images/minimize.png b/src/main/resources/static/images/minimize.png
new file mode 100644
index 00000000..097bfca8
Binary files /dev/null and b/src/main/resources/static/images/minimize.png differ
diff --git a/src/main/resources/static/images/newAction.png b/src/main/resources/static/images/newAction.png
new file mode 100644
index 00000000..9ec4db8f
Binary files /dev/null and b/src/main/resources/static/images/newAction.png differ
diff --git a/src/main/resources/static/images/saveAction.png b/src/main/resources/static/images/saveAction.png
new file mode 100644
index 00000000..c318014d
Binary files /dev/null and b/src/main/resources/static/images/saveAction.png differ
diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties
index 51b2c64e..cc85e990 100644
--- a/src/test/resources/application-test.properties
+++ b/src/test/resources/application-test.properties
@@ -17,4 +17,4 @@ sebserver.webservice.api.exam.endpoint=/exam-api
sebserver.webservice.api.exam.accessTokenValiditySeconds=1800
sebserver.webservice.api.exam.refreshTokenValiditySeconds=-1
-
+sebserver.webservice.api.redirect.unauthorized=none