From f21f959ad2298af89a0a4aae4ab26cd2580d8c51 Mon Sep 17 00:00:00 2001 From: anhefti Date: Mon, 28 Jan 2019 16:58:06 +0100 Subject: [PATCH] setup gui and fix profiles --- pom.xml | 17 +- .../java/ch/ethz/seb/sebserver/SEBServer.java | 2 +- .../ethz/seb/sebserver/WebSecurityConfig.java | 74 +- .../seb/sebserver/gbl/profile/GuiProfile.java | 2 +- .../seb/sebserver/gui/GuiTestController.java | 31 - .../sebserver/gui/GuiWebsecurityConfig.java | 54 +- .../seb/sebserver/gui/RAPConfiguration.java | 145 ++++ .../seb/sebserver/gui/RAPSpringConfig.java | 59 ++ .../remote/webservice/api/BuildErrorCall.java | 26 + .../remote/webservice/api/RestCall.java | 188 +++++ .../remote/webservice/api/RestCallError.java | 38 + .../remote/webservice/api/RestService.java | 65 ++ .../auth/AuthorizationContextHolder.java | 23 + .../remote/webservice/auth/CurrentUser.java | 56 ++ .../OAuth2AuthorizationContextHolder.java | 265 +++++++ .../auth/SEBServerAuthorizationContext.java | 32 + .../auth/WebserviceURIBuilderSupplier.java | 40 + .../ClientSessionWebSecurityConfig.java | 62 +- .../weblayer/WebServiceUserDetails.java | 2 + .../oauth/WebClientDetailsService.java | 1 + .../config/application-dev-gui.properties | 9 +- .../config/application-dev-ws.properties | 5 +- .../config/application-dev.properties | 2 +- src/main/resources/static/css/sebserver.css | 681 ++++++++++++++++++ .../static/images/blueBackground.png | Bin 0 -> 162 bytes .../resources/static/images/deleteAction.png | Bin 0 -> 148 bytes .../static/images/ethz_logo_white.png | Bin 0 -> 2756 bytes src/main/resources/static/images/maximize.png | Bin 0 -> 209 bytes src/main/resources/static/images/minimize.png | Bin 0 -> 219 bytes .../resources/static/images/newAction.png | Bin 0 -> 165 bytes .../resources/static/images/saveAction.png | Bin 0 -> 165 bytes .../resources/application-test.properties | 2 +- 32 files changed, 1787 insertions(+), 94 deletions(-) delete mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/GuiTestController.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/RAPConfiguration.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/RAPSpringConfig.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/BuildErrorCall.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCallError.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestService.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/AuthorizationContextHolder.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/CurrentUser.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/OAuth2AuthorizationContextHolder.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/SEBServerAuthorizationContext.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIBuilderSupplier.java create mode 100644 src/main/resources/static/css/sebserver.css create mode 100644 src/main/resources/static/images/blueBackground.png create mode 100644 src/main/resources/static/images/deleteAction.png create mode 100644 src/main/resources/static/images/ethz_logo_white.png create mode 100644 src/main/resources/static/images/maximize.png create mode 100644 src/main/resources/static/images/minimize.png create mode 100644 src/main/resources/static/images/newAction.png create mode 100644 src/main/resources/static/images/saveAction.png 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> 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 0000000000000000000000000000000000000000..41c6fa2024ebe55e77d74e8a30639017480e0298 GIT binary patch literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1SFZ~=vx6P#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!pt+}uV@QPi+p~s(3<^99hI0~@?ph@$p7?Y3 p8_V)<-AT)4Xr^_|M59u^F-}YN>1kV0JppJ0gQu&X%Q~loCIEAiFx3D6 literal 0 HcmV?d00001 diff --git a/src/main/resources/static/images/deleteAction.png b/src/main/resources/static/images/deleteAction.png new file mode 100644 index 0000000000000000000000000000000000000000..3ddfaf8a2774ce17e993742f19b2d707fc57aa34 GIT binary patch literal 148 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@RheXipc%kP60RiAaYAsjMH3KjZ|y zKMuUiA9~pT#D+43fBlCy)UAlI&U@Qpx@B?q#HV7T&TEpP!>gTe~DWM4f2a7RT literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f0c70ee9a3dbe5e858c67283a24dde6fdaaaf318 GIT binary patch literal 2756 zcmV;#3On_QP)Px#1ZP1_K>z@;j|==^1pojFZb?KzRCr$PoKMJAMHImOK3uqP;X=qoixw`7jEsa@ zv~c0Vpr}AWp@oZrf`Wnq7Y2qwivkxdA`B8-6lAn0C@^SYP*Gu#QBY8kP@s@dV326P zGw*!Uxp(f&oO|E9@1=e}7-sIAGynhGnLG2|njBZIT-k`ZF1UR8@{3yR1d*v)jg1;) zZjk2PlcYl#{#5S>-s>mQ$XxPBI>mgA0qH!gNlKc>)w{~F+6lySmpZQpK2y7kZE9zs zuFftkEuGiLxk0+y)hT?Xwk75r-ShLxqa$BqaHrbBmugqZ4gjb&{53?Wk!)k;_(JV@ zbBEeOl807D{8a6EP|Vjz#Qw@acc)nz0JadSF_5rJJeD4f$$6 zJ4tor!M5S|YR`jOzGez-xPf-stdGLPCIM)+x;vuD2j{HP7Y6DlO=*_Knl4Eph{3bpt%1y8frG z7P0|erGec5f33EQ+#XZe_TA9Q!dvPDYJb>8}v`R=JF;$58paz*K#Ct@O$b!xvs@@4I z>(KdUqMZkw(g|=iA4;)P5Klaz>;vIm)+i$Q?N+hK-&OA-qNhP5%wF|Qkn~h7=wV3* zI*4g}4U}h$oQ|*4pb`9H-$Cb`dKW=;9cDlJM=+-_e`@%Pn0IjQRMk$!Snm-?gaU1tj6HSFZLbW)d7mmU6}7Q zNEURCVy)F+4UoMx(F%IUF+b>I7IePBN(OKM;lYX8>>+MXhY2%433ReEw}AX{RUH-5 zc}J7XB4t)}H&O?fn1}dQO9yta}6K zpHlAx#M>rX|AD^D@Qr;C&+GAj1AD)EH9%(jPIiEz*H{)x-Z1jkjejSlHdy$mE8Y3MkM z;eK324W!JfB08@1HF}3Nh=e(!-U-MaR93==B`ws5T0g6Zldwko#Axvjl z;*YKjg5+%@;2%$b1Mc9G&fq&TdoQ3%H*Og5lUYIW{2QKex{Tyx+ciLxF!JgS8IhNxydfU>jmg zraA|qK8{cyVV#WoW8C|KAWy6tbgPGN%!oIl7v50B|2KoHbK*cIlZE{{bWH8O*tW|J zw)@oTI z`nbQxeIG^|q=bDtR*(CMuW>-6b{A6b;gcYF`HF#ZHu&}@_8sC&>V~)_4Z8$Bq4pq% zuYCpDa|Occ%c0q&_5i3ZK}h%n0eeHm?$IbZLB+HD5o)i0PZ_?LTxOZ4v0zV}7f4FDOgwJGjqhI=5Z^e4zGVNXN6} zYGeCYs1rK?yldyNep%#6zgSSp;N1Y1I)rB+_TB&!W(($B$se{XcjPjkx8a8T-Zj=I z>68YIS1$|Yv2B-|80_+6&_HW3q)zOh3Hh{3pwVW&8dP9=BbKAqK(0YtX=wYJ%+af( zP`*u=lcc+H2jwwl$v(4Sui37>0mkxffCnw>z>~k5)Ry|6cKIX3G%M{HpsefB3uVO} zZ;jdkP@Vadc(=q04O07V9XDbXrrrRPol=MJE(6^gVB(a&CV77)Ufj>q`Kwp8Tf|$8 zD-Bm+m)b(u?5Vqr-2l7R(HUajA&y+##4{7p@+|wZ5PrySfbAfSw?^#%@XUNeDQ1uY zdDE^H)Dus(Ly!;G8w6Z4CGp8}=Pl9;m5=)vo&VV&bRuyO-VRF7r$f8m&d!d+ZR^W3 zuyVwBh%=X7=x_q%@qFzoVbdGD0T#jlKsf4efQfq+Gxt0;!1ep$KzUrtK=Skox?wiP z{UPp0f}jV4YoBfw|D^)`ZKPvAHDjg`r4{2RjD16O!m?See&pJ7CL7=gi7o63G%Ag&xH-_t zUgi>zd#^y-ifBL_p1b7LE6}LzpJ)gD->`b$t+5b?++PpimaD>XL%9O}+cAxcj6BPo zsLs6={_T4A{Qpq`ak#EPBip5F0iHYWCLxmhQO8xMGd}!nI~9L0BcK!iB9gFao#45! zE6}JUS8=-_p6q2V0eSTbG&<7YowEx5$<^8}h@*C+RXXCJ%XozA{KXdjjUOrTEWehL zUp5`WK8%S!X2BiP#(i3&NI)Fx9lamayElY&U4a(Rjn|t6p5-?NQZ~6Ex*H?E3Txck z=h?2^yV|KbaM7{hI zYv7{yqy|aqWEsCvH^5L^TmCOLz{HVV8d<2T;s%&_s{4p82$#LECLntPojsVw{)6XB zm@}At=QmLbp(cUwD=Q@*ZZ|TcS?ZtoavHb#q6iDYB0M0000< KMNUMnLSTZKOJsHc literal 0 HcmV?d00001 diff --git a/src/main/resources/static/images/maximize.png b/src/main/resources/static/images/maximize.png new file mode 100644 index 0000000000000000000000000000000000000000..f7f38d9d2de696abf44835aaeb5d54b05f608299 GIT binary patch literal 209 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCil8JzX3_JiL=7SQjU}V3t=%Yc$x%G(pCLH*x_($A9LJMF#xV3@00}G+dd) zlF;^_?UBF)20n&K%qta73MKKjGfvV`yjmb}Ad^#gN*iOPhA*c=hMT>>WN8XliqZxrNL&38{3T|9c(Aoy=Uiqz@T6` zsktNKaAHG>cf!dj5{DBR(l`T8icECNYaFA%YxPIlN zWmYXqG`l1n3)OpX7YLnY6j9c(->iHqHu=M+$Lx~4^Y)6Y;h4MjCkqdUQ?{0&u%)7M z*Oibo7FSP~(paHbm+coqrftrdqAyaq{7FCemv==f<&D2GY+2{uW{i-xJ{9}T-V7pC9-23cM@(d&5ZZZ{tFld<$6j)t;U0!b{tY%8EM_UK*!16lBdjS@ z&_~jT(@AFXVultT3lTxSK2sj~h8Uk8j2g`BT0GSov~Dp^S*XRp@aD9w%%k%zB|v)_ NJYD@<);T3K0RR(&GHU<; literal 0 HcmV?d00001 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