setup gui and fix profiles
17
pom.xml
|
@ -178,12 +178,10 @@
|
|||
<version>1.3.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.pagehelper</groupId>
|
||||
<artifactId>pagehelper-spring-boot-starter</artifactId>
|
||||
<version>1.2.10</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<groupId>com.github.pagehelper</groupId>
|
||||
<artifactId>pagehelper-spring-boot-starter</artifactId>
|
||||
<version>1.2.10</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring / Spring Boot -->
|
||||
<dependency>
|
||||
|
@ -226,6 +224,13 @@
|
|||
<artifactId>spring-security-jwt</artifactId>
|
||||
<version>1.0.9.RELEASE</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Eclipse RAP / RWT -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.rap</groupId>
|
||||
<artifactId>org.eclipse.rap.rwt</artifactId>
|
||||
<version>3.5.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Misc -->
|
||||
<dependency>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
145
src/main/java/ch/ethz/seb/sebserver/gui/RAPConfiguration.java
Normal file
|
@ -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<String, String> properties = new HashMap<>();
|
||||
properties.put(WebClient.PAGE_TITLE, "SEB Server");
|
||||
properties.put(WebClient.BODY_HTML, "<big>Loading Application<big>");
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
59
src/main/java/ch/ethz/seb/sebserver/gui/RAPSpringConfig.java
Normal file
|
@ -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<ServletContextListener> listenerRegistrationBean() {
|
||||
final ServletListenerRegistrationBean<ServletContextListener> bean =
|
||||
new ServletListenerRegistrationBean<>();
|
||||
bean.setListener(new RWTServletContextListener());
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ServletRegistrationBean<RWTServlet> servletRegistrationBean() {
|
||||
return new ServletRegistrationBean<>(new RWTServlet(), this.entrypoint + "/*");
|
||||
}
|
||||
|
||||
}
|
|
@ -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<T> extends RestCall<T> {
|
||||
|
||||
private final Throwable error;
|
||||
|
||||
protected BuildErrorCall(final Throwable error) {
|
||||
super(null, null, null, null);
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<T> exchange(final RestCallBuilder builder) {
|
||||
return Result.ofError(this.error);
|
||||
}
|
||||
}
|
|
@ -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<T> {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(RestCall.class);
|
||||
|
||||
private RestService restService;
|
||||
private JSONMapper jsonMapper;
|
||||
protected final TypeReference<T> typeRef;
|
||||
protected final HttpMethod httpMethod;
|
||||
protected final MediaType contentType;
|
||||
protected final String path;
|
||||
|
||||
protected RestCall(
|
||||
final TypeReference<T> 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<T> exchange(final RestCallBuilder builder) {
|
||||
try {
|
||||
final ResponseEntity<String> 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<List<APIMessage>>() {
|
||||
}));
|
||||
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<List<APIMessage>>() {
|
||||
}));
|
||||
} 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<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
private final Map<String, String> 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<T> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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<APIMessage> errors = new ArrayList<>();
|
||||
|
||||
public RestCallError(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public RestCallError(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public List<APIMessage> getErrorMessages() {
|
||||
return this.errors;
|
||||
}
|
||||
|
||||
public boolean hasErrorMessages() {
|
||||
return !this.errors.isEmpty();
|
||||
}
|
||||
|
||||
}
|
|
@ -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 <T> RestCall<T> getRestCall(final Class<? extends RestCall<T>> type) {
|
||||
try {
|
||||
final RestCall<T> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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> T doExecute(
|
||||
final URI url,
|
||||
final HttpMethod method,
|
||||
final RequestCallback requestCallback,
|
||||
final ResponseExtractor<T> 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<String> 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<UserInfo> 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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
sebserver.gui.date.displayformat=EEEE, dd MMMM yyyy - HH:mm
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ server.address=localhost
|
|||
server.port=8080
|
||||
server.servlet.context-path=/
|
||||
|
||||
|
||||
sebserver.webservice.api.redirect.unauthorized=http://localhost:8080/gui
|
||||
|
||||
|
||||
|
||||
|
|
681
src/main/resources/static/css/sebserver.css
Normal file
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
BIN
src/main/resources/static/images/blueBackground.png
Normal file
After Width: | Height: | Size: 162 B |
BIN
src/main/resources/static/images/deleteAction.png
Normal file
After Width: | Height: | Size: 148 B |
BIN
src/main/resources/static/images/ethz_logo_white.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
src/main/resources/static/images/maximize.png
Normal file
After Width: | Height: | Size: 209 B |
BIN
src/main/resources/static/images/minimize.png
Normal file
After Width: | Height: | Size: 219 B |
BIN
src/main/resources/static/images/newAction.png
Normal file
After Width: | Height: | Size: 165 B |
BIN
src/main/resources/static/images/saveAction.png
Normal file
After Width: | Height: | Size: 165 B |
|
@ -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
|
||||
|
|