SEBSERV-440 implemented
This commit is contained in:
parent
0d5d7b3894
commit
50456b8d9b
23 changed files with 749 additions and 466 deletions
|
@ -74,7 +74,10 @@ public class APIMessage implements Serializable {
|
|||
EXAM_IMPORT_ERROR_AUTO_CONFIG("1610", HttpStatus.PARTIAL_CONTENT,
|
||||
"Failed to automatically create and link exam configuration from the exam template to the exam"),
|
||||
EXAM_IMPORT_ERROR_AUTO_CONFIG_LINKING("1611", HttpStatus.PARTIAL_CONTENT,
|
||||
"Failed to automatically link auto-generated exam configuration to the exam");
|
||||
"Failed to automatically link auto-generated exam configuration to the exam"),
|
||||
|
||||
CLIENT_CONNECTION_INTEGRITY_VIOLATION("1700", HttpStatus.NOT_ACCEPTABLE,
|
||||
"SEB client connection is not in valid state to apply requested operation");
|
||||
|
||||
public final String messageCode;
|
||||
public final HttpStatus httpStatus;
|
||||
|
|
|
@ -440,7 +440,7 @@ public final class ClientConnection implements GrantEntity {
|
|||
builder.append(this.clientAddress);
|
||||
builder.append(", remoteProctoringRoomId=");
|
||||
builder.append(this.remoteProctoringRoomId);
|
||||
builder.append(", virtualClientId=");
|
||||
builder.append(", sebClientUserId=");
|
||||
builder.append(this.sebClientUserId);
|
||||
builder.append(", creationTime=");
|
||||
builder.append(this.creationTime);
|
||||
|
|
|
@ -364,7 +364,7 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
|
|||
data.examId,
|
||||
ConnectionStatus.CONNECTION_REQUESTED.name(),
|
||||
data.connectionToken,
|
||||
null,
|
||||
data.userSessionId,
|
||||
data.clientAddress,
|
||||
data.sebClientUserId,
|
||||
BooleanUtils.toInteger(data.vdi, 1, 0, 0),
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.security.Principal;
|
||||
import java.util.Collection;
|
||||
|
||||
|
@ -158,4 +159,18 @@ public interface SEBClientConnectionService {
|
|||
* happened */
|
||||
Result<Collection<EntityKey>> disableConnections(final String[] connectionTokens, final Long institutionId);
|
||||
|
||||
/** Streams the requested exam configuration to given HttpServletResponse output stream
|
||||
*
|
||||
* @param institutionId the institution identifier
|
||||
* @param examId the exam identifier
|
||||
* @param connectionToken the connection identifier token
|
||||
* @param ipAddress the IP Address of the SEB client request
|
||||
* @param response HttpServletResponse instance to stream the exam configuration to */
|
||||
void streamExamConfig(
|
||||
Long institutionId,
|
||||
Long examId,
|
||||
String connectionToken,
|
||||
String ipAddress,
|
||||
HttpServletResponse response);
|
||||
|
||||
}
|
||||
|
|
|
@ -28,12 +28,12 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
|
|||
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
|
||||
|
||||
/** Service for SEB instruction handling.
|
||||
*
|
||||
* <p>
|
||||
* SEB instructions are sent as response of a SEB Ping on a active SEB Connection
|
||||
* If there is an instruction in the queue for a specified SEB Client. */
|
||||
public interface SEBClientInstructionService {
|
||||
|
||||
static final Logger log = LoggerFactory.getLogger(SEBClientInstructionService.class);
|
||||
Logger log = LoggerFactory.getLogger(SEBClientInstructionService.class);
|
||||
|
||||
/** Get the underling WebserviceInfo
|
||||
*
|
||||
|
@ -46,10 +46,10 @@ public interface SEBClientInstructionService {
|
|||
void init();
|
||||
|
||||
/** Used to register a SEB client instruction for one or more active client connections
|
||||
* within an other background thread. This is none-blocking.
|
||||
* within another background thread. This is none-blocking.
|
||||
*
|
||||
* @param clientInstruction the ClientInstruction instance to register
|
||||
* @return A Result refer to a void marker or to an error if happened */
|
||||
**/
|
||||
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
|
||||
default void registerInstructionAsync(final ClientInstruction clientInstruction) {
|
||||
registerInstruction(clientInstruction, false)
|
||||
|
@ -69,7 +69,7 @@ public interface SEBClientInstructionService {
|
|||
/** Used to register a SEB client instruction for one or more active client connections
|
||||
*
|
||||
* @param clientInstruction the ClientInstruction instance to register
|
||||
* @param needsConfirm indicates whether the SEB instruction needs a confirmation or not
|
||||
* @param needsConfirmation indicates whether the SEB instruction needs a confirmation or not
|
||||
* @return A Result refer to a void marker or to an error if happened */
|
||||
default Result<Void> registerInstruction(
|
||||
final ClientInstruction clientInstruction,
|
||||
|
@ -91,6 +91,7 @@ public interface SEBClientInstructionService {
|
|||
* @param type The InstructionType
|
||||
* @param attributes The instruction's attributes
|
||||
* @param connectionToken a connectionToken to register the instruction for.
|
||||
* @param checkActive indicates if the involved client connection shall be checked for active status or not
|
||||
* @param needsConfirm indicates whether the SEB instruction needs a confirmation or not
|
||||
* @return A Result refer to a void marker or to an error if happened */
|
||||
Result<Void> registerInstruction(
|
||||
|
@ -98,6 +99,7 @@ public interface SEBClientInstructionService {
|
|||
InstructionType type,
|
||||
Map<String, String> attributes,
|
||||
String connectionToken,
|
||||
boolean checkActive,
|
||||
boolean needsConfirm);
|
||||
|
||||
/** Used to register a SEB client instruction for one or more active client connections
|
||||
|
@ -117,9 +119,9 @@ public interface SEBClientInstructionService {
|
|||
|
||||
/** Get a SEB instruction for the specified SEB Client connection or null of there
|
||||
* is currently no SEB instruction in the queue.
|
||||
*
|
||||
* <p>
|
||||
* NOTE: If this call returns a SEB instruction instance, this instance is considered
|
||||
* as processed for the specified SEB Client afterwards and will be removed from the queue
|
||||
* as processed for the specified SEB Client afterward and will be removed from the queue
|
||||
*
|
||||
* @param connectionToken the SEB Client connection token
|
||||
* @return SEB instruction to sent to the SEB Client or null */
|
||||
|
@ -131,7 +133,7 @@ public interface SEBClientInstructionService {
|
|||
* @param instructionConfirm the instruction confirm identifier */
|
||||
void confirmInstructionDone(String connectionToken, String instructionConfirm);
|
||||
|
||||
/** Used to cleanup out-dated instructions on the persistent storage */
|
||||
/** Used to clean up out-dated instructions on the persistent storage */
|
||||
void cleanupInstructions();
|
||||
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ public interface SEBClientSessionService extends ExamUpdateTask, SessionUpdateTa
|
|||
/** Used to update the app signature key grants of all active SEB connections that miss a grant */
|
||||
void updateASKGrants();
|
||||
|
||||
/** Used to cleanup old instructions from the persistent storage */
|
||||
/** Used to clean up old instructions from the persistent storage */
|
||||
void cleanupInstructions();
|
||||
|
||||
/** Notify a ping for a certain client connection.
|
||||
|
@ -66,7 +66,7 @@ public interface SEBClientSessionService extends ExamUpdateTask, SessionUpdateTa
|
|||
* @param instructionConfirm the instruction confirm identifier */
|
||||
void confirmInstructionDone(String connectionToken, String instructionConfirm);
|
||||
|
||||
/** Use this to get the get the specific indicator values for a given client connection.
|
||||
/** Use this to get the specific indicator values for a given client connection.
|
||||
*
|
||||
* @param clientConnection The client connection values
|
||||
* @return Result refer to ClientConnectionData instance containing the given clientConnection plus the indicator
|
||||
|
|
|
@ -227,27 +227,30 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
updateExamCache(examId);
|
||||
}
|
||||
|
||||
final Exam exam = this.examSessionCacheService.getRunningExam(examId);
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
if (this.examSessionCacheService.isRunning(exam)) {
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Exam {} is running and cached", examId);
|
||||
final Exam exam = this.examSessionCacheService.getRunningExam(examId);
|
||||
|
||||
if (this.examSessionCacheService.isRunning(exam)) {
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Exam {} is running and cached", examId);
|
||||
}
|
||||
|
||||
return exam;
|
||||
} else {
|
||||
if (exam != null) {
|
||||
log.info("Exam {} is not running anymore. Flush caches", exam);
|
||||
flushCache(exam);
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.info("Exam {} is not currently running", examId);
|
||||
}
|
||||
|
||||
throw new NoSuchElementException(
|
||||
"No currently running exam found for id: " + examId);
|
||||
}
|
||||
|
||||
return Result.of(exam);
|
||||
} else {
|
||||
if (exam != null) {
|
||||
log.info("Exam {} is not running anymore. Flush caches", exam);
|
||||
flushCache(exam);
|
||||
}
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.info("Exam {} is not currently running", examId);
|
||||
}
|
||||
|
||||
return Result.ofError(new NoSuchElementException(
|
||||
"No currently running exam found for id: " + examId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -8,22 +8,28 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.security.Principal;
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
|
||||
import ch.ethz.seb.sebserver.gbl.model.Entity;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.tomcat.jni.Address;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
@ -38,7 +44,6 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
|
|||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
|
||||
|
@ -72,6 +77,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
private final DistributedIndicatorValueService distributedPingCache;
|
||||
private final SecurityKeyService securityKeyService;
|
||||
private final SEBClientEventBatchService sebClientEventBatchService;
|
||||
private final SEBClientInstructionService sebClientInstructionService;
|
||||
private final JSONMapper jsonMapper;
|
||||
private final boolean isDistributedSetup;
|
||||
|
||||
protected SEBClientConnectionServiceImpl(
|
||||
|
@ -82,7 +89,9 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
final ClientIndicatorFactory clientIndicatorFactory,
|
||||
final SecurityKeyService securityKeyService,
|
||||
final WebserviceInfo webserviceInfo,
|
||||
final SEBClientEventBatchService sebClientEventBatchService) {
|
||||
final SEBClientEventBatchService sebClientEventBatchService,
|
||||
final SEBClientInstructionService sebClientInstructionService,
|
||||
final JSONMapper jsonMapper) {
|
||||
|
||||
this.examSessionService = examSessionService;
|
||||
this.examSessionCacheService = examSessionService.getExamSessionCacheService();
|
||||
|
@ -94,6 +103,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
this.securityKeyService = securityKeyService;
|
||||
this.isDistributedSetup = webserviceInfo.isDistributed();
|
||||
this.sebClientEventBatchService = sebClientEventBatchService;
|
||||
this.sebClientInstructionService = sebClientInstructionService;
|
||||
this.jsonMapper = jsonMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -143,29 +154,38 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
clientId);
|
||||
}
|
||||
|
||||
final String connectionToken = createToken();
|
||||
|
||||
if (examId != null) {
|
||||
checkExamIntegrity(
|
||||
examId,
|
||||
null,
|
||||
institutionId,
|
||||
(principal != null) ? principal.getName() : "--",
|
||||
connectionToken,
|
||||
clientAddress);
|
||||
}
|
||||
|
||||
final String updateUserSessionId = updateUserSessionId(
|
||||
examId,
|
||||
null,
|
||||
clientId,
|
||||
sebMachineName,
|
||||
null);
|
||||
|
||||
// Create ClientConnection in status CONNECTION_REQUESTED for further processing
|
||||
final String connectionToken = createToken();
|
||||
final ClientConnection clientConnection = this.clientConnectionDAO.createNew(new ClientConnection(
|
||||
null,
|
||||
institutionId, // Set the institution identifier that was checked against integrity before
|
||||
examId, // Set the exam identifier if available otherwise it is null
|
||||
ConnectionStatus.CONNECTION_REQUESTED, // Initial state
|
||||
connectionToken, // The generated connection token that identifies this connection
|
||||
null,
|
||||
(clientAddress != null) ? clientAddress : Constants.EMPTY_NOTE, // The IP address of the connecting client, verified on SEB Server side
|
||||
institutionId,
|
||||
examId,
|
||||
ConnectionStatus.CONNECTION_REQUESTED,
|
||||
connectionToken,
|
||||
updateUserSessionId,
|
||||
(clientAddress != null) ? clientAddress : Constants.EMPTY_NOTE,
|
||||
sebOsName,
|
||||
sebMachineName,
|
||||
sebVersion,
|
||||
clientId, // The client identifier sent by the SEB client if available
|
||||
clientConfig.vdiType != VDIType.NO, // The VDI flag to indicate if this is a VDI prime connection
|
||||
clientId,
|
||||
clientConfig.vdiType != VDIType.NO,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
|
@ -218,83 +238,58 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
ClientConnection clientConnection = getClientConnection(connectionToken);
|
||||
final ClientConnection clientConnection = getClientConnection(connectionToken);
|
||||
final Long _examId = clientConnection.examId != null ? clientConnection.examId : examId;
|
||||
|
||||
checkInstitutionalIntegrity(institutionId, clientConnection);
|
||||
checkExamIntegrity(examId, clientConnection);
|
||||
|
||||
// connection integrity check
|
||||
if (!clientConnection.status.clientActiveStatus) {
|
||||
log.error(
|
||||
"ClientConnection integrity violation: client connection is not in expected state: {}",
|
||||
clientConnection);
|
||||
throw new IllegalArgumentException(
|
||||
"ClientConnection integrity violation: client connection is not in expected state");
|
||||
}
|
||||
if (StringUtils.isNoneBlank(clientAddress) &&
|
||||
StringUtils.isNotBlank(clientConnection.clientAddress) &&
|
||||
!clientAddress.equals(clientConnection.clientAddress)) {
|
||||
// log SEB client IP address change
|
||||
log.error(
|
||||
"ClientConnection integrity violation: client address mismatch: {}, {}",
|
||||
clientAddress,
|
||||
clientConnection.clientAddress);
|
||||
sebLogClientAddressMismatch(clientAddress, clientConnection);
|
||||
}
|
||||
|
||||
if (examId != null) {
|
||||
checkExamIntegrity(
|
||||
examId,
|
||||
institutionId,
|
||||
StringUtils.isNoneBlank(userSessionId) ? userSessionId : clientConnection.userSessionId,
|
||||
clientConnection.clientAddress);
|
||||
}
|
||||
connectionStatusIntegrityCheck(clientConnection, clientAddress);
|
||||
checkExamIntegrity(examId, clientConnection.examId, institutionId, clientConnection.connectionToken, clientConnection.info);
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(
|
||||
"SEB client connection, update ClientConnection for "
|
||||
+ "connectionToken {} "
|
||||
+ "institutionId: {}"
|
||||
+ "exam: {} "
|
||||
+ "client address: {} "
|
||||
+ "userSessionId: {}"
|
||||
+ "clientId: {}",
|
||||
connectionToken,
|
||||
institutionId,
|
||||
examId,
|
||||
clientAddress,
|
||||
userSessionId,
|
||||
clientId);
|
||||
"SEB client connection, update ClientConnection for connectionToken {} institutionId: {} exam: {} client address: {} userSessionId: {} clientId: {}",
|
||||
connectionToken, institutionId, examId, clientAddress, userSessionId, clientId);
|
||||
}
|
||||
|
||||
// userSessionId integrity check
|
||||
clientConnection = updateUserSessionId(userSessionId, clientConnection, examId);
|
||||
final String updateUserSessionId = updateUserSessionId(
|
||||
_examId,
|
||||
userSessionId,
|
||||
clientId,
|
||||
sebMachineName,
|
||||
clientConnection);
|
||||
|
||||
final ConnectionStatus currentStatus = clientConnection.getStatus();
|
||||
final String signatureHash = StringUtils.isNotBlank(appSignatureKey)
|
||||
? getSignatureHash(appSignatureKey, connectionToken, _examId)
|
||||
: null;
|
||||
final ClientConnection updateConnection = new ClientConnection(
|
||||
clientConnection.id,
|
||||
null,
|
||||
examId,
|
||||
(userSessionId != null && currentStatus == ConnectionStatus.CONNECTION_REQUESTED)
|
||||
? ConnectionStatus.AUTHENTICATED
|
||||
: null,
|
||||
null,
|
||||
updateUserSessionId,
|
||||
StringUtils.isNotBlank(clientAddress) ? clientAddress : null,
|
||||
StringUtils.isNotBlank(sebOsName) && clientConnection.sebOSName == null ? sebOsName : null,
|
||||
StringUtils.isNotBlank(sebMachineName) && clientConnection.sebMachineName == null ? sebMachineName : null,
|
||||
StringUtils.isNotBlank(sebVersion) && clientConnection.sebVersion == null ? sebVersion : null,
|
||||
StringUtils.isNotBlank(clientId) && clientConnection.sebClientUserId == null ? clientId : null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
signatureHash,
|
||||
null);
|
||||
|
||||
final ClientConnection updatedClientConnection = this.clientConnectionDAO
|
||||
.save(new ClientConnection(
|
||||
clientConnection.id,
|
||||
null,
|
||||
examId,
|
||||
(userSessionId != null) ? ConnectionStatus.AUTHENTICATED : null,
|
||||
null,
|
||||
clientConnection.userSessionId,
|
||||
StringUtils.isNoneBlank(clientAddress) ? clientAddress : null,
|
||||
StringUtils.isNoneBlank(sebOsName) ? sebOsName : null,
|
||||
StringUtils.isNoneBlank(sebMachineName) ? sebMachineName : null,
|
||||
StringUtils.isNoneBlank(sebVersion) ? sebVersion : null,
|
||||
StringUtils.isNoneBlank(clientId) ? clientId : null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
getSignatureHash(
|
||||
appSignatureKey,
|
||||
connectionToken,
|
||||
clientConnection.examId != null ? clientConnection.examId : examId),
|
||||
null))
|
||||
.save(updateConnection)
|
||||
.getOrThrow();
|
||||
|
||||
// initialize distributed indicator value caches if possible and needed
|
||||
|
@ -332,62 +327,31 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
ClientConnection clientConnection = getClientConnection(connectionToken);
|
||||
final ClientConnection clientConnection = getClientConnection(connectionToken);
|
||||
final Long _examId = clientConnection.examId != null ? clientConnection.examId : examId;
|
||||
|
||||
// overall connection status integrity check
|
||||
if (!clientConnection.status.clientActiveStatus) {
|
||||
log.warn("ClientConnection integrity violation: client connection is not in expected state: {}",
|
||||
clientConnection);
|
||||
throw new IllegalArgumentException(
|
||||
"ClientConnection integrity violation: client connection is not in expected state");
|
||||
}
|
||||
|
||||
// check IP address change
|
||||
if (StringUtils.isNoneBlank(clientAddress) &&
|
||||
StringUtils.isNotBlank(clientConnection.clientAddress) &&
|
||||
!clientAddress.equals(clientConnection.clientAddress)) {
|
||||
// log client IP address change
|
||||
log.warn(
|
||||
"ClientConnection integrity violation: client address mismatch: {}, {}",
|
||||
clientAddress,
|
||||
clientConnection.clientAddress);
|
||||
sebLogClientAddressMismatch(clientAddress, clientConnection);
|
||||
}
|
||||
connectionStatusIntegrityCheck(clientConnection, clientAddress);
|
||||
checkInstitutionalIntegrity(institutionId, clientConnection);
|
||||
checkExamIntegrity(
|
||||
examId,
|
||||
clientConnection.examId,
|
||||
institutionId,
|
||||
clientConnection.connectionToken,
|
||||
clientConnection.info);
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(
|
||||
"SEB client connection, establish ClientConnection for "
|
||||
+ "connectionToken {} "
|
||||
+ "institutionId: {}"
|
||||
+ "exam: {} "
|
||||
+ "client address: {} "
|
||||
+ "userSessionId: {}"
|
||||
+ "clientId: {}",
|
||||
connectionToken,
|
||||
institutionId,
|
||||
examId,
|
||||
clientAddress,
|
||||
userSessionId,
|
||||
clientId);
|
||||
"SEB client connection, establish ClientConnection for connectionToken {} institutionId: {} exam: {} client address: {} userSessionId: {} clientId: {}",
|
||||
connectionToken, institutionId, examId, clientAddress, userSessionId, clientId);
|
||||
}
|
||||
|
||||
checkInstitutionalIntegrity(institutionId, clientConnection);
|
||||
checkExamIntegrity(examId, clientConnection);
|
||||
clientConnection = updateUserSessionId(userSessionId, clientConnection, examId);
|
||||
|
||||
// connection integrity check
|
||||
if (clientConnection.status != ConnectionStatus.ACTIVE) {
|
||||
if (clientConnection.status == ConnectionStatus.CONNECTION_REQUESTED) {
|
||||
log.warn("ClientConnection integrity warning: client connection is not authenticated: {}",
|
||||
clientConnection);
|
||||
} else if (clientConnection.status != ConnectionStatus.AUTHENTICATED) {
|
||||
log.error("ClientConnection integrity violation: client connection is not in expected state: {}",
|
||||
clientConnection);
|
||||
throw new IllegalArgumentException(
|
||||
"ClientConnection integrity violation: client connection is not in expected state");
|
||||
}
|
||||
}
|
||||
|
||||
final String updateUserSessionId = updateUserSessionId(
|
||||
_examId,
|
||||
userSessionId,
|
||||
clientId,
|
||||
sebMachineName,
|
||||
clientConnection);
|
||||
final Boolean proctoringEnabled = this.examAdminService
|
||||
.isProctoringEnabled(clientConnection.examId)
|
||||
.getOr(false);
|
||||
|
@ -407,12 +371,12 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
currentExamId,
|
||||
ConnectionStatus.ACTIVE,
|
||||
null,
|
||||
clientConnection.userSessionId,
|
||||
StringUtils.isNoneBlank(clientAddress) ? clientAddress : null,
|
||||
StringUtils.isNoneBlank(sebOsName) ? sebOsName : null,
|
||||
StringUtils.isNoneBlank(sebMachineName) ? sebMachineName : null,
|
||||
StringUtils.isNoneBlank(sebVersion) ? sebVersion : null,
|
||||
StringUtils.isNoneBlank(clientId) ? clientId : null,
|
||||
updateUserSessionId,
|
||||
StringUtils.isNotBlank(clientAddress) ? clientAddress : null,
|
||||
StringUtils.isNotBlank(sebOsName) ? sebOsName : null,
|
||||
StringUtils.isNotBlank(sebMachineName) ? sebMachineName : null,
|
||||
StringUtils.isNotBlank(sebVersion) ? sebVersion : null,
|
||||
StringUtils.isNotBlank(clientId) ? clientId : null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
|
@ -422,10 +386,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
null,
|
||||
proctoringEnabled,
|
||||
null,
|
||||
getSignatureHash(
|
||||
appSignatureKey,
|
||||
connectionToken,
|
||||
clientConnection.examId != null ? clientConnection.examId : examId),
|
||||
getSignatureHash(appSignatureKey, connectionToken, _examId),
|
||||
null);
|
||||
|
||||
// ClientConnection integrity check
|
||||
|
@ -445,22 +406,10 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
throw new IllegalStateException("ClientConnection integrity violation");
|
||||
}
|
||||
|
||||
// Removed this since VDI integration was postponed and has not been reactivated since then.
|
||||
// final ClientConnection connectionToSave = handleVDISetup(
|
||||
// currentVdiConnectionId,
|
||||
// establishedClientConnection);
|
||||
//
|
||||
final ClientConnection updatedClientConnection = this.clientConnectionDAO
|
||||
.save(establishedClientConnection)
|
||||
.getOrThrow();
|
||||
|
||||
// check exam integrity for established connection
|
||||
checkExamIntegrity(
|
||||
establishedClientConnection.examId,
|
||||
institutionId,
|
||||
establishedClientConnection.userSessionId,
|
||||
establishedClientConnection.clientAddress);
|
||||
|
||||
// initialize distributed indicator value caches if possible and needed
|
||||
if (examId != null && this.isDistributedSetup) {
|
||||
this.clientIndicatorFactory.initializeDistributedCaches(clientConnection);
|
||||
|
@ -482,66 +431,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
private ClientConnection handleVDISetup(
|
||||
final String currentVdiConnectionId,
|
||||
final ClientConnection establishedClientConnection) {
|
||||
|
||||
if (currentVdiConnectionId == null) {
|
||||
return establishedClientConnection;
|
||||
}
|
||||
|
||||
final Result<ClientConnectionRecord> vdiPairConnectionResult =
|
||||
this.clientConnectionDAO.getVDIPairCompanion(
|
||||
establishedClientConnection.examId,
|
||||
establishedClientConnection.sebClientUserId);
|
||||
|
||||
if (!vdiPairConnectionResult.hasValue()) {
|
||||
return establishedClientConnection;
|
||||
}
|
||||
|
||||
final ClientConnectionRecord vdiPairCompanion = vdiPairConnectionResult.get();
|
||||
final Long vdiExamId = (establishedClientConnection.examId != null)
|
||||
? establishedClientConnection.examId
|
||||
: vdiPairCompanion.getExamId();
|
||||
final ClientConnection updatedConnection = new ClientConnection(
|
||||
establishedClientConnection.id,
|
||||
null,
|
||||
vdiExamId,
|
||||
establishedClientConnection.status,
|
||||
null,
|
||||
establishedClientConnection.userSessionId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
establishedClientConnection.sebClientUserId,
|
||||
null,
|
||||
vdiPairCompanion.getConnectionToken(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
establishedClientConnection.screenProctoringGroupUpdate,
|
||||
null,
|
||||
establishedClientConnection.remoteProctoringRoomUpdate,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
// Update other connection with token and exam id
|
||||
final ClientConnection connection = this.clientConnectionDAO
|
||||
.save(new ClientConnection(
|
||||
vdiPairCompanion.getId(), null,
|
||||
vdiExamId, null, null, null, null, null, null,
|
||||
establishedClientConnection.connectionToken,
|
||||
null, null, null, null, null, null, null, null, null, null, null, null))
|
||||
.getOrThrow();
|
||||
|
||||
reloadConnectionCache(vdiPairCompanion.getConnectionToken(), connection.examId);
|
||||
return updatedConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<ClientConnection> closeConnection(
|
||||
final String connectionToken,
|
||||
|
@ -605,10 +494,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
.getConnectionData(connectionToken)
|
||||
.getOrThrow();
|
||||
|
||||
// An active connection can only be disabled if we have a missing ping
|
||||
if (connectionData.clientConnection.status == ConnectionStatus.ACTIVE &&
|
||||
!BooleanUtils.isTrue(connectionData.getMissingPing())) {
|
||||
|
||||
// A connection can only be disabled if we have a missing ping
|
||||
if (!BooleanUtils.isTrue(connectionData.getMissingPing())) {
|
||||
return connectionData.clientConnection;
|
||||
}
|
||||
|
||||
|
@ -662,46 +549,162 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
|
||||
@Override
|
||||
public Result<Collection<EntityKey>> disableConnections(final String[] connectionTokens, final Long institutionId) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
return Stream.of(connectionTokens)
|
||||
.map(token -> disableConnection(token, institutionId)
|
||||
.onError(error -> log.error("Failed to disable SEB client connection: {}", token))
|
||||
.getOr(null))
|
||||
.filter(Objects::nonNull)
|
||||
.map(Entity::getEntityKey)
|
||||
.collect(Collectors.toList());
|
||||
});
|
||||
return Result.tryCatch(() -> Stream.of(connectionTokens)
|
||||
.map(token -> disableConnection(token, institutionId)
|
||||
.onError(error -> log.error("Failed to disable SEB client connection: {}", token))
|
||||
.getOr(null))
|
||||
.filter(Objects::nonNull)
|
||||
.map(Entity::getEntityKey)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
// SEBSERV-475 IP address change during handshake is possible but is logged within SEB logs
|
||||
private void sebLogClientAddressMismatch(
|
||||
final String clientAddress,
|
||||
final ClientConnection clientConnection) {
|
||||
public void streamExamConfig(
|
||||
final Long institutionId,
|
||||
final Long examId,
|
||||
final String connectionToken,
|
||||
final String ipAddress,
|
||||
final HttpServletResponse response) {
|
||||
|
||||
try {
|
||||
final long now = Utils.getMillisecondsNow();
|
||||
this.sebClientEventBatchService.accept(new SEBClientEventBatchService.EventData(
|
||||
clientConnection.connectionToken,
|
||||
now,
|
||||
new ClientEvent(
|
||||
null,
|
||||
clientConnection.id,
|
||||
ClientEvent.EventType.WARN_LOG,
|
||||
now, now, null,
|
||||
"SEB Client IP address changed: " +
|
||||
clientConnection.clientAddress +
|
||||
" -> " +
|
||||
clientAddress
|
||||
)));
|
||||
|
||||
// if an examId is provided with the request, update the connection first
|
||||
if (examId != null) {
|
||||
final ClientConnection connection = this.updateClientConnection(
|
||||
connectionToken,
|
||||
institutionId,
|
||||
examId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null)
|
||||
.getOrThrow();
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Updated connection: {}", connection);
|
||||
}
|
||||
} else {
|
||||
|
||||
// check connection status
|
||||
final ClientConnectionDataInternal cc = this.examSessionCacheService
|
||||
.getClientConnection(connectionToken);
|
||||
if (cc != null) {
|
||||
connectionStatusIntegrityCheck(cc.clientConnection, ipAddress);
|
||||
}
|
||||
}
|
||||
|
||||
final ServletOutputStream outputStream = response.getOutputStream();
|
||||
|
||||
try {
|
||||
|
||||
this.examSessionService
|
||||
.streamDefaultExamConfig(
|
||||
institutionId,
|
||||
connectionToken,
|
||||
outputStream);
|
||||
|
||||
response.setStatus(HttpStatus.OK.value());
|
||||
|
||||
} catch (final Exception e) {
|
||||
final APIMessage errorMessage = APIMessage.ErrorMessage.GENERIC.of(e.getMessage());
|
||||
outputStream.write(Utils.toByteArray(this.jsonMapper.writeValueAsString(errorMessage)));
|
||||
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
|
||||
} finally {
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
}
|
||||
|
||||
} catch (final IllegalArgumentException e) {
|
||||
final Collection<APIMessage> errorMessages = Arrays.asList(
|
||||
APIMessage.ErrorMessage.CLIENT_CONNECTION_INTEGRITY_VIOLATION.of(e.getMessage()));
|
||||
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
writeSEBClientErrors(response, errorMessages);
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to log SEB client IP address change: ", e);
|
||||
log.error(
|
||||
"Unexpected error while trying to stream SEB Exam Configuration to client with connection: {}",
|
||||
connectionToken, e);
|
||||
|
||||
final Collection<APIMessage> errorMessages = Arrays.asList(
|
||||
APIMessage.ErrorMessage.GENERIC.of(e.getMessage()));
|
||||
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
writeSEBClientErrors(response, errorMessages);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkExamRunning(final Long examId, final String user, final String address) {
|
||||
private void writeSEBClientErrors(HttpServletResponse response, Collection<APIMessage> errorMessages) {
|
||||
try {
|
||||
response.getOutputStream().write(Utils.toByteArray(this.jsonMapper.writeValueAsString(errorMessages)));
|
||||
} catch (final Exception e1) {
|
||||
log.error("Failed to write error to response: ", e1);
|
||||
}
|
||||
}
|
||||
|
||||
private void connectionStatusIntegrityCheck(
|
||||
final ClientConnection clientConnection,
|
||||
final String clientAddress) {
|
||||
|
||||
// overall connection status integrity check
|
||||
if (!clientConnection.status.clientActiveStatus) {
|
||||
log.warn(
|
||||
"ClientConnection integrity violation: client connection is not in expected state: {}",
|
||||
clientConnection);
|
||||
|
||||
// SEBSERV-440 send quit instruction to SEB
|
||||
sebClientInstructionService.registerInstruction(
|
||||
clientConnection.examId,
|
||||
ClientInstruction.InstructionType.SEB_QUIT,
|
||||
Collections.emptyMap(),
|
||||
clientConnection.connectionToken,
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
"ClientConnection integrity violation: client connection is not in expected state");
|
||||
}
|
||||
|
||||
// SEBSERV-475 IP address change during handshake is possible but is logged within SEB logs
|
||||
if (StringUtils.isNoneBlank(clientAddress) &&
|
||||
StringUtils.isNotBlank(clientConnection.clientAddress) &&
|
||||
!clientAddress.equals(clientConnection.clientAddress)) {
|
||||
|
||||
// log SEB client IP address change
|
||||
log.warn(
|
||||
"ClientConnection integrity violation: client address mismatch: {}, {}",
|
||||
clientAddress,
|
||||
clientConnection.clientAddress);
|
||||
|
||||
try {
|
||||
final long now = Utils.getMillisecondsNow();
|
||||
this.sebClientEventBatchService.accept(new SEBClientEventBatchService.EventData(
|
||||
clientConnection.connectionToken,
|
||||
now,
|
||||
new ClientEvent(
|
||||
null,
|
||||
clientConnection.id,
|
||||
ClientEvent.EventType.WARN_LOG,
|
||||
now, now, null,
|
||||
"SEB Client IP address changed: " +
|
||||
clientConnection.clientAddress +
|
||||
" -> " +
|
||||
clientAddress
|
||||
)));
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to log SEB client IP address change: ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void checkExamRunning(final Long examId, final String ccToken, final String ccInfo) {
|
||||
if (examId != null && !this.examSessionService.isExamRunning(examId)) {
|
||||
examNotRunningException(examId, user, address);
|
||||
log.warn("The exam {} is not running. Called by: {} info {}", examId, ccToken, ccInfo);
|
||||
throw new APIConstraintViolationException(
|
||||
"The exam " + examId + " is not running");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -727,99 +730,123 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
|
|||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
private void examNotRunningException(final Long examId, final String user, final String address) {
|
||||
log.warn("The exam {} is not running. Called by: {} | on: {}", examId, user, address);
|
||||
throw new APIConstraintViolationException(
|
||||
"The exam " + examId + " is not running");
|
||||
}
|
||||
|
||||
private void checkExamIntegrity(final Long examId, final ClientConnection clientConnection) {
|
||||
if (examId != null &&
|
||||
clientConnection.examId != null &&
|
||||
!examId.equals(clientConnection.examId)) {
|
||||
|
||||
log.error("Exam integrity violation: another examId is already set for the connection: {}",
|
||||
clientConnection);
|
||||
throw new IllegalArgumentException(
|
||||
"Exam integrity violation: another examId is already set for the connection");
|
||||
}
|
||||
checkExamRunning(examId, clientConnection.userSessionId, clientConnection.clientAddress);
|
||||
}
|
||||
|
||||
private ClientConnection updateUserSessionId(
|
||||
private String updateUserSessionId(
|
||||
final Long examId,
|
||||
final String userSessionId,
|
||||
ClientConnection clientConnection,
|
||||
final Long examId) {
|
||||
final String clientId,
|
||||
final String sebMachineName,
|
||||
final ClientConnection clientConnection) {
|
||||
|
||||
if (StringUtils.isNoneBlank(userSessionId)) {
|
||||
if (StringUtils.isNoneBlank(clientConnection.userSessionId)) {
|
||||
if (clientConnection.userSessionId.contains(userSessionId)) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("SEB sent LMS userSessionId but clientConnection has already a userSessionId");
|
||||
try {
|
||||
|
||||
if (clientConnection == null) {
|
||||
if (StringUtils.isNotBlank(clientId)) {
|
||||
return clientId;
|
||||
}
|
||||
if (StringUtils.isNotBlank(sebMachineName)) {
|
||||
return sebMachineName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// we don't have real userSessionId yet, so we use a placeholder of available
|
||||
if (StringUtils.isBlank(userSessionId)) {
|
||||
if (clientConnection.userSessionId == null) {
|
||||
if (clientConnection.sebClientUserId != null) {
|
||||
return clientConnection.sebClientUserId;
|
||||
}
|
||||
if (StringUtils.isNotBlank(clientId)) {
|
||||
return clientId;
|
||||
}
|
||||
if (clientConnection.sebMachineName != null) {
|
||||
return clientConnection.sebMachineName;
|
||||
}
|
||||
if (StringUtils.isNotBlank(sebMachineName)) {
|
||||
return sebMachineName;
|
||||
}
|
||||
if (clientConnection.clientAddress != null) {
|
||||
return clientConnection.clientAddress;
|
||||
}
|
||||
return null;
|
||||
} else if (clientConnection.userSessionId.equals(clientConnection.clientAddress)) {
|
||||
if (clientConnection.sebClientUserId != null) {
|
||||
return clientConnection.sebClientUserId;
|
||||
}
|
||||
if (StringUtils.isNotBlank(clientId)) {
|
||||
return clientId;
|
||||
}
|
||||
if (clientConnection.sebMachineName != null) {
|
||||
return clientConnection.sebMachineName;
|
||||
}
|
||||
if (StringUtils.isNotBlank(sebMachineName)) {
|
||||
return sebMachineName;
|
||||
}
|
||||
return clientConnection;
|
||||
} else {
|
||||
log.warn(
|
||||
"Possible client integrity violation: clientConnection has already a userSessionId: {} : {}",
|
||||
userSessionId, clientConnection.userSessionId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// try to get user account display name (SEBSERV-228)
|
||||
String accountId = userSessionId;
|
||||
try {
|
||||
final String newAccountId = this.examSessionService
|
||||
.getRunningExam((clientConnection.examId != null)
|
||||
? clientConnection.examId
|
||||
: examId)
|
||||
.flatMap(exam -> this.examSessionService.getLmsAPIService().getLmsAPITemplate(exam.lmsSetupId))
|
||||
.map(template -> template.getExamineeName(userSessionId))
|
||||
.getOr(userSessionId);
|
||||
|
||||
if (StringUtils.isNotBlank(clientConnection.userSessionId)) {
|
||||
accountId = newAccountId +
|
||||
Constants.SPACE +
|
||||
Constants.EMBEDDED_LIST_SEPARATOR +
|
||||
Constants.SPACE +
|
||||
clientConnection.userSessionId;
|
||||
} else {
|
||||
accountId = newAccountId;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
log.warn("Unexpected error while trying to get user account display name: {}", e.getMessage());
|
||||
if (examId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// create new ClientConnection for update
|
||||
final ClientConnection authenticatedClientConnection = new ClientConnection(
|
||||
clientConnection.id,
|
||||
null, null,
|
||||
ConnectionStatus.AUTHENTICATED, null,
|
||||
accountId,
|
||||
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
|
||||
// we got a userSessionId, so we want to resolve it via LMS binding
|
||||
final String accountId = this.examSessionService
|
||||
.getRunningExam(examId)
|
||||
.flatMap(exam -> this.examSessionService
|
||||
.getLmsAPIService()
|
||||
.getLmsAPITemplate(exam.lmsSetupId))
|
||||
.map(template -> template.getExamineeName(userSessionId))
|
||||
.getOr(userSessionId);
|
||||
|
||||
clientConnection = this.clientConnectionDAO
|
||||
.save(authenticatedClientConnection)
|
||||
.getOrThrow();
|
||||
// if userSessionId is not set yet or a placeholder is set, just use the new account name
|
||||
if (clientConnection.userSessionId == null ||
|
||||
clientConnection.userSessionId.equals(clientConnection.sebClientUserId) ||
|
||||
clientConnection.userSessionId.equals(clientConnection.sebMachineName) ||
|
||||
clientConnection.userSessionId.equals(clientConnection.clientAddress)) {
|
||||
return accountId;
|
||||
}
|
||||
|
||||
// otherwise apply new name
|
||||
return accountId +
|
||||
Constants.SPACE +
|
||||
Constants.EMBEDDED_LIST_SEPARATOR +
|
||||
Constants.SPACE +
|
||||
clientConnection.userSessionId;
|
||||
} catch (final Exception e) {
|
||||
log.error("Unexpected error while try to update userSessionId for connection: {}", clientConnection, e);
|
||||
return null;
|
||||
}
|
||||
return clientConnection;
|
||||
}
|
||||
|
||||
private void checkExamIntegrity(
|
||||
final Long examId,
|
||||
final Long currentExamId,
|
||||
final Long institutionId,
|
||||
final String user,
|
||||
final String address) {
|
||||
final String ccToken,
|
||||
final String ccInfo) {
|
||||
|
||||
if (examId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isDistributedSetup) {
|
||||
// if the cached Exam is not up to date anymore, we have to update the cache first
|
||||
// if the cached Exam is not up-to-date anymore, we have to update the cache first
|
||||
final Result<Exam> updateExamCache = this.examSessionService.updateExamCache(examId);
|
||||
if (updateExamCache.hasError()) {
|
||||
log.warn("Failed to update Exam-Cache for Exam: {}", examId);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentExamId != null && !examId.equals(currentExamId)) {
|
||||
log.error("Exam integrity violation: another examId is already set for the connection: {}", ccToken);
|
||||
throw new IllegalArgumentException(
|
||||
"Exam integrity violation: another examId is already set for the connection");
|
||||
}
|
||||
|
||||
// check Exam is running and not locked
|
||||
checkExamRunning(examId, user, address);
|
||||
checkExamRunning(examId, ccToken, ccInfo);
|
||||
if (this.examSessionService.isExamLocked(examId)) {
|
||||
throw new APIConstraintViolationException(
|
||||
"Exam is currently on update and locked for new SEB Client connections");
|
||||
|
|
|
@ -103,34 +103,36 @@ public class SEBClientInstructionServiceImpl implements SEBClientInstructionServ
|
|||
final InstructionType type,
|
||||
final Map<String, String> attributes,
|
||||
final String connectionToken,
|
||||
final boolean checkActive,
|
||||
final boolean needsConfirm) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final boolean isActive = this.clientConnectionDAO
|
||||
if (checkActive && !this.clientConnectionDAO
|
||||
.isInInstructionStatus(examId, connectionToken)
|
||||
.getOr(false);
|
||||
.getOr(false)) {
|
||||
|
||||
if (isActive) {
|
||||
try {
|
||||
|
||||
final String attributesString = (attributes != null && !attributes.isEmpty())
|
||||
? this.jsonMapper.writeValueAsString(attributes)
|
||||
: null;
|
||||
|
||||
this.clientInstructionDAO
|
||||
.insert(examId, type, attributesString, connectionToken, needsConfirm)
|
||||
.map(this::putToCache)
|
||||
.onError(error -> log.error("Failed to register instruction: {}", error.getMessage()))
|
||||
.getOrThrow();
|
||||
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException("Unexpected: ", e);
|
||||
}
|
||||
} else {
|
||||
log.warn(
|
||||
"The SEB client connection : {} is not in a ready state to process instructions. Instruction registration has been skipped",
|
||||
"The SEB client connection : {} is not in a ready state to process instructions. " +
|
||||
"Instruction registration has been skipped",
|
||||
connectionToken);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
final String attributesString = (attributes != null && !attributes.isEmpty())
|
||||
? this.jsonMapper.writeValueAsString(attributes)
|
||||
: null;
|
||||
|
||||
this.clientInstructionDAO
|
||||
.insert(examId, type, attributesString, connectionToken, needsConfirm)
|
||||
.map(this::putToCache)
|
||||
.onError(error -> log.error("Failed to register instruction: {}", error.getMessage()))
|
||||
.getOrThrow();
|
||||
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException("Unexpected: ", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -154,7 +154,9 @@ public class SEBClientNotificationServiceImpl implements SEBClientNotificationSe
|
|||
InstructionType.NOTIFICATION_CONFIRM,
|
||||
connectionToken,
|
||||
attributes);
|
||||
this.sebClientInstructionService.registerInstruction(clientInstruction);
|
||||
this.sebClientInstructionService
|
||||
.registerInstruction(clientInstruction)
|
||||
.onError(error -> log.error("Failed to confirm instruction SEB client side: ", error));
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
@ -15,6 +16,8 @@ import java.util.concurrent.ScheduledFuture;
|
|||
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.ehcache.impl.internal.concurrent.ConcurrentHashMap;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -42,7 +45,6 @@ public class SEBClientPingBatchService implements SEBClientPingService {
|
|||
private final ExamSessionCacheService examSessionCacheService;
|
||||
private final SEBClientInstructionService sebClientInstructionService;
|
||||
|
||||
|
||||
private final Set<String> pingKeys = new HashSet<>();
|
||||
private final Map<String, String> pings = new ConcurrentHashMap<>();
|
||||
private final Map<String, String> instructions = new ConcurrentHashMap<>();
|
||||
|
@ -69,7 +71,6 @@ public class SEBClientPingBatchService implements SEBClientPingService {
|
|||
try {
|
||||
this.pingKeys.clear();
|
||||
this.pingKeys.addAll(this.pings.keySet());
|
||||
//final Set<String> connections = new HashSet<>(this.pings.keySet());
|
||||
this.pingKeys.stream().forEach(cid -> processPing(
|
||||
cid,
|
||||
this.pings.remove(cid),
|
||||
|
@ -119,11 +120,23 @@ public class SEBClientPingBatchService implements SEBClientPingService {
|
|||
return;
|
||||
}
|
||||
|
||||
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
|
||||
final ClientConnectionDataInternal connectionData = this.examSessionCacheService
|
||||
.getClientConnection(connectionToken);
|
||||
|
||||
if (activeClientConnection != null) {
|
||||
activeClientConnection.notifyPing(timestamp);
|
||||
if (connectionData != null) {
|
||||
if (connectionData.clientConnection.status == ClientConnection.ConnectionStatus.DISABLED) {
|
||||
// SEBSERV-440 send quit instruction to SEB
|
||||
sebClientInstructionService.registerInstruction(
|
||||
connectionData.clientConnection.examId,
|
||||
ClientInstruction.InstructionType.SEB_QUIT,
|
||||
Collections.emptyMap(),
|
||||
connectionData.clientConnection.connectionToken,
|
||||
false,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
connectionData.notifyPing(timestamp);
|
||||
} else {
|
||||
log.error("Failed to get ClientConnectionDataInternal for: {}", connectionToken);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -50,11 +54,23 @@ public class SEBClientPingBlockingService implements SEBClientPingService {
|
|||
return null;
|
||||
}
|
||||
|
||||
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService
|
||||
final ClientConnectionDataInternal connectionData = this.examSessionCacheService
|
||||
.getClientConnection(connectionToken);
|
||||
|
||||
if (activeClientConnection != null) {
|
||||
activeClientConnection.notifyPing(Utils.getMillisecondsNow());
|
||||
if (connectionData != null) {
|
||||
if (connectionData.clientConnection.status == ClientConnection.ConnectionStatus.DISABLED) {
|
||||
// SEBSERV-440 send quit instruction to SEB
|
||||
sebClientInstructionService.registerInstruction(
|
||||
connectionData.clientConnection.examId,
|
||||
ClientInstruction.InstructionType.SEB_QUIT,
|
||||
Collections.emptyMap(),
|
||||
connectionData.clientConnection.connectionToken,
|
||||
false,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
connectionData.notifyPing(Utils.getMillisecondsNow());
|
||||
} else {
|
||||
log.error("Failed to get ClientConnectionDataInternal for: {}", connectionToken);
|
||||
}
|
||||
|
|
|
@ -653,6 +653,7 @@ public class RemoteProctoringRoomServiceImpl implements RemoteProctoringRoomServ
|
|||
InstructionType.SEB_RECONFIGURE_SETTINGS,
|
||||
attributes,
|
||||
connectionToken,
|
||||
true,
|
||||
true)
|
||||
.onError(error -> log.error(
|
||||
"Failed to register reconfiguring instruction for connection: {}",
|
||||
|
@ -852,6 +853,7 @@ public class RemoteProctoringRoomServiceImpl implements RemoteProctoringRoomServ
|
|||
InstructionType.SEB_PROCTORING,
|
||||
attributes,
|
||||
connectionToken,
|
||||
true,
|
||||
true)
|
||||
.onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error))
|
||||
.getOrThrow();
|
||||
|
|
|
@ -476,6 +476,7 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
|||
InstructionType.SEB_PROCTORING,
|
||||
attributes,
|
||||
ccRecord.getConnectionToken(),
|
||||
true,
|
||||
true)
|
||||
.onError(error -> log.error(
|
||||
"Failed to register screen proctoring join instruction for SEB connection: {}",
|
||||
|
|
|
@ -524,6 +524,7 @@ public class ZoomProctoringService implements RemoteProctoringService {
|
|||
InstructionType.SEB_PROCTORING,
|
||||
attributes,
|
||||
connectionToken,
|
||||
true,
|
||||
true)
|
||||
.onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error));
|
||||
}
|
||||
|
|
|
@ -210,7 +210,7 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
|
|||
}
|
||||
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
public ResponseEntity<Object> handleUnexpected(
|
||||
public ResponseEntity<Object> handleAccessDenied(
|
||||
final AccessDeniedException ex,
|
||||
final WebRequest request) {
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import java.security.Principal;
|
|||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -136,7 +137,7 @@ public class ExamAPI_V1_Controller {
|
|||
clientConnection.connectionToken);
|
||||
|
||||
// Crate list of running exams
|
||||
List<RunningExamInfo> result;
|
||||
final List<RunningExamInfo> result;
|
||||
if (examId == null) {
|
||||
result = this.examSessionService.getRunningExamsForInstitution(institutionId)
|
||||
.getOrThrow()
|
||||
|
@ -189,7 +190,7 @@ public class ExamAPI_V1_Controller {
|
|||
@RequestParam(name = API.EXAM_API_USER_SESSION_ID, required = false) final String userSessionId,
|
||||
@RequestParam(name = API.EXAM_API_PARAM_SEB_VERSION, required = false) final String sebVersion,
|
||||
@RequestParam(name = API.EXAM_API_PARAM_SEB_OS_NAME, required = false) final String sebOSName,
|
||||
@RequestParam(name = API.EXAM_API_PARAM_SEB_MACHINE_NAME, required = false) final String sebMachinName,
|
||||
@RequestParam(name = API.EXAM_API_PARAM_SEB_MACHINE_NAME, required = false) final String sebMachineName,
|
||||
@RequestParam(
|
||||
name = API.EXAM_API_PARAM_SIGNATURE_KEY,
|
||||
required = false) final String browserSignatureKey,
|
||||
|
@ -212,7 +213,7 @@ public class ExamAPI_V1_Controller {
|
|||
remoteAddr,
|
||||
sebVersion,
|
||||
sebOSName,
|
||||
sebMachinName,
|
||||
sebMachineName,
|
||||
userSessionId,
|
||||
clientId,
|
||||
browserSignatureKey)
|
||||
|
@ -315,8 +316,22 @@ public class ExamAPI_V1_Controller {
|
|||
final HttpServletRequest request,
|
||||
final HttpServletResponse response) {
|
||||
|
||||
Long examId;
|
||||
try {
|
||||
examId = Long.parseLong(Objects.requireNonNull(formParams.getFirst(API.EXAM_API_PARAM_EXAM_ID)));
|
||||
} catch (final Exception e) {
|
||||
examId = null;
|
||||
}
|
||||
final Long _examId = examId;
|
||||
final String remoteAddr = this.getClientAddress(request);
|
||||
|
||||
return CompletableFuture.runAsync(
|
||||
() -> streamExamConfig(connectionToken, formParams, principal, response),
|
||||
() -> this.sebClientConnectionService.streamExamConfig(
|
||||
getInstitutionId(principal),
|
||||
_examId,
|
||||
connectionToken,
|
||||
remoteAddr,
|
||||
response),
|
||||
this.executor);
|
||||
}
|
||||
|
||||
|
@ -328,7 +343,6 @@ public class ExamAPI_V1_Controller {
|
|||
public void ping(final HttpServletRequest request, final HttpServletResponse response) {
|
||||
|
||||
final String connectionToken = request.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN);
|
||||
//final String pingNumString = request.getParameter(API.EXAM_API_PING_NUMBER);
|
||||
final String instructionConfirm = request.getParameter(API.EXAM_API_PING_INSTRUCTION_CONFIRM);
|
||||
|
||||
final String instruction = this.sebClientSessionService
|
||||
|
@ -373,73 +387,7 @@ public class ExamAPI_V1_Controller {
|
|||
.getOr(null));
|
||||
}
|
||||
|
||||
private void streamExamConfig(
|
||||
final String connectionToken,
|
||||
final MultiValueMap<String, String> formParams,
|
||||
final Principal principal,
|
||||
final HttpServletResponse response) {
|
||||
|
||||
final Long institutionId = getInstitutionId(principal);
|
||||
|
||||
try {
|
||||
|
||||
// if an examId is provided with the request, update the connection first
|
||||
if (formParams != null && formParams.containsKey(API.EXAM_API_PARAM_EXAM_ID)) {
|
||||
final String examId = formParams.getFirst(API.EXAM_API_PARAM_EXAM_ID);
|
||||
final ClientConnection connection = this.sebClientConnectionService.updateClientConnection(
|
||||
connectionToken,
|
||||
institutionId,
|
||||
Long.valueOf(examId),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null)
|
||||
.getOrThrow();
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Updated connection: {}", connection);
|
||||
}
|
||||
}
|
||||
|
||||
final ServletOutputStream outputStream = response.getOutputStream();
|
||||
|
||||
try {
|
||||
|
||||
this.examSessionService
|
||||
.streamDefaultExamConfig(
|
||||
institutionId,
|
||||
connectionToken,
|
||||
outputStream);
|
||||
|
||||
response.setStatus(HttpStatus.OK.value());
|
||||
|
||||
} catch (final Exception e) {
|
||||
final APIMessage errorMessage = APIMessage.ErrorMessage.GENERIC.of(e.getMessage());
|
||||
outputStream.write(Utils.toByteArray(this.jsonMapper.writeValueAsString(errorMessage)));
|
||||
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
|
||||
} finally {
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
}
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Unexpected error while trying to stream SEB Exam Configuration to client with connection: {}",
|
||||
connectionToken,
|
||||
e);
|
||||
|
||||
final APIMessage errorMessage = APIMessage.ErrorMessage.GENERIC.of(e.getMessage());
|
||||
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
try {
|
||||
response.getOutputStream().write(Utils.toByteArray(this.jsonMapper.writeValueAsString(errorMessage)));
|
||||
} catch (final Exception e1) {
|
||||
log.error("Failed to write error to response: ", e1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getClientAddress(final HttpServletRequest request) {
|
||||
try {
|
||||
|
|
|
@ -463,6 +463,7 @@ public class ExamMonitoringController {
|
|||
|
||||
checkPrivileges(institutionId, examId);
|
||||
|
||||
// TODO do this in async in new thread
|
||||
if (connectionToken.contains(Constants.LIST_SEPARATOR)) {
|
||||
// If we have a bunch of client connections to disable, make it asynchronously and respond to the caller immediately
|
||||
this.executor.execute(() -> {
|
||||
|
@ -563,8 +564,8 @@ public class ExamMonitoringController {
|
|||
final EnumSet<ConnectionStatus> filterStates = EnumSet.noneOf(ConnectionStatus.class);
|
||||
if (StringUtils.isNotBlank(hiddenStates)) {
|
||||
final String[] split = StringUtils.split(hiddenStates, Constants.LIST_SEPARATOR);
|
||||
for (int i = 0; i < split.length; i++) {
|
||||
filterStates.add(ConnectionStatus.valueOf(split[i]));
|
||||
for (final String s : split) {
|
||||
filterStates.add(ConnectionStatus.valueOf(s));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -583,8 +584,8 @@ public class ExamMonitoringController {
|
|||
if (StringUtils.isNotBlank(hiddenClientGroups)) {
|
||||
filterClientGroups = new HashSet<>();
|
||||
final String[] split = StringUtils.split(hiddenClientGroups, Constants.LIST_SEPARATOR);
|
||||
for (int i = 0; i < split.length; i++) {
|
||||
filterClientGroups.add(Long.parseLong(split[i]));
|
||||
for (final String s : split) {
|
||||
filterClientGroups.add(Long.parseLong(s));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
|
|||
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService;
|
||||
|
||||
/** A ClientDetailsService to manage different API clients of SEB Server webservice API.
|
||||
*
|
||||
* <p>
|
||||
* Currently supporting two client types for the two different API's on
|
||||
* SEB Server webservice;
|
||||
* - Administration API for administrative purpose using password grant type with refresh token
|
||||
|
@ -44,7 +44,7 @@ public class WebClientDetailsService implements ClientDetailsService {
|
|||
}
|
||||
|
||||
/** Load a client by the client id. This method must not return null.
|
||||
*
|
||||
* <p>
|
||||
* This checks first if the given clientId matches the client id of AdminAPIClientDetails.
|
||||
* If not, iterating through LMSSetup's and matches the sebClientname of each LMSSetup.
|
||||
* If there is a match, a ClientDetails object is created from LMSSetup and returned.
|
||||
|
|
|
@ -2437,7 +2437,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
|
|||
connections = connectionsCall.get();
|
||||
assertFalse(connections.isEmpty());
|
||||
conData = connections.iterator().next();
|
||||
assertEquals("DISABLED", conData.clientConnection.status.name());
|
||||
assertEquals("CLOSED", conData.clientConnection.status.name());
|
||||
|
||||
// get client logs
|
||||
final Result<Page<ExtendedClientEvent>> clientLogPage = restService
|
||||
|
@ -2520,7 +2520,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
|
|||
assertFalse(ccDataPage.content.isEmpty());
|
||||
final ClientConnectionData clientConnectionData = ccDataPage.content.get(0);
|
||||
assertNotNull(clientConnectionData);
|
||||
assertEquals("DISABLED", clientConnectionData.clientConnection.status.toString());
|
||||
assertEquals("CLOSED", clientConnectionData.clientConnection.status.toString());
|
||||
|
||||
connectionDatacall = restService
|
||||
.getBuilder(GetFinishedExamClientConnectionPage.class)
|
||||
|
|
|
@ -0,0 +1,247 @@
|
|||
package ch.ethz.seb.sebserver.webservice.integration.api.exam;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.security.Principal;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
|
||||
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.ClientConnection;
|
||||
import ch.ethz.seb.sebserver.gbl.model.session.RunningExamInfo;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.weblayer.api.APIExceptionHandler;
|
||||
import ch.ethz.seb.sebserver.webservice.weblayer.api.ExamAPI_V1_Controller;
|
||||
import ch.ethz.seb.sebserver.webservice.weblayer.api.ExamAdministrationController;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.context.request.async.StandardServletAsyncWebRequest;
|
||||
|
||||
@Sql(scripts = { "classpath:schema-test.sql", "classpath:data-test.sql", "classpath:data-test-additional.sql" })
|
||||
public class SEBConnectionAPITest extends ExamAPIIntegrationTester {
|
||||
|
||||
@Autowired
|
||||
private ExamAPI_V1_Controller examAPI_V1_Controller;
|
||||
@Autowired
|
||||
private ExamAdministrationController examAdministrationController;
|
||||
@Autowired
|
||||
private ClientConnectionDAO clientConnectionDAO;
|
||||
@Autowired
|
||||
private APIExceptionHandler apiExceptionHandler;
|
||||
@Autowired
|
||||
private UserDAO userDAO;
|
||||
@MockBean
|
||||
private UserService userService;
|
||||
|
||||
@Test
|
||||
public void testRunningExam6Available() {
|
||||
|
||||
Mockito.when(userService.getCurrentUser()).thenReturn(userDAO.sebServerUserByUsername("admin").get());
|
||||
|
||||
final HttpServletRequest requestMock = Mockito.mock(HttpServletRequest.class);
|
||||
Page<Exam> page = examAdministrationController.getPage(
|
||||
1L,
|
||||
1,
|
||||
10,
|
||||
null,
|
||||
null,
|
||||
requestMock);
|
||||
|
||||
assertNotNull(page);
|
||||
final Optional<Exam> runningExam = page.content.stream().filter(e -> e.status == Exam.ExamStatus.RUNNING).findFirst();
|
||||
assertTrue(runningExam.isPresent());
|
||||
final Exam exam = runningExam.get();
|
||||
assertEquals("Exam [id=2, institutionId=1, lmsSetupId=1, externalId=quiz6, name=quiz6, description=null, startTime=null, endTime=null, startURL=https://test.lms.mockup, type=MANAGED, owner=admin, supporter=[admin], status=RUNNING, browserExamKeys=null, active=true, lastUpdate=null]", exam.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadRequestError() throws Exception {
|
||||
final Principal principal = Mockito.mock(Principal.class);
|
||||
final HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
|
||||
final HttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
final CompletableFuture<Collection<RunningExamInfo>> apiCall = examAPI_V1_Controller.handshakeCreate(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
principal,
|
||||
request,
|
||||
response);
|
||||
|
||||
try {
|
||||
apiCall.get();
|
||||
} catch (final Exception e) {
|
||||
final ResponseEntity<Object> responseEntity = apiExceptionHandler
|
||||
.handleAccessDenied(
|
||||
(AccessDeniedException) e.getCause(),
|
||||
new StandardServletAsyncWebRequest(request, response));
|
||||
|
||||
final String jsonBody = new JSONMapper().writeValueAsString(responseEntity.getBody());
|
||||
assertEquals("[{\"messageCode\":\"1001\",\"systemMessage\":\"FORBIDDEN\",\"details\":\"Unknown or illegal client access\",\"attributes\":[]}]", jsonBody);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOrdinarySEBHandshakeWithKnownExam() throws Exception {
|
||||
|
||||
// ******************************************************************
|
||||
// 1. Handshake creation
|
||||
HttpServletResponse httpServletResponse = handshakePOST(
|
||||
1L,
|
||||
2L,
|
||||
"baba",
|
||||
"Win",
|
||||
"3.5",
|
||||
"m2000");
|
||||
|
||||
// 1.1 Assert response
|
||||
assertEquals("200", String.valueOf(httpServletResponse.getStatus()));
|
||||
final String connectionToken = httpServletResponse.getHeader("SEBConnectionToken");
|
||||
|
||||
assertNotNull(connectionToken);
|
||||
assertNotNull(httpServletResponse.getHeader("SEBExamSalt"));
|
||||
assertEquals(
|
||||
"[{\"examId\":\"2\",\"name\":\"quiz6\",\"url\":\"https://test.lms.mockup\",\"lmsType\":\"MOCKUP\"}]",
|
||||
((MockHttpServletResponse) httpServletResponse).getContentAsString());
|
||||
|
||||
// 1.2 Assert persistent data
|
||||
ClientConnection clientConnection = clientConnectionDAO
|
||||
.byConnectionToken(connectionToken)
|
||||
.getOrThrow();
|
||||
assertNotNull(clientConnection);
|
||||
assertEquals("CONNECTION_REQUESTED", clientConnection.getStatus().name());
|
||||
assertEquals(connectionToken, clientConnection.connectionToken);
|
||||
assertEquals(1, (long) clientConnection.institutionId);
|
||||
assertEquals(2, (long) clientConnection.examId);
|
||||
assertEquals("127.0.0.1", clientConnection.clientAddress);
|
||||
assertEquals("baba", clientConnection.sebClientUserId);
|
||||
assertEquals("Win", clientConnection.sebOSName);
|
||||
assertEquals("3.5", clientConnection.sebVersion);
|
||||
assertEquals("m2000", clientConnection.sebMachineName);
|
||||
// userSessionId should be client id if not given yet
|
||||
assertEquals("baba", clientConnection.userSessionId);
|
||||
|
||||
// ******************************************************************
|
||||
// 2. Handshake update
|
||||
httpServletResponse = handshakePATCH(
|
||||
connectionToken,
|
||||
2L,
|
||||
"John Doe",
|
||||
"baba2",
|
||||
"Mac",
|
||||
"3.7",
|
||||
"fvbfvfsv",
|
||||
null);
|
||||
|
||||
// 2.1 Assert response
|
||||
assertEquals("200", String.valueOf(httpServletResponse.getStatus()));
|
||||
assertNotNull(httpServletResponse.getHeader("SEBExamSalt"));
|
||||
|
||||
// 2.2 Assert persistent data
|
||||
clientConnection = clientConnectionDAO
|
||||
.byConnectionToken(connectionToken)
|
||||
.getOrThrow();
|
||||
assertNotNull(clientConnection);
|
||||
assertEquals("AUTHENTICATED", clientConnection.getStatus().name());
|
||||
assertEquals(connectionToken, clientConnection.connectionToken);
|
||||
assertEquals(1, (long) clientConnection.institutionId);
|
||||
assertEquals(2, (long) clientConnection.examId);
|
||||
assertEquals("127.0.0.1", clientConnection.clientAddress);
|
||||
assertEquals("baba", clientConnection.sebClientUserId); // should not be possible to overwrite previous setting
|
||||
assertEquals("Win", clientConnection.sebOSName); // should not be possible to overwrite previous setting
|
||||
assertEquals("3.5", clientConnection.sebVersion); // should not be possible to overwrite previous setting
|
||||
assertEquals("m2000", clientConnection.sebMachineName); // should not be possible to overwrite previous setting
|
||||
|
||||
}
|
||||
|
||||
private HttpServletResponse handshakePOST(
|
||||
final Long institutionId,
|
||||
final Long examId,
|
||||
final String client_id,
|
||||
final String seb_os_name,
|
||||
final String seb_version,
|
||||
final String seb_machine_name) throws Exception {
|
||||
|
||||
final Principal principal = Mockito.mock(Principal.class);
|
||||
final HttpServletRequest request = new MockHttpServletRequest("POST", "/exam-api/v1/handshake ") ;
|
||||
final HttpServletResponse response = new MockHttpServletResponse();
|
||||
Mockito.when(principal.getName()).thenReturn("test");
|
||||
|
||||
final MultiValueMap<String, String> formParams = new LinkedMultiValueMap<>();
|
||||
formParams.add(API.EXAM_API_PARAM_SEB_VERSION, seb_version);
|
||||
formParams.add(API.EXAM_API_PARAM_SEB_OS_NAME, seb_os_name);
|
||||
formParams.add(API.EXAM_API_PARAM_SEB_MACHINE_NAME, seb_machine_name);
|
||||
|
||||
final CompletableFuture<Collection<RunningExamInfo>> apiCall = examAPI_V1_Controller
|
||||
.handshakeCreate(institutionId, examId, client_id, formParams, principal, request, response);
|
||||
|
||||
response.getOutputStream().print(new JSONMapper().writeValueAsString(apiCall.get()));
|
||||
return response;
|
||||
}
|
||||
|
||||
private HttpServletResponse handshakePATCH(
|
||||
final String connectionToken,
|
||||
final Long examId,
|
||||
final String userSessionId,
|
||||
final String client_id,
|
||||
final String seb_os_name,
|
||||
final String seb_version,
|
||||
final String seb_machine_name,
|
||||
final String askHash) throws Exception {
|
||||
|
||||
final Principal principal = Mockito.mock(Principal.class);
|
||||
final HttpServletRequest request = new MockHttpServletRequest("PATCH", "/exam-api/v1/handshake ") ;
|
||||
final HttpServletResponse response = new MockHttpServletResponse();
|
||||
Mockito.when(principal.getName()).thenReturn("test");
|
||||
|
||||
examAPI_V1_Controller.handshakeUpdate(
|
||||
connectionToken, examId, userSessionId, seb_version, seb_os_name,
|
||||
seb_machine_name, askHash, client_id, principal, request, response)
|
||||
.get();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private HttpServletResponse handshakePUT(
|
||||
final String connectionToken,
|
||||
final Long examId,
|
||||
final String userSessionId,
|
||||
final String client_id,
|
||||
final String seb_os_name,
|
||||
final String seb_version,
|
||||
final String seb_machine_name,
|
||||
final String askHash) throws Exception {
|
||||
|
||||
final Principal principal = Mockito.mock(Principal.class);
|
||||
final HttpServletRequest request = new MockHttpServletRequest("PUT", "/exam-api/v1/handshake ") ;
|
||||
final HttpServletResponse response = new MockHttpServletResponse();
|
||||
Mockito.when(principal.getName()).thenReturn("test");
|
||||
|
||||
examAPI_V1_Controller.handshakeEstablish(
|
||||
connectionToken, examId, userSessionId, seb_version, seb_os_name,
|
||||
seb_machine_name, askHash, client_id, principal, request, response)
|
||||
.get();
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
|
@ -441,15 +441,15 @@ public class SebConnectionTest extends ExamAPIIntegrationTester {
|
|||
.build()
|
||||
.execute();
|
||||
|
||||
assertTrue(records.size() == 1);
|
||||
assertEquals(1, records.size());
|
||||
final ClientConnectionRecord clientConnectionRecord = records.get(0);
|
||||
assertEquals("1", String.valueOf(clientConnectionRecord.getInstitutionId()));
|
||||
assertEquals("2", String.valueOf(clientConnectionRecord.getExamId()));
|
||||
assertEquals("CLOSED", String.valueOf(clientConnectionRecord.getStatus()));
|
||||
assertNotNull(clientConnectionRecord.getConnectionToken());
|
||||
assertNotNull(clientConnectionRecord.getClientAddress());
|
||||
assertNull(clientConnectionRecord.getExamUserSessionId());
|
||||
assertTrue(clientConnectionRecord.getVdi() == 0);
|
||||
assertNotNull(clientConnectionRecord.getExamUserSessionId());
|
||||
assertEquals(0, (int) clientConnectionRecord.getVdi());
|
||||
assertNull(clientConnectionRecord.getVirtualClientAddress());
|
||||
|
||||
// check cache after update
|
||||
|
|
|
@ -66,7 +66,7 @@ public class SEBClientInstructionServiceTest extends AdministrationAPIIntegratio
|
|||
|
||||
// register instruction
|
||||
this.sebClientInstructionService.registerInstruction(
|
||||
2L, InstructionType.SEB_QUIT, Collections.emptyMap(), "testToken", false);
|
||||
2L, InstructionType.SEB_QUIT, Collections.emptyMap(), "testToken", true,false);
|
||||
|
||||
// check on DB
|
||||
all = this.clientInstructionDAO
|
||||
|
@ -100,7 +100,7 @@ public class SEBClientInstructionServiceTest extends AdministrationAPIIntegratio
|
|||
public void testRegisterWithConfirm() {
|
||||
// register instruction
|
||||
this.sebClientInstructionService.registerInstruction(
|
||||
2L, InstructionType.SEB_RECONFIGURE_SETTINGS, Collections.emptyMap(), "testToken", true);
|
||||
2L, InstructionType.SEB_RECONFIGURE_SETTINGS, Collections.emptyMap(), "testToken", true,true);
|
||||
|
||||
// check on DB
|
||||
Collection<ClientInstructionRecord> all = this.clientInstructionDAO
|
||||
|
@ -142,7 +142,7 @@ public class SEBClientInstructionServiceTest extends AdministrationAPIIntegratio
|
|||
attributes.put("attr1", "123");
|
||||
attributes.put("attr2", "345");
|
||||
this.sebClientInstructionService.registerInstruction(
|
||||
2L, InstructionType.SEB_RECONFIGURE_SETTINGS, attributes, "testToken", true);
|
||||
2L, InstructionType.SEB_RECONFIGURE_SETTINGS, attributes, "testToken", true,true);
|
||||
|
||||
// check on DB
|
||||
Collection<ClientInstructionRecord> all = this.clientInstructionDAO
|
||||
|
|
Loading…
Reference in a new issue