setup gui and fix profiles

This commit is contained in:
anhefti 2019-01-28 16:58:06 +01:00
parent eccbd47d39
commit f21f959ad2
32 changed files with 1787 additions and 94 deletions

17
pom.xml
View file

@ -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>

View file

@ -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) {

View file

@ -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";
}
}

View file

@ -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 {
}

View file

@ -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";
}
}

View file

@ -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
}
}

View 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);
}
}
};
}

View 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 + "/*");
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}
}

View file

@ -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());
}
}

View file

@ -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();
}
}
}

View file

@ -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());
}
}
}

View file

@ -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();
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -4,7 +4,7 @@ server.address=localhost
server.port=8080
server.servlet.context-path=/
sebserver.webservice.api.redirect.unauthorized=http://localhost:8080/gui

View 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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

View file

@ -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