refactored client connection (more integrity checks)

This commit is contained in:
anhefti 2019-09-02 10:20:51 +02:00
parent bdd2b668e5
commit edb751de87
8 changed files with 143 additions and 107 deletions

View file

@ -81,7 +81,7 @@ public final class ClientConnection implements GrantEntity {
@JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_IDENTIFER) final String userSessionId, @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_IDENTIFER) final String userSessionId,
@JsonProperty(Domain.CLIENT_CONNECTION.ATTR_CLIENT_ADDRESS) final String clientAddress, @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_CLIENT_ADDRESS) final String clientAddress,
@JsonProperty(Domain.CLIENT_CONNECTION.ATTR_VIRTUAL_CLIENT_ADDRESS) final String virtualClientAddress, @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_VIRTUAL_CLIENT_ADDRESS) final String virtualClientAddress,
@JsonProperty(Domain.CLIENT_CONNECTION.ATTR_CREATION_TIME) final Long creationTim) { @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_CREATION_TIME) final Long creationTime) {
this.id = id; this.id = id;
this.institutionId = institutionId; this.institutionId = institutionId;
@ -91,7 +91,7 @@ public final class ClientConnection implements GrantEntity {
this.userSessionId = userSessionId; this.userSessionId = userSessionId;
this.clientAddress = clientAddress; this.clientAddress = clientAddress;
this.virtualClientAddress = virtualClientAddress; this.virtualClientAddress = virtualClientAddress;
this.creationTime = creationTim; this.creationTime = creationTime;
} }
@Override @Override

View file

@ -8,36 +8,29 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.batch; package ch.ethz.seb.sebserver.webservice.servicelayer.batch;
import org.quartz.CronScheduleBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
public class BatchConfig { public class BatchConfig {
@Bean // @Bean
public JobDetail jobADetails() { // public JobDetail jobADetails() {
return JobBuilder // return JobBuilder
.newJob(SimpleBatchJob.class) // .newJob(SimpleBatchJob.class)
.withIdentity("sampleJobA") // .withIdentity("sampleJobA")
.build(); // .build();
} // }
//
@Bean // @Bean
public Trigger jobATrigger(final JobDetail jobADetails) { // public Trigger jobATrigger(final JobDetail jobADetails) {
//
return TriggerBuilder // return TriggerBuilder
.newTrigger() // .newTrigger()
.forJob(jobADetails) // .forJob(jobADetails)
.withIdentity("sampleTriggerA") // .withIdentity("sampleTriggerA")
//
.withSchedule(CronScheduleBuilder.cronSchedule("0/30 0 0 ? * * *")) // .withSchedule(CronScheduleBuilder.cronSchedule("0/30 0 0 ? * * *"))
.startNow() // .build();
.build(); // }
}
} }

View file

