From c414586fece6c5c390a5d7b0b71ac0bddd68d208 Mon Sep 17 00:00:00 2001 From: anhefti Date: Wed, 3 Nov 2021 13:18:40 +0100 Subject: [PATCH] SEBSERV-191 finished back-end implementation --- .../ch/ethz/seb/sebserver/gbl/api/API.java | 2 + .../ch/ethz/seb/sebserver/gbl/util/Utils.java | 7 ++ .../exam/SEBClientEventAdminService.java | 3 - .../impl/SEBClientEventAdminServiceImpl.java | 43 ++++++++----- .../exam/impl/SEBClientEventCSVExporter.java | 60 +++++++++++++++-- .../weblayer/api/ClientEventController.java | 64 +++++++++++++++++-- 6 files changed, 152 insertions(+), 27 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index e523be9a..b89fb671 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -197,6 +197,8 @@ public final class API { public static final String SEB_CLIENT_EVENT_ENDPOINT = "/seb-client-event"; public static final String SEB_CLIENT_EVENT_SEARCH_PATH_SEGMENT = "/search"; + public static final String SEB_CLIENT_EVENT_EXPORT_PATH_SEGMENT = "/export"; + public static final String SEB_CLIENT_EVENT_EXPORT_TYPE = "exportType"; public static final String SEB_CLIENT_EVENT_EXTENDED_PAGE_ENDPOINT = SEB_CLIENT_EVENT_ENDPOINT + SEB_CLIENT_EVENT_SEARCH_PATH_SEGMENT; 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 8d79797b..84c47e63 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 @@ -666,4 +666,11 @@ public final class Utils { } } + public static String toCSVString(final String text) { + if (StringUtils.isBlank(text)) { + return StringUtils.EMPTY; + } + return Constants.DOUBLE_QUOTE + text.replace("\"", "\"\"") + Constants.DOUBLE_QUOTE; + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/SEBClientEventAdminService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/SEBClientEventAdminService.java index 8acb9081..9acacb4a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/SEBClientEventAdminService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/SEBClientEventAdminService.java @@ -10,13 +10,11 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.exam; import java.io.OutputStream; import java.util.Collection; -import java.util.function.Predicate; import org.springframework.scheduling.annotation.Async; import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig; import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; -import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.ExportType; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; @@ -30,7 +28,6 @@ public interface SEBClientEventAdminService { OutputStream output, FilterMap filterMap, String sort, - final Predicate predicate, ExportType exportType, boolean includeConnectionDetails, boolean includeExamDetails); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventAdminServiceImpl.java index be6b95bd..759b5445 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventAdminServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventAdminServiceImpl.java @@ -13,11 +13,11 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; +import java.util.EnumSet; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; -import java.util.function.Predicate; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -31,14 +31,17 @@ import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport.ErrorEntry; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; -import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.ExportType; +import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord; 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.impl.SEBServerUser; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.SEBClientEventAdminService; @@ -55,16 +58,19 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic private final ClientEventDAO clientEventDAO; private final SEBClientEventExportTransactionHandler sebClientEventExportTransactionHandler; private final EnumMap exporter; + private final AuthorizationService authorizationService; public SEBClientEventAdminServiceImpl( final PaginationService paginationService, final ClientEventDAO clientEventDAO, final SEBClientEventExportTransactionHandler sebClientEventExportTransactionHandler, - final Collection exporter) { + final Collection exporter, + final AuthorizationService authorizationService) { this.paginationService = paginationService; this.clientEventDAO = clientEventDAO; this.sebClientEventExportTransactionHandler = sebClientEventExportTransactionHandler; + this.authorizationService = authorizationService; this.exporter = new EnumMap<>(ExportType.class); exporter.forEach(exp -> this.exporter.putIfAbsent(exp.exportType(), exp)); @@ -102,7 +108,6 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic final OutputStream output, final FilterMap filterMap, final String sort, - final Predicate predicate, final ExportType exportType, final boolean includeConnectionDetails, final boolean includeExamDetails) { @@ -111,7 +116,7 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic this.exporter.get(exportType), includeConnectionDetails, includeExamDetails, - new Pager(filterMap, sort, predicate), + new Pager(filterMap, sort), output) .run(); @@ -147,17 +152,28 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic public void run() { + final SEBServerUser currentUser = SEBClientEventAdminServiceImpl.this.authorizationService + .getUserService() + .getCurrentUser(); + final EnumSet userRoles = currentUser.getUserRoles(); + final boolean isSupporterOnly = userRoles.size() == 1 && userRoles.contains(UserRole.EXAM_SUPPORTER); + // first stream header line this.exporter.streamHeader(this.output, this.includeConnectionDetails, this.includeExamDetails); // then batch with the pager and stream line per line while (this.pager.hasNext()) { this.pager.next().forEach(rec -> { - this.exporter.streamData( - this.output, - rec, - this.includeConnectionDetails ? getConnection(rec.getClientConnectionId()) : null, - this.includeExamDetails ? getExam(rec.getClientConnectionId()) : null); + + final Exam exam = getExam(rec.getClientConnectionId()); + + if (!isSupporterOnly || exam.isOwner(currentUser.uuid())) { + this.exporter.streamData( + this.output, + rec, + this.includeConnectionDetails ? getConnection(rec.getClientConnectionId()) : null, + this.includeExamDetails ? getExam(rec.getClientConnectionId()) : null); + } }); } } @@ -195,7 +211,6 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic private final FilterMap filterMap; private final String sort; - private final Predicate predicate; private int pageNumber = 0; private final int pageSize = 100; @@ -204,12 +219,10 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic public Pager( final FilterMap filterMap, - final String sort, - final Predicate predicate) { + final String sort) { this.filterMap = filterMap; this.sort = sort; - this.predicate = predicate; fetchNext(); } @@ -235,7 +248,7 @@ public class SEBClientEventAdminServiceImpl implements SEBClientEventAdminServic this.sort, ClientEventRecordDynamicSqlSupport.clientEventRecord.name(), () -> SEBClientEventAdminServiceImpl.this.sebClientEventExportTransactionHandler - .allMatching(this.filterMap, this.predicate)) + .allMatching(this.filterMap, Utils.truePredicate())) .getOrThrow(); this.pageNumber++; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventCSVExporter.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventCSVExporter.java index a3290593..151b82c0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventCSVExporter.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventCSVExporter.java @@ -8,16 +8,25 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl; +import java.io.IOException; import java.io.OutputStream; +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.session.ClientEvent.EventType; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.ExportType; +import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.SEBClientEventExporter; public class SEBClientEventCSVExporter implements SEBClientEventExporter { + private static final Logger log = LoggerFactory.getLogger(SEBClientEventCSVExporter.class); + @Override public ExportType exportType() { return ExportType.CSV; @@ -29,7 +38,24 @@ public class SEBClientEventCSVExporter implements SEBClientEventExporter { final boolean includeConnectionDetails, final boolean includeExamDetails) { - // TODO + final StringBuilder builder = new StringBuilder(); + builder.append("Event Type,Message,Value,Client Time (UTC),Server Time (UTC)"); + + if (includeConnectionDetails) { + builder.append(",User Session-ID,Client Machine,Connection Status,Connection Token"); + } + + if (includeExamDetails) { + builder.append("Exam Name,Exam Description,Exam Type,Start Time (LMS),End Time (LMS)"); + } + + builder.append(Constants.CARRIAGE_RETURN); + + try { + output.write(Utils.toByteArray(builder)); + } catch (final IOException e) { + log.error("Failed to stream header: ", e); + } } @Override @@ -39,11 +65,37 @@ public class SEBClientEventCSVExporter implements SEBClientEventExporter { final ClientConnectionRecord connectionData, final Exam examData) { - // TODO - } + final StringBuilder builder = new StringBuilder(); + final EventType type = EventType.byId(eventData.getType()); - private String toCSVString(final String text) { + builder.append(type.name()); + builder.append(Utils.toCSVString(eventData.getText())); + builder.append(eventData.getNumericValue()); + builder.append(Utils.toDateTimeUTC(eventData.getClientTime())); + builder.append(Utils.toDateTimeUTC(eventData.getServerTime())); + if (connectionData != null) { + builder.append(Utils.toCSVString(connectionData.getExamUserSessionId())); + builder.append(Utils.toCSVString(connectionData.getClientAddress())); + builder.append(connectionData.getStatus()); + builder.append(connectionData.getConnectionToken()); + } + + if (examData != null) { + builder.append(Utils.toCSVString(examData.getName())); + builder.append(Utils.toCSVString(examData.getDescription())); + builder.append(examData.getType().name()); + builder.append(examData.getStartTime()); + builder.append(examData.getEndTime()); + } + + builder.append(Constants.CARRIAGE_RETURN); + + try { + output.write(Utils.toByteArray(builder)); + } catch (final IOException e) { + log.error("Failed to stream header: ", e); + } } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientEventController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientEventController.java index 947ca057..c7865d04 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientEventController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientEventController.java @@ -8,13 +8,19 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api; +import java.io.IOException; import java.util.Collection; import java.util.EnumSet; import java.util.List; +import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.mybatis.dynamic.sql.SqlTable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.RequestMapping; @@ -31,6 +37,7 @@ import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; import ch.ethz.seb.sebserver.gbl.model.GrantEntity; import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; +import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.ExportType; import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; @@ -54,6 +61,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe @RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.SEB_CLIENT_EVENT_ENDPOINT) public class ClientEventController extends ReadonlyEntityController { + private static final Logger log = LoggerFactory.getLogger(ClientEventController.class); + private final ExamDAO examDao; private final ClientEventDAO clientEventDAO; private final SEBClientEventAdminService sebClientEventAdminService; @@ -138,6 +147,49 @@ public class ClientEventController extends ReadonlyEntityController allRequestParams, + final HttpServletRequest request, + final HttpServletResponse response) throws IOException { + + // at least current user must have base read access for specified entity type within its own institution + checkReadPrivilege(institutionId); + + final FilterMap filterMap = new FilterMap(allRequestParams, request.getQueryString()); + populateFilterMap(filterMap, institutionId, sort); + + final ServletOutputStream outputStream = response.getOutputStream(); + + try { + + this.sebClientEventAdminService.exportSEBClientLogs( + outputStream, + filterMap, + sort, + type, + false, + false); + + response.setStatus(HttpStatus.OK.value()); + } catch (final Exception e) { + log.error("Unexpected error while trying to export SEB client logs: ", e); + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + } finally { + outputStream.flush(); + outputStream.close(); + } + } + @Override public Collection getDependencies( final String modelId, @@ -154,12 +206,14 @@ public class ClientEventController extends ReadonlyEntityController checkReadAccess(final ClientEvent entity) { + final EnumSet userRoles = this.authorization + .getUserService() + .getCurrentUser() + .getUserRoles(); + final boolean isSupporterOnly = userRoles.size() == 1 && userRoles.contains(UserRole.EXAM_SUPPORTER); + return Result.tryCatch(() -> { - final EnumSet userRoles = this.authorization - .getUserService() - .getCurrentUser() - .getUserRoles(); - final boolean isSupporterOnly = userRoles.size() == 1 && userRoles.contains(UserRole.EXAM_SUPPORTER); + if (isSupporterOnly) { // check owner grant be getting exam return super.checkReadAccess(entity)