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

View file

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

View file

@ -209,6 +209,9 @@ public final class Result<T> {
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);

View file

@ -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<Exam> 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<String, String> additionalAttributes = new HashMap<>(quizData.getAdditionalAttributes());
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 */
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
*
* @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;
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<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
public Result<Exam> saveSecurityKeySettings(
final Long institutionId,
@ -148,7 +188,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
@Override
public Result<Exam> 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<Exam> saveAdditionalAttributesForMoodleExams(final Exam exam) {
private Result<Exam> 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;

View file

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

View file

@ -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<String> getAppSignatureKeySalt(Long examId);
/** 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);
}
// flash connection token cache for exam if available
if (examId != null) {
this.clientConnectionDAO.evictConnectionTokenCache(examId);
}

View file

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

View file

@ -512,8 +512,13 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
protected Result<Exam> notifyCreated(final Exam entity) {
final List<APIMessage> 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<Exam, Exam> {
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);
}
}