refactor proctoring

This commit is contained in:
anhefti 2021-02-17 08:43:27 +01:00
parent ad7f06e521
commit 39f4c85d22
13 changed files with 368 additions and 398 deletions

View file

@ -52,6 +52,13 @@ public interface ClientConnectionDAO extends
return getConnectionTokens(examId);
}
/** Get a list of all connection tokens of all connections of an exam
* that are in state active
*
* @param examId The exam identifier
* @return Result refer to the collection of connection tokens or to an error when happened */
Result<Collection<String>> getActiveConnctionTokens(Long examId);
/** Get a collection of all client connections records that needs a room update
* and that are in the status ACTIVE.
* This also flags the involved connections for no update needed within the

View file

@ -146,6 +146,25 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
.collect(Collectors.toList()));
}
@Override
@Transactional(readOnly = true)
public Result<Collection<String>> getActiveConnctionTokens(final Long examId) {
return Result.tryCatch(() -> this.clientConnectionRecordMapper
.selectByExample()
.where(
ClientConnectionRecordDynamicSqlSupport.examId,
SqlBuilder.isEqualTo(examId))
.and(
ClientConnectionRecordDynamicSqlSupport.status,
SqlBuilder.isEqualTo(ConnectionStatus.ACTIVE.name()))
.build()
.execute()
.stream()
.map(ClientConnectionRecord::getConnectionToken)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList()));
}
@Override
@Transactional
public Result<Collection<ClientConnectionRecord>> getAllConnectionIdsForRoomUpdateActive() {

View file

@ -45,25 +45,25 @@ public interface ExamAdminService {
*
* @param examId the exam instance
* @return Result refer to ExamProctoring data for the exam. */
default Result<ProctoringSettings> getExamProctoring(final Exam exam) {
default Result<ProctoringSettings> getExamProctoringSettings(final Exam exam) {
if (exam == null || exam.id == null) {
return Result.ofRuntimeError("Invalid Exam model");
}
return getExamProctoring(exam.id);
return getExamProctoringSettings(exam.id);
}
/** Get ExamProctoring data for a certain exam to an error when happened.
*
* @param examId the exam identifier
* @return Result refer to ExamProctoring data for the exam. */
Result<ProctoringSettings> getExamProctoring(Long examId);
Result<ProctoringSettings> getExamProctoringSettings(Long examId);
/** Save the given ExamProctoring data for an existing Exam.
*
* @param examId the exam identifier
* @param examProctoring The ExamProctoring data to save for the exam
* @return Result refer to saved ExamProctoring data or to an error when happened. */
Result<ProctoringSettings> saveExamProctoring(Long examId, ProctoringSettings examProctoring);
Result<ProctoringSettings> saveExamProctoringSettings(Long examId, ProctoringSettings examProctoring);
/** This indicates if proctoring is set and enabled for a certain exam.
*
@ -85,7 +85,25 @@ public interface ExamAdminService {
/** Get the exam proctoring service implementation of specified type.
*
* @param type exam proctoring service server type
* @return Result refer to the ExamProctoringService or to an error when happened */
public Result<ExamProctoringService> getExamProctoringService(final ProctoringServerType type);
* @return ExamProctoringService instance */
Result<ExamProctoringService> getExamProctoringService(final ProctoringServerType type);
/** Get the exam proctoring service implementation of specified type.
*
* @param settings the ProctoringSettings that defines the ProctoringServerType
* @return ExamProctoringService instance */
default Result<ExamProctoringService> getExamProctoringService(final ProctoringSettings settings) {
return Result.tryCatch(() -> getExamProctoringService(settings.serverType).getOrThrow());
}
default Result<ExamProctoringService> getExamProctoringService(final Exam exam) {
return Result.tryCatch(() -> getExamProctoringService(exam.id).getOrThrow());
}
default Result<ExamProctoringService> getExamProctoringService(final Long examId) {
return getExamProctoringSettings(examId)
.flatMap(this::getExamProctoringService);
}
}

View file

@ -186,7 +186,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
}
@Override
public Result<ProctoringSettings> getExamProctoring(final Long examId) {
public Result<ProctoringSettings> getExamProctoringSettings(final Long examId) {
return this.additionalAttributesDAO.getAdditionalAttributes(EntityType.EXAM, examId)
.map(attrs -> attrs.stream()
.collect(Collectors.toMap(
@ -206,7 +206,8 @@ public class ExamAdminServiceImpl implements ExamAdminService {
@Override
@Transactional
public Result<ProctoringSettings> saveExamProctoring(final Long examId, final ProctoringSettings examProctoring) {
public Result<ProctoringSettings> saveExamProctoringSettings(final Long examId,
final ProctoringSettings examProctoring) {
return Result.tryCatch(() -> {
this.additionalAttributesDAO.saveAdditionalAttribute(
@ -264,7 +265,8 @@ public class ExamAdminServiceImpl implements ExamAdminService {
@Override
public Result<ExamProctoringService> getExamProctoringService(final ProctoringServerType type) {
return this.examProctoringServiceFactory.getExamProctoringService(type);
return this.examProctoringServiceFactory
.getExamProctoringService(type);
}
private Boolean getEnabled(final Map<String, AdditionalAttributeRecord> mapping) {

View file

@ -14,6 +14,8 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.util.Result;
/** Defines functionality to deal with proctoring rooms in a generic way (independent from meeting service) */
public interface ExamProctoringRoomService {
/** Get all existing default proctoring rooms of an exam.
@ -40,8 +42,10 @@ public interface ExamProctoringRoomService {
* This attaches or detaches client connections from or to proctoring rooms of an exam in one batch.
* New client connections that are coming in and are established only mark itself for
* proctoring room update if proctoring is enabled for the specified exam. This batch processing
* then makes the update synchronous to not create to to many rooms or several rooms with the same
* name of an exam. */
* then makes the update synchronously to keep track on room creation and naming
*
* If for a specified exam the town-hall room is active incoming client connection are instructed to
* join the town-hall room. If not, incoming client connection are instructed to join a collecting room. */
void updateProctoringCollectingRooms();
/** This creates a town-hall room for a specific exam. The exam must be active and running

View file

@ -8,11 +8,17 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.Collection;
import org.apache.commons.lang3.StringUtils;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
public interface ExamProctoringService {
@ -27,49 +33,69 @@ public interface ExamProctoringService {
* @return Result refer to true if the settings are correct and the proctoring server can be accessed. */
Result<Boolean> testExamProctoring(final ProctoringSettings examProctoring);
/** Used to get the proctor's room connection data.
*
* @param proctoringSettings the proctoring settings
* @param roomName the name of the room
* @param subject name of the room
* @return SEBProctoringConnectionData that contains all connection data */
Result<SEBProctoringConnection> createProctorPublicRoomConnection(
final ProctoringSettings proctoringSettings,
final String roomName,
final String subject);
Result<SEBProctoringConnection> getProctorRoomConnection(
ProctoringSettings proctoringSettings,
String roomName,
String subject);
Result<SEBProctoringConnection> getClientExamCollectingRoomConnection(
final ProctoringSettings proctoringSettings,
final ClientConnection connection);
Result<SEBProctoringConnection> sendJoinRoomToClients(
ProctoringSettings proctoringSettings,
Collection<String> clientConnectionTokens,
String roomName,
String subject);
Result<SEBProctoringConnection> getClientExamCollectingRoomConnection(
final ProctoringSettings proctoringSettings,
final String connectionToken,
final String roomName,
final String subject);
Result<Void> sendJoinCollectingRoomToClients(
ProctoringSettings proctoringSettings,
Collection<String> clientConnectionTokens);
Result<SEBProctoringConnection> getClientRoomConnection(
final ProctoringSettings examProctoring,
final String connectionToken,
final String roomName,
final String subject);
default String verifyRoomName(final String requestedRoomName, final String connectionToken) {
if (StringUtils.isNotBlank(requestedRoomName)) {
return requestedRoomName;
}
Result<SEBProctoringConnection> createProctoringConnection(
final ProctoringServerType proctoringServerType,
final String connectionToken,
final String url,
final String appKey,
final CharSequence appSecret,
final String clientName,
final String clientKey,
final String roomName,
final String subject,
final Long expTime,
final boolean moderator);
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
return urlEncoder.encodeToString(
Utils.toByteArray(connectionToken));
}
Result<String> createClientAccessToken(
final ProctoringSettings proctoringSettings,
final String connectionToken,
final String roomName);
// /** Used to get the proctor's room connection data.
// *
// * @param proctoringSettings the proctoring settings
// * @param roomName the name of the room
// * @param subject name of the room
// * @return SEBProctoringConnectionData that contains all connection data */
// Result<SEBProctoringConnection> createProctorPublicRoomConnection(
// final ProctoringSettings proctoringSettings,
// final String roomName,
// final String subject);
//
// Result<SEBProctoringConnection> getClientExamCollectingRoomConnection(
// final ProctoringSettings proctoringSettings,
// final ClientConnection connection);
//
// Result<SEBProctoringConnection> getClientExamCollectingRoomConnection(
// final ProctoringSettings proctoringSettings,
// final String connectionToken,
// final String roomName,
// final String subject);
//
// Result<SEBProctoringConnection> getClientRoomConnection(
// final ProctoringSettings examProctoring,
// final String connectionToken,
// final String roomName,
// final String subject);
//
// Result<SEBProctoringConnection> createProctoringConnection(
// final ProctoringServerType proctoringServerType,
// final String connectionToken,
// final String url,
// final String appKey,
// final CharSequence appSecret,
// final String clientName,
// final String clientKey,
// final String roomName,
// final String subject,
// final Long expTime,
// final boolean moderator);
}

View file

@ -155,9 +155,12 @@ public interface ExamSessionService {
Long examId,
Predicate<ClientConnectionData> filter);
default Result<Collection<ClientConnectionData>> getAllActiveConnectionData(final Long examId) {
return getConnectionData(examId, ACTIVE_CONNECTION_DATA_FILTER);
}
/** Gets all connection tokens of active client connection that are related to a specified exam
* from persistence storage without caching involved.
*
* @param examId the exam identifier
* @return Result refer to the collection of connection tokens or to an error when happened. */
Result<Collection<String>> getActiveConnectionTokens(final Long examId);
/** Use this to check if the current cached running exam is up to date
* and if not to flush the cache.

View file

@ -13,10 +13,16 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;
@ -26,8 +32,9 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
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;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.gbl.util.Result;
@ -36,12 +43,15 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.Authorization
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.RemoteProctoringRoomDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
@Lazy
@Service
@WebServiceProfile
public class ExamJITSIProctoringService implements ExamProctoringService {
private static final Logger log = LoggerFactory.getLogger(ExamJITSIProctoringService.class);
private static final String JITSI_ACCESS_TOKEN_HEADER =
"{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
@ -51,17 +61,20 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
private final RemoteProctoringRoomDAO remoteProctoringRoomDAO;
private final AuthorizationService authorizationService;
private final ExamSessionService examSessionService;
private final SEBClientInstructionService sebClientInstructionService;
private final Cryptor cryptor;
protected ExamJITSIProctoringService(
final RemoteProctoringRoomDAO remoteProctoringRoomDAO,
final AuthorizationService authorizationService,
final ExamSessionService examSessionService,
final SEBClientInstructionService sebClientInstructionService,
final Cryptor cryptor) {
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
this.authorizationService = authorizationService;
this.examSessionService = examSessionService;
this.sebClientInstructionService = sebClientInstructionService;
this.cryptor = cryptor;
}
@ -77,7 +90,124 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
}
@Override
public Result<SEBProctoringConnection> createProctorPublicRoomConnection(
public Result<SEBProctoringConnection> getProctorRoomConnection(
final ProctoringSettings proctoringSettings,
final String roomName,
final String subject) {
return this.createProctorPublicRoomConnection(
proctoringSettings,
roomName,
StringUtils.isNoneBlank(subject) ? subject : roomName);
}
@Override
public Result<SEBProctoringConnection> sendJoinRoomToClients(
final ProctoringSettings proctoringSettings,
final Collection<String> clientConnectionTokens,
final String roomName,
final String subject) {
return Result.tryCatch(() -> {
clientConnectionTokens
.stream()
.forEach(connectionToken -> {
final SEBProctoringConnection proctoringConnection =
getClientRoomConnection(
proctoringSettings,
connectionToken,
verifyRoomName(roomName, connectionToken),
(StringUtils.isNotBlank(subject)) ? subject : roomName)
.onError(error -> log.error(
"Failed to get client room connection data for {} cause: {}",
connectionToken,
error.getMessage()))
.get();
if (proctoringConnection != null) {
sendJoinInstruction(
proctoringSettings.examId,
connectionToken,
proctoringConnection);
}
});
return createProctorPublicRoomConnection(
proctoringSettings,
roomName,
(StringUtils.isNotBlank(subject)) ? subject : roomName)
.getOrThrow();
});
}
@Override
public Result<Void> sendJoinCollectingRoomToClients(
final ProctoringSettings proctoringSettings,
final Collection<String> clientConnectionTokens) {
return Result.tryCatch(() -> {
clientConnectionTokens
.stream()
.forEach(connectionToken -> {
final ClientConnectionData clientConnection = this.examSessionService
.getConnectionData(connectionToken)
.getOrThrow();
final String roomName = this.remoteProctoringRoomDAO
.getRoomName(clientConnection.clientConnection.getRemoteProctoringRoomId())
.getOrThrow();
final SEBProctoringConnection proctoringConnection = getClientExamCollectingRoomConnection(
proctoringSettings,
clientConnection.clientConnection.connectionToken,
roomName,
clientConnection.clientConnection.userSessionId)
.getOrThrow();
sendJoinInstruction(
proctoringSettings.examId,
clientConnection.clientConnection.connectionToken,
proctoringConnection);
});
});
}
private void sendJoinInstruction(
final Long examId,
final String connectionToken,
final SEBProctoringConnection proctoringConnection) {
final Map<String, String> attributes = new HashMap<>();
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.SERVICE_TYPE,
ProctoringSettings.ProctoringServerType.JITSI_MEET.name());
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.METHOD,
ClientInstruction.ProctoringInstructionMethod.JOIN.name());
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_URL,
proctoringConnection.serverURL);
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_ROOM,
proctoringConnection.roomName);
if (StringUtils.isNotBlank(proctoringConnection.subject)) {
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_ROOM_SUBJECT,
proctoringConnection.subject);
}
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_TOKEN,
proctoringConnection.accessToken);
this.sebClientInstructionService.registerInstruction(
examId,
InstructionType.SEB_PROCTORING,
attributes,
connectionToken,
true)
.onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error));
}
private Result<SEBProctoringConnection> createProctorPublicRoomConnection(
final ProctoringSettings proctoringSettings,
final String roomName,
final String subject) {
@ -99,35 +229,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
});
}
@Override
public Result<SEBProctoringConnection> getClientExamCollectingRoomConnection(
final ProctoringSettings proctoringSettings,
final ClientConnection connection) {
return Result.tryCatch(() -> {
final String roomName = this.remoteProctoringRoomDAO
.getRoomName(connection.getRemoteProctoringRoomId())
.getOrThrow();
return createProctoringConnection(
proctoringSettings.serverType,
null,
proctoringSettings.serverURL,
proctoringSettings.appKey,
proctoringSettings.getAppSecret(),
connection.userSessionId,
"seb-client",
roomName,
connection.userSessionId,
forExam(proctoringSettings),
false)
.getOrThrow();
});
}
@Override
public Result<SEBProctoringConnection> getClientExamCollectingRoomConnection(
private Result<SEBProctoringConnection> getClientExamCollectingRoomConnection(
final ProctoringSettings proctoringSettings,
final String connectionToken,
final String roomName,
@ -154,8 +256,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
});
}
@Override
public Result<SEBProctoringConnection> getClientRoomConnection(
private Result<SEBProctoringConnection> getClientRoomConnection(
final ProctoringSettings proctoringSettings,
final String connectionToken,
final String roomName,
@ -185,8 +286,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
}
@Override
public Result<SEBProctoringConnection> createProctoringConnection(
protected Result<SEBProctoringConnection> createProctoringConnection(
final ProctoringServerType proctoringServerType,
final String connectionToken,
final String url,
@ -227,35 +327,6 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
});
}
@Override
public Result<String> createClientAccessToken(
final ProctoringSettings proctoringSettings,
final String connectionToken,
final String roomName) {
return Result.tryCatch(() -> {
final ClientConnectionData connectionData = this.examSessionService
.getConnectionData(connectionToken)
.getOrThrow();
final String host = UriComponentsBuilder.fromHttpUrl(proctoringSettings.serverURL)
.build()
.getHost();
final CharSequence decryptedSecret = this.cryptor.decrypt(proctoringSettings.appSecret);
return internalCreateAccessToken(
proctoringSettings.appKey,
decryptedSecret,
connectionData.clientConnection.userSessionId,
"seb-client",
roomName,
forExam(proctoringSettings),
host,
false);
});
}
protected String internalCreateAccessToken(
final String appKey,
final CharSequence appSecret,

View file

@ -8,9 +8,8 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.slf4j.Logger;
@ -19,12 +18,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.ProctoringInstructionMethod;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
@ -34,7 +28,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.RemoteProctoringRoomDAO
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringRoomService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
@Lazy
@Service
@ -45,20 +38,17 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
private final RemoteProctoringRoomDAO remoteProctoringRoomDAO;
private final ClientConnectionDAO clientConnectionDAO;
private final SEBClientInstructionService sebInstructionService;
private final ExamAdminService examAdminService;
private final ExamSessionService examSessionService;
public ExamProctoringRoomServiceImpl(
final RemoteProctoringRoomDAO remoteProctoringRoomDAO,
final ClientConnectionDAO clientConnectionDAO,
final SEBClientInstructionService sebInstructionService,
final ExamAdminService examAdminService,
final ExamSessionService examSessionService) {
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
this.clientConnectionDAO = clientConnectionDAO;
this.sebInstructionService = sebInstructionService;
this.examAdminService = examAdminService;
this.examSessionService = examSessionService;
}
@ -84,7 +74,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
this.clientConnectionDAO.getAllConnectionIdsForRoomUpdateActive()
.getOrThrow()
.stream()
.forEach(this::assignToRoom);
.forEach(this::assignToCollectingRoom);
this.clientConnectionDAO.getAllConnectionIdsForRoomUpdateInactive()
.getOrThrow()
@ -114,40 +104,40 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
this.remoteProctoringRoomDAO.deleteTownhallRoom(examId);
}
private void assignToRoom(final ClientConnectionRecord cc) {
private void assignToCollectingRoom(final ClientConnectionRecord cc) {
try {
final RemoteProctoringRoom proctoringRoom = getProctoringRoom(
cc.getExamId(),
cc.getConnectionToken());
if (proctoringRoom != null) {
this.clientConnectionDAO.assignToProctoringRoom(
cc.getId(),
cc.getConnectionToken(),
proctoringRoom.id)
.onError(error -> log.error("Failed to assign to proctoring room: ", error))
.getOrThrow();
this.clientConnectionDAO.assignToProctoringRoom(
cc.getId(),
cc.getConnectionToken(),
proctoringRoom.id)
.getOrThrow();
final Result<RemoteProctoringRoom> townhallRoomResult = this.remoteProctoringRoomDAO
.getTownhallRoom(cc.getExamId());
if (townhallRoomResult.hasValue()) {
final RemoteProctoringRoom townhallRoom = townhallRoomResult.get();
applyProcotringInstruction(
cc.getExamId(),
cc.getConnectionToken(),
townhallRoom.name,
townhallRoom.subject);
} else {
applyProcotringInstruction(
cc.getExamId(),
cc.getConnectionToken(),
proctoringRoom.name,
proctoringRoom.subject);
}
final Result<RemoteProctoringRoom> townhallRoomResult = this.remoteProctoringRoomDAO
.getTownhallRoom(cc.getExamId());
if (townhallRoomResult.hasValue()) {
final RemoteProctoringRoom townhallRoom = townhallRoomResult.get();
applyProcotringInstruction(
cc.getExamId(),
cc.getConnectionToken(),
townhallRoom.name,
townhallRoom.subject)
.getOrThrow();
} else {
applyProcotringInstruction(
cc.getExamId(),
cc.getConnectionToken(),
proctoringRoom.name,
proctoringRoom.subject)
.getOrThrow();
}
} catch (final Exception e) {
log.error("Failed to update client connection for proctoring room: ", e);
this.clientConnectionDAO.setNeedsRoomUpdate(cc.getId());
log.error("Failed to assign connection to collecting room: {}", cc, e);
}
}
@ -167,7 +157,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
private RemoteProctoringRoom getProctoringRoom(final Long examId, final String connectionToken) {
try {
final ProctoringSettings proctoringSettings = this.examAdminService
.getExamProctoring(examId)
.getExamProctoringSettings(examId)
.getOrThrow();
return this.remoteProctoringRoomDAO.reservePlaceInCollectingRoom(
examId,
@ -184,63 +174,21 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
}
}
private void applyProcotringInstruction(
private Result<Void> applyProcotringInstruction(
final Long examId,
final String connectionToken,
final String roomName,
final String subject) {
try {
// apply a SEB_PROCOTIRNG instruction for the specified SEB client connection
final ProctoringSettings proctoringSettings = this.examAdminService
.getExamProctoring(examId)
.getOrThrow();
return this.examAdminService
.getExamProctoringSettings(examId)
.flatMap(proctoringSettings -> this.examAdminService
.getExamProctoringService(proctoringSettings.serverType)
.getOrThrow()
.sendJoinCollectingRoomToClients(
proctoringSettings,
Arrays.asList(connectionToken)));
final SEBProctoringConnection proctoringData =
this.examAdminService.getExamProctoringService(proctoringSettings.serverType)
.flatMap(s -> s.getClientExamCollectingRoomConnection(
proctoringSettings,
connectionToken,
roomName,
subject))
.getOrThrow();
final Map<String, String> attributes = new HashMap<>();
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.SERVICE_TYPE,
ProctoringServerType.JITSI_MEET.name());
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.METHOD,
ProctoringInstructionMethod.JOIN.name());
if (proctoringSettings.serverType == ProctoringServerType.JITSI_MEET) {
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_ROOM,
proctoringData.roomName);
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_ROOM_SUBJECT,
proctoringData.subject);
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_URL,
proctoringData.serverURL);
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_TOKEN,
proctoringData.accessToken);
}
this.sebInstructionService.registerInstruction(
examId,
InstructionType.SEB_PROCTORING,
attributes,
connectionToken,
true);
} catch (final Exception e) {
log.error(
"Failed to process proctoring initialization for established SEB client connection: {}",
connectionToken, e);
}
}
}

View file

@ -379,6 +379,12 @@ public class ExamSessionServiceImpl implements ExamSessionService {
}
}
@Override
public Result<Collection<String>> getActiveConnectionTokens(final Long examId) {
return this.clientConnectionDAO
.getActiveConnctionTokens(examId);
}
@Override
public Result<Exam> updateExamCache(final Long examId) {
final Exam exam = this.examSessionCacheService.getRunningExam(examId);

View file

@ -396,7 +396,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
checkReadPrivilege(institutionId);
return this.entityDAO.byPK(modelId)
.flatMap(this.authorization::checkRead)
.flatMap(this.examAdminService::getExamProctoring)
.flatMap(this.examAdminService::getExamProctoringSettings)
.getOrThrow();
}
@ -417,7 +417,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
return this.entityDAO.byPK(examId)
.flatMap(this.authorization::checkModify)
.map(exam -> {
this.examAdminService.saveExamProctoring(examId, examProctoring);
this.examAdminService.saveExamProctoringSettings(examId, examProctoring);
return exam;
})
.flatMap(this.userActivityLogDAO::logModify)

View file

@ -9,8 +9,6 @@
package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.util.Arrays;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@ -39,8 +37,6 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
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.servicelayer.authorization.AuthorizationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
@ -112,7 +108,7 @@ public class ExamProctoringController {
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public SEBProctoringConnection getProctorRoomData(
public SEBProctoringConnection getProctorRoomConnection(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
@ -125,13 +121,11 @@ public class ExamProctoringController {
return this.examSessionService.getRunningExam(examId)
.flatMap(this.authorization::checkRead)
.flatMap(this.examAdminService::getExamProctoring)
.flatMap(proc -> this.examAdminService
.getExamProctoringService(proc.serverType)
.flatMap(s -> s.createProctorPublicRoomConnection(
proc,
roomName,
StringUtils.isNoneBlank(subject) ? subject : roomName)))
.flatMap(this.examAdminService::getExamProctoringService)
.flatMap(service -> service.getProctorRoomConnection(
this.examAdminService.getExamProctoringSettings(examId).getOrThrow(),
roomName,
StringUtils.isNoneBlank(subject) ? subject : roomName))
.getOrThrow();
}
@ -223,10 +217,17 @@ public class ExamProctoringController {
final ProctoringSettings proctoringSettings = this.examSessionService
.getRunningExam(examId)
.flatMap(this.examAdminService::getExamProctoring)
.flatMap(this.examAdminService::getExamProctoringSettings)
.getOrThrow();
sendJoinInstructions(connectionTokens, proctoringSettings);
this.examAdminService
.getExamProctoringService(proctoringSettings.serverType)
.flatMap(service -> service.sendJoinCollectingRoomToClients(
proctoringSettings,
Arrays.asList(StringUtils.split(
connectionTokens,
Constants.LIST_SEPARATOR_CHAR))))
.onError(error -> log.error("Failed to send rejoin collecting room to: {}", connectionTokens, error));
}
@RequestMapping(
@ -254,55 +255,19 @@ public class ExamProctoringController {
final ProctoringSettings settings = this.examSessionService
.getRunningExam(examId)
.flatMap(this.examAdminService::getExamProctoring)
.flatMap(this.examAdminService::getExamProctoringSettings)
.getOrThrow();
final ExamProctoringService examProctoringService = this.examAdminService
return this.examAdminService
.getExamProctoringService(settings.serverType)
.flatMap(service -> service.sendJoinRoomToClients(
settings,
Arrays.asList(StringUtils.split(
connectionTokens,
Constants.LIST_SEPARATOR_CHAR)),
roomName,
subject))
.getOrThrow();
if (StringUtils.isNotBlank(connectionTokens)) {
Arrays.asList(connectionTokens.split(Constants.LIST_SEPARATOR))
.stream()
.forEach(connectionToken -> {
final SEBProctoringConnection proctoringConnection =
examProctoringService
.getClientRoomConnection(
settings,
connectionToken,
verifyRoomName(roomName, connectionToken),
(StringUtils.isNotBlank(subject)) ? subject : roomName)
.onError(error -> log.error(
"Failed to get client room connection data for {} cause: {}",
connectionToken,
error.getMessage()))
.get();
if (proctoringConnection != null) {
sendJoinInstruction(settings.examId, connectionToken, proctoringConnection)
.onError(error -> log.error(
"Failed to send proctoring leave instruction to client: {} cause: {}",
connectionToken,
error.getMessage()));
}
});
}
return examProctoringService.createProctorPublicRoomConnection(
settings,
roomName,
(StringUtils.isNotBlank(subject)) ? subject : roomName)
.getOrThrow();
}
private String verifyRoomName(final String requestedRoomName, final String connectionToken) {
if (StringUtils.isNotBlank(requestedRoomName)) {
return requestedRoomName;
}
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
return urlEncoder.encodeToString(
Utils.toByteArray(connectionToken));
}
@RequestMapping(
@ -342,48 +307,27 @@ public class ExamProctoringController {
final ProctoringSettings settings = this.examSessionService
.getRunningExam(examId)
.flatMap(this.examAdminService::getExamProctoring)
.flatMap(this.examAdminService::getExamProctoringSettings)
.getOrThrow();
final ExamProctoringService examProctoringService = this.examAdminService
// First create and get the town-hall room for specified exam
final RemoteProctoringRoom townhallRoom = this.examProcotringRoomService
.createTownhallRoom(examId, subject)
.onError(error -> {
log.error("Failed to create town-hall room: ", error);
this.examProcotringRoomService.disposeTownhallRoom(examId);
})
.getOrThrow();
// Then send a join instruction to all active clients of the exam to join the town-hall
return this.examAdminService
.getExamProctoringService(settings.serverType)
.getOrThrow();
// first create and register a room to collect all connection of the exam
// As long as the room exists new connections will join this room immediately
// after have been applied to the default collecting room
final RemoteProctoringRoom townhallRoom = this.examProcotringRoomService.createTownhallRoom(examId, subject)
.onError(error -> this.examProcotringRoomService.disposeTownhallRoom(examId))
.getOrThrow();
// get all active connections for the exam and send the join instruction
this.examSessionService.getAllActiveConnectionData(examId)
.getOrThrow()
.stream()
.forEach(cc -> {
final SEBProctoringConnection data = examProctoringService
.getClientRoomConnection(
settings,
cc.clientConnection.connectionToken,
townhallRoom.name,
townhallRoom.subject)
.onError(error -> log.error(
"Failed to get client room connection data for {} cause: {}",
cc.clientConnection.connectionToken,
error.getMessage()))
.get();
if (data != null) {
sendJoinInstruction(examId, cc.clientConnection.connectionToken, data)
.onError(error -> log.error(
"Failed to send proctoring leave instruction to client: {} ",
cc.clientConnection.connectionToken, error));
}
});
return examProctoringService.createProctorPublicRoomConnection(
settings,
townhallRoom.name,
townhallRoom.subject)
.flatMap(service -> service.sendJoinRoomToClients(
settings,
this.examSessionService.getActiveConnectionTokens(examId)
.getOrThrow(),
townhallRoom.name,
townhallRoom.subject))
.getOrThrow();
}
@ -403,7 +347,7 @@ public class ExamProctoringController {
final ProctoringSettings settings = this.examSessionService
.getRunningExam(examId)
.flatMap(this.examAdminService::getExamProctoring)
.flatMap(this.examAdminService::getExamProctoringSettings)
.getOrThrow();
final ExamProctoringService examProctoringService = this.examAdminService
@ -413,25 +357,11 @@ public class ExamProctoringController {
// first unregister the current room to collect all connection of the exam
this.examProcotringRoomService.disposeTownhallRoom(examId);
// get all active connections for the exam and send the join instruction
this.examSessionService.getConnectionData(
examId,
ExamSessionService.ACTIVE_CONNECTION_DATA_FILTER)
.getOrThrow()
.stream()
.forEach(cc -> {
examProctoringService
.getClientExamCollectingRoomConnection(
settings,
cc.clientConnection)
.flatMap(data -> this.sendJoinInstruction(
examId,
cc.clientConnection.connectionToken,
data))
.onError(error -> log.error("Failed to send rejoin for: {} cause: {}",
cc.clientConnection.connectionToken,
error.getMessage()));
});
// then get all active connections for the exam and send the rejoin to collecting room instruction
examProctoringService.sendJoinCollectingRoomToClients(
settings,
this.examSessionService.getActiveConnectionTokens(examId)
.getOrThrow());
}
private void sendBroadcastInstructions(
@ -486,19 +416,19 @@ public class ExamProctoringController {
private void sendBroadcastInstructionToClientsInExam(final Long examId, final Map<String, String> attributes) {
this.examSessionService
.getAllActiveConnectionData(examId)
.getActiveConnectionTokens(examId)
.getOrThrow()
.stream()
.forEach(connection -> {
.forEach(connectionToken -> {
this.sebInstructionService.registerInstruction(
examId,
InstructionType.SEB_RECONFIGURE_SETTINGS,
attributes,
connection.clientConnection.connectionToken,
connectionToken,
true)
.onError(error -> log.error(
"Failed to register reconfiguring instruction for connection: {}",
connection.clientConnection.connectionToken,
connectionToken,
error));
});
}
@ -527,70 +457,6 @@ public class ExamProctoringController {
});
}
private void sendJoinInstructions(
final String connectionTokens,
final ProctoringSettings proctoringSettings) {
final ExamProctoringService examProctoringService = this.examAdminService
.getExamProctoringService(proctoringSettings.serverType)
.getOrThrow();
Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
.stream()
.forEach(connectionToken -> {
sendJoinInstructionToClient(proctoringSettings, examProctoringService, connectionToken);
});
}
private void sendJoinInstructionToClient(
final ProctoringSettings proctoringSettings,
final ExamProctoringService examProctoringService,
final String connectionToken) {
this.examSessionService
.getConnectionData(connectionToken)
.flatMap(connection -> examProctoringService.getClientExamCollectingRoomConnection(
proctoringSettings,
connection.clientConnection))
.flatMap(data -> this.sendJoinInstruction(
proctoringSettings.examId,
connectionToken, data))
.onError(error -> log.error("Failed to send rejoin for: {} cause: {}",
connectionToken,
error.getMessage()));
}
private Result<Void> sendJoinInstruction(
final Long examId,
final String connectionToken,
final SEBProctoringConnection data) {
final Map<String, String> attributes = new HashMap<>();
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.SERVICE_TYPE,
ProctoringSettings.ProctoringServerType.JITSI_MEET.name());
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.METHOD,
ClientInstruction.ProctoringInstructionMethod.JOIN.name());
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_URL,
data.serverURL);
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_ROOM,
data.roomName);
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_TOKEN,
data.accessToken);
return this.sebInstructionService.registerInstruction(
examId,
InstructionType.SEB_PROCTORING,
attributes,
connectionToken,
true);
}
private void checkAccess(final Long institutionId, final Long examId) {
this.authorization.check(
PrivilegeType.READ,

View file

@ -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, cryptorMock);
new ExamJITSIProctoringService(null, null, null, null, cryptorMock);
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, cryptorMock);
new ExamJITSIProctoringService(null, null, null, null, cryptorMock);
final SEBProctoringConnection data = examJITSIProctoringService.createProctoringConnection(
ProctoringServerType.JITSI_MEET,
"connectionToken",