@ -15,11 +15,17 @@ import java.util.function.Predicate;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
/** A Service to handle running exam sessions */ /** A Service to handle running exam sessions */
public interface ExamSessionService { public interface ExamSessionService {
/** Get the underling ExamDAO service.
*
* @return the underling ExamDAO service. */
ExamDAO getExamDAO();
/** Indicates whether an Exam is currently running or not. /** Indicates whether an Exam is currently running or not.
* *
* @param examId the PK of the Exam to test * @param examId the PK of the Exam to test

View file

@ -8,6 +8,8 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session; package ch.ethz.seb.sebserver.webservice.servicelayer.session;
import java.security.Principal;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
@ -25,12 +27,14 @@ public interface SebClientConnectionService {
* A connection-token to identify the connection is generated and stored within the * A connection-token to identify the connection is generated and stored within the
* returned ClientConnection. * returned ClientConnection.
* *
* @param principal the client connection Principal from REST controller interface
* @param institutionId The institution identifier * @param institutionId The institution identifier
* @param clientAddress The clients remote IP address * @param clientAddress The clients remote IP address
* @param examId the exam identifier (can be null) * @param examId the exam identifier (can be null)
* @return A Result refer to the newly created ClientConnection in state: CONNECTION_REQUESTED, or refer to an error * @return A Result refer to the newly created ClientConnection in state: CONNECTION_REQUESTED, or refer to an error
* if happened */ * if happened */
Result<ClientConnection> createClientConnection( Result<ClientConnection> createClientConnection(
Principal principal,
Long institutionId, Long institutionId,
String clientAddress, String clientAddress,
Long examId); Long examId);

View file

@ -58,6 +58,11 @@ public class ExamSessionServiceImpl implements ExamSessionService {
this.cacheManager = cacheManager; this.cacheManager = cacheManager;
} }
@Override
public ExamDAO getExamDAO() {
return this.examDAO;
}
@Override @Override
public boolean isExamRunning(final Long examId) { public boolean isExamRunning(final Long examId) {
return !getRunningExam(examId).hasError(); return !getRunningExam(examId).hasError();
@ -199,7 +204,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
// evict also cached ping record // evict also cached ping record
this.examSessionCacheService.evictPingRecord(token); this.examSessionCacheService.evictPingRecord(token);
}); });
} catch (Exception e) { } catch (final Exception e) {
log.error("Unexpected error while trying to flush cache for exam: ", exam, e); log.error("Unexpected error while trying to flush cache for exam: ", exam, e);
} }
} }

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.security.Principal;
import java.util.UUID; import java.util.UUID;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -23,10 +24,12 @@ 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.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
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.session.EventHandlingStrategy; import ch.ethz.seb.sebserver.webservice.servicelayer.session.EventHandlingStrategy;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.PingHandlingStrategy; import ch.ethz.seb.sebserver.webservice.servicelayer.session.PingHandlingStrategy;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SebClientConnectionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SebClientConnectionService;
import ch.ethz.seb.sebserver.webservice.weblayer.api.APIConstraintViolationException;
@Lazy @Lazy
@Service @Service
@ -40,29 +43,51 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
private final EventHandlingStrategy eventHandlingStrategy; private final EventHandlingStrategy eventHandlingStrategy;
private final ClientConnectionDAO clientConnectionDAO; private final ClientConnectionDAO clientConnectionDAO;
private final PingHandlingStrategy pingHandlingStrategy; private final PingHandlingStrategy pingHandlingStrategy;
private final SebClientConfigDAO sebClientConfigDAO;
protected SebClientConnectionServiceImpl( protected SebClientConnectionServiceImpl(
final ExamSessionService examSessionService, final ExamSessionService examSessionService,
final ExamSessionCacheService examSessionCacheService, final ExamSessionCacheService examSessionCacheService,
final ClientConnectionDAO clientConnectionDAO, final ClientConnectionDAO clientConnectionDAO,
final EventHandlingStrategyFactory eventHandlingStrategyFactory, final EventHandlingStrategyFactory eventHandlingStrategyFactory,
final PingHandlingStrategyFactory pingHandlingStrategyFactory) { final PingHandlingStrategyFactory pingHandlingStrategyFactory,
final SebClientConfigDAO sebClientConfigDAO) {
this.examSessionService = examSessionService; this.examSessionService = examSessionService;
this.examSessionCacheService = examSessionCacheService; this.examSessionCacheService = examSessionCacheService;
this.clientConnectionDAO = clientConnectionDAO; this.clientConnectionDAO = clientConnectionDAO;
this.pingHandlingStrategy = pingHandlingStrategyFactory.get(); this.pingHandlingStrategy = pingHandlingStrategyFactory.get();
this.eventHandlingStrategy = eventHandlingStrategyFactory.get(); this.eventHandlingStrategy = eventHandlingStrategyFactory.get();
this.sebClientConfigDAO = sebClientConfigDAO;
} }
@Override @Override
public Result<ClientConnection> createClientConnection( public Result<ClientConnection> createClientConnection(
final Principal principal,
final Long institutionId, final Long institutionId,
final String clientAddress, final String clientAddress,
final Long examId) { final Long examId) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final Long clientsInstitution = getInstitutionId(principal);
if (!clientsInstitution.equals(institutionId)) {
log.error("Institutional integrity violation: requested institution: {} authenticated institution: {}",
institutionId,
clientsInstitution);
throw new APIConstraintViolationException("Institutional integrity violation");
}
if (log.isDebugEnabled()) {
log.debug("Request received on Exam Client Connection create endpoint: "
+ "institution: {} "
+ "exam: {} "
+ "client-address: {}",
institutionId,
examId,
clientAddress);
}
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("SEB client connection attempt, create ClientConnection for " log.debug("SEB client connection attempt, create ClientConnection for "
+ "instituion {} " + "instituion {} "
@ -131,21 +156,16 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
final ClientConnection clientConnection = getClientConnection(connectionToken); final ClientConnection clientConnection = getClientConnection(connectionToken);
checkInstitutionalIntegrity( checkInstitutionalIntegrity(institutionId, clientConnection);
institutionId, checkExamIntegrity(examId, clientConnection);
clientConnection);
// examId integrity check // connection integrity check
if (examId != null && if (clientConnection.status != ConnectionStatus.CONNECTION_REQUESTED) {
clientConnection.examId != null && log.error("ClientConnection integrity violation: client connection is not in expected state: {}",
!examId.equals(clientConnection.examId)) {
log.error("Exam integrity violation: another examId is already set for the connection: {}",
clientConnection); clientConnection);
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Exam integrity violation: another examId is already set for the connection"); "ClientConnection integrity violation: client connection is not in expected state");
} }
checkExamRunning(examId);
// userSessionId integrity check // userSessionId integrity check
if (userSessionId != null && if (userSessionId != null &&
@ -169,7 +189,7 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
clientConnection.id, clientConnection.id,
null, null,
examId, examId,
null, (userSessionId != null) ? ConnectionStatus.AUTHENTICATED : null,
null, null,
userSessionId, userSessionId,
null, null,
@ -219,20 +239,21 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
userSessionId); userSessionId);
} }
checkExamRunning(examId);
final ClientConnection clientConnection = getClientConnection(connectionToken); final ClientConnection clientConnection = getClientConnection(connectionToken);
checkInstitutionalIntegrity(institutionId, clientConnection);
checkExamIntegrity(examId, clientConnection);
checkInstitutionalIntegrity( // connection integrity check
institutionId, if (clientConnection.status == ConnectionStatus.CONNECTION_REQUESTED) {
// TODO discuss if we need a flag on exam domain level that indicates whether unauthenticated connection
// are allowed or not
log.warn("ClientConnection integrity warning: client connection is not authenticated: {}",
clientConnection); clientConnection);
} else if (clientConnection.status != ConnectionStatus.AUTHENTICATED) {
// Exam integrity log.error("ClientConnection integrity violation: client connection is not in expected state: {}",
if (clientConnection.examId != null && examId != null && !examId.equals(clientConnection.examId)) {
log.error("Exam integrity violation with examId: {} on clientConnection: {}",
examId,
clientConnection); clientConnection);
throw new IllegalAccessError("Exam integrity violation"); throw new IllegalArgumentException(
"ClientConnection integrity violation: client connection is not in expected state");
} }
final String virtualClientAddress = getVirtualClientAddress( final String virtualClientAddress = getVirtualClientAddress(
@ -240,6 +261,7 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
clientAddress, clientAddress,
clientConnection.clientAddress); clientConnection.clientAddress);
// create new ClientConnection for update
final ClientConnection establishedClientConnection = new ClientConnection( final ClientConnection establishedClientConnection = new ClientConnection(
clientConnection.id, clientConnection.id,
null, null,
@ -312,7 +334,9 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
.byConnectionToken(connectionToken) .byConnectionToken(connectionToken)
.getOrThrow(); .getOrThrow();
final ClientConnection updatedClientConnection = this.clientConnectionDAO.save(new ClientConnection( ClientConnection updatedClientConnection;
if (clientConnection.status != ConnectionStatus.CLOSED) {
updatedClientConnection = this.clientConnectionDAO.save(new ClientConnection(
clientConnection.id, clientConnection.id,
null, null,
null, null,
@ -327,6 +351,10 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
log.debug("SEB client connection: successfully closed ClientConnection: {}", log.debug("SEB client connection: successfully closed ClientConnection: {}",
clientConnection); clientConnection);
} }
} else {
log.warn("SEB client connection is already closed: {}", clientConnection);
updatedClientConnection = clientConnection;
}
// evict cached ClientConnection // evict cached ClientConnection
this.examSessionCacheService.evictClientConnection(connectionToken); this.examSessionCacheService.evictClientConnection(connectionToken);
@ -390,9 +418,10 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
return clientConnection; return clientConnection;
} }
private void checkInstitutionalIntegrity(final Long institutionId, final ClientConnection clientConnection) private void checkInstitutionalIntegrity(
throws IllegalAccessError { final Long institutionId,
// Institutional integrity final ClientConnection clientConnection) throws IllegalAccessError {
if (!institutionId.equals(clientConnection.institutionId)) { if (!institutionId.equals(clientConnection.institutionId)) {
log.error("Instituion integrity violation with institution: {} on clientConnection: {}", log.error("Instituion integrity violation with institution: {} on clientConnection: {}",
institutionId, institutionId,
@ -437,4 +466,23 @@ public class SebClientConnectionServiceImpl implements SebClientConnectionServic
.getType() == ExamType.VDI; .getType() == ExamType.VDI;
} }
private Long getInstitutionId(final Principal principal) {
final String clientId = principal.getName();
return this.sebClientConfigDAO.byClientName(clientId)
.getOrThrow().institutionId;
}
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);
}
} }

View file

@ -43,7 +43,6 @@ import ch.ethz.seb.sebserver.gbl.model.session.PingResponse;
import ch.ethz.seb.sebserver.gbl.model.session.RunningExamInfo; import ch.ethz.seb.sebserver.gbl.model.session.RunningExamInfo;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO;
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.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@ -56,7 +55,6 @@ public class ExamAPI_V1_Controller {
private static final Logger log = LoggerFactory.getLogger(ExamAPI_V1_Controller.class); private static final Logger log = LoggerFactory.getLogger(ExamAPI_V1_Controller.class);
private final ExamDAO examDAO;
private final LmsSetupDAO lmsSetupDAO; private final LmsSetupDAO lmsSetupDAO;
private final ExamSessionService examSessionService; private final ExamSessionService examSessionService;
private final SebClientConnectionService sebClientConnectionService; private final SebClientConnectionService sebClientConnectionService;
@ -64,14 +62,12 @@ public class ExamAPI_V1_Controller {
private final JSONMapper jsonMapper; private final JSONMapper jsonMapper;
protected ExamAPI_V1_Controller( protected ExamAPI_V1_Controller(
final ExamDAO examDAO,
final LmsSetupDAO lmsSetupDAO, final LmsSetupDAO lmsSetupDAO,
final ExamSessionService examSessionService, final ExamSessionService examSessionService,
final SebClientConnectionService sebClientConnectionService, final SebClientConnectionService sebClientConnectionService,
final SebClientConfigDAO sebClientConfigDAO, final SebClientConfigDAO sebClientConfigDAO,
final JSONMapper jsonMapper) { final JSONMapper jsonMapper) {
this.examDAO = examDAO;
this.lmsSetupDAO = lmsSetupDAO; this.lmsSetupDAO = lmsSetupDAO;
this.examSessionService = examSessionService; this.examSessionService = examSessionService;
this.sebClientConnectionService = sebClientConnectionService; this.sebClientConnectionService = sebClientConnectionService;
@ -101,25 +97,17 @@ public class ExamAPI_V1_Controller {
final Long examId = (examIdRequestParam != null) final Long examId = (examIdRequestParam != null)
? examIdRequestParam ? examIdRequestParam
: mapper.getLong(API.EXAM_API_PARAM_EXAM_ID); : mapper.getLong(API.EXAM_API_PARAM_EXAM_ID);
final Long clientsInstitution = getInstitutionId(principal);
if (!clientsInstitution.equals(institutionId)) { // Create and get new ClientConnection if all integrity checks passes
log.error("Institutional integrity violation: requested institution: {} authenticated institution: {}", final ClientConnection clientConnection = this.sebClientConnectionService
institutionId, .createClientConnection(principal, institutionId, remoteAddr, examId)
clientsInstitution); .getOrThrow();
throw new APIConstraintViolationException("Institutional integrity violation");
}
if (log.isDebugEnabled()) { response.setHeader(
log.debug("Request received on Exam Client Connection create endpoint: " API.EXAM_API_SEB_CONNECTION_TOKEN,
+ "institution: {} " clientConnection.connectionToken);
+ "exam: {} "
+ "client-address: {}",
institutionId,
examId,
remoteAddr);
}
// Crate list of running exams
List<RunningExamInfo> result; List<RunningExamInfo> result;
if (examId == null) { if (examId == null) {
result = this.examSessionService.getRunningExamsForInstitution(institutionId) result = this.examSessionService.getRunningExamsForInstitution(institutionId)
@ -128,7 +116,7 @@ public class ExamAPI_V1_Controller {
.map(this::createRunningExamInfo) .map(this::createRunningExamInfo)
.collect(Collectors.toList()); .collect(Collectors.toList());
} else { } else {
final Exam exam = this.examDAO.byPK(examId) final Exam exam = this.examSessionService.getExamDAO().byPK(examId)
.getOrThrow(); .getOrThrow();
result = Arrays.asList(createRunningExamInfo(exam)); result = Arrays.asList(createRunningExamInfo(exam));
@ -139,25 +127,9 @@ public class ExamAPI_V1_Controller {
throw new IllegalStateException("There are no currently running exams"); throw new IllegalStateException("There are no currently running exams");
} }
final ClientConnection clientConnection = this.sebClientConnectionService
.createClientConnection(institutionId, remoteAddr, examId)
.getOrThrow();
response.setHeader(
API.EXAM_API_SEB_CONNECTION_TOKEN,
clientConnection.connectionToken);
return result; return result;
} }
private RunningExamInfo createRunningExamInfo(final Exam exam) {
return new RunningExamInfo(
exam,
this.lmsSetupDAO.byPK(exam.lmsSetupId)
.map(lms -> lms.lmsType)
.getOr(null));
}
@RequestMapping( @RequestMapping(
path = API.EXAM_API_HANDSHAKE_ENDPOINT, path = API.EXAM_API_HANDSHAKE_ENDPOINT,
method = RequestMethod.PATCH, method = RequestMethod.PATCH,
@ -352,4 +324,12 @@ public class ExamAPI_V1_Controller {
.getOrThrow().institutionId; .getOrThrow().institutionId;
} }
private RunningExamInfo createRunningExamInfo(final Exam exam) {
return new RunningExamInfo(
exam,
this.lmsSetupDAO.byPK(exam.lmsSetupId)
.map(lms -> lms.lmsType)
.getOr(null));
}
} }

View file

@ -216,7 +216,7 @@ public class SebConnectionTest extends ExamAPIIntegrationTester {
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("CONNECTION_REQUESTED", String.valueOf(clientConnectionRecord.getStatus())); assertEquals("AUTHENTICATED", String.valueOf(clientConnectionRecord.getStatus()));
assertNotNull(clientConnectionRecord.getConnectionToken()); assertNotNull(clientConnectionRecord.getConnectionToken());
assertNotNull(clientConnectionRecord.getClientAddress()); assertNotNull(clientConnectionRecord.getClientAddress());
assertEquals("userSessionId", clientConnectionRecord.getExamUserSessionIdentifer()); assertEquals("userSessionId", clientConnectionRecord.getExamUserSessionIdentifer());