SEBSERV-372 implementation

This commit is contained in:
anhefti 2022-12-19 09:26:08 +01:00
parent cfb02143cf
commit df13b4dff1
11 changed files with 146 additions and 81 deletions

View file

@ -108,6 +108,8 @@ public final class API {
public static final String EXAM_API_EXAM_SIGNATURE_SALT_HEADER = "SEBExamSalt"; public static final String EXAM_API_EXAM_SIGNATURE_SALT_HEADER = "SEBExamSalt";
public static final String EXAM_API_EXAM_ALT_BEK = "SEBServerBEK";
public static final String EXAM_API_USER_SESSION_ID = "seb_user_session_id"; public static final String EXAM_API_USER_SESSION_ID = "seb_user_session_id";
public static final String EXAM_API_HANDSHAKE_ENDPOINT = "/handshake"; public static final String EXAM_API_HANDSHAKE_ENDPOINT = "/handshake";

View file

@ -69,8 +69,10 @@ public final class Exam implements GrantEntity {
public static final String ADDITIONAL_ATTR_NUMERICAL_TRUST_THRESHOLD = "NUMERICAL_TRUST_THRESHOLD"; public static final String ADDITIONAL_ATTR_NUMERICAL_TRUST_THRESHOLD = "NUMERICAL_TRUST_THRESHOLD";
/** This attribute name is used to store the signature check encryption certificate is one is used */ /** This attribute name is used to store the signature check encryption certificate is one is used */
public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_CERT_ALIAS = "SIGNATURE_KEY_CERT_ALIAS"; public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_CERT_ALIAS = "SIGNATURE_KEY_CERT_ALIAS";
/** This attribute name is used to store the per exam generated app-signature-key encryption salt */
public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_SALT = "SIGNATURE_KEY_SALT"; public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_SALT = "SIGNATURE_KEY_SALT";
/** This attribute name is used to store the per Moolde(plugin) exam generated alternative BEK */
public static final String ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK = "ALTERNATIVE_SEB_BEK";
public enum ExamStatus { public enum ExamStatus {
UP_COMING, UP_COMING,

View file

@ -209,6 +209,9 @@ public final class Result<T> {
if (result instanceof Result) { if (result instanceof Result) {
throw new IllegalArgumentException("Use flatMap instead!"); throw new IllegalArgumentException("Use flatMap instead!");
} }
if (result == null) {
return Result.ofError(new RuntimeException("Map to null value."));
}
return Result.of(result); return Result.of(result);
} catch (final Exception e) { } catch (final Exception e) {
return Result.ofError(e); return Result.ofError(e);

View file

@ -26,7 +26,6 @@ import java.util.stream.Collectors;
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.mybatis.dynamic.sql.update.UpdateDSL; import org.mybatis.dynamic.sql.update.UpdateDSL;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.security.crypto.keygen.KeyGenerators; import org.springframework.security.crypto.keygen.KeyGenerators;
@ -67,23 +66,17 @@ public class ExamDAOImpl implements ExamDAO {
private final ExamRecordDAO examRecordDAO; private final ExamRecordDAO examRecordDAO;
private final ApplicationEventPublisher applicationEventPublisher; private final ApplicationEventPublisher applicationEventPublisher;
private final AdditionalAttributesDAO additionalAttributesDAO; private final AdditionalAttributesDAO additionalAttributesDAO;
private final boolean appSignatureKeyEnabled;
private final int defaultNumericalTrustThreshold;
public ExamDAOImpl( public ExamDAOImpl(
final ExamRecordMapper examRecordMapper, final ExamRecordMapper examRecordMapper,
final ExamRecordDAO examRecordDAO, final ExamRecordDAO examRecordDAO,
final ApplicationEventPublisher applicationEventPublisher, final ApplicationEventPublisher applicationEventPublisher,
final AdditionalAttributesDAO additionalAttributesDAO, final AdditionalAttributesDAO additionalAttributesDAO) {
final @Value("${sebserver.webservice.api.admin.exam.app.signature.key.enabled:false}") boolean appSignatureKeyEnabled,
final @Value("${sebserver.webservice.api.admin.exam.app.signature.key.numerical.threshold:2}") int defaultNumericalTrustThreshold) {
this.examRecordMapper = examRecordMapper; this.examRecordMapper = examRecordMapper;
this.examRecordDAO = examRecordDAO; this.examRecordDAO = examRecordDAO;
this.applicationEventPublisher = applicationEventPublisher; this.applicationEventPublisher = applicationEventPublisher;
this.additionalAttributesDAO = additionalAttributesDAO; this.additionalAttributesDAO = additionalAttributesDAO;
this.appSignatureKeyEnabled = appSignatureKeyEnabled;
this.defaultNumericalTrustThreshold = defaultNumericalTrustThreshold;
} }
@Override @Override
@ -189,7 +182,6 @@ public class ExamDAOImpl implements ExamDAO {
public Result<Exam> createNew(final Exam exam) { public Result<Exam> createNew(final Exam exam) {
return this.examRecordDAO return this.examRecordDAO
.createNew(exam) .createNew(exam)
.map(this::initAdditionalAttributes)
.flatMap(rec -> saveAdditionalAttributes(exam, rec)) .flatMap(rec -> saveAdditionalAttributes(exam, rec))
.flatMap(this::toDomainModel); .flatMap(this::toDomainModel);
} }
@ -765,30 +757,6 @@ public class ExamDAOImpl implements ExamDAO {
}); });
} }
private ExamRecord initAdditionalAttributes(final ExamRecord rec) {
try {
final Long examId = rec.getId();
this.additionalAttributesDAO.initAdditionalAttribute(
EntityType.EXAM,
examId,
Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED,
String.valueOf(this.appSignatureKeyEnabled));
this.additionalAttributesDAO.initAdditionalAttribute(
EntityType.EXAM,
examId,
Exam.ADDITIONAL_ATTR_NUMERICAL_TRUST_THRESHOLD,
String.valueOf(this.defaultNumericalTrustThreshold));
final CharSequence salt = KeyGenerators.string().generateKey();
this.additionalAttributesDAO.initAdditionalAttribute(
EntityType.EXAM,
examId,
Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_SALT, salt.toString());
} catch (final Exception e) {
log.error("Failed to init additional attributes: ", e);
}
return rec;
}
private QuizData saveAdditionalQuizAttributes(final Long examId, final QuizData quizData) { private QuizData saveAdditionalQuizAttributes(final Long examId, final QuizData quizData) {
final Map<String, String> additionalAttributes = new HashMap<>(quizData.getAdditionalAttributes()); final Map<String, String> additionalAttributes = new HashMap<>(quizData.getAdditionalAttributes());
if (StringUtils.isNotBlank(quizData.description)) { if (StringUtils.isNotBlank(quizData.description)) {

View file

@ -38,6 +38,12 @@ public interface ExamAdminService {
* @return Result refer to the domain object or to an error when happened */ * @return Result refer to the domain object or to an error when happened */
Result<Exam> examForPK(Long examId); Result<Exam> examForPK(Long examId);
/** Initializes initial additional attributes for a yet created exam.
*
* @param exam The exam that has been created
* @return The exam with the initial additional attributes */
Result<Exam> initAdditionalAttributes(final Exam exam);
/** Saves additional attributes for the exam that are specific to a type of LMS /** Saves additional attributes for the exam that are specific to a type of LMS
* *
* @param exam The Exam to add the LMS specific attributes * @param exam The Exam to add the LMS specific attributes

View file

@ -8,14 +8,19 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl; package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl;
import java.security.MessageDigest;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.UUID;
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.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -35,6 +40,7 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus;
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.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationNodeDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationNodeDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
@ -59,6 +65,8 @@ public class ExamAdminServiceImpl implements ExamAdminService {
private final ConfigurationNodeDAO configurationNodeDAO; private final ConfigurationNodeDAO configurationNodeDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO; private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final LmsAPIService lmsAPIService; private final LmsAPIService lmsAPIService;
private final boolean appSignatureKeyEnabled;
private final int defaultNumericalTrustThreshold;
protected ExamAdminServiceImpl( protected ExamAdminServiceImpl(
final ExamDAO examDAO, final ExamDAO examDAO,
@ -66,7 +74,9 @@ public class ExamAdminServiceImpl implements ExamAdminService {
final AdditionalAttributesDAO additionalAttributesDAO, final AdditionalAttributesDAO additionalAttributesDAO,
final ConfigurationNodeDAO configurationNodeDAO, final ConfigurationNodeDAO configurationNodeDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO, final ExamConfigurationMapDAO examConfigurationMapDAO,
final LmsAPIService lmsAPIService) { final LmsAPIService lmsAPIService,
final @Value("${sebserver.webservice.api.admin.exam.app.signature.key.enabled:false}") boolean appSignatureKeyEnabled,
final @Value("${sebserver.webservice.api.admin.exam.app.signature.key.numerical.threshold:2}") int defaultNumericalTrustThreshold) {
this.examDAO = examDAO; this.examDAO = examDAO;
this.proctoringServiceSettingsService = proctoringServiceSettingsService; this.proctoringServiceSettingsService = proctoringServiceSettingsService;
@ -74,6 +84,8 @@ public class ExamAdminServiceImpl implements ExamAdminService {
this.configurationNodeDAO = configurationNodeDAO; this.configurationNodeDAO = configurationNodeDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO; this.examConfigurationMapDAO = examConfigurationMapDAO;
this.lmsAPIService = lmsAPIService; this.lmsAPIService = lmsAPIService;
this.appSignatureKeyEnabled = appSignatureKeyEnabled;
this.defaultNumericalTrustThreshold = defaultNumericalTrustThreshold;
} }
@Override @Override
@ -81,6 +93,34 @@ public class ExamAdminServiceImpl implements ExamAdminService {
return this.examDAO.byPK(examId); return this.examDAO.byPK(examId);
} }
@Override
public Result<Exam> initAdditionalAttributes(final Exam exam) {
return Result.tryCatch(() -> {
final Long examId = exam.getId();
// initialize App-Signature-Key feature attributes
this.additionalAttributesDAO.initAdditionalAttribute(
EntityType.EXAM,
examId,
Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED,
String.valueOf(this.appSignatureKeyEnabled));
this.additionalAttributesDAO.initAdditionalAttribute(
EntityType.EXAM,
examId,
Exam.ADDITIONAL_ATTR_NUMERICAL_TRUST_THRESHOLD,
String.valueOf(this.defaultNumericalTrustThreshold));
this.additionalAttributesDAO.initAdditionalAttribute(
EntityType.EXAM,
examId,
Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_SALT,
KeyGenerators.string().generateKey().toString());
return exam;
});
}
@Override @Override
public Result<Exam> saveSecurityKeySettings( public Result<Exam> saveSecurityKeySettings(
final Long institutionId, final Long institutionId,
@ -148,7 +188,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
@Override @Override
public Result<Exam> saveLMSAttributes(final Exam exam) { public Result<Exam> saveLMSAttributes(final Exam exam) {
return saveAdditionalAttributesForMoodleExams(exam); return initAdditionalAttributesForMoodleExams(exam);
} }
@Override @Override
@ -208,7 +248,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
.getExamProctoringService(settings.serverType)); .getExamProctoringService(settings.serverType));
} }
private Result<Exam> saveAdditionalAttributesForMoodleExams(final Exam exam) { private Result<Exam> initAdditionalAttributesForMoodleExams(final Exam exam) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final LmsAPITemplate lmsTemplate = this.lmsAPIService final LmsAPITemplate lmsTemplate = this.lmsAPIService
.getLmsAPITemplate(exam.lmsSetupId) .getLmsAPITemplate(exam.lmsSetupId)
@ -221,7 +261,26 @@ public class ExamAdminServiceImpl implements ExamAdminService {
exam.id, exam.id,
QuizData.QUIZ_ATTR_NAME, QuizData.QUIZ_ATTR_NAME,
quizData.name)) quizData.name))
.getOrThrow(); .onError(error -> log.error("Failed to create additional moodle quiz name attribute: ", error));
}
if (lmsTemplate.lmsSetup().lmsType == LmsType.MOODLE_PLUGIN) {
// Save additional Browser Exam Key for Moodle plugin integration SEBSERV-372
try {
final String moodleBEKUUID = UUID.randomUUID().toString();
final MessageDigest hasher = MessageDigest.getInstance(Constants.SHA_256);
hasher.update(Utils.toByteArray(moodleBEKUUID));
final String moodleBEK = Hex.toHexString(hasher.digest());
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
exam.id,
Exam.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK,
moodleBEK).getOrThrow();
} catch (final Exception e) {
log.error("Failed to create additional moodle SEB BEK attribute: ", e);
}
} }
return exam; return exam;

View file

@ -343,7 +343,7 @@ public class SecurityKeyServiceImpl implements SecurityKeyService {
private String createSignatureHash(final CharSequence signature) { private String createSignatureHash(final CharSequence signature) {
try { try {
final MessageDigest hasher = MessageDigest.getInstance("SHA-256"); final MessageDigest hasher = MessageDigest.getInstance(Constants.SHA_256);
hasher.update(Utils.toByteArray(signature)); hasher.update(Utils.toByteArray(signature));
final String signatureHash = Hex.toHexString(hasher.digest()); final String signatureHash = Hex.toHexString(hasher.digest());
return signatureHash; return signatureHash;

View file

@ -63,6 +63,11 @@ public interface ExamSessionService {
* @return the underling LmsAPIService */ * @return the underling LmsAPIService */
LmsAPIService getLmsAPIService(); LmsAPIService getLmsAPIService();
/** Get the app-signature-key for the given exam.
* Ensures that if no app-signature-key exists already for the exam, a new on is created and stored
*
* @param examId The exam identifier
* @return App-Signature-Key value for the exam */
Result<String> getAppSignatureKeySalt(Long examId); Result<String> getAppSignatureKeySalt(Long examId);
/** Use this to check the consistency of a running Exam. /** Use this to check the consistency of a running Exam.

View file

@ -173,7 +173,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
this.clientIndicatorFactory.initializeDistributedCaches(clientConnection); this.clientIndicatorFactory.initializeDistributedCaches(clientConnection);
} }
// flash connection token cache for exam if available
if (examId != null) { if (examId != null) {
this.clientConnectionDAO.evictConnectionTokenCache(examId); this.clientConnectionDAO.evictConnectionTokenCache(examId);
} }

View file

@ -145,20 +145,15 @@ public class ExamAPI_V1_Controller {
.filter(this::checkConsistency) .filter(this::checkConsistency)
.collect(Collectors.toList()); .collect(Collectors.toList());
} else { } else {
final Exam exam = this.examSessionService final Exam exam = this.examSessionService
.getExamDAO() .getExamDAO()
.byPK(examId) .byPK(examId)
.getOrThrow(); .getOrThrow();
result = Arrays.asList(createRunningExamInfo(exam)); result = Arrays.asList(createRunningExamInfo(exam));
processASKSalt(response, clientConnection);
this.examSessionService processAlternativeBEK(response, clientConnection.examId);
.getAppSignatureKeySalt(clientConnection.examId)
.onSuccess(salt -> response.setHeader(API.EXAM_API_EXAM_SIGNATURE_SALT_HEADER, salt))
.onError(error -> log.error(
"Failed to get security key salt for connection: {}",
clientConnection,
error));
} }
if (result.isEmpty()) { if (result.isEmpty()) {
@ -209,27 +204,23 @@ public class ExamAPI_V1_Controller {
final String remoteAddr = this.getClientAddress(request); final String remoteAddr = this.getClientAddress(request);
final Long institutionId = getInstitutionId(principal); final Long institutionId = getInstitutionId(principal);
final ClientConnection clientConnection = this.sebClientConnectionService.updateClientConnection( final ClientConnection clientConnection = this.sebClientConnectionService
connectionToken, .updateClientConnection(
institutionId, connectionToken,
examId, institutionId,
remoteAddr, examId,
sebVersion, remoteAddr,
sebOSName, sebVersion,
sebMachinName, sebOSName,
userSessionId, sebMachinName,
clientId, userSessionId,
browserSignatureKey) clientId,
browserSignatureKey)
.getOrThrow(); .getOrThrow();
if (clientConnection.examId != null) { if (clientConnection.examId != null) {
this.examSessionService processASKSalt(response, clientConnection);
.getAppSignatureKeySalt(clientConnection.examId) processAlternativeBEK(response, clientConnection.examId);
.onSuccess(salt -> response.setHeader(API.EXAM_API_EXAM_SIGNATURE_SALT_HEADER, salt))
.onError(error -> log.error(
"Failed to get security key salt for connection: {}",
clientConnection,
error));
} }
}, },
this.executor); this.executor);
@ -251,7 +242,8 @@ public class ExamAPI_V1_Controller {
required = false) final String browserSignatureKey, required = false) final String browserSignatureKey,
@RequestParam(name = API.EXAM_API_PARAM_CLIENT_ID, required = false) final String clientId, @RequestParam(name = API.EXAM_API_PARAM_CLIENT_ID, required = false) final String clientId,
final Principal principal, final Principal principal,
final HttpServletRequest request) { final HttpServletRequest request,
final HttpServletResponse response) {
return CompletableFuture.runAsync( return CompletableFuture.runAsync(
() -> { () -> {
@ -259,18 +251,23 @@ public class ExamAPI_V1_Controller {
final String remoteAddr = this.getClientAddress(request); final String remoteAddr = this.getClientAddress(request);
final Long institutionId = getInstitutionId(principal); final Long institutionId = getInstitutionId(principal);
this.sebClientConnectionService.establishClientConnection( final ClientConnection clientConnection = this.sebClientConnectionService
connectionToken, .establishClientConnection(
institutionId, connectionToken,
examId, institutionId,
remoteAddr, examId,
sebVersion, remoteAddr,
sebOSName, sebVersion,
sebMachinName, sebOSName,
userSessionId, sebMachinName,
clientId, userSessionId,
browserSignatureKey) clientId,
browserSignatureKey)
.getOrThrow(); .getOrThrow();
if (clientConnection.examId != null) {
processAlternativeBEK(response, clientConnection.examId);
}
}, },
this.executor); this.executor);
} }
@ -467,4 +464,23 @@ public class ExamAPI_V1_Controller {
} }
} }
private void processASKSalt(final HttpServletResponse response, final ClientConnection clientConnection) {
this.examSessionService
.getAppSignatureKeySalt(clientConnection.examId)
.onSuccess(salt -> response.setHeader(API.EXAM_API_EXAM_SIGNATURE_SALT_HEADER, salt))
.onError(error -> log.error(
"Failed to get security key salt for connection: {}",
clientConnection,
error));
}
private void processAlternativeBEK(final HttpServletResponse response, final Long examId) {
if (examId == null) {
return;
}
this.examSessionService.getRunningExam(examId)
.map(exam -> exam.getAdditionalAttribute(Exam.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK))
.onSuccess(bek -> response.setHeader(API.EXAM_API_EXAM_ALT_BEK, bek));
}
} }

View file

@ -512,8 +512,13 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
protected Result<Exam> notifyCreated(final Exam entity) { protected Result<Exam> notifyCreated(final Exam entity) {
final List<APIMessage> errors = new ArrayList<>(); final List<APIMessage> errors = new ArrayList<>();
this.examTemplateService this.examAdminService
.addDefinedIndicators(entity) .initAdditionalAttributes(entity)
.onErrorDo(error -> {
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_ATTRIBUTES.of(error));
return entity;
})
.flatMap(this.examTemplateService::addDefinedIndicators)
.onErrorDo(error -> { .onErrorDo(error -> {
errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_INDICATOR.of(error)); errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_INDICATOR.of(error));
return entity; return entity;
@ -549,7 +554,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
API.PARAM_MODEL_ID + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + entity.getModelId())); API.PARAM_MODEL_ID + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + entity.getModelId()));
throw new APIMessageException(errors); throw new APIMessageException(errors);
} else { } else {
return Result.of(entity); return this.examDAO.byPK(entity.id);
} }
} }