SEBSERV-440 implemented

This commit is contained in:
anhefti 2023-11-27 13:15:36 +01:00
parent 0d5d7b3894
commit 50456b8d9b
23 changed files with 749 additions and 466 deletions

View file

@ -74,7 +74,10 @@ public class APIMessage implements Serializable {
EXAM_IMPORT_ERROR_AUTO_CONFIG("1610", HttpStatus.PARTIAL_CONTENT, EXAM_IMPORT_ERROR_AUTO_CONFIG("1610", HttpStatus.PARTIAL_CONTENT,
"Failed to automatically create and link exam configuration from the exam template to the exam"), "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, 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 String messageCode;
public final HttpStatus httpStatus; public final HttpStatus httpStatus;

View file

@ -440,7 +440,7 @@ public final class ClientConnection implements GrantEntity {
builder.append(this.clientAddress); builder.append(this.clientAddress);
builder.append(", remoteProctoringRoomId="); builder.append(", remoteProctoringRoomId=");
builder.append(this.remoteProctoringRoomId); builder.append(this.remoteProctoringRoomId);
builder.append(", virtualClientId="); builder.append(", sebClientUserId=");
builder.append(this.sebClientUserId); builder.append(this.sebClientUserId);
builder.append(", creationTime="); builder.append(", creationTime=");
builder.append(this.creationTime); builder.append(this.creationTime);

View file

@ -364,7 +364,7 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
data.examId, data.examId,
ConnectionStatus.CONNECTION_REQUESTED.name(), ConnectionStatus.CONNECTION_REQUESTED.name(),
data.connectionToken, data.connectionToken,
null, data.userSessionId,
data.clientAddress, data.clientAddress,
data.sebClientUserId, data.sebClientUserId,
BooleanUtils.toInteger(data.vdi, 1, 0, 0), BooleanUtils.toInteger(data.vdi, 1, 0, 0),

View file

@ -8,6 +8,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session; package ch.ethz.seb.sebserver.webservice.servicelayer.session;
import javax.servlet.http.HttpServletResponse;
import java.security.Principal; import java.security.Principal;
import java.util.Collection; import java.util.Collection;
@ -158,4 +159,18 @@ public interface SEBClientConnectionService {
* happened */ * happened */
Result<Collection<EntityKey>> disableConnections(final String[] connectionTokens, final Long institutionId); 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);
} }

View file

@ -28,12 +28,12 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo; import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
/** Service for SEB instruction handling. /** Service for SEB instruction handling.
* * <p>
* SEB instructions are sent as response of a SEB Ping on a active SEB Connection * 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. */ * If there is an instruction in the queue for a specified SEB Client. */
public interface SEBClientInstructionService { public interface SEBClientInstructionService {
static final Logger log = LoggerFactory.getLogger(SEBClientInstructionService.class); Logger log = LoggerFactory.getLogger(SEBClientInstructionService.class);
/** Get the underling WebserviceInfo /** Get the underling WebserviceInfo
* *
@ -46,10 +46,10 @@ public interface SEBClientInstructionService {
void init(); void init();
/** Used to register a SEB client instruction for one or more active client connections /** 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 * @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) @Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
default void registerInstructionAsync(final ClientInstruction clientInstruction) { default void registerInstructionAsync(final ClientInstruction clientInstruction) {
registerInstruction(clientInstruction, false) registerInstruction(clientInstruction, false)
@ -69,7 +69,7 @@ public interface SEBClientInstructionService {
/** Used to register a SEB client instruction for one or more active client connections /** Used to register a SEB client instruction for one or more active client connections
* *
* @param clientInstruction the ClientInstruction instance to register * @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 */ * @return A Result refer to a void marker or to an error if happened */
default Result<Void> registerInstruction( default Result<Void> registerInstruction(
final ClientInstruction clientInstruction, final ClientInstruction clientInstruction,
@ -91,6 +91,7 @@ public interface SEBClientInstructionService {
* @param type The InstructionType * @param type The InstructionType
* @param attributes The instruction's attributes * @param attributes The instruction's attributes
* @param connectionToken a connectionToken to register the instruction for. * @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 * @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 */ * @return A Result refer to a void marker or to an error if happened */
Result<Void> registerInstruction( Result<Void> registerInstruction(
@ -98,6 +99,7 @@ public interface SEBClientInstructionService {
InstructionType type, InstructionType type,
Map<String, String> attributes, Map<String, String> attributes,
String connectionToken, String connectionToken,
boolean checkActive,
boolean needsConfirm); boolean needsConfirm);
/** Used to register a SEB client instruction for one or more active client connections /** 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 /** Get a SEB instruction for the specified SEB Client connection or null of there
* is currently no SEB instruction in the queue. * is currently no SEB instruction in the queue.
* * <p>
* NOTE: If this call returns a SEB instruction instance, this instance is considered * 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 * @param connectionToken the SEB Client connection token
* @return SEB instruction to sent to the SEB Client or null */ * @return SEB instruction to sent to the SEB Client or null */
@ -131,7 +133,7 @@ public interface SEBClientInstructionService {
* @param instructionConfirm the instruction confirm identifier */ * @param instructionConfirm the instruction confirm identifier */
void confirmInstructionDone(String connectionToken, String instructionConfirm); 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(); void cleanupInstructions();
} }

View file

@ -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 */ /** Used to update the app signature key grants of all active SEB connections that miss a grant */
void updateASKGrants(); void updateASKGrants();
/** Used to cleanup old instructions from the persistent storage */ /** Used to clean up old instructions from the persistent storage */
void cleanupInstructions(); void cleanupInstructions();
/** Notify a ping for a certain client connection. /** Notify a ping for a certain client connection.
@ -66,7 +66,7 @@ public interface SEBClientSessionService extends ExamUpdateTask, SessionUpdateTa
* @param instructionConfirm the instruction confirm identifier */ * @param instructionConfirm the instruction confirm identifier */
void confirmInstructionDone(String connectionToken, String instructionConfirm); 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 * @param clientConnection The client connection values
* @return Result refer to ClientConnectionData instance containing the given clientConnection plus the indicator * @return Result refer to ClientConnectionData instance containing the given clientConnection plus the indicator

View file

@ -227,27 +227,30 @@ public class ExamSessionServiceImpl implements ExamSessionService {
updateExamCache(examId); updateExamCache(examId);
} }
final Exam exam = this.examSessionCacheService.getRunningExam(examId); return Result.tryCatch(() -> {
if (this.examSessionCacheService.isRunning(exam)) { final Exam exam = this.examSessionCacheService.getRunningExam(examId);
if (log.isTraceEnabled()) {
log.trace("Exam {} is running and cached", 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 @Override

View file

@ -8,22 +8,28 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.security.Principal; import java.security.Principal;
import java.util.Collection; import java.util.*;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; 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.Entity;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; 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.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.jni.Address;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service; 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.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo; 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.ClientConnectionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
@ -72,6 +77,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
private final DistributedIndicatorValueService distributedPingCache; private final DistributedIndicatorValueService distributedPingCache;
private final SecurityKeyService securityKeyService; private final SecurityKeyService securityKeyService;
private final SEBClientEventBatchService sebClientEventBatchService; private final SEBClientEventBatchService sebClientEventBatchService;
private final SEBClientInstructionService sebClientInstructionService;
private final JSONMapper jsonMapper;
private final boolean isDistributedSetup; private final boolean isDistributedSetup;
protected SEBClientConnectionServiceImpl( protected SEBClientConnectionServiceImpl(
@ -82,7 +89,9 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
final ClientIndicatorFactory clientIndicatorFactory, final ClientIndicatorFactory clientIndicatorFactory,
final SecurityKeyService securityKeyService, final SecurityKeyService securityKeyService,
final WebserviceInfo webserviceInfo, final WebserviceInfo webserviceInfo,
final SEBClientEventBatchService sebClientEventBatchService) { final SEBClientEventBatchService sebClientEventBatchService,
final SEBClientInstructionService sebClientInstructionService,
final JSONMapper jsonMapper) {
this.examSessionService = examSessionService; this.examSessionService = examSessionService;
this.examSessionCacheService = examSessionService.getExamSessionCacheService(); this.examSessionCacheService = examSessionService.getExamSessionCacheService();
@ -94,6 +103,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
this.securityKeyService = securityKeyService; this.securityKeyService = securityKeyService;
this.isDistributedSetup = webserviceInfo.isDistributed(); this.isDistributedSetup = webserviceInfo.isDistributed();
this.sebClientEventBatchService = sebClientEventBatchService; this.sebClientEventBatchService = sebClientEventBatchService;
this.sebClientInstructionService = sebClientInstructionService;
this.jsonMapper = jsonMapper;
} }
@Override @Override
@ -143,29 +154,38 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
clientId); clientId);
} }
final String connectionToken = createToken();
if (examId != null) { if (examId != null) {
checkExamIntegrity( checkExamIntegrity(
examId, examId,
null,
institutionId, institutionId,
(principal != null) ? principal.getName() : "--", connectionToken,
clientAddress); clientAddress);
} }
final String updateUserSessionId = updateUserSessionId(
examId,
null,
clientId,
sebMachineName,
null);
// Create ClientConnection in status CONNECTION_REQUESTED for further processing // Create ClientConnection in status CONNECTION_REQUESTED for further processing
final String connectionToken = createToken();
final ClientConnection clientConnection = this.clientConnectionDAO.createNew(new ClientConnection( final ClientConnection clientConnection = this.clientConnectionDAO.createNew(new ClientConnection(
null, null,
institutionId, // Set the institution identifier that was checked against integrity before institutionId,
examId, // Set the exam identifier if available otherwise it is null examId,
ConnectionStatus.CONNECTION_REQUESTED, // Initial state ConnectionStatus.CONNECTION_REQUESTED,
connectionToken, // The generated connection token that identifies this connection connectionToken,
null, updateUserSessionId,
(clientAddress != null) ? clientAddress : Constants.EMPTY_NOTE, // The IP address of the connecting client, verified on SEB Server side (clientAddress != null) ? clientAddress : Constants.EMPTY_NOTE,
sebOsName, sebOsName,
sebMachineName, sebMachineName,
sebVersion, sebVersion,
clientId, // The client identifier sent by the SEB client if available clientId,
clientConfig.vdiType != VDIType.NO, // The VDI flag to indicate if this is a VDI prime connection clientConfig.vdiType != VDIType.NO,
null, null,
null, null,
null, null,
@ -218,83 +238,58 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
ClientConnection clientConnection = getClientConnection(connectionToken); final ClientConnection clientConnection = getClientConnection(connectionToken);
final Long _examId = clientConnection.examId != null ? clientConnection.examId : examId;
checkInstitutionalIntegrity(institutionId, clientConnection); checkInstitutionalIntegrity(institutionId, clientConnection);
checkExamIntegrity(examId, clientConnection); connectionStatusIntegrityCheck(clientConnection, clientAddress);
checkExamIntegrity(examId, clientConnection.examId, institutionId, clientConnection.connectionToken, clientConnection.info);
// 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);
}
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug( log.debug(
"SEB client connection, update ClientConnection for " "SEB client connection, update ClientConnection for connectionToken {} institutionId: {} exam: {} client address: {} userSessionId: {} clientId: {}",
+ "connectionToken {} " connectionToken, institutionId, examId, clientAddress, userSessionId, clientId);
+ "institutionId: {}"
+ "exam: {} "
+ "client address: {} "
+ "userSessionId: {}"
+ "clientId: {}",
connectionToken,
institutionId,
examId,
clientAddress,
userSessionId,
clientId);
} }
// userSessionId integrity check final String updateUserSessionId = updateUserSessionId(
clientConnection = updateUserSessionId(userSessionId, clientConnection, examId); _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 final ClientConnection updatedClientConnection = this.clientConnectionDAO
.save(new ClientConnection( .save(updateConnection)
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))
.getOrThrow(); .getOrThrow();
// initialize distributed indicator value caches if possible and needed // initialize distributed indicator value caches if possible and needed
@ -332,62 +327,31 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
return Result.tryCatch(() -> { 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 connectionStatusIntegrityCheck(clientConnection, clientAddress);
if (!clientConnection.status.clientActiveStatus) { checkInstitutionalIntegrity(institutionId, clientConnection);
log.warn("ClientConnection integrity violation: client connection is not in expected state: {}", checkExamIntegrity(
clientConnection); examId,
throw new IllegalArgumentException( clientConnection.examId,
"ClientConnection integrity violation: client connection is not in expected state"); institutionId,
} clientConnection.connectionToken,
clientConnection.info);
// 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);
}
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug( log.debug(
"SEB client connection, establish ClientConnection for " "SEB client connection, establish ClientConnection for connectionToken {} institutionId: {} exam: {} client address: {} userSessionId: {} clientId: {}",
+ "connectionToken {} " connectionToken, institutionId, examId, clientAddress, userSessionId, clientId);
+ "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 final Boolean proctoringEnabled = this.examAdminService
.isProctoringEnabled(clientConnection.examId) .isProctoringEnabled(clientConnection.examId)
.getOr(false); .getOr(false);
@ -407,12 +371,12 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
currentExamId, currentExamId,
ConnectionStatus.ACTIVE, ConnectionStatus.ACTIVE,
null, null,
clientConnection.userSessionId, updateUserSessionId,
StringUtils.isNoneBlank(clientAddress) ? clientAddress : null, StringUtils.isNotBlank(clientAddress) ? clientAddress : null,
StringUtils.isNoneBlank(sebOsName) ? sebOsName : null, StringUtils.isNotBlank(sebOsName) ? sebOsName : null,
StringUtils.isNoneBlank(sebMachineName) ? sebMachineName : null, StringUtils.isNotBlank(sebMachineName) ? sebMachineName : null,
StringUtils.isNoneBlank(sebVersion) ? sebVersion : null, StringUtils.isNotBlank(sebVersion) ? sebVersion : null,
StringUtils.isNoneBlank(clientId) ? clientId : null, StringUtils.isNotBlank(clientId) ? clientId : null,
null, null,
null, null,
null, null,
@ -422,10 +386,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
null, null,
proctoringEnabled, proctoringEnabled,
null, null,
getSignatureHash( getSignatureHash(appSignatureKey, connectionToken, _examId),
appSignatureKey,
connectionToken,
clientConnection.examId != null ? clientConnection.examId : examId),
null); null);
// ClientConnection integrity check // ClientConnection integrity check
@ -445,22 +406,10 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
throw new IllegalStateException("ClientConnection integrity violation"); 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 final ClientConnection updatedClientConnection = this.clientConnectionDAO
.save(establishedClientConnection) .save(establishedClientConnection)
.getOrThrow(); .getOrThrow();
// check exam integrity for established connection
checkExamIntegrity(
establishedClientConnection.examId,
institutionId,
establishedClientConnection.userSessionId,
establishedClientConnection.clientAddress);
// initialize distributed indicator value caches if possible and needed // initialize distributed indicator value caches if possible and needed
if (examId != null && this.isDistributedSetup) { if (examId != null && this.isDistributedSetup) {
this.clientIndicatorFactory.initializeDistributedCaches(clientConnection); 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 @Override
public Result<ClientConnection> closeConnection( public Result<ClientConnection> closeConnection(
final String connectionToken, final String connectionToken,
@ -605,10 +494,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
.getConnectionData(connectionToken) .getConnectionData(connectionToken)
.getOrThrow(); .getOrThrow();
// An active connection can only be disabled if we have a missing ping // A connection can only be disabled if we have a missing ping
if (connectionData.clientConnection.status == ConnectionStatus.ACTIVE && if (!BooleanUtils.isTrue(connectionData.getMissingPing())) {
!BooleanUtils.isTrue(connectionData.getMissingPing())) {
return connectionData.clientConnection; return connectionData.clientConnection;
} }
@ -662,46 +549,162 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
@Override @Override
public Result<Collection<EntityKey>> disableConnections(final String[] connectionTokens, final Long institutionId) { public Result<Collection<EntityKey>> disableConnections(final String[] connectionTokens, final Long institutionId) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> Stream.of(connectionTokens)
.map(token -> disableConnection(token, institutionId)
return Stream.of(connectionTokens) .onError(error -> log.error("Failed to disable SEB client connection: {}", token))
.map(token -> disableConnection(token, institutionId) .getOr(null))
.onError(error -> log.error("Failed to disable SEB client connection: {}", token)) .filter(Objects::nonNull)
.getOr(null)) .map(Entity::getEntityKey)
.filter(Objects::nonNull) .collect(Collectors.toList()));
.map(Entity::getEntityKey)
.collect(Collectors.toList());
});
} }
// SEBSERV-475 IP address change during handshake is possible but is logged within SEB logs public void streamExamConfig(
private void sebLogClientAddressMismatch( final Long institutionId,
final String clientAddress, final Long examId,
final ClientConnection clientConnection) { final String connectionToken,
final String ipAddress,
final HttpServletResponse response) {
try { try {
final long now = Utils.getMillisecondsNow();
this.sebClientEventBatchService.accept(new SEBClientEventBatchService.EventData( // if an examId is provided with the request, update the connection first
clientConnection.connectionToken, if (examId != null) {
now, final ClientConnection connection = this.updateClientConnection(
new ClientEvent( connectionToken,
null, institutionId,
clientConnection.id, examId,
ClientEvent.EventType.WARN_LOG, null,
now, now, null, null,
"SEB Client IP address changed: " + null,
clientConnection.clientAddress + null,
" -> " + null,
clientAddress 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) { } 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)) { 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(); 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) { private String updateUserSessionId(
if (examId != null && final Long examId,
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(
final String userSessionId, final String userSessionId,
ClientConnection clientConnection, final String clientId,
final Long examId) { final String sebMachineName,
final ClientConnection clientConnection) {
if (StringUtils.isNoneBlank(userSessionId)) { try {
if (StringUtils.isNoneBlank(clientConnection.userSessionId)) {
if (clientConnection.userSessionId.contains(userSessionId)) { if (clientConnection == null) {
if (log.isDebugEnabled()) { if (StringUtils.isNotBlank(clientId)) {
log.debug("SEB sent LMS userSessionId but clientConnection has already a userSessionId"); 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) if (examId == null) {
String accountId = userSessionId; return null;
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());
} }
// create new ClientConnection for update // we got a userSessionId, so we want to resolve it via LMS binding
final ClientConnection authenticatedClientConnection = new ClientConnection( final String accountId = this.examSessionService
clientConnection.id, .getRunningExam(examId)
null, null, .flatMap(exam -> this.examSessionService
ConnectionStatus.AUTHENTICATED, null, .getLmsAPIService()
accountId, .getLmsAPITemplate(exam.lmsSetupId))
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); .map(template -> template.getExamineeName(userSessionId))
.getOr(userSessionId);
clientConnection = this.clientConnectionDAO // if userSessionId is not set yet or a placeholder is set, just use the new account name
.save(authenticatedClientConnection) if (clientConnection.userSessionId == null ||
.getOrThrow(); 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( private void checkExamIntegrity(
final Long examId, final Long examId,
final Long currentExamId,
final Long institutionId, final Long institutionId,
final String user, final String ccToken,
final String address) { final String ccInfo) {
if (examId == null) {
return;
}
if (this.isDistributedSetup) { 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); final Result<Exam> updateExamCache = this.examSessionService.updateExamCache(examId);
if (updateExamCache.hasError()) { if (updateExamCache.hasError()) {
log.warn("Failed to update Exam-Cache for Exam: {}", examId); 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 // check Exam is running and not locked
checkExamRunning(examId, user, address); checkExamRunning(examId, ccToken, ccInfo);
if (this.examSessionService.isExamLocked(examId)) { if (this.examSessionService.isExamLocked(examId)) {
throw new APIConstraintViolationException( throw new APIConstraintViolationException(
"Exam is currently on update and locked for new SEB Client connections"); "Exam is currently on update and locked for new SEB Client connections");

View file

@ -103,34 +103,36 @@ public class SEBClientInstructionServiceImpl implements SEBClientInstructionServ
final InstructionType type, final InstructionType type,
final Map<String, String> attributes, final Map<String, String> attributes,
final String connectionToken, final String connectionToken,
final boolean checkActive,
final boolean needsConfirm) { final boolean needsConfirm) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final boolean isActive = this.clientConnectionDAO if (checkActive && !this.clientConnectionDAO
.isInInstructionStatus(examId, connectionToken) .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( 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); 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);
} }
}); });
} }

View file

@ -154,7 +154,9 @@ public class SEBClientNotificationServiceImpl implements SEBClientNotificationSe
InstructionType.NOTIFICATION_CONFIRM, InstructionType.NOTIFICATION_CONFIRM,
connectionToken, connectionToken,
attributes); attributes);
this.sebClientInstructionService.registerInstruction(clientInstruction); this.sebClientInstructionService
.registerInstruction(clientInstruction)
.onError(error -> log.error("Failed to confirm instruction SEB client side: ", error));
return notification; return notification;
} }

View file

@ -8,6 +8,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -15,6 +16,8 @@ import java.util.concurrent.ScheduledFuture;
import javax.annotation.PreDestroy; 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.apache.commons.lang3.StringUtils;
import org.ehcache.impl.internal.concurrent.ConcurrentHashMap; import org.ehcache.impl.internal.concurrent.ConcurrentHashMap;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -42,7 +45,6 @@ public class SEBClientPingBatchService implements SEBClientPingService {
private final ExamSessionCacheService examSessionCacheService; private final ExamSessionCacheService examSessionCacheService;
private final SEBClientInstructionService sebClientInstructionService; private final SEBClientInstructionService sebClientInstructionService;
private final Set<String> pingKeys = new HashSet<>(); private final Set<String> pingKeys = new HashSet<>();
private final Map<String, String> pings = new ConcurrentHashMap<>(); private final Map<String, String> pings = new ConcurrentHashMap<>();
private final Map<String, String> instructions = new ConcurrentHashMap<>(); private final Map<String, String> instructions = new ConcurrentHashMap<>();
@ -69,7 +71,6 @@ public class SEBClientPingBatchService implements SEBClientPingService {
try { try {
this.pingKeys.clear(); this.pingKeys.clear();
this.pingKeys.addAll(this.pings.keySet()); this.pingKeys.addAll(this.pings.keySet());
//final Set<String> connections = new HashSet<>(this.pings.keySet());
this.pingKeys.stream().forEach(cid -> processPing( this.pingKeys.stream().forEach(cid -> processPing(
cid, cid,
this.pings.remove(cid), this.pings.remove(cid),
@ -119,11 +120,23 @@ public class SEBClientPingBatchService implements SEBClientPingService {
return; return;
} }
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService final ClientConnectionDataInternal connectionData = this.examSessionCacheService
.getClientConnection(connectionToken); .getClientConnection(connectionToken);
if (activeClientConnection != null) { if (connectionData != null) {
activeClientConnection.notifyPing(timestamp); 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 { } else {
log.error("Failed to get ClientConnectionDataInternal for: {}", connectionToken); log.error("Failed to get ClientConnectionDataInternal for: {}", connectionToken);
} }

View file

@ -8,6 +8,10 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; 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.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -50,11 +54,23 @@ public class SEBClientPingBlockingService implements SEBClientPingService {
return null; return null;
} }
final ClientConnectionDataInternal activeClientConnection = this.examSessionCacheService final ClientConnectionDataInternal connectionData = this.examSessionCacheService
.getClientConnection(connectionToken); .getClientConnection(connectionToken);
if (activeClientConnection != null) { if (connectionData != null) {
activeClientConnection.notifyPing(Utils.getMillisecondsNow()); 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 { } else {
log.error("Failed to get ClientConnectionDataInternal for: {}", connectionToken); log.error("Failed to get ClientConnectionDataInternal for: {}", connectionToken);
} }

View file

@ -653,6 +653,7 @@ public class RemoteProctoringRoomServiceImpl implements RemoteProctoringRoomServ
InstructionType.SEB_RECONFIGURE_SETTINGS, InstructionType.SEB_RECONFIGURE_SETTINGS,
attributes, attributes,
connectionToken, connectionToken,
true,
true) true)
.onError(error -> log.error( .onError(error -> log.error(
"Failed to register reconfiguring instruction for connection: {}", "Failed to register reconfiguring instruction for connection: {}",
@ -852,6 +853,7 @@ public class RemoteProctoringRoomServiceImpl implements RemoteProctoringRoomServ
InstructionType.SEB_PROCTORING, InstructionType.SEB_PROCTORING,
attributes, attributes,
connectionToken, connectionToken,
true,
true) true)
.onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error)) .onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error))
.getOrThrow(); .getOrThrow();

View file

@ -476,6 +476,7 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
InstructionType.SEB_PROCTORING, InstructionType.SEB_PROCTORING,
attributes, attributes,
ccRecord.getConnectionToken(), ccRecord.getConnectionToken(),
true,
true) true)
.onError(error -> log.error( .onError(error -> log.error(
"Failed to register screen proctoring join instruction for SEB connection: {}", "Failed to register screen proctoring join instruction for SEB connection: {}",

View file

@ -524,6 +524,7 @@ public class ZoomProctoringService implements RemoteProctoringService {
InstructionType.SEB_PROCTORING, InstructionType.SEB_PROCTORING,
attributes, attributes,
connectionToken, connectionToken,
true,
true) true)
.onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error)); .onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error));
} }

View file

@ -210,7 +210,7 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
} }
@ExceptionHandler(AccessDeniedException.class) @ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<Object> handleUnexpected( public ResponseEntity<Object> handleAccessDenied(
final AccessDeniedException ex, final AccessDeniedException ex,
final WebRequest request) { final WebRequest request) {

View file

@ -14,6 +14,7 @@ import java.security.Principal;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -136,7 +137,7 @@ public class ExamAPI_V1_Controller {
clientConnection.connectionToken); clientConnection.connectionToken);
// Crate list of running exams // Crate list of running exams
List<RunningExamInfo> result; final List<RunningExamInfo> result;
if (examId == null) { if (examId == null) {
result = this.examSessionService.getRunningExamsForInstitution(institutionId) result = this.examSessionService.getRunningExamsForInstitution(institutionId)
.getOrThrow() .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_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_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_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( @RequestParam(
name = API.EXAM_API_PARAM_SIGNATURE_KEY, name = API.EXAM_API_PARAM_SIGNATURE_KEY,
required = false) final String browserSignatureKey, required = false) final String browserSignatureKey,
@ -212,7 +213,7 @@ public class ExamAPI_V1_Controller {
remoteAddr, remoteAddr,
sebVersion, sebVersion,
sebOSName, sebOSName,
sebMachinName, sebMachineName,
userSessionId, userSessionId,
clientId, clientId,
browserSignatureKey) browserSignatureKey)
@ -315,8 +316,22 @@ public class ExamAPI_V1_Controller {
final HttpServletRequest request, final HttpServletRequest request,
final HttpServletResponse response) { 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( return CompletableFuture.runAsync(
() -> streamExamConfig(connectionToken, formParams, principal, response), () -> this.sebClientConnectionService.streamExamConfig(
getInstitutionId(principal),
_examId,
connectionToken,
remoteAddr,
response),
this.executor); this.executor);
} }
@ -328,7 +343,6 @@ public class ExamAPI_V1_Controller {
public void ping(final HttpServletRequest request, final HttpServletResponse response) { public void ping(final HttpServletRequest request, final HttpServletResponse response) {
final String connectionToken = request.getHeader(API.EXAM_API_SEB_CONNECTION_TOKEN); 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 instructionConfirm = request.getParameter(API.EXAM_API_PING_INSTRUCTION_CONFIRM);
final String instruction = this.sebClientSessionService final String instruction = this.sebClientSessionService
@ -373,73 +387,7 @@ public class ExamAPI_V1_Controller {
.getOr(null)); .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) { private String getClientAddress(final HttpServletRequest request) {
try { try {

View file

@ -463,6 +463,7 @@ public class ExamMonitoringController {
checkPrivileges(institutionId, examId); checkPrivileges(institutionId, examId);
// TODO do this in async in new thread
if (connectionToken.contains(Constants.LIST_SEPARATOR)) { 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 // If we have a bunch of client connections to disable, make it asynchronously and respond to the caller immediately
this.executor.execute(() -> { this.executor.execute(() -> {
@ -563,8 +564,8 @@ public class ExamMonitoringController {
final EnumSet<ConnectionStatus> filterStates = EnumSet.noneOf(ConnectionStatus.class); final EnumSet<ConnectionStatus> filterStates = EnumSet.noneOf(ConnectionStatus.class);
if (StringUtils.isNotBlank(hiddenStates)) { if (StringUtils.isNotBlank(hiddenStates)) {
final String[] split = StringUtils.split(hiddenStates, Constants.LIST_SEPARATOR); final String[] split = StringUtils.split(hiddenStates, Constants.LIST_SEPARATOR);
for (int i = 0; i < split.length; i++) { for (final String s : split) {
filterStates.add(ConnectionStatus.valueOf(split[i])); filterStates.add(ConnectionStatus.valueOf(s));
} }
} }
@ -583,8 +584,8 @@ public class ExamMonitoringController {
if (StringUtils.isNotBlank(hiddenClientGroups)) { if (StringUtils.isNotBlank(hiddenClientGroups)) {
filterClientGroups = new HashSet<>(); filterClientGroups = new HashSet<>();
final String[] split = StringUtils.split(hiddenClientGroups, Constants.LIST_SEPARATOR); final String[] split = StringUtils.split(hiddenClientGroups, Constants.LIST_SEPARATOR);
for (int i = 0; i < split.length; i++) { for (final String s : split) {
filterClientGroups.add(Long.parseLong(split[i])); filterClientGroups.add(Long.parseLong(s));
} }
} }

View file

@ -21,7 +21,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService;
/** A ClientDetailsService to manage different API clients of SEB Server webservice API. /** 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 * Currently supporting two client types for the two different API's on
* SEB Server webservice; * SEB Server webservice;
* - Administration API for administrative purpose using password grant type with refresh token * - 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. /** 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. * 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 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. * If there is a match, a ClientDetails object is created from LMSSetup and returned.

View file

@ -2437,7 +2437,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
connections = connectionsCall.get(); connections = connectionsCall.get();
assertFalse(connections.isEmpty()); assertFalse(connections.isEmpty());
conData = connections.iterator().next(); conData = connections.iterator().next();
assertEquals("DISABLED", conData.clientConnection.status.name()); assertEquals("CLOSED", conData.clientConnection.status.name());
// get client logs // get client logs
final Result<Page<ExtendedClientEvent>> clientLogPage = restService final Result<Page<ExtendedClientEvent>> clientLogPage = restService
@ -2520,7 +2520,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
assertFalse(ccDataPage.content.isEmpty()); assertFalse(ccDataPage.content.isEmpty());
final ClientConnectionData clientConnectionData = ccDataPage.content.get(0); final ClientConnectionData clientConnectionData = ccDataPage.content.get(0);
assertNotNull(clientConnectionData); assertNotNull(clientConnectionData);
assertEquals("DISABLED", clientConnectionData.clientConnection.status.toString()); assertEquals("CLOSED", clientConnectionData.clientConnection.status.toString());
connectionDatacall = restService connectionDatacall = restService
.getBuilder(GetFinishedExamClientConnectionPage.class) .getBuilder(GetFinishedExamClientConnectionPage.class)

View file

@ -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;
}
}

View file

@ -441,15 +441,15 @@ public class SebConnectionTest extends ExamAPIIntegrationTester {
.build() .build()
.execute(); .execute();
assertTrue(records.size() == 1); assertEquals(1, records.size());
final ClientConnectionRecord clientConnectionRecord = records.get(0); final ClientConnectionRecord clientConnectionRecord = records.get(0);
assertEquals("1", String.valueOf(clientConnectionRecord.getInstitutionId())); assertEquals("1", String.valueOf(clientConnectionRecord.getInstitutionId()));
assertEquals("2", String.valueOf(clientConnectionRecord.getExamId())); assertEquals("2", String.valueOf(clientConnectionRecord.getExamId()));
assertEquals("CLOSED", String.valueOf(clientConnectionRecord.getStatus())); assertEquals("CLOSED", String.valueOf(clientConnectionRecord.getStatus()));
assertNotNull(clientConnectionRecord.getConnectionToken()); assertNotNull(clientConnectionRecord.getConnectionToken());
assertNotNull(clientConnectionRecord.getClientAddress()); assertNotNull(clientConnectionRecord.getClientAddress());
assertNull(clientConnectionRecord.getExamUserSessionId()); assertNotNull(clientConnectionRecord.getExamUserSessionId());
assertTrue(clientConnectionRecord.getVdi() == 0); assertEquals(0, (int) clientConnectionRecord.getVdi());
assertNull(clientConnectionRecord.getVirtualClientAddress()); assertNull(clientConnectionRecord.getVirtualClientAddress());
// check cache after update // check cache after update

View file

@ -66,7 +66,7 @@ public class SEBClientInstructionServiceTest extends AdministrationAPIIntegratio
// register instruction // register instruction
this.sebClientInstructionService.registerInstruction( this.sebClientInstructionService.registerInstruction(
2L, InstructionType.SEB_QUIT, Collections.emptyMap(), "testToken", false); 2L, InstructionType.SEB_QUIT, Collections.emptyMap(), "testToken", true,false);
// check on DB // check on DB
all = this.clientInstructionDAO all = this.clientInstructionDAO
@ -100,7 +100,7 @@ public class SEBClientInstructionServiceTest extends AdministrationAPIIntegratio
public void testRegisterWithConfirm() { public void testRegisterWithConfirm() {
// register instruction // register instruction
this.sebClientInstructionService.registerInstruction( 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 // check on DB
Collection<ClientInstructionRecord> all = this.clientInstructionDAO Collection<ClientInstructionRecord> all = this.clientInstructionDAO
@ -142,7 +142,7 @@ public class SEBClientInstructionServiceTest extends AdministrationAPIIntegratio
attributes.put("attr1", "123"); attributes.put("attr1", "123");
attributes.put("attr2", "345"); attributes.put("attr2", "345");
this.sebClientInstructionService.registerInstruction( this.sebClientInstructionService.registerInstruction(
2L, InstructionType.SEB_RECONFIGURE_SETTINGS, attributes, "testToken", true); 2L, InstructionType.SEB_RECONFIGURE_SETTINGS, attributes, "testToken", true,true);
// check on DB // check on DB
Collection<ClientInstructionRecord> all = this.clientInstructionDAO Collection<ClientInstructionRecord> all = this.clientInstructionDAO