SEBSERV-372 implementation
This commit is contained in:
parent
cfb02143cf
commit
df13b4dff1
11 changed files with 146 additions and 81 deletions
|
@ -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";
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue