diff --git a/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java b/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java index b93bfcac..35a2a280 100644 --- a/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java @@ -36,10 +36,13 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; +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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler; import org.springframework.util.ResourceUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -102,12 +105,28 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements E web .ignoring() .antMatchers("/error") - .antMatchers(this.examAPIDiscoveryEndpoint); + .antMatchers(this.examAPIDiscoveryEndpoint) + .and(); + } + + @Override + public void configure(final HttpSecurity http) throws Exception { + http + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .antMatcher("/**") + .authorizeRequests() + .anyRequest() + .permitAll() + .and() + .exceptionHandling() + .accessDeniedHandler(new OAuth2AccessDeniedHandler()); } @RequestMapping("/error") public void handleError(final HttpServletResponse response) throws IOException { - response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); + //response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); response.setHeader(HttpHeaders.LOCATION, this.unauthorizedRedirect); response.flushBuffer(); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnectionData.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnectionData.java index b784c713..85853694 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnectionData.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnectionData.java @@ -16,15 +16,15 @@ import com.fasterxml.jackson.annotation.JsonProperty; public class ClientConnectionData { - @JsonProperty + @JsonProperty("clientConnection") public final ClientConnection clientConnection; - @JsonProperty + @JsonProperty("indicatorValues") public final Collection indicatorValues; @JsonCreator protected ClientConnectionData( - @JsonProperty final ClientConnection clientConnection, - @JsonProperty final Collection indicatorValues) { + @JsonProperty("clientConnection") final ClientConnection clientConnection, + @JsonProperty("indicatorValues") final Collection indicatorValues) { this.clientConnection = clientConnection; this.indicatorValues = indicatorValues; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java index 48a2c86d..8379ca97 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java @@ -23,6 +23,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collector; import java.util.stream.Collectors; @@ -38,6 +39,9 @@ import ch.ethz.seb.sebserver.gbl.Constants; public final class Utils { + public static final Predicate TRUE_PREDICATE = v -> true; + public static final Predicate FALSE_PREDICATE = v -> false; + private static final Logger log = LoggerFactory.getLogger(Utils.class); /** This Collector can be used within stream collect to get one expected singleton element from @@ -339,4 +343,14 @@ public final class Utils { return (text == null) ? null : Constants.PERCENTAGE + text + Constants.PERCENTAGE; } + @SuppressWarnings("unchecked") + public static Predicate truePredicate() { + return (Predicate) TRUE_PREDICATE; + } + + @SuppressWarnings("unchecked") + public static Predicate falsePredicate() { + return (Predicate) FALSE_PREDICATE; + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java index f51df7a3..fbe345c9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java @@ -50,7 +50,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeleteExamCo import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeleteIndicator; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMappingsPage; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicators; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicatorPage; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.GetQuizData; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.ImportAsExam; @@ -337,7 +337,7 @@ public class ExamForm implements TemplateComposer { INDICATOR_LIST_TITLE_KEY); final EntityTable indicatorTable = - this.pageService.entityTableBuilder(restService.getRestCall(GetIndicators.class)) + this.pageService.entityTableBuilder(restService.getRestCall(GetIndicatorPage.class)) .withRestCallAdapter(builder -> builder.withQueryParam( Indicator.FILTER_ATTR_EXAM_ID, entityKey.modelId)) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/LmsSetupForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/LmsSetupForm.java index e1e6fb63..869dd5f3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/LmsSetupForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/LmsSetupForm.java @@ -28,6 +28,7 @@ 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; import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; import ch.ethz.seb.sebserver.gui.form.FormBuilder; import ch.ethz.seb.sebserver.gui.form.FormHandle; @@ -256,7 +257,7 @@ public class LmsSetupForm implements TemplateComposer { // reset previous errors formHandle.process( - name -> true, + Utils.truePredicate(), fieldAccessor -> fieldAccessor.resetError()); // first test the connection on ad hoc object diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java index a81478d1..73ae3329 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java @@ -8,26 +8,129 @@ package ch.ethz.seb.sebserver.gui.content; +import java.util.Collection; +import java.util.function.Consumer; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.gui.service.ResourceService; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; +import ch.ethz.seb.sebserver.gui.service.push.ServerPushContext; +import ch.ethz.seb.sebserver.gui.service.push.ServerPushService; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicators; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetConnectionData; +import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionPoll; +import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; @Lazy @Component @GuiProfile public class MonitoringRunningExam implements TemplateComposer { - public MonitoringRunningExam() { - // TODO Auto-generated constructor stub + private static final Logger log = LoggerFactory.getLogger(MonitoringRunningExam.class); + + private final ServerPushService serverPushService; + private final PageService pageService; + private final ResourceService resourceService; + private final long pollInterval; + + protected MonitoringRunningExam( + final ServerPushService serverPushService, + final PageService pageService, + final ResourceService resourceService, + @Value("${sebserver.gui.webservice.poll-interval:500}") final long pollInterval) { + + this.serverPushService = serverPushService; + this.pageService = pageService; + this.resourceService = resourceService; + this.pollInterval = pollInterval; } @Override public void compose(final PageContext pageContext) { - // TODO Auto-generated method stub + //final CurrentUser currentUser = this.resourceService.getCurrentUser(); + final RestService restService = this.resourceService.getRestService(); + final WidgetFactory widgetFactory = this.pageService.getWidgetFactory(); + + final EntityKey entityKey = pageContext.getEntityKey(); + + final Exam exam = restService.getBuilder(GetExam.class) + .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) + .call() + .getOrThrow(); + + final Collection indicators = restService.getBuilder(GetIndicators.class) + .withQueryParam(Indicator.FILTER_ATTR_EXAM_ID, entityKey.modelId) + .call() + .getOrThrow(); + + final Composite content = this.pageService.getWidgetFactory().defaultPageLayout( + pageContext.getParent(), + new LocTextKey("sebserver.monitoring.exam", exam.name)); + + final Composite tablePane = new Composite(content, SWT.NONE); + tablePane.setLayout(new GridLayout()); + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); + gridData.heightHint = 100; + tablePane.setLayoutData(gridData); + + final ClientConnectionTable clientTable = new ClientConnectionTable( + widgetFactory, + tablePane, + exam, + indicators); + + final RestCall>.RestCallBuilder restCall = + restService.getBuilder(GetConnectionData.class) + .withURIVariable(API.PARAM_MODEL_ID, exam.getModelId()); + + final ClientConnectionPoll clientConnectionPoll = new ClientConnectionPoll( + restCall, + clientTable, + this.pollInterval); + + this.serverPushService.runServerPush( + new ServerPushContext(content, Utils.truePredicate()), + clientConnectionPoll, + updateTable(clientTable)); } + private final Consumer updateTable(final ClientConnectionTable clientTable) { + return context -> { + if (!context.isDisposed()) { + try { + clientTable.updateGUI(); + context.layout(); + } catch (final Exception e) { + if (log.isWarnEnabled()) { + log.warn("Unexpected error while trying to update GUI: ", e); + } + } + } + }; + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java index 8d52f445..dac11754 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/Form.java @@ -163,7 +163,7 @@ public final class Form implements FormBinding { public void allVisible() { process( - name -> true, + Utils.truePredicate(), ffa -> ffa.setVisible(true)); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/FormHandle.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/FormHandle.java index d787274a..dd4d4c2c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/FormHandle.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/FormHandle.java @@ -17,6 +17,7 @@ import org.slf4j.LoggerFactory; import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gui.form.Form.FormFieldAccessor; import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; @@ -77,7 +78,7 @@ public class FormHandle { public Result doAPIPost() { // reset all errors that may still be displayed this.form.process( - name -> true, + Utils.truePredicate(), fieldAccessor -> fieldAccessor.resetError()); // post diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/AbstractExportCall.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/AbstractExportCall.java index fc26f34c..dac46b2b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/AbstractExportCall.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/AbstractExportCall.java @@ -29,8 +29,8 @@ public abstract class AbstractExportCall extends RestCall { @Override protected Result exchange(final RestCallBuilder builder) { try { - final ResponseEntity responseEntity = this.restService - .getWebserviceAPIRestTemplate() + final ResponseEntity responseEntity = builder + .getRestTemplate() .exchange( builder.buildURI(), this.httpMethod, diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java index 18abfef4..8b1cf3d9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java @@ -27,6 +27,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClientResponseException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonProcessingException; @@ -81,7 +83,10 @@ public abstract class RestCall { } - protected RestCall init(final RestService restService, final JSONMapper jsonMapper) { + protected RestCall init( + final RestService restService, + final JSONMapper jsonMapper) { + this.restService = restService; this.jsonMapper = jsonMapper; return this; @@ -92,8 +97,7 @@ public abstract class RestCall { log.debug("Call webservice API on {} for {}", this.path, builder); try { - final ResponseEntity responseEntity = this.restService - .getWebserviceAPIRestTemplate() + final ResponseEntity responseEntity = builder.restTemplate .exchange( builder.buildURI(), this.httpMethod, @@ -141,7 +145,9 @@ public abstract class RestCall { } public RestCallBuilder newBuilder() { - return new RestCallBuilder(); + return new RestCallBuilder( + this.restService.getWebserviceAPIRestTemplate(), + this.restService.getWebserviceURIBuilder()); } public RestCall.RestCallBuilder newBuilder(final RestCall.RestCallBuilder builder) { @@ -168,12 +174,16 @@ public abstract class RestCall { public class RestCallBuilder { + private RestTemplate restTemplate; + private UriComponentsBuilder uriComponentsBuilder; private final HttpHeaders httpHeaders; private String body = null; private final MultiValueMap queryParams; private final Map uriVariables; - protected RestCallBuilder() { + protected RestCallBuilder(final RestTemplate restTemplate, final UriComponentsBuilder uriComponentsBuilder) { + this.restTemplate = restTemplate; + this.uriComponentsBuilder = uriComponentsBuilder; this.httpHeaders = new HttpHeaders(); this.queryParams = new LinkedMultiValueMap<>(); this.uriVariables = new HashMap<>(); @@ -183,12 +193,28 @@ public abstract class RestCall { } public RestCallBuilder(final RestCall.RestCallBuilder builder) { + this.restTemplate = builder.restTemplate; + this.uriComponentsBuilder = builder.uriComponentsBuilder; this.httpHeaders = builder.httpHeaders; this.body = builder.body; this.queryParams = new LinkedMultiValueMap<>(builder.queryParams); this.uriVariables = new HashMap<>(builder.uriVariables); } + public RestTemplate getRestTemplate() { + return this.restTemplate; + } + + public RestCallBuilder withRestTemplate(final RestTemplate restTemplate) { + this.restTemplate = restTemplate; + return this; + } + + public RestCallBuilder withUriComponentsBuilder(final UriComponentsBuilder uriComponentsBuilder) { + this.uriComponentsBuilder = uriComponentsBuilder; + return this; + } + public RestCallBuilder withHeaders(final HttpHeaders headers) { this.httpHeaders.addAll(headers); return this; @@ -284,7 +310,8 @@ public abstract class RestCall { } public String buildURI() { - return RestCall.this.restService.getWebserviceURIBuilder() + return this.uriComponentsBuilder + .cloneBuilder() .path(RestCall.this.path) .queryParams(this.queryParams) .toUriString(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetIndicatorPage.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetIndicatorPage.java new file mode 100644 index 00000000..a36f2294 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetIndicatorPage.java @@ -0,0 +1,41 @@ +/* + * 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.exam; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.Page; +import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class GetIndicatorPage extends RestCall> { + + public GetIndicatorPage() { + super(new TypeKey<>( + CallType.GET_PAGE, + EntityType.INDICATOR, + new TypeReference>() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_INDICATOR_ENDPOINT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetIndicators.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetIndicators.java index abdd13b7..b194f437 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetIndicators.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetIndicators.java @@ -8,34 +8,30 @@ package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam; +import java.util.List; + import org.springframework.context.annotation.Lazy; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import com.fasterxml.jackson.core.type.TypeReference; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.EntityType; -import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.PageToListCallAdapter; @Lazy @Component @GuiProfile -public class GetIndicators extends RestCall> { +public class GetIndicators extends PageToListCallAdapter { public GetIndicators() { - super(new TypeKey<>( - CallType.GET_PAGE, + super( + GetIndicatorPage.class, EntityType.INDICATOR, - new TypeReference>() { - }), - HttpMethod.GET, - MediaType.APPLICATION_FORM_URLENCODED, + new TypeReference>() { + }, API.EXAM_INDICATOR_ENDPOINT); } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetConnectionData.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetConnectionData.java new file mode 100644 index 00000000..8bec906a --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetConnectionData.java @@ -0,0 +1,42 @@ +/* + * 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.session; + +import java.util.Collection; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class GetConnectionData extends RestCall> { + + public GetConnectionData() { + super(new TypeKey<>( + CallType.GET_LIST, + EntityType.CLIENT_CONNECTION, + new TypeReference>() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_MONITORING_ENDPOINT + API.MODEL_ID_VAR_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceConnectionData.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceConnectionData.java new file mode 100644 index 00000000..22a2f631 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceConnectionData.java @@ -0,0 +1,114 @@ +/* + * 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.web.util.UriComponentsBuilder; + +public class WebserviceConnectionData { + + final String id; + final String webserviceProtocol; + final String webserviceServerAdress; + final String webserviceServerPort; + final String webserviceAPIPath; + final String webserviceServerAddress; + + private final UriComponentsBuilder webserviceURIBuilder; + + protected WebserviceConnectionData( + final String id, + final String webserviceProtocol, + final String webserviceServerAdress, + final String webserviceServerPort, + final String webserviceAPIPath) { + + this.id = id; + this.webserviceProtocol = webserviceProtocol; + this.webserviceServerAdress = webserviceServerAdress; + this.webserviceServerPort = webserviceServerPort; + this.webserviceAPIPath = webserviceAPIPath; + + this.webserviceServerAddress = webserviceProtocol + "://" + webserviceServerAdress + ":" + webserviceServerPort; + this.webserviceURIBuilder = UriComponentsBuilder + .fromHttpUrl(webserviceProtocol + "://" + webserviceServerAdress) + .port(webserviceServerPort) + .path(webserviceAPIPath); + } + + public String getId() { + return this.id; + } + + public String getWebserviceProtocol() { + return this.webserviceProtocol; + } + + public String getWebserviceServerAdress() { + return this.webserviceServerAdress; + } + + public String getWebserviceServerPort() { + return this.webserviceServerPort; + } + + public String getWebserviceAPIPath() { + return this.webserviceAPIPath; + } + + public String getWebserviceServerAddress() { + return this.webserviceServerAddress; + } + + public UriComponentsBuilder getWebserviceURIBuilder() { + return this.webserviceURIBuilder.cloneBuilder(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.id == null) ? 0 : this.id.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + final WebserviceConnectionData other = (WebserviceConnectionData) obj; + if (this.id == null) { + if (other.id != null) + return false; + } else if (!this.id.equals(other.id)) + return false; + return true; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("WebserviceConnectionData [id="); + builder.append(this.id); + builder.append(", webserviceProtocol="); + builder.append(this.webserviceProtocol); + builder.append(", webserviceServerAdress="); + builder.append(this.webserviceServerAdress); + builder.append(", webserviceServerPort="); + builder.append(this.webserviceServerPort); + builder.append(", webserviceAPIPath="); + builder.append(this.webserviceAPIPath); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionPoll.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionPoll.java new file mode 100644 index 00000000..c6f144fa --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionPoll.java @@ -0,0 +1,58 @@ +/* + * 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.session; + +import java.util.Collection; +import java.util.Collections; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; +import ch.ethz.seb.sebserver.gui.service.push.ServerPushContext; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +public class ClientConnectionPoll implements Consumer { + + private static final Logger log = LoggerFactory.getLogger(ClientConnectionPoll.class); + + private final RestCall>.RestCallBuilder restCallBuilder; + private final ClientConnectionTable clientConnectionTable; + private final long pollInterval; + + public ClientConnectionPoll( + final RestCall>.RestCallBuilder restCallBuilder, + final ClientConnectionTable clientConnectionTable, + final long pollInterval) { + + this.restCallBuilder = restCallBuilder; + this.clientConnectionTable = clientConnectionTable; + this.pollInterval = pollInterval; + } + + @Override + public void accept(final ServerPushContext pushContext) { + try { + Thread.sleep(this.pollInterval); + } catch (final Exception e) { + if (log.isDebugEnabled()) { + log.debug("unexpected error while sleep: ", e); + } + } + + this.clientConnectionTable.updateValues(this.restCallBuilder + .call() + .get(t -> { + log.error("Error poll connection data: ", t); + return Collections.emptyList(); + })); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionTable.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionTable.java new file mode 100644 index 00000000..4a32ddca --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionTable.java @@ -0,0 +1,289 @@ +/* + * 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.session; + +import java.util.Collection; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; +import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType; +import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.Threshold; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; +import ch.ethz.seb.sebserver.gbl.model.session.IndicatorValue; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; + +public final class ClientConnectionTable { + + private static final Logger log = LoggerFactory.getLogger(ClientConnectionTable.class); + + private final static LocTextKey CONNECTION_ID_TEXT_KEY = + new LocTextKey("sebserver.monitoring.connection.list.column.id"); + private final static LocTextKey CONNECTION_ADDRESS_TEXT_KEY = + new LocTextKey("sebserver.monitoring.connection.list.column.address"); + private final static LocTextKey CONNECTION_STATUS_TEXT_KEY = + new LocTextKey("sebserver.monitoring.connection.list.column.status"); + + private final WidgetFactory widgetFactory; + private final Exam exam; + private final EnumMap indicatorMapping; + private final Table table; + + private final Color color1; + private final Color color2; + private final Color color3; + private int tableWidth; + + private final Map tableMapping; + + public ClientConnectionTable( + final WidgetFactory widgetFactory, + final Composite tableRoot, + final Exam exam, + final Collection indicators) { + + this.widgetFactory = widgetFactory; + this.exam = exam; + + final Display display = tableRoot.getDisplay(); + + this.indicatorMapping = new EnumMap<>(IndicatorType.class); + int i = 3; + for (final Indicator indicator : indicators) { + this.indicatorMapping.put(indicator.type, new IndicatorData(indicator, i, display)); + i++; + } + + this.table = widgetFactory.tableLocalized(tableRoot); + this.table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + this.table.setLayout(new GridLayout()); + + widgetFactory.tableColumnLocalized( + this.table, + CONNECTION_ID_TEXT_KEY); + widgetFactory.tableColumnLocalized( + this.table, + CONNECTION_ADDRESS_TEXT_KEY); + widgetFactory.tableColumnLocalized( + this.table, + CONNECTION_STATUS_TEXT_KEY); + for (final Indicator indDef : indicators) { + final TableColumn tc = new TableColumn(this.table, SWT.NONE); + tc.setText(indDef.name); + } + + this.table.setHeaderVisible(true); + this.table.setLinesVisible(true); + + this.color1 = new Color(display, new RGB(0, 255, 0), 100); + this.color2 = new Color(display, new RGB(249, 166, 2), 100); + this.color3 = new Color(display, new RGB(255, 0, 0), 100); + + this.tableMapping = new HashMap<>(); + this.table.layout(); + } + + public WidgetFactory getWidgetFactory() { + return this.widgetFactory; + } + + public Exam getExam() { + return this.exam; + } + + public void updateValues(final Collection connectionInfo) { + for (final ClientConnectionData data : connectionInfo) { + final UpdatableTableItem tableItem = this.tableMapping.computeIfAbsent( + data.getConnectionId(), + userIdentifier -> new UpdatableTableItem(this.table, data.getConnectionId())); + tableItem.push(data); + } + } + + public void updateGUI() { + for (final UpdatableTableItem uti : this.tableMapping.values()) { + if (uti.tableItem == null) { + createTableItem(uti); + updateIndicatorValues(uti); + updateConnectionStatusColor(uti); + } else { + if (!uti.connectionData.clientConnection.status + .equals(uti.previous_connectionData.clientConnection.status)) { + uti.tableItem.setText(0, uti.getConnectionIdentifer()); + uti.tableItem.setText(1, uti.getStatusName()); + updateConnectionStatusColor(uti); + } + if (uti.hasStatus(ConnectionStatus.ESTABLISHED)) { + updateIndicatorValues(uti); + } + } + uti.tableItem.getDisplay(); + } + + adaptTableWidth(); + } + + private void createTableItem(final UpdatableTableItem uti) { + uti.tableItem = new TableItem(this.table, SWT.NONE); + uti.tableItem.setText(0, uti.getConnectionIdentifer()); + uti.tableItem.setText(1, uti.getConnectionAddress()); + uti.tableItem.setText(2, uti.getStatusName()); + } + + private void adaptTableWidth() { + final Rectangle area = this.table.getParent().getClientArea(); + if (this.tableWidth != area.width) { + final int columnWidth = area.width / this.table.getColumnCount(); + for (final TableColumn column : this.table.getColumns()) { + column.setWidth(columnWidth); + } + this.table.layout(true, true); + //this.table.pack(); + this.tableWidth = area.width; + } + } + + private void updateIndicatorValues(final UpdatableTableItem uti) { + + for (final IndicatorValue iv : uti.connectionData.indicatorValues) { + final IndicatorData indicatorData = this.indicatorMapping.get(iv.getType()); + if (indicatorData != null) { + uti.tableItem.setText(indicatorData.index, String.valueOf(iv.getValue())); + uti.tableItem.setBackground( + indicatorData.index, + this.getColorForValue(indicatorData, iv.getValue())); + } + } + } + + private void updateConnectionStatusColor(final UpdatableTableItem uti) { + switch (uti.connectionData.clientConnection.status) { + case ESTABLISHED: { + uti.tableItem.setBackground(1, this.color1); + break; + } + case ABORTED: { + uti.tableItem.setBackground(1, this.color3); + break; + } + default: { + uti.tableItem.setBackground(1, this.color2); + } + } + } + + private Color getColorForValue(final IndicatorData indicatorData, final double value) { + + for (int i = 0; i < indicatorData.thresholdColor.length; i++) { + if (value >= indicatorData.thresholdColor[i].value) { + return indicatorData.thresholdColor[i].color; + } + } + + return this.color1; + } + + private static final class UpdatableTableItem { + + final Long connectionId; + TableItem tableItem; + ClientConnectionData previous_connectionData; + ClientConnectionData connectionData; + + private UpdatableTableItem(final Table parent, final Long connectionId) { + this.tableItem = null; + this.connectionId = connectionId; + } + + public String getStatusName() { + if (this.connectionData != null && this.connectionData.clientConnection.status != null) { + return this.connectionData.clientConnection.status.name(); + } + return ConnectionStatus.UNDEFINED.name(); + } + + public String getConnectionAddress() { + if (this.connectionData != null && this.connectionData.clientConnection.clientAddress != null) { + return this.connectionData.clientConnection.clientAddress; + } + return Constants.EMPTY_NOTE; + } + + public String getConnectionIdentifer() { + if (this.connectionData != null && this.connectionData.clientConnection.userSessionId != null) { + return this.connectionData.clientConnection.userSessionId; + } + + return "- " + this.connectionId + " -"; + } + + public boolean hasStatus(final ConnectionStatus status) { + if (this.connectionData != null && this.connectionData.clientConnection != null) { + return status == this.connectionData.clientConnection.status; + } + + return false; + } + + public void push(final ClientConnectionData connectionData) { + this.previous_connectionData = this.connectionData; + this.connectionData = connectionData; + } + } + + private static final class IndicatorData { + final int index; + final Indicator indicator; + final ThresholdColor[] thresholdColor; + + protected IndicatorData(final Indicator indicator, final int index, final Display display) { + this.indicator = indicator; + this.index = index; + this.thresholdColor = new ThresholdColor[indicator.thresholds.size()]; + for (int i = 0; i < indicator.thresholds.size(); i++) { + this.thresholdColor[i] = new ThresholdColor(indicator.thresholds.get(i), display); + } + } + + } + + private static final class ThresholdColor { + final double value; + final Color color; + + protected ThresholdColor(final Threshold threshold, final Display display) { + this.value = threshold.value; + final RGB rgb = new RGB( + Integer.parseInt(threshold.color.substring(0, 2), 16), + Integer.parseInt(threshold.color.substring(2, 4), 16), + Integer.parseInt(threshold.color.substring(4, 6), 16)); + this.color = new Color(display, rgb, 100); + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java index 972dba98..1b236035 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java @@ -370,6 +370,13 @@ public class WidgetFactory { return table; } + public TableColumn tableColumnLocalized( + final Table table, + final LocTextKey locTextKey) { + + return tableColumnLocalized(table, locTextKey, null); + } + public TableColumn tableColumnLocalized( final Table table, final LocTextKey locTextKey, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/EntityDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/EntityDAO.java index 7ffaf6a8..bc053102 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/EntityDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/EntityDAO.java @@ -26,6 +26,7 @@ import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityName; import ch.ethz.seb.sebserver.gbl.model.ModelIdAware; import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Utils; /** Defines generic interface for all Entity based Data Access Objects * @@ -129,7 +130,7 @@ public interface EntityDAO { * @return Result referring to collection of all matching entities or an error if happened */ @Transactional(readOnly = true) default Result> allMatching(final FilterMap filterMap) { - return allMatching(filterMap, e -> true); + return allMatching(filterMap, Utils.truePredicate()); } /** Get a (unordered) collection of all Entities that matches a given filter criteria diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/APIExceptionHandler.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/APIExceptionHandler.java index b0553168..b58f0594 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/APIExceptionHandler.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/APIExceptionHandler.java @@ -21,6 +21,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -69,6 +70,16 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler { return new ResponseEntity<>(valErrors, HttpStatus.BAD_REQUEST); } + @ExceptionHandler(OAuth2Exception.class) + public ResponseEntity handleBeanValidationException( + final OAuth2Exception ex, + final WebRequest request) { + + log.error("OAuth2Exception: ", ex); + final APIMessage message = APIMessage.ErrorMessage.UNAUTHORIZED.of(ex.getMessage()); + return new ResponseEntity<>(message, HttpStatus.UNAUTHORIZED); + } + @ExceptionHandler(BeanValidationException.class) public ResponseEntity handleBeanValidationException( final BeanValidationException ex, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java index b30f2bf4..30fa4ab5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java @@ -8,6 +8,7 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api; +import java.io.IOException; import java.security.Principal; import java.util.Arrays; import java.util.Collection; @@ -77,16 +78,16 @@ public class ExamAPI_V1_Controller { @RequestMapping( path = API.EXAM_API_HANDSHAKE_ENDPOINT, - method = RequestMethod.GET, + method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Collection handshakeCreate( @RequestParam(name = API.PARAM_INSTITUTION_ID, required = false) final Long instIdRequestParam, @RequestParam(name = API.EXAM_API_PARAM_EXAM_ID, required = false) final Long examIdRequestParam, - @RequestBody final MultiValueMap formParams, + @RequestBody(required = false) final MultiValueMap formParams, final Principal principal, final HttpServletRequest request, - final HttpServletResponse response) { + final HttpServletResponse response) throws IOException { final POSTMapper mapper = new POSTMapper(formParams); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java index bcf89b6e..b5739b78 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java @@ -9,12 +9,14 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import org.springframework.http.MediaType; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @@ -25,8 +27,10 @@ import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType; import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PermissionDeniedException; @@ -122,7 +126,7 @@ public class ExamMonitoringController { } final List exams = new ArrayList<>(this.examSessionService - .getFilteredRunningExams(filterMap, exam -> true) + .getFilteredRunningExams(filterMap, Utils.truePredicate()) .getOrThrow()); return ExamAdministrationController.buildSortedExamPage( @@ -132,4 +136,29 @@ public class ExamMonitoringController { exams); } + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT, + method = RequestMethod.GET, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Collection getConnectionData( + @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long examId) { + + // check if user has EXAM_SUPPORTER privilege. + final SEBServerUser currentUser = this.authorization + .getUserService() + .getCurrentUser(); + + if (!currentUser.getUserRoles().contains(UserRole.EXAM_SUPPORTER)) { + throw new PermissionDeniedException( + EntityType.EXAM, + PrivilegeType.READ, + currentUser.getUserInfo().uuid); + } + + return this.examSessionService + .getConnectionData(examId) + .getOrThrow(); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserActivityLogController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserActivityLogController.java index 220a615a..91e43e40 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserActivityLogController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserActivityLogController.java @@ -101,7 +101,8 @@ public class UserActivityLogController { Utils.toMilliSeconds(to), activityTypes, entityTypes, - log -> true).getOrThrow(); + Utils.truePredicate()) + .getOrThrow(); }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java index 2a23fa0c..1d54a93c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java @@ -10,17 +10,16 @@ package ch.ethz.seb.sebserver.webservice.weblayer.oauth; import java.util.Collections; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; -import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.ClientRegistrationException; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.stereotype.Component; -import ch.ethz.seb.sebserver.WebSecurityConfig; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebClientConfigService; @@ -35,13 +34,11 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebClientConfigSe @Component public class WebClientDetailsService implements ClientDetailsService { + private static final Logger log = LoggerFactory.getLogger(WebClientDetailsService.class); + private final SebClientConfigService sebClientConfigService; private final AdminAPIClientDetails adminClientDetails; - @Autowired - @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, final SebClientConfigService sebClientConfigService) { @@ -72,10 +69,18 @@ public class WebClientDetailsService implements ClientDetailsService { } return getForExamClientAPI(clientId) - .getOrThrow(); + .get(t -> { + log.error("Client not found: ", t); + throw new AccessDeniedException(t.getMessage()); + }); } protected Result getForExamClientAPI(final String clientId) { + + if (log.isDebugEnabled()) { + log.debug("Trying to get ClientDetails for client: {}", clientId); + } + return this.sebClientConfigService.getEncodedClientSecret(clientId) .map(pwd -> { final BaseClientDetails baseClientDetails = new BaseClientDetails( diff --git a/src/main/resources/config/application-dev-gui.properties b/src/main/resources/config/application-dev-gui.properties index eb7374ec..ae47b6a6 100644 --- a/src/main/resources/config/application-dev-gui.properties +++ b/src/main/resources/config/application-dev-gui.properties @@ -10,6 +10,8 @@ sebserver.gui.webservice.protocol=http sebserver.gui.webservice.address=localhost sebserver.gui.webservice.port=8080 sebserver.gui.webservice.apipath=/admin-api/v1 +# defines the polling interval that is used to poll the webservice for client connection data on a monitored exam page +sebserver.gui.webservice.poll-interval=200 sebserver.gui.theme=css/sebserver.css diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 6e5fde3c..0c77e4c5 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -878,3 +878,9 @@ sebserver.monitoring.exam.list.column.type=Type sebserver.monitoring.exam.list.column.startTime=Start Time sebserver.monitoring.exam.list.column.endTime=End Time +sebserver.monitoring.exam=Monitoring Exam: {0} + +sebserver.monitoring.connection.list.column.id=Identifier +sebserver.monitoring.connection.list.column.address=IP Address +sebserver.monitoring.connection.list.column.status=Status + diff --git a/src/test/java/ch/ethz/seb/sebserver/HTTPClientBot.java b/src/test/java/ch/ethz/seb/sebserver/HTTPClientBot.java new file mode 100644 index 00000000..11b10ec0 --- /dev/null +++ b/src/test/java/ch/ethz/seb/sebserver/HTTPClientBot.java @@ -0,0 +1,462 @@ +/* + * 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; + +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.ParameterizedTypeReference; +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.http.converter.StringHttpMessageConverter; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.client.http.OAuth2ErrorHandler; +import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.model.session.RunningExam; +import ch.ethz.seb.sebserver.gbl.util.Utils; + +public class HTTPClientBot { + + private static final long ONE_SECOND = 1000; // milliseconds + private static final long TEN_SECONDS = 10 * ONE_SECOND; + private static final long ONE_MINUTE = 60 * ONE_SECOND; + + private static final Logger log = LoggerFactory.getLogger(HTTPClientBot.class); + + private final ExecutorService executorService = Executors.newFixedThreadPool(10); + + private final List scopes = Arrays.asList("read", "write"); + + private final String webserviceAddress; + private final String accessTokenEndpoint; + private final String clientId; + private final String clientSecret; + private final String apiPath; + private final String apiVersion; + private final String examId; + private final String institutionId; + + private final int numberOfConnections; + + private final long pingInterval; + private final long errorInterval; + private final long runtime; + private final int connectionAttempts; + + public HTTPClientBot(final Map args) { + this.webserviceAddress = args.getOrDefault("webserviceAddress", "http://localhost:8080"); + this.accessTokenEndpoint = args.getOrDefault("accessTokenEndpoint", "/oauth/token"); + this.clientId = args.getOrDefault("clientId", "TO_SET"); + this.clientSecret = args.getOrDefault("clientSecret", "TO_SET"); + this.apiPath = args.getOrDefault("apiPath", "/exam-api"); + this.apiVersion = args.getOrDefault("apiVersion", "v1"); + this.examId = args.getOrDefault("examId", "2"); + this.institutionId = args.getOrDefault("institutionId", "1"); + this.numberOfConnections = Integer.parseInt(args.getOrDefault("numberOfConnections", "1")); + this.pingInterval = Long.parseLong(args.getOrDefault("pingInterval", "200")); + this.errorInterval = Long.parseLong(args.getOrDefault("errorInterval", String.valueOf(TEN_SECONDS))); + this.runtime = Long.parseLong(args.getOrDefault("runtime", String.valueOf(ONE_MINUTE))); + this.connectionAttempts = Integer.parseInt(args.getOrDefault("connectionAttempts", "3")); + + for (int i = 0; i < this.numberOfConnections; i++) { + this.executorService.execute(new ConnectionBot("connection_" + i)); + } + + this.executorService.shutdown(); + } + + public static void main(final String[] args) { + final Map argsMap = new HashMap<>(); + if (args.length > 0) { + for (final String arg : StringUtils.split(args[0], Constants.LIST_SEPARATOR)) { + final String[] nameValue = StringUtils.split(arg, Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR); + argsMap.put(nameValue[0], nameValue[1]); + } + } + new HTTPClientBot(argsMap); + } + + private final class ConnectionBot implements Runnable { + + private final String name; + private final OAuth2RestTemplate restTemplate = createRestTemplate(); + + private final String handshakeURI = HTTPClientBot.this.webserviceAddress + + HTTPClientBot.this.apiPath + "/" + + HTTPClientBot.this.apiVersion + "/handshake"; + private final String configurartionURI = HTTPClientBot.this.webserviceAddress + + HTTPClientBot.this.apiPath + "/" + + HTTPClientBot.this.apiVersion + "/configuration"; + private final String pingURI = HTTPClientBot.this.webserviceAddress + + HTTPClientBot.this.apiPath + "/" + + HTTPClientBot.this.apiVersion + "/sebping"; + private final String eventURI = HTTPClientBot.this.webserviceAddress + + HTTPClientBot.this.apiPath + "/" + + HTTPClientBot.this.apiVersion + "/seblog"; + + private final HttpEntity connectBody; + + protected ConnectionBot(final String name) { + this.name = name; + final MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + this.connectBody = new HttpEntity<>(API.PARAM_INSTITUTION_ID + + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + + HTTPClientBot.this.institutionId + + Constants.FORM_URL_ENCODED_SEPARATOR + + API.EXAM_API_PARAM_EXAM_ID + + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + + HTTPClientBot.this.examId, + headers); + + } + + @Override + public void run() { + log.info("ConnectionBot {} : Client-Connection-Bot started: {}\n" + + "webserviceAddress: {}\n" + + "accessTokenEndpoint: {}\n" + + "clientId: {}\n" + + "clientSecret: {}\n" + + "apiPath: {}\n" + + "apiVersion: {}\n" + + "examId: {}\n" + + "institutionId: {}\n" + + "pingInterval: {}\n" + + "errorInterval: {}\n" + + "runtime: {}\n", this.name, + HTTPClientBot.this.webserviceAddress, + HTTPClientBot.this.accessTokenEndpoint, + HTTPClientBot.this.clientId, + HTTPClientBot.this.clientSecret, + HTTPClientBot.this.apiPath, + HTTPClientBot.this.apiVersion, + HTTPClientBot.this.examId, + HTTPClientBot.this.institutionId, + HTTPClientBot.this.pingInterval, + HTTPClientBot.this.errorInterval); + + int attempt = 0; + + while (attempt < HTTPClientBot.this.connectionAttempts) { + attempt++; + log.info("ConnectionBot {} : Try to request access-token; attempt: {}", this.name, attempt); + try { + + this.restTemplate.getAccessToken(); + + final String connectionToken = createConnection(); + if (connectionToken != null) { + if (getConfig(connectionToken) && establishConnection(connectionToken)) { + + final PingEntity pingHeader = new PingEntity(connectionToken); + final EventEntity eventHeader = new EventEntity(connectionToken); + + try { + final long startTime = System.currentTimeMillis(); + final long endTime = startTime + HTTPClientBot.this.runtime; + long currentTime = startTime; + long lastPingTime = startTime; + long lastErrorTime = startTime; + + while (currentTime < endTime) { + if (currentTime - lastPingTime >= HTTPClientBot.this.pingInterval) { + pingHeader.next(); + sendPing(pingHeader); + lastPingTime = currentTime; + } + if (currentTime - lastErrorTime >= HTTPClientBot.this.errorInterval) { + eventHeader.next(); + sendErrorEvent(eventHeader); + lastErrorTime = currentTime; + } + try { + Thread.sleep(50); + } catch (final Exception e) { + } + currentTime = System.currentTimeMillis(); + } + } catch (final Throwable t) { + log.error("ConnectionBot {} : Error sending events: ", this.name, t); + } finally { + disconnect(connectionToken); + } + } + } + + } catch (final Exception e) { + log.error("ConnectionBot {} : Failed to request access-token: ", this.name, e); + if (attempt >= HTTPClientBot.this.connectionAttempts) { + log.error("ConnectionBot {} : Gave up afer {} connection attempts: ", this.name, attempt); + } + } + } + } + + private String createConnection() { + log.info("ConnectionBot {} : init connection", this.name); + + try { + final ResponseEntity> exchange = this.restTemplate.exchange( + this.handshakeURI, + HttpMethod.POST, + this.connectBody, + new ParameterizedTypeReference>() { + }); + + final HttpStatus statusCode = exchange.getStatusCode(); + if (statusCode.isError()) { + throw new RuntimeException("Webservice answered with error: " + exchange.getBody()); + } + + final Collection body = exchange.getBody(); + final String token = exchange.getHeaders().getFirst(API.EXAM_API_SEB_CONNECTION_TOKEN); + + log.info("ConnectionBot {} : successfully created connection, token: {} body: {} ", token, body); + + return token; + } catch (final Exception e) { + log.error("ConnectionBot {} : Failed to init connection", e); + return null; + } + } + + public boolean getConfig(final String connectionToken) { + final HttpEntity configHeader = new HttpEntity<>( + API.EXAM_API_PARAM_EXAM_ID + + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + + HTTPClientBot.this.examId); + configHeader.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + configHeader.getHeaders().set(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken); + + log.info("ConnectionBot {} : get SEB Configuration", this.name); + + try { + final ResponseEntity exchange = this.restTemplate.exchange( + this.configurartionURI, + HttpMethod.GET, + configHeader, + new ParameterizedTypeReference() { + }); + + final HttpStatus statusCode = exchange.getStatusCode(); + if (statusCode.isError()) { + throw new RuntimeException("Webservice answered with error: " + exchange.getBody()); + } + + final byte[] config = exchange.getBody(); + + if (log.isDebugEnabled()) { + log.debug("ConnectionBot {} : successfully requested exam config: " + Utils.toString(config)); + } else { + log.info("ConnectionBot {} : successfully requested exam config"); + } + + return true; + } catch (final Exception e) { + log.error("ConnectionBot {} : Failed get SEB Configuration", e); + return false; + } + } + + public boolean establishConnection(final String connectionToken) { + final HttpEntity configHeader = new HttpEntity<>( + API.EXAM_API_USER_SESSION_ID + + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + + this.name); + configHeader.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + configHeader.getHeaders().set(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken); + + log.info("ConnectionBot {} : Trying to establish SEB client connection", this.name); + + try { + + final ResponseEntity exchange = this.restTemplate.exchange( + this.handshakeURI, + HttpMethod.PUT, + configHeader, + new ParameterizedTypeReference<>() { + }); + + final HttpStatus statusCode = exchange.getStatusCode(); + if (statusCode.isError()) { + throw new RuntimeException("Webservice answered with error: " + exchange.getBody()); + } + + log.info("ConnectionBot {} : successfully established SEB client connection"); + + return true; + } catch (final Exception e) { + log.error("ConnectionBot {} : Failed get established SEB client connection", e); + return false; + } + } + + private boolean sendPing(final HttpEntity pingHeader) { + try { + + this.restTemplate.exchange( + this.pingURI, + HttpMethod.POST, + pingHeader, + new ParameterizedTypeReference<>() { + }); + + return true; + } catch (final Exception e) { + log.error("ConnectionBot {} : Failed send ping", e); + return false; + } + } + + private boolean sendErrorEvent(final HttpEntity eventHeader) { + try { + + this.restTemplate.exchange( + this.eventURI, + HttpMethod.POST, + eventHeader, + new ParameterizedTypeReference<>() { + }); + + return true; + } catch (final Exception e) { + log.error("ConnectionBot {} : Failed send ping", e); + return false; + } + } + + public boolean disconnect(final String connectionToken) { + final HttpEntity configHeader = new HttpEntity<>(null); + configHeader.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + configHeader.getHeaders().set(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken); + + log.info("ConnectionBot {} : Trying to delete SEB client connection", this.name); + + try { + + final ResponseEntity exchange = this.restTemplate.exchange( + this.handshakeURI, + HttpMethod.DELETE, + configHeader, + new ParameterizedTypeReference<>() { + }); + + final HttpStatus statusCode = exchange.getStatusCode(); + if (statusCode.isError()) { + throw new RuntimeException("Webservice answered with error: " + exchange.getBody()); + } + + log.info("ConnectionBot {} : successfully deleted SEB client connection"); + + return true; + } catch (final Exception e) { + log.error("ConnectionBot {} : Failed get deleted SEB client connection", e); + return false; + } + } + } + + private OAuth2RestTemplate createRestTemplate() { + final ClientCredentialsResourceDetails clientCredentialsResourceDetails = + new ClientCredentialsResourceDetails(); + clientCredentialsResourceDetails.setAccessTokenUri(this.webserviceAddress + this.accessTokenEndpoint); + clientCredentialsResourceDetails.setClientId(this.clientId); + clientCredentialsResourceDetails.setClientSecret(this.clientSecret); + clientCredentialsResourceDetails.setScope(this.scopes); + + final OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(clientCredentialsResourceDetails); + restTemplate.setErrorHandler(new OAuth2ErrorHandler(clientCredentialsResourceDetails)); + restTemplate + .getMessageConverters() + .add(0, new StringHttpMessageConverter(Charset.forName("UTF-8"))); + + return restTemplate; + } + + private static class PingEntity extends HttpEntity { + private final String pingBodyTemplate = API.EXAM_API_PING_TIMESTAMP + + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + + "{}" + + Constants.FORM_URL_ENCODED_SEPARATOR + + API.EXAM_API_PING_NUMBER + + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + + "{}"; + + private long timestamp = 0; + private int count = 0; + + protected PingEntity(final String connectionToken) { + super(); + getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + getHeaders().set(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken); + } + + void next() { + this.timestamp = System.currentTimeMillis(); + this.count++; + } + + @Override + public String getBody() { + return String.format(this.pingBodyTemplate, this.timestamp, this.count); + } + + @Override + public boolean hasBody() { + return true; + } + } + + private static class EventEntity extends HttpEntity { + private final String eventBodyTemplate = + "{ \"type\": \"ERROR_LOG\", \"timestamp\": {}, \"text\": \"some error\" }"; + + private long timestamp = 0; + + protected EventEntity(final String connectionToken) { + super(); + getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + getHeaders().set(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken); + } + + void next() { + this.timestamp = System.currentTimeMillis(); + } + + @Override + public String getBody() { + return String.format(this.eventBodyTemplate, this.timestamp); + } + + @Override + public boolean hasBody() { + return true; + } + } + +} diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java index 97a33f38..127db17b 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java @@ -136,7 +136,7 @@ public abstract class ExamAPIIntegrationTester { final Long institutionId, final Long examId) throws Exception { - final MockHttpServletRequestBuilder builder = get(this.endpoint + "/handshake") + final MockHttpServletRequestBuilder builder = post(this.endpoint + "/handshake") .header("Content-Type", "application/x-www-form-urlencoded") .header("Authorization", "Bearer " + accessToken) .accept(MediaType.APPLICATION_JSON_UTF8_VALUE); diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/SebConnectionTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/SebConnectionTest.java index a76bd021..cdbef32b 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/SebConnectionTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/SebConnectionTest.java @@ -168,7 +168,7 @@ public class SebConnectionTest extends ExamAPIIntegrationTester { new TypeReference>() { }); final APIMessage error = errorMessage.iterator().next(); - assertEquals(ErrorMessage.GENERIC.messageCode, error.messageCode); + assertEquals(ErrorMessage.ILLEGAL_API_ARGUMENT.messageCode, error.messageCode); } @Test