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,
"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;

View file

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

View file

@ -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),

View file

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

View file

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

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 */
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

View file

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

View file

@ -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");

View file

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

View file

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

View file

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

View file

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

View file

@ -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();

View file

@ -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: {}",

View file

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

View file

@ -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) {

View file

@ -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 {

View file

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

View file

@ -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.

View file

@ -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)

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()
.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

View file

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