From df13b4dff1cbb6198b24cd8f24979f82a5edd581 Mon Sep 17 00:00:00 2001 From: anhefti Date: Mon, 19 Dec 2022 09:26:08 +0100 Subject: [PATCH] SEBSERV-372 implementation --- .../ch/ethz/seb/sebserver/gbl/api/API.java | 2 + .../seb/sebserver/gbl/model/exam/Exam.java | 4 +- .../ethz/seb/sebserver/gbl/util/Result.java | 3 + .../servicelayer/dao/impl/ExamDAOImpl.java | 34 +------ .../servicelayer/exam/ExamAdminService.java | 6 ++ .../exam/impl/ExamAdminServiceImpl.java | 67 +++++++++++++- .../impl/SecurityKeyServiceImpl.java | 2 +- .../session/ExamSessionService.java | 5 + .../impl/SEBClientConnectionServiceImpl.java | 1 - .../weblayer/api/ExamAPI_V1_Controller.java | 92 +++++++++++-------- .../api/ExamAdministrationController.java | 11 ++- 11 files changed, 146 insertions(+), 81 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index 8f2df61c..5089d231 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -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_ALT_BEK = "SEBServerBEK"; + public static final String EXAM_API_USER_SESSION_ID = "seb_user_session_id"; public static final String EXAM_API_HANDSHAKE_ENDPOINT = "/handshake"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java index 40267bcc..2c7e9646 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java @@ -69,8 +69,10 @@ public final class Exam implements GrantEntity { 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 */ 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"; + /** 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 { UP_COMING, diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java index e2aa8d8f..41acee38 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java @@ -209,6 +209,9 @@ public final class Result { if (result instanceof Result) { throw new IllegalArgumentException("Use flatMap instead!"); } + if (result == null) { + return Result.ofError(new RuntimeException("Map to null value.")); + } return Result.of(result); } catch (final Exception e) { return Result.ofError(e); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java index 34f7e00a..5c2994e2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java @@ -26,7 +26,6 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.mybatis.dynamic.sql.update.UpdateDSL; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; import org.springframework.security.crypto.keygen.KeyGenerators; @@ -67,23 +66,17 @@ public class ExamDAOImpl implements ExamDAO { private final ExamRecordDAO examRecordDAO; private final ApplicationEventPublisher applicationEventPublisher; private final AdditionalAttributesDAO additionalAttributesDAO; - private final boolean appSignatureKeyEnabled; - private final int defaultNumericalTrustThreshold; public ExamDAOImpl( final ExamRecordMapper examRecordMapper, final ExamRecordDAO examRecordDAO, final ApplicationEventPublisher applicationEventPublisher, - 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) { + final AdditionalAttributesDAO additionalAttributesDAO) { this.examRecordMapper = examRecordMapper; this.examRecordDAO = examRecordDAO; this.applicationEventPublisher = applicationEventPublisher; this.additionalAttributesDAO = additionalAttributesDAO; - this.appSignatureKeyEnabled = appSignatureKeyEnabled; - this.defaultNumericalTrustThreshold = defaultNumericalTrustThreshold; } @Override @@ -189,7 +182,6 @@ public class ExamDAOImpl implements ExamDAO { public Result createNew(final Exam exam) { return this.examRecordDAO .createNew(exam) - .map(this::initAdditionalAttributes) .flatMap(rec -> saveAdditionalAttributes(exam, rec)) .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) { final Map additionalAttributes = new HashMap<>(quizData.getAdditionalAttributes()); if (StringUtils.isNotBlank(quizData.description)) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java index 7e6b2bba..adf23018 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java @@ -38,6 +38,12 @@ public interface ExamAdminService { * @return Result refer to the domain object or to an error when happened */ Result 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 initAdditionalAttributes(final Exam exam); + /** Saves additional attributes for the exam that are specific to a type of LMS * * @param exam The Exam to add the LMS specific attributes diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java index 7fec8634..a0b4ba44 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java @@ -8,14 +8,19 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl; +import java.security.MessageDigest; import java.util.Arrays; import java.util.List; +import java.util.UUID; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.util.encoders.Hex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; +import org.springframework.security.crypto.keygen.KeyGenerators; import org.springframework.stereotype.Service; 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.profile.WebServiceProfile; 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.ConfigurationNodeDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO; @@ -59,6 +65,8 @@ public class ExamAdminServiceImpl implements ExamAdminService { private final ConfigurationNodeDAO configurationNodeDAO; private final ExamConfigurationMapDAO examConfigurationMapDAO; private final LmsAPIService lmsAPIService; + private final boolean appSignatureKeyEnabled; + private final int defaultNumericalTrustThreshold; protected ExamAdminServiceImpl( final ExamDAO examDAO, @@ -66,7 +74,9 @@ public class ExamAdminServiceImpl implements ExamAdminService { final AdditionalAttributesDAO additionalAttributesDAO, final ConfigurationNodeDAO configurationNodeDAO, 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.proctoringServiceSettingsService = proctoringServiceSettingsService; @@ -74,6 +84,8 @@ public class ExamAdminServiceImpl implements ExamAdminService { this.configurationNodeDAO = configurationNodeDAO; this.examConfigurationMapDAO = examConfigurationMapDAO; this.lmsAPIService = lmsAPIService; + this.appSignatureKeyEnabled = appSignatureKeyEnabled; + this.defaultNumericalTrustThreshold = defaultNumericalTrustThreshold; } @Override @@ -81,6 +93,34 @@ public class ExamAdminServiceImpl implements ExamAdminService { return this.examDAO.byPK(examId); } + @Override + public Result 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 public Result saveSecurityKeySettings( final Long institutionId, @@ -148,7 +188,7 @@ public class ExamAdminServiceImpl implements ExamAdminService { @Override public Result saveLMSAttributes(final Exam exam) { - return saveAdditionalAttributesForMoodleExams(exam); + return initAdditionalAttributesForMoodleExams(exam); } @Override @@ -208,7 +248,7 @@ public class ExamAdminServiceImpl implements ExamAdminService { .getExamProctoringService(settings.serverType)); } - private Result saveAdditionalAttributesForMoodleExams(final Exam exam) { + private Result initAdditionalAttributesForMoodleExams(final Exam exam) { return Result.tryCatch(() -> { final LmsAPITemplate lmsTemplate = this.lmsAPIService .getLmsAPITemplate(exam.lmsSetupId) @@ -221,7 +261,26 @@ public class ExamAdminServiceImpl implements ExamAdminService { exam.id, QuizData.QUIZ_ATTR_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; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/impl/SecurityKeyServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/impl/SecurityKeyServiceImpl.java index e7c348d1..105753be 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/impl/SecurityKeyServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/impl/SecurityKeyServiceImpl.java @@ -343,7 +343,7 @@ public class SecurityKeyServiceImpl implements SecurityKeyService { private String createSignatureHash(final CharSequence signature) { try { - final MessageDigest hasher = MessageDigest.getInstance("SHA-256"); + final MessageDigest hasher = MessageDigest.getInstance(Constants.SHA_256); hasher.update(Utils.toByteArray(signature)); final String signatureHash = Hex.toHexString(hasher.digest()); return signatureHash; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java index 93183f83..5e391601 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java @@ -63,6 +63,11 @@ public interface ExamSessionService { * @return the underling LmsAPIService */ 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 getAppSignatureKeySalt(Long examId); /** Use this to check the consistency of a running Exam. diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java index 7aca42f0..fb03ae40 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java @@ -173,7 +173,6 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic this.clientIndicatorFactory.initializeDistributedCaches(clientConnection); } - // flash connection token cache for exam if available if (examId != null) { this.clientConnectionDAO.evictConnectionTokenCache(examId); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java index a2136153..eff85305 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java @@ -145,20 +145,15 @@ public class ExamAPI_V1_Controller { .filter(this::checkConsistency) .collect(Collectors.toList()); } else { + final Exam exam = this.examSessionService .getExamDAO() .byPK(examId) .getOrThrow(); result = Arrays.asList(createRunningExamInfo(exam)); - - 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)); + processASKSalt(response, clientConnection); + processAlternativeBEK(response, clientConnection.examId); } if (result.isEmpty()) { @@ -209,27 +204,23 @@ public class ExamAPI_V1_Controller { final String remoteAddr = this.getClientAddress(request); final Long institutionId = getInstitutionId(principal); - final ClientConnection clientConnection = this.sebClientConnectionService.updateClientConnection( - connectionToken, - institutionId, - examId, - remoteAddr, - sebVersion, - sebOSName, - sebMachinName, - userSessionId, - clientId, - browserSignatureKey) + final ClientConnection clientConnection = this.sebClientConnectionService + .updateClientConnection( + connectionToken, + institutionId, + examId, + remoteAddr, + sebVersion, + sebOSName, + sebMachinName, + userSessionId, + clientId, + browserSignatureKey) .getOrThrow(); if (clientConnection.examId != null) { - 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)); + processASKSalt(response, clientConnection); + processAlternativeBEK(response, clientConnection.examId); } }, this.executor); @@ -251,7 +242,8 @@ public class ExamAPI_V1_Controller { required = false) final String browserSignatureKey, @RequestParam(name = API.EXAM_API_PARAM_CLIENT_ID, required = false) final String clientId, final Principal principal, - final HttpServletRequest request) { + final HttpServletRequest request, + final HttpServletResponse response) { return CompletableFuture.runAsync( () -> { @@ -259,18 +251,23 @@ public class ExamAPI_V1_Controller { final String remoteAddr = this.getClientAddress(request); final Long institutionId = getInstitutionId(principal); - this.sebClientConnectionService.establishClientConnection( - connectionToken, - institutionId, - examId, - remoteAddr, - sebVersion, - sebOSName, - sebMachinName, - userSessionId, - clientId, - browserSignatureKey) + final ClientConnection clientConnection = this.sebClientConnectionService + .establishClientConnection( + connectionToken, + institutionId, + examId, + remoteAddr, + sebVersion, + sebOSName, + sebMachinName, + userSessionId, + clientId, + browserSignatureKey) .getOrThrow(); + + if (clientConnection.examId != null) { + processAlternativeBEK(response, clientConnection.examId); + } }, 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)); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index a2ad89fd..4e9712c5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -512,8 +512,13 @@ public class ExamAdministrationController extends EntityController { protected Result notifyCreated(final Exam entity) { final List errors = new ArrayList<>(); - this.examTemplateService - .addDefinedIndicators(entity) + this.examAdminService + .initAdditionalAttributes(entity) + .onErrorDo(error -> { + errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_ATTRIBUTES.of(error)); + return entity; + }) + .flatMap(this.examTemplateService::addDefinedIndicators) .onErrorDo(error -> { errors.add(ErrorMessage.EXAM_IMPORT_ERROR_AUTO_INDICATOR.of(error)); return entity; @@ -549,7 +554,7 @@ public class ExamAdministrationController extends EntityController { API.PARAM_MODEL_ID + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + entity.getModelId())); throw new APIMessageException(errors); } else { - return Result.of(entity); + return this.examDAO.byPK(entity.id); } }