SEBSERV-148 refactoring and backend

This commit is contained in:
anhefti 2021-02-24 16:38:30 +01:00
parent 2f2a318f9d
commit 3dddaf9051
12 changed files with 195 additions and 55 deletions

View file

@ -52,7 +52,9 @@ public class APIMessage implements Serializable {
EXAM_CONSISTENCY_VALIDATION_CONFIG("1401", HttpStatus.OK, "No SEB Exam Configuration defined for the Exam"),
EXAM_CONSISTENCY_VALIDATION_SEB_RESTRICTION("1402", HttpStatus.OK,
"SEB restriction API available but Exam not restricted on LMS side yet"),
EXAM_CONSISTENCY_VALIDATION_INDICATOR("1403", HttpStatus.OK, "No Indicator defined for the Exam");
EXAM_CONSISTENCY_VALIDATION_INDICATOR("1403", HttpStatus.OK, "No Indicator defined for the Exam"),
BINDING_ERROR("1500", HttpStatus.BAD_REQUEST, "External binding error");
public final String messageCode;
public final HttpStatus httpStatus;

View file

@ -25,7 +25,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.ValidProctoringS
public class ProctoringServiceSettings implements Entity {
public enum ProctoringServerType {
JITSI_MEET
JITSI_MEET,
ZOOM
}
public static final String ATTR_ENABLE_PROCTORING = "enableProctoring";

View file

@ -35,7 +35,7 @@ public class ProctoringServlet extends HttpServlet {
private static final long serialVersionUID = 3475978419653411800L;
// @formatter:off
private static final String HTML =
private static final String JITSI_WINDOW_HTML =
"<!DOCTYPE html>" +
"<html>" +
"<head>" +
@ -103,15 +103,22 @@ public class ProctoringServlet extends HttpServlet {
(ProctoringWindowData) httpSession
.getAttribute(ProctoringGUIService.SESSION_ATTR_PROCTORING_DATA);
final String script = String.format(
HTML,
proctoringData.connectionData.serverHost,
proctoringData.connectionData.roomName,
proctoringData.connectionData.accessToken,
proctoringData.connectionData.serverHost,
proctoringData.connectionData.subject);
resp.getOutputStream().println(script);
switch (proctoringData.connectionData.proctoringServerType) {
case JITSI_MEET: {
final String script = String.format(
JITSI_WINDOW_HTML,
proctoringData.connectionData.serverHost,
proctoringData.connectionData.roomName,
proctoringData.connectionData.accessToken,
proctoringData.connectionData.serverHost,
proctoringData.connectionData.subject);
resp.getOutputStream().println(script);
break;
}
default:
throw new RuntimeException(
"Unsupported proctoring server type: " + proctoringData.connectionData.proctoringServerType);
}
}
private boolean isAuthenticated(

View file

@ -8,13 +8,13 @@
package ch.ethz.seb.sebserver.gui.form;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.commons.lang3.StringUtils;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
@ -35,8 +35,6 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError;
public class FormHandle<T extends Entity> {
private static final Logger log = LoggerFactory.getLogger(FormHandle.class);
public static final String FIELD_VALIDATION_LOCTEXT_PREFIX = "sebserver.form.validation.fieldError.";
private final PageService pageService;
@ -137,30 +135,49 @@ public class FormHandle<T extends Entity> {
public boolean handleError(final Exception error) {
if (error instanceof RestCallError) {
((RestCallError) error)
final List<APIMessage> fieldValidationErrors = ((RestCallError) error)
.getErrorMessages()
.stream()
.filter(APIMessage.ErrorMessage.FIELD_VALIDATION::isOf)
.collect(Collectors.toList());
final List<APIMessage> noneFieldValidationErrors = ((RestCallError) error)
.getErrorMessages()
.stream()
.filter(message -> !APIMessage.ErrorMessage.FIELD_VALIDATION.isOf(message))
.collect(Collectors.toList());
fieldValidationErrors
.stream()
.map(FieldValidationError::new)
.forEach(fve -> this.form.process(
name -> name.equals(fve.fieldName),
fieldAccessor -> showValidationError(fieldAccessor, fve)));
if (!noneFieldValidationErrors.isEmpty()) {
handleUnexpectedError(new RestCallError(
PageContext.GENERIC_SAVE_ERROR_TEXT_KEY,
noneFieldValidationErrors));
return false;
}
return true;
} else {
log.error("Unexpected error while trying to post form: {}", error.getMessage());
final EntityType resultType = this.post.getEntityType();
if (resultType != null) {
this.pageContext.notifySaveError(resultType, error);
} else {
this.pageContext.notifyError(
new LocTextKey(PageContext.GENERIC_SAVE_ERROR_TEXT_KEY, Constants.EMPTY_NOTE),
error);
}
handleUnexpectedError(error);
return false;
}
}
private void handleUnexpectedError(final Exception error) {
if (this.post != null && this.post.getEntityType() != null) {
this.pageContext.notifySaveError(this.post.getEntityType(), error);
} else {
this.pageContext.notifyError(
new LocTextKey(PageContext.GENERIC_SAVE_ERROR_TEXT_KEY, StringUtils.EMPTY),
error);
}
}
public boolean hasAnyError() {
return this.form.hasAnyError();
}

View file

@ -61,11 +61,11 @@ public interface ExamAdminService {
/** Save the given proctoring service settings for an existing Exam.
*
* @param examId the exam identifier
* @param examProctoring The proctoring service settings to save for the exam
* @param proctoringServiceSettings The proctoring service settings to save for the exam
* @return Result refer to saved proctoring service settings or to an error when happened. */
Result<ProctoringServiceSettings> saveProctoringServiceSettings(
Long examId,
ProctoringServiceSettings examProctoring);
ProctoringServiceSettings proctoringServiceSettings);
/** This indicates if proctoring is set and enabled for a certain exam.
*

View file

@ -206,47 +206,49 @@ public class ExamAdminServiceImpl implements ExamAdminService {
@Override
@Transactional
public Result<ProctoringServiceSettings> saveProctoringServiceSettings(final Long examId,
final ProctoringServiceSettings examProctoring) {
public Result<ProctoringServiceSettings> saveProctoringServiceSettings(
final Long examId,
final ProctoringServiceSettings proctoringServiceSettings) {
return Result.tryCatch(() -> {
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
examId,
ProctoringServiceSettings.ATTR_ENABLE_PROCTORING,
String.valueOf(examProctoring.enableProctoring));
String.valueOf(proctoringServiceSettings.enableProctoring));
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
examId,
ProctoringServiceSettings.ATTR_SERVER_TYPE,
examProctoring.serverType.name());
proctoringServiceSettings.serverType.name());
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
examId,
ProctoringServiceSettings.ATTR_SERVER_URL,
examProctoring.serverURL);
proctoringServiceSettings.serverURL);
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
examId,
ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE,
String.valueOf(examProctoring.collectingRoomSize));
String.valueOf(proctoringServiceSettings.collectingRoomSize));
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
examId,
ProctoringServiceSettings.ATTR_APP_KEY,
examProctoring.appKey);
proctoringServiceSettings.appKey);
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
examId,
ProctoringServiceSettings.ATTR_APP_SECRET,
this.cryptor.encrypt(examProctoring.appSecret).toString());
this.cryptor.encrypt(proctoringServiceSettings.appSecret).toString());
return examProctoring;
return proctoringServiceSettings;
});
}

View file

@ -26,9 +26,9 @@ public interface ExamProctoringService {
/** Use this to test the proctoring service settings against the remote proctoring server.
*
* @param examProctoring the settings to test
* @param proctoringSettings the settings to test
* @return Result refer to true if the settings are correct and the proctoring server can be accessed. */
Result<Boolean> testExamProctoring(final ProctoringServiceSettings examProctoring);
Result<Boolean> testExamProctoring(final ProctoringServiceSettings proctoringSettings);
/** Gets the room connection data for a certain room for the proctor.
*

View file

@ -24,14 +24,22 @@ import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType;
@ -50,6 +58,10 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructio
@WebServiceProfile
public class ExamJITSIProctoringService implements ExamProctoringService {
private static final String SEB_SERVER_KEY = "seb-server";
private static final String SEB_CLIENT_KEY = "seb-client";
private static final Logger log = LoggerFactory.getLogger(ExamJITSIProctoringService.class);
private static final String JITSI_ACCESS_TOKEN_HEADER =
@ -63,19 +75,22 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
private final ExamSessionService examSessionService;
private final SEBClientInstructionService sebClientInstructionService;
private final Cryptor cryptor;
private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
protected ExamJITSIProctoringService(
final RemoteProctoringRoomDAO remoteProctoringRoomDAO,
final AuthorizationService authorizationService,
final ExamSessionService examSessionService,
final SEBClientInstructionService sebClientInstructionService,
final Cryptor cryptor) {
final Cryptor cryptor,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService) {
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
this.authorizationService = authorizationService;
this.examSessionService = examSessionService;
this.sebClientInstructionService = sebClientInstructionService;
this.cryptor = cryptor;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
}
@Override
@ -84,9 +99,31 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
}
@Override
public Result<Boolean> testExamProctoring(final ProctoringServiceSettings examProctoring) {
// TODO Auto-generated method stub
return null;
public Result<Boolean> testExamProctoring(final ProctoringServiceSettings proctoringSettings) {
return Result.tryCatch(() -> {
if (proctoringSettings.serverURL != null && proctoringSettings.serverURL.contains("?")) {
throw new FieldValidationException(
"serverURL",
"proctoringSettings:serverURL:invalidURL");
}
final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService
.getClientHttpRequestFactory()
.getOrThrow();
try {
final RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
final ResponseEntity<String> result =
restTemplate.getForEntity(proctoringSettings.serverURL, String.class);
if (result.getStatusCode() != HttpStatus.OK) {
throw new APIMessageException(APIMessage.ErrorMessage.BINDING_ERROR);
}
} catch (final Exception e) {
throw new APIMessageException(APIMessage.ErrorMessage.BINDING_ERROR, e.getMessage());
}
return true;
});
}
@Override
@ -220,7 +257,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
proctoringSettings.appKey,
proctoringSettings.getAppSecret(),
this.authorizationService.getUserService().getCurrentUser().getUsername(),
"seb-server",
SEB_SERVER_KEY,
roomName,
subject,
forExam(proctoringSettings),
@ -247,7 +284,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
proctoringSettings.appKey,
proctoringSettings.getAppSecret(),
clientConnection.clientConnection.userSessionId,
"seb-client",
SEB_CLIENT_KEY,
roomName,
subject,
forExam(proctoringSettings),
@ -276,7 +313,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
proctoringSettings.appKey,
proctoringSettings.getAppSecret(),
connectionData.clientConnection.userSessionId,
"seb-client",
SEB_CLIENT_KEY,
roomName,
subject,
expTime,

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2021 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.webservice.servicelayer.session.impl;
import java.util.Collection;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringService;
@Lazy
@Service
@WebServiceProfile
public class ExamZOOMProctoringService implements ExamProctoringService {
@Override
public ProctoringServerType getType() {
return ProctoringServerType.ZOOM;
}
@Override
public Result<Boolean> testExamProctoring(final ProctoringServiceSettings examProctoring) {
// TODO Auto-generated method stub
return null;
}
@Override
public Result<ProctoringRoomConnection> getProctorRoomConnection(
final ProctoringServiceSettings proctoringSettings,
final String roomName,
final String subject) {
// TODO Auto-generated method stub
return null;
}
@Override
public Result<ProctoringRoomConnection> sendJoinRoomToClients(
final ProctoringServiceSettings proctoringSettings,
final Collection<String> clientConnectionTokens,
final String roomName, final String subject) {
// TODO Auto-generated method stub
return null;
}
@Override
public Result<Void> sendJoinCollectingRoomToClients(
final ProctoringServiceSettings proctoringSettings,
final Collection<String> clientConnectionTokens) {
// TODO Auto-generated method stub
return null;
}
}

View file

@ -411,13 +411,16 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@PathVariable(API.PARAM_MODEL_ID) final Long examId,
@Valid @RequestBody final ProctoringServiceSettings examProctoring) {
@Valid @RequestBody final ProctoringServiceSettings proctoringServiceSettings) {
checkModifyPrivilege(institutionId);
return this.entityDAO.byPK(examId)
.flatMap(this.authorization::checkModify)
.map(exam -> {
this.examAdminService.saveProctoringServiceSettings(examId, examProctoring);
this.examAdminService.getExamProctoringService(proctoringServiceSettings.serverType)
.flatMap(service -> service.testExamProctoring(proctoringServiceSettings))
.getOrThrow();
this.examAdminService.saveProctoringServiceSettings(examId, proctoringServiceSettings);
return exam;
})
.flatMap(this.userActivityLogDAO::logModify)

View file

@ -653,7 +653,10 @@ sebserver.exam.proctoring.form.secret=Secret
sebserver.exam.proctoring.form.secret.tooltip=The secret used to access the proctoring service
sebserver.exam.proctoring.type.servertype.JITSI_MEET=Jitsi Meet Server
sebserver.exam.proctoring.type.servertype.JITSI_MEET.tooltip=Use a Jitsi Meet Server for proctoring
sebserver.exam.proctoring.type.servertype.JITSI_MEET.tooltip=Use a Jitsi Meet server for proctoring
sebserver.exam.proctoring.type.servertype.ZOOM=Zoom Server
sebserver.exam.proctoring.type.servertype.ZOOM.tooltip=Use a Zoom meeting server for proctoring
################################
# Connection Configuration

View file

@ -17,8 +17,8 @@ import java.security.NoSuchAlgorithmException;
import org.junit.Test;
import org.mockito.Mockito;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
public class ExamJITSIProctoringServiceTest {
@ -28,7 +28,7 @@ public class ExamJITSIProctoringServiceTest {
final Cryptor cryptorMock = Mockito.mock(Cryptor.class);
Mockito.when(cryptorMock.decrypt(Mockito.any())).thenReturn("fbvgeghergrgrthrehreg123");
final ExamJITSIProctoringService examJITSIProctoringService =
new ExamJITSIProctoringService(null, null, null, null, cryptorMock);
new ExamJITSIProctoringService(null, null, null, null, cryptorMock, null);
String accessToken = examJITSIProctoringService.createPayload(
"test-app",
@ -62,7 +62,7 @@ public class ExamJITSIProctoringServiceTest {
final Cryptor cryptorMock = Mockito.mock(Cryptor.class);
Mockito.when(cryptorMock.decrypt(Mockito.any())).thenReturn("fbvgeghergrgrthrehreg123");
final ExamJITSIProctoringService examJITSIProctoringService =
new ExamJITSIProctoringService(null, null, null, null, cryptorMock);
new ExamJITSIProctoringService(null, null, null, null, cryptorMock, null);
final ProctoringRoomConnection data = examJITSIProctoringService.createProctoringConnection(
ProctoringServerType.JITSI_MEET,
"connectionToken",