SEBSERV-482 implementation no testing yet
This commit is contained in:
parent
74d4b1ff7d
commit
ac21400388
32 changed files with 428 additions and 171 deletions
|
@ -27,7 +27,7 @@ import ch.ethz.seb.sebserver.gbl.profile.ProdWebServiceProfile;
|
|||
|
||||
/** SEB-Server (Safe Exam Browser Server) is a server component to maintain and support
|
||||
* Exams running with SEB (Safe Exam Browser). TODO add link(s)
|
||||
*
|
||||
* <p>
|
||||
* SEB-Server uses Spring Boot as main framework is divided into two main components,
|
||||
* a webservice component that implements the business logic, persistence management
|
||||
* and defines a REST API to expose the services over HTTP. The second component is a
|
||||
|
@ -49,7 +49,7 @@ public class SEBServer {
|
|||
}
|
||||
|
||||
/*
|
||||
* Add an additional redirect Connector on http port to redirect all http calls
|
||||
* Add a redirect Connector on http port to redirect all http calls
|
||||
* to https.
|
||||
*
|
||||
* NOTE: This works with TomcatServletWebServerFactory and embedded tomcat.
|
||||
|
|
|
@ -39,25 +39,10 @@ import ch.ethz.seb.sebserver.gbl.util.Utils;
|
|||
public final class Exam implements GrantEntity {
|
||||
|
||||
public static final Exam EMPTY_EXAM = new Exam(
|
||||
-1L,
|
||||
-1L,
|
||||
-1L,
|
||||
Constants.EMPTY_NOTE,
|
||||
false,
|
||||
Constants.EMPTY_NOTE,
|
||||
null,
|
||||
null,
|
||||
ExamType.UNDEFINED,
|
||||
null,
|
||||
null,
|
||||
ExamStatus.FINISHED,
|
||||
Boolean.FALSE,
|
||||
null,
|
||||
Boolean.FALSE,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
-1L, -1L, -1L, Constants.EMPTY_NOTE, false, Constants.EMPTY_NOTE,
|
||||
null, null, ExamType.UNDEFINED, null, null, ExamStatus.FINISHED,
|
||||
null, Boolean.FALSE, null, Boolean.FALSE, null,
|
||||
null, null, null);
|
||||
|
||||
public static final String FILTER_ATTR_TYPE = "type";
|
||||
public static final String FILTER_ATTR_STATUS = "status";
|
||||
|
@ -130,6 +115,9 @@ public final class Exam implements GrantEntity {
|
|||
@JsonProperty(EXAM.ATTR_STATUS)
|
||||
public final ExamStatus status;
|
||||
|
||||
@JsonProperty(EXAM.ATTR_QUIT_PASSWORD)
|
||||
public final String quitPassword;
|
||||
|
||||
@JsonProperty(EXAM.ATTR_LMS_SEB_RESTRICTION)
|
||||
public final Boolean sebRestriction;
|
||||
|
||||
|
@ -170,6 +158,7 @@ public final class Exam implements GrantEntity {
|
|||
@JsonProperty(EXAM.ATTR_OWNER) final String owner,
|
||||
@JsonProperty(EXAM.ATTR_SUPPORTER) final Collection<String> supporter,
|
||||
@JsonProperty(EXAM.ATTR_STATUS) final ExamStatus status,
|
||||
@JsonProperty(EXAM.ATTR_QUIT_PASSWORD) final String quitPassword,
|
||||
@JsonProperty(EXAM.ATTR_LMS_SEB_RESTRICTION) final Boolean sebRestriction,
|
||||
@JsonProperty(EXAM.ATTR_BROWSER_KEYS) final String browserExamKeys,
|
||||
@JsonProperty(EXAM.ATTR_ACTIVE) final Boolean active,
|
||||
|
@ -189,6 +178,7 @@ public final class Exam implements GrantEntity {
|
|||
this.type = type;
|
||||
this.owner = owner;
|
||||
this.status = (status != null) ? status : getStatusFromDate(startTime, endTime);
|
||||
this.quitPassword = quitPassword;
|
||||
this.sebRestriction = sebRestriction;
|
||||
this.browserExamKeys = browserExamKeys;
|
||||
this.active = (active != null) ? active : Boolean.TRUE;
|
||||
|
@ -219,6 +209,7 @@ public final class Exam implements GrantEntity {
|
|||
this.type = postMap.getEnum(EXAM.ATTR_TYPE, ExamType.class, ExamType.UNDEFINED);
|
||||
this.owner = postMap.getString(EXAM.ATTR_OWNER);
|
||||
this.status = postMap.getEnum(EXAM.ATTR_STATUS, ExamStatus.class, getStatusFromDate(this.startTime, this.endTime));
|
||||
this.quitPassword = postMap.getString(EXAM.ATTR_QUIT_PASSWORD);
|
||||
this.sebRestriction = null;
|
||||
this.browserExamKeys = null;
|
||||
this.active = postMap.getBoolean(EXAM.ATTR_ACTIVE);
|
||||
|
@ -262,6 +253,7 @@ public final class Exam implements GrantEntity {
|
|||
EXAM.ATTR_STATUS,
|
||||
ExamStatus.class,
|
||||
getStatusFromDate(this.startTime, this.endTime));
|
||||
this.quitPassword = mapper.getString(EXAM.ATTR_QUIT_PASSWORD);
|
||||
this.sebRestriction = null;
|
||||
this.browserExamKeys = mapper.getString(EXAM.ATTR_BROWSER_KEYS);
|
||||
this.active = mapper.getBoolean(EXAM.ATTR_ACTIVE);
|
||||
|
@ -389,6 +381,10 @@ public final class Exam implements GrantEntity {
|
|||
return this.status;
|
||||
}
|
||||
|
||||
public String getQuitPassword() {
|
||||
return quitPassword;
|
||||
}
|
||||
|
||||
public String getBrowserExamKeys() {
|
||||
return this.browserExamKeys;
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ public class ExamTemplate implements GrantEntity {
|
|||
public static final String FILTER_ATTR_EXAM_TYPE = EXAM_TEMPLATE.ATTR_EXAM_TYPE;
|
||||
public static final String ATTR_CLIENT_GROUP_TEMPLATES = "CLIENT_GROUP_TEMPLATES";
|
||||
public static final String ATTR_EXAM_ATTRIBUTES = "EXAM_ATTRIBUTES";
|
||||
public static final String ATTR_QUIT_PASSWORD = "quitPassword";
|
||||
|
||||
@JsonProperty(EXAM_TEMPLATE.ATTR_ID)
|
||||
public final Long id;
|
||||
|
@ -243,8 +244,7 @@ public class ExamTemplate implements GrantEntity {
|
|||
}
|
||||
|
||||
public static ExamTemplate createNew(final Long institutionId) {
|
||||
return new ExamTemplate(null, institutionId, null, null, ExamType.UNDEFINED, null, null, false, null, null,
|
||||
null);
|
||||
return new ExamTemplate(null, institutionId, null, null, ExamType.UNDEFINED, null, null, false, null, null, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import javax.crypto.spec.GCMParameterSpec;
|
|||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.bouncycastle.jcajce.provider.keystore.pkcs12.PKCS12KeyStoreSpi;
|
||||
import org.bouncycastle.jcajce.provider.keystore.pkcs12.PKCS12KeyStoreSpi.BCPKCS12KeyStore;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -132,6 +133,19 @@ public class Cryptor {
|
|||
});
|
||||
}
|
||||
|
||||
public Result<CharSequence> encryptCheckAlreadyEncrypted(final CharSequence text) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
// try to decrypt to check if it is already encrypted
|
||||
final Result<CharSequence> decryption = this.decrypt(text);
|
||||
if (decryption.hasError()) {
|
||||
return encrypt(text).getOrThrow();
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static Result<CharSequence> decrypt(final CharSequence cipher, final CharSequence secret) {
|
||||
return Result.tryCatch(() -> {
|
||||
if (cipher == null) {
|
||||
|
@ -183,4 +197,5 @@ public class Cryptor {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -83,61 +83,33 @@ public class ExamForm implements TemplateComposer {
|
|||
protected static final String ATTR_EDITABLE = "ATTR_EDITABLE";
|
||||
protected static final String ATTR_EXAM_STATUS = "ATTR_EXAM_STATUS";
|
||||
|
||||
public static final LocTextKey EXAM_FORM_TITLE_KEY =
|
||||
new LocTextKey("sebserver.exam.form.title");
|
||||
public static final LocTextKey EXAM_FORM_TITLE_IMPORT_KEY =
|
||||
new LocTextKey("sebserver.exam.form.title.import");
|
||||
|
||||
private static final LocTextKey FORM_SUPPORTER_TEXT_KEY =
|
||||
new LocTextKey("sebserver.exam.form.supporter");
|
||||
private static final LocTextKey FORM_STATUS_TEXT_KEY =
|
||||
new LocTextKey("sebserver.exam.form.status");
|
||||
private static final LocTextKey FORM_TYPE_TEXT_KEY =
|
||||
new LocTextKey("sebserver.exam.form.type");
|
||||
private static final LocTextKey FORM_END_TIME_TEXT_KEY =
|
||||
new LocTextKey("sebserver.exam.form.endtime");
|
||||
private static final LocTextKey FORM_START_TIME_TEXT_KEY =
|
||||
new LocTextKey("sebserver.exam.form.starttime");
|
||||
private static final LocTextKey FORM_DESCRIPTION_TEXT_KEY =
|
||||
new LocTextKey("sebserver.exam.form.description");
|
||||
private static final LocTextKey FORM_NAME_TEXT_KEY =
|
||||
new LocTextKey("sebserver.exam.form.name");
|
||||
private static final LocTextKey FORM_QUIZ_ID_TEXT_KEY =
|
||||
new LocTextKey("sebserver.exam.form.quizid");
|
||||
private static final LocTextKey FORM_QUIZ_URL_TEXT_KEY =
|
||||
new LocTextKey("sebserver.exam.form.quizurl");
|
||||
private static final LocTextKey FORM_LMSSETUP_TEXT_KEY =
|
||||
new LocTextKey("sebserver.exam.form.lmssetup");
|
||||
private final static LocTextKey ACTION_MESSAGE_SEB_RESTRICTION_RELEASE =
|
||||
new LocTextKey("sebserver.exam.action.sebrestriction.release.confirm");
|
||||
private static final LocTextKey FORM_EXAM_TEMPLATE_TEXT_KEY =
|
||||
new LocTextKey("sebserver.exam.form.examTemplate");
|
||||
private static final LocTextKey FORM_EXAM_TEMPLATE_ERROR =
|
||||
new LocTextKey("sebserver.exam.form.examTemplate.error");
|
||||
private static final LocTextKey EXAM_ARCHIVE_CONFIRM =
|
||||
new LocTextKey("sebserver.exam.action.archive.confirm");
|
||||
|
||||
private final static LocTextKey CONSISTENCY_MESSAGE_TITLE =
|
||||
new LocTextKey("sebserver.exam.consistency.title");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGE_MISSING_SUPPORTER =
|
||||
new LocTextKey("sebserver.exam.consistency.missing-supporter");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGE_MISSING_INDICATOR =
|
||||
new LocTextKey("sebserver.exam.consistency.missing-indicator");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGE_MISSING_CONFIG =
|
||||
new LocTextKey("sebserver.exam.consistency.missing-config");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGE_MISSING_SEB_RESTRICTION =
|
||||
new LocTextKey("sebserver.exam.consistency.missing-seb-restriction");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGE_VALIDATION_LMS_CONNECTION =
|
||||
new LocTextKey("sebserver.exam.consistency.no-lms-connection");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGEINVALID_ID_REFERENCE =
|
||||
new LocTextKey("sebserver.exam.consistency.invalid-lms-id");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGE_SEB_RESTRICTION_MISMATCH =
|
||||
new LocTextKey("sebserver.exam.consistencyseb-restriction-mismatch");
|
||||
|
||||
private final static LocTextKey AUTO_GEN_CONFIG_ERROR_TITLE =
|
||||
new LocTextKey("sebserver.exam.autogen.error.config.title");
|
||||
private final static LocTextKey AUTO_GEN_CONFIG_ERROR_TEXT =
|
||||
new LocTextKey("sebserver.exam.autogen.error.config.text");
|
||||
public static final LocTextKey EXAM_FORM_TITLE_KEY = new LocTextKey("sebserver.exam.form.title");
|
||||
public static final LocTextKey EXAM_FORM_TITLE_IMPORT_KEY = new LocTextKey("sebserver.exam.form.title.import");
|
||||
private static final LocTextKey FORM_SUPPORTER_TEXT_KEY = new LocTextKey("sebserver.exam.form.supporter");
|
||||
private static final LocTextKey FORM_QUIT_PWD_TEXT_KEY = new LocTextKey("sebserver.exam.form.quitpwd");
|
||||
private static final LocTextKey FORM_STATUS_TEXT_KEY = new LocTextKey("sebserver.exam.form.status");
|
||||
private static final LocTextKey FORM_TYPE_TEXT_KEY = new LocTextKey("sebserver.exam.form.type");
|
||||
private static final LocTextKey FORM_END_TIME_TEXT_KEY = new LocTextKey("sebserver.exam.form.endtime");
|
||||
private static final LocTextKey FORM_START_TIME_TEXT_KEY = new LocTextKey("sebserver.exam.form.starttime");
|
||||
private static final LocTextKey FORM_DESCRIPTION_TEXT_KEY = new LocTextKey("sebserver.exam.form.description");
|
||||
private static final LocTextKey FORM_NAME_TEXT_KEY = new LocTextKey("sebserver.exam.form.name");
|
||||
private static final LocTextKey FORM_QUIZ_ID_TEXT_KEY = new LocTextKey("sebserver.exam.form.quizid");
|
||||
private static final LocTextKey FORM_QUIZ_URL_TEXT_KEY = new LocTextKey("sebserver.exam.form.quizurl");
|
||||
private static final LocTextKey FORM_LMSSETUP_TEXT_KEY = new LocTextKey("sebserver.exam.form.lmssetup");
|
||||
private final static LocTextKey ACTION_MESSAGE_SEB_RESTRICTION_RELEASE = new LocTextKey("sebserver.exam.action.sebrestriction.release.confirm");
|
||||
private static final LocTextKey FORM_EXAM_TEMPLATE_TEXT_KEY = new LocTextKey("sebserver.exam.form.examTemplate");
|
||||
private static final LocTextKey FORM_EXAM_TEMPLATE_ERROR = new LocTextKey("sebserver.exam.form.examTemplate.error");
|
||||
private static final LocTextKey EXAM_ARCHIVE_CONFIRM = new LocTextKey("sebserver.exam.action.archive.confirm");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGE_TITLE = new LocTextKey("sebserver.exam.consistency.title");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGE_MISSING_SUPPORTER = new LocTextKey("sebserver.exam.consistency.missing-supporter");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGE_MISSING_INDICATOR = new LocTextKey("sebserver.exam.consistency.missing-indicator");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGE_MISSING_CONFIG = new LocTextKey("sebserver.exam.consistency.missing-config");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGE_MISSING_SEB_RESTRICTION = new LocTextKey("sebserver.exam.consistency.missing-seb-restriction");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGE_VALIDATION_LMS_CONNECTION = new LocTextKey("sebserver.exam.consistency.no-lms-connection");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGEINVALID_ID_REFERENCE = new LocTextKey("sebserver.exam.consistency.invalid-lms-id");
|
||||
private final static LocTextKey CONSISTENCY_MESSAGE_SEB_RESTRICTION_MISMATCH = new LocTextKey("sebserver.exam.consistencyseb-restriction-mismatch");
|
||||
private final static LocTextKey AUTO_GEN_CONFIG_ERROR_TITLE = new LocTextKey("sebserver.exam.autogen.error.config.title");
|
||||
private final static LocTextKey AUTO_GEN_CONFIG_ERROR_TEXT = new LocTextKey("sebserver.exam.autogen.error.config.text");
|
||||
|
||||
private final Map<String, LocTextKey> consistencyMessageMapping;
|
||||
private final PageService pageService;
|
||||
|
@ -507,13 +479,20 @@ public class ExamForm implements TemplateComposer {
|
|||
.withInputSpan(7)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.password(
|
||||
Domain.EXAM.ATTR_QUIT_PASSWORD,
|
||||
FORM_QUIT_PWD_TEXT_KEY,
|
||||
exam.quitPassword)
|
||||
.withInputSpan(3)
|
||||
.withEmptyCellSeparation(false))
|
||||
|
||||
.addField(FormBuilder.multiComboSelection(
|
||||
Domain.EXAM.ATTR_SUPPORTER,
|
||||
FORM_SUPPORTER_TEXT_KEY,
|
||||
StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR),
|
||||
this.resourceService::examSupporterResources)
|
||||
.withInputSpan(7)
|
||||
.withEmptyCellSeparation(false))
|
||||
.withEmptyCellSpan(4))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -615,12 +594,19 @@ public class ExamForm implements TemplateComposer {
|
|||
this.resourceService::examTypeResources)
|
||||
.mandatory(true))
|
||||
|
||||
.addField(FormBuilder.password(
|
||||
Domain.EXAM.ATTR_QUIT_PASSWORD,
|
||||
FORM_QUIT_PWD_TEXT_KEY,
|
||||
exam.quitPassword))
|
||||
|
||||
.addField(FormBuilder.multiComboSelection(
|
||||
Domain.EXAM.ATTR_SUPPORTER,
|
||||
FORM_SUPPORTER_TEXT_KEY,
|
||||
StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR),
|
||||
this.resourceService::examSupporterResources))
|
||||
|
||||
|
||||
|
||||
.buildFor(importFromLMS
|
||||
? this.restService.getRestCall(ImportAsExam.class)
|
||||
: newExam
|
||||
|
@ -643,6 +629,7 @@ public class ExamForm implements TemplateComposer {
|
|||
null,
|
||||
null,
|
||||
ExamStatus.UP_COMING,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
true,
|
||||
|
@ -679,13 +666,16 @@ public class ExamForm implements TemplateComposer {
|
|||
.call()
|
||||
.getOrThrow();
|
||||
|
||||
final String quitPassword = examTemplate.getExamAttributes().get(ExamTemplate.ATTR_QUIT_PASSWORD);
|
||||
form.setFieldValue(Domain.EXAM.ATTR_TYPE, examTemplate.examType.name());
|
||||
form.setFieldValue(
|
||||
Domain.EXAM.ATTR_SUPPORTER,
|
||||
StringUtils.join(examTemplate.supporter, Constants.LIST_SEPARATOR));
|
||||
form.setFieldValue(Domain.EXAM.ATTR_QUIT_PASSWORD, quitPassword);
|
||||
} else {
|
||||
form.setFieldValue(Domain.EXAM.ATTR_TYPE, Exam.ExamType.UNDEFINED.name());
|
||||
form.setFieldValue(Domain.EXAM.ATTR_SUPPORTER, null);
|
||||
form.setFieldValue(Domain.EXAM.ATTR_QUIT_PASSWORD, null);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
context.notifyError(FORM_EXAM_TEMPLATE_ERROR, e);
|
||||
|
|
|
@ -227,4 +227,5 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
|
|||
* @param updateId The update identifier given by the update task */
|
||||
void markLMSAvailability(final String externalQuizId, final boolean available, final String updateId);
|
||||
|
||||
void updateQuitPassword(Exam exam, String quitPassword);
|
||||
}
|
||||
|
|
|
@ -195,6 +195,13 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
.onError(error -> log.error("Failed to mark LMS not available: {}", externalQuizId, error));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateQuitPassword(final Exam exam, final String quitPassword) {
|
||||
this.examRecordDAO
|
||||
.updateQuitPassword(exam, quitPassword)
|
||||
.onError(err -> log.error("Failed to update quit password on exam: {}", exam, err));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Exam> setSEBRestriction(final Long examId, final boolean sebRestriction) {
|
||||
return this.examRecordDAO
|
||||
|
@ -789,6 +796,7 @@ public class ExamDAOImpl implements ExamDAO {
|
|||
record.getOwner(),
|
||||
supporter,
|
||||
status,
|
||||
record.getQuitPassword(),
|
||||
BooleanUtils.toBooleanObject(record.getLmsSebRestriction()),
|
||||
record.getBrowserKeys(),
|
||||
BooleanUtils.toBooleanObject(record.getActive()),
|
||||
|
|
|
@ -9,15 +9,13 @@
|
|||
package ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl;
|
||||
|
||||
import static ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport.*;
|
||||
import static ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport.quitPassword;
|
||||
import static org.mybatis.dynamic.sql.SqlBuilder.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
|
@ -63,13 +61,16 @@ public class ExamRecordDAO {
|
|||
|
||||
private final ExamRecordMapper examRecordMapper;
|
||||
private final ClientConnectionRecordMapper clientConnectionRecordMapper;
|
||||
private final Cryptor cryptor;
|
||||
|
||||
public ExamRecordDAO(
|
||||
final ExamRecordMapper examRecordMapper,
|
||||
final ClientConnectionRecordMapper clientConnectionRecordMapper) {
|
||||
final ClientConnectionRecordMapper clientConnectionRecordMapper,
|
||||
final Cryptor cryptor) {
|
||||
|
||||
this.examRecordMapper = examRecordMapper;
|
||||
this.clientConnectionRecordMapper = clientConnectionRecordMapper;
|
||||
this.cryptor = cryptor;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
|
@ -136,7 +137,7 @@ public class ExamRecordDAO {
|
|||
.build()
|
||||
.execute()
|
||||
.stream()
|
||||
.map(rec -> rec.getInstitutionId())
|
||||
.map(ExamRecord::getInstitutionId)
|
||||
.collect(Collectors.toList());
|
||||
});
|
||||
}
|
||||
|
@ -234,6 +235,33 @@ public class ExamRecordDAO {
|
|||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Result<ExamRecord> updateQuitPassword(final Exam exam, final String pwd) {
|
||||
return Result.tryCatch(() -> {
|
||||
final String examQuitPassword = exam.quitPassword != null
|
||||
? this.cryptor
|
||||
.decrypt(exam.quitPassword)
|
||||
.getOr(exam.quitPassword)
|
||||
.toString()
|
||||
: null;
|
||||
|
||||
if (Objects.equals(examQuitPassword, pwd)) {
|
||||
return this.examRecordMapper.selectByPrimaryKey(exam.id);
|
||||
}
|
||||
|
||||
UpdateDSL.updateWithMapper(examRecordMapper::update, examRecord)
|
||||
.set(quitPassword).equalTo(getEncryptedQuitPassword(pwd))
|
||||
.where(id, isEqualTo(exam.id))
|
||||
.build()
|
||||
.execute();
|
||||
|
||||
|
||||
return this.examRecordMapper.selectByPrimaryKey(exam.id);
|
||||
})
|
||||
.onError(TransactionHandler::rollback);
|
||||
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Result<ExamRecord> updateState(final Long examId, final ExamStatus status, final String updateId) {
|
||||
return recordById(examId)
|
||||
|
@ -270,41 +298,39 @@ public class ExamRecordDAO {
|
|||
}
|
||||
|
||||
if (exam.status != null && !exam.status.name().equals(oldRecord.getStatus())) {
|
||||
log.info("Exam state change on save. Exam. {}, Old state: {}, new state: {}",
|
||||
exam.externalId,
|
||||
oldRecord.getStatus(),
|
||||
exam.status);
|
||||
log.info(
|
||||
"Exam state change on save. Exam. {}, Old state: {}, new state: {}",
|
||||
exam.externalId,
|
||||
oldRecord.getStatus(),
|
||||
exam.status);
|
||||
}
|
||||
|
||||
final ExamRecord examRecord = new ExamRecord(
|
||||
exam.id,
|
||||
null, null, null, null,
|
||||
(exam.supporter != null)
|
||||
? StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR)
|
||||
: null,
|
||||
(exam.type != null)
|
||||
? exam.type.name()
|
||||
: null,
|
||||
null,
|
||||
exam.browserExamKeys,
|
||||
null,
|
||||
1, // seb restriction (deprecated)
|
||||
null, // updating
|
||||
null, // lastUpdate
|
||||
null, // active
|
||||
exam.examTemplateId,
|
||||
Utils.getMillisecondsNow(),
|
||||
exam.lmsSetupId == null ? exam.name : null,
|
||||
exam.lmsSetupId == null ? exam.startTime : null,
|
||||
exam.lmsSetupId == null ? exam.endTime : null,
|
||||
null);
|
||||
UpdateDSL.updateWithMapper(examRecordMapper::update, examRecord)
|
||||
.set(supporter).equalTo((exam.supporter != null)
|
||||
? StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR)
|
||||
: null)
|
||||
.set(type).equalTo((exam.type != null)
|
||||
? exam.type.name()
|
||||
: ExamType.UNDEFINED.name())
|
||||
.set(quitPassword).equalTo(getEncryptedQuitPassword(exam.quitPassword))
|
||||
.set(browserKeys).equalToWhenPresent(exam.browserExamKeys)
|
||||
.set(lmsSebRestriction).equalTo(1) // seb restriction (deprecated)
|
||||
.set(examTemplateId).equalTo(exam.examTemplateId)
|
||||
.set(lastModified).equalTo(Utils.getMillisecondsNow())
|
||||
.set(quizName).equalToWhenPresent(exam.lmsSetupId == null ? exam.name : null)
|
||||
.set(quizStartTime).equalToWhenPresent(exam.lmsSetupId == null ? exam.startTime : null)
|
||||
.set(quizEndTime).equalToWhenPresent(exam.lmsSetupId == null ? exam.endTime : null)
|
||||
.where(id, isEqualTo(exam.id))
|
||||
.build()
|
||||
.execute();
|
||||
|
||||
this.examRecordMapper.updateByPrimaryKeySelective(examRecord);
|
||||
return this.examRecordMapper.selectByPrimaryKey(exam.id);
|
||||
})
|
||||
.onError(TransactionHandler::rollback);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Transactional
|
||||
public Result<ExamRecord> updateFromQuizData(
|
||||
final Long examId,
|
||||
|
@ -445,7 +471,7 @@ public class ExamRecordDAO {
|
|||
? StringUtils.join(exam.supporter, Constants.LIST_SEPARATOR_CHAR)
|
||||
: null,
|
||||
(exam.type != null) ? exam.type.name() : ExamType.UNDEFINED.name(),
|
||||
null, // quitPassword
|
||||
getEncryptedQuitPassword(exam.quitPassword),
|
||||
null, // browser keys
|
||||
(exam.status != null) ? exam.status.name() : ExamStatus.UP_COMING.name(),
|
||||
1, // seb restriction (deprecated)
|
||||
|
@ -554,4 +580,14 @@ public class ExamRecordDAO {
|
|||
});
|
||||
}
|
||||
|
||||
private String getEncryptedQuitPassword(final String pwd) {
|
||||
return (StringUtils.isNotBlank(pwd))
|
||||
? this.cryptor
|
||||
.encryptCheckAlreadyEncrypted(pwd)
|
||||
.onError(err -> log.error("failed to encrypt quit password, skip...", err))
|
||||
.getOr(pwd)
|
||||
.toString()
|
||||
: null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -210,6 +210,15 @@ public class ExamTemplateDAOImpl implements ExamTemplateDAO {
|
|||
indicatorsJSON,
|
||||
BooleanUtils.toInteger(data.institutionalDefault));
|
||||
|
||||
final String quitPassword = data.getExamAttributes().get(ExamTemplate.ATTR_QUIT_PASSWORD);
|
||||
if (StringUtils.isNotBlank(quitPassword)) {
|
||||
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||
EntityType.EXAM_TEMPLATE,
|
||||
data.id,
|
||||
ExamTemplate.ATTR_QUIT_PASSWORD,
|
||||
quitPassword);
|
||||
}
|
||||
|
||||
this.examTemplateRecordMapper.insert(newRecord);
|
||||
return newRecord;
|
||||
})
|
||||
|
|
|
@ -163,6 +163,8 @@ public interface ExamAdminService {
|
|||
* @param exam the exam that has been changed and saved */
|
||||
void notifyExamSaved(Exam exam);
|
||||
|
||||
void applyQuitPassword(Exam entity);
|
||||
|
||||
static void newExamFieldValidation(final POSTMapper postParams) {
|
||||
noLMSFieldValidation(new Exam(postParams));
|
||||
}
|
||||
|
@ -337,5 +339,4 @@ public interface ExamAdminService {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -8,11 +8,13 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.servicelayer.exam;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
|
||||
public interface ExamConfigurationValueService {
|
||||
|
||||
public static final String CONFIG_ATTR_NAME_QUIT_LINK = "quitURL";
|
||||
public static final String CONFIG_ATTR_NAME_QUIT_SECRET = "hashedQuitPassword";
|
||||
public static final String CONFIG_ATTR_NAME_ALLOWED_SEB_VERSION = "sebAllowedVersions";
|
||||
String CONFIG_ATTR_NAME_QUIT_LINK = "quitURL";
|
||||
String CONFIG_ATTR_NAME_QUIT_SECRET = "hashedQuitPassword";
|
||||
String CONFIG_ATTR_NAME_ALLOWED_SEB_VERSION = "sebAllowedVersions";
|
||||
|
||||
/** Get the actual SEB settings attribute value for the exam configuration mapped as default configuration
|
||||
* to the given exam
|
||||
|
@ -29,7 +31,7 @@ public interface ExamConfigurationValueService {
|
|||
*
|
||||
* @param examId The exam identifier
|
||||
* @param configAttributeName The name of the SEB settings attribute
|
||||
* @param The default value that is given back if there is no value from configuration
|
||||
* @param defaultValue default value that is given back if there is no value from configuration
|
||||
* @return The current value of the above SEB settings attribute and given exam. */
|
||||
String getMappedDefaultConfigAttributeValue(
|
||||
Long examId,
|
||||
|
@ -39,8 +41,18 @@ public interface ExamConfigurationValueService {
|
|||
/** Get the quitPassword SEB Setting from the Exam Configuration that is applied to the given exam.
|
||||
*
|
||||
* @param examId Exam identifier
|
||||
* @return the vlaue of the quitPassword SEB Setting */
|
||||
String getQuitSecret(Long examId);
|
||||
* @return the value of the quitPassword SEB Setting */
|
||||
String getQuitPassword(Long examId);
|
||||
|
||||
String getQuitPasswordFromConfigTemplate(Long configTemplateId);
|
||||
|
||||
/** Used to apply the quit pass given from the exam to all exam configuration for the exam.
|
||||
*
|
||||
* @param examId The exam identifier
|
||||
* @param quitPassword The quit password to set to all exam configuration of the given exam
|
||||
* @return Result to the given exam id or to an error when happened
|
||||
*/
|
||||
Result<Long> applyQuitPasswordToConfigs(Long examId, String quitPassword);
|
||||
|
||||
/** Get the quitLink SEB Setting from the Exam Configuration that is applied to the given exam.
|
||||
*
|
||||
|
|
|
@ -328,6 +328,13 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
|||
this.proctoringAdminService.notifyExamSaved(exam);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyQuitPassword(final Exam exam) {
|
||||
this.examConfigurationValueService
|
||||
.applyQuitPasswordToConfigs(exam.id, exam.quitPassword)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
private Result<Exam> initAdditionalAttributesForMoodleExams(final Exam exam) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
|
|
|
@ -8,6 +8,11 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Configuration;
|
||||
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -59,24 +64,16 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
|
|||
|
||||
final Long configId = this.examConfigurationMapDAO
|
||||
.getDefaultConfigurationNode(examId)
|
||||
.flatMap(nodeId -> this.configurationDAO.getConfigurationLastStableVersion(nodeId))
|
||||
.flatMap(this.configurationDAO::getConfigurationLastStableVersion)
|
||||
.map(config -> config.id)
|
||||
.onError(error -> log.warn("Failed to get default Exam Config for exam: {} cause: {}",
|
||||
examId, error.getMessage()))
|
||||
.getOr(null);
|
||||
|
||||
if (configId == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
final Long attrId = this.configurationAttributeDAO
|
||||
.getAttributeIdByName(configAttributeName)
|
||||
.onError(error -> log.error("Failed to get attribute id with name: {} for exam: {}",
|
||||
configAttributeName, examId, error))
|
||||
.getOr(null);
|
||||
|
||||
return this.configurationValueDAO
|
||||
.getConfigAttributeValue(configId, attrId)
|
||||
.getConfigAttributeValue(configId, getAttributeId(configAttributeName))
|
||||
.onError(error -> log.warn(
|
||||
"Failed to get exam config attribute: {} {} error: {}",
|
||||
examId,
|
||||
|
@ -92,8 +89,10 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public String getQuitSecret(final Long examId) {
|
||||
public String getQuitPassword(final Long examId) {
|
||||
try {
|
||||
|
||||
final String quitSecretEncrypted = getMappedDefaultConfigAttributeValue(
|
||||
|
@ -119,6 +118,86 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
|
|||
return StringUtils.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQuitPasswordFromConfigTemplate(final Long configTemplateId) {
|
||||
try {
|
||||
|
||||
final Long configId = this.configurationDAO
|
||||
.getFollowupConfigurationId(configTemplateId)
|
||||
.getOrThrow();
|
||||
|
||||
return this.configurationValueDAO
|
||||
.getConfigAttributeValue(configId, getAttributeId(CONFIG_ATTR_NAME_QUIT_SECRET))
|
||||
.getOrThrow();
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to get quit password from configuration template", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Long> applyQuitPasswordToConfigs(final Long examId, final String quitSecret) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
final String oldQuitPassword = this.getQuitPassword(examId);
|
||||
final String newQuitPassword = quitSecret != null
|
||||
? this.cryptor
|
||||
.decrypt(quitSecret)
|
||||
.getOr(quitSecret)
|
||||
.toString()
|
||||
: null;
|
||||
|
||||
if (Objects.equals(oldQuitPassword, newQuitPassword)) {
|
||||
return examId;
|
||||
}
|
||||
|
||||
final Long configNodeId = this.examConfigurationMapDAO
|
||||
.getDefaultConfigurationNode(examId)
|
||||
.getOr(null);
|
||||
|
||||
if (configNodeId == null) {
|
||||
log.info("No Exam Configuration found for exam {} to apply quitPassword", examId);
|
||||
return examId;
|
||||
}
|
||||
|
||||
final Long attrId = getAttributeId(CONFIG_ATTR_NAME_QUIT_SECRET);
|
||||
if (attrId == null) {
|
||||
return examId;
|
||||
}
|
||||
|
||||
final Configuration followupConfig = this.configurationDAO.getFollowupConfiguration(configNodeId)
|
||||
.onError(error -> log.warn("Failed to get followup config for {} cause {}",
|
||||
configNodeId,
|
||||
error.getMessage()))
|
||||
.getOr(null);
|
||||
|
||||
final ConfigurationValue configurationValue = new ConfigurationValue(
|
||||
null,
|
||||
followupConfig.institutionId,
|
||||
followupConfig.id,
|
||||
attrId,
|
||||
0,
|
||||
quitSecret
|
||||
);
|
||||
|
||||
this.configurationValueDAO
|
||||
.save(configurationValue)
|
||||
.onError(err -> log.error(
|
||||
"Failed to save quit password to config value: {}",
|
||||
configurationValue,
|
||||
err));
|
||||
|
||||
// TODO possible without save to history?
|
||||
this.configurationDAO
|
||||
.saveToHistory(configNodeId)
|
||||
.onError(error -> log.warn("Failed to save to history for exam: {} cause: {}",
|
||||
examId, error.getMessage()));
|
||||
|
||||
return examId;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQuitLink(final Long examId) {
|
||||
try {
|
||||
|
@ -149,4 +228,12 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue
|
|||
}
|
||||
}
|
||||
|
||||
private Long getAttributeId(final String configAttributeName) {
|
||||
return this.configurationAttributeDAO
|
||||
.getAttributeIdByName(configAttributeName)
|
||||
.onError(error -> log.error("Failed to get attribute id with name: {}",
|
||||
configAttributeName, error))
|
||||
.getOr(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -228,7 +228,6 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
|
|||
.getOrThrow(error -> new APIMessageException(
|
||||
ErrorMessage.EXAM_IMPORT_ERROR_AUTO_CONFIG_LINKING,
|
||||
error));
|
||||
|
||||
}
|
||||
} else {
|
||||
if (log.isDebugEnabled()) {
|
||||
|
|
|
@ -165,7 +165,6 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
|
|||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Result<Exam> saveSEBRestrictionToExam(final Exam exam, final SEBRestriction sebRestriction) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
|
@ -181,6 +180,7 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
|
|||
exam.supporter,
|
||||
exam.status,
|
||||
null,
|
||||
null,
|
||||
(browserExamKeys != null && !browserExamKeys.isEmpty())
|
||||
? StringUtils.join(browserExamKeys, Constants.LIST_SEPARATOR_CHAR)
|
||||
: StringUtils.EMPTY,
|
||||
|
|
|
@ -136,7 +136,7 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
|
|||
final ArrayList<String> beks = new ArrayList<>(sebRestrictionData.browserExamKeys);
|
||||
final ArrayList<String> configKeys = new ArrayList<>(sebRestrictionData.configKeys);
|
||||
final String quitLink = this.examConfigurationValueService.getQuitLink(exam.id);
|
||||
final String quitSecret = this.examConfigurationValueService.getQuitSecret(exam.id);
|
||||
final String quitSecret = this.examConfigurationValueService.getQuitPassword(exam.id);
|
||||
final String additionalBEK = exam.getAdditionalAttribute(
|
||||
SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK);
|
||||
|
||||
|
|
|
@ -368,7 +368,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
|
|||
post.configKeys = new ArrayList<>(restriction.configKeys);
|
||||
if (this.restrictWithAdditionalAttributes) {
|
||||
post.quitLink = this.examConfigurationValueService.getQuitLink(restriction.examId);
|
||||
post.quitSecret = this.examConfigurationValueService.getQuitSecret(restriction.examId);
|
||||
post.quitSecret = this.examConfigurationValueService.getQuitPassword(restriction.examId);
|
||||
}
|
||||
final RestrictionData r =
|
||||
this.apiPost(restTemplate, url, post, RestrictionDataPost.class, RestrictionData.class);
|
||||
|
|
|
@ -18,32 +18,32 @@ public interface ExamConfigUpdateService {
|
|||
|
||||
/** Used to process a SEB Exam Configuration change that can also effect some
|
||||
* running exams that as the specified configuration attached.
|
||||
*
|
||||
* <p>
|
||||
* This deals with the whole functionality the underling data-structure provides. So
|
||||
* it assumes a N to M relationship between a SEB Exam Configuration and an Exam even
|
||||
* if this may currently not be possible in case of implemented restrictions.
|
||||
*
|
||||
* <p>
|
||||
* First of all a consistency check is applied that checks if there is no running Exam
|
||||
* involved that has currently active SEB Client connection. Active SEB Client connections are
|
||||
* established connections that are not yet closed and connection attempts that are older the a
|
||||
* defined time interval.
|
||||
*
|
||||
* <p>
|
||||
* After this check passed, the system places an update-lock on each Exam that is involved on the
|
||||
* data-base level and commit this immediately so that this can prevent new SEB Client connection
|
||||
* database level and commit this immediately so that this can prevent new SEB Client connection
|
||||
* attempts to be allowed.
|
||||
*
|
||||
* <p>
|
||||
* After placing update-locks the fist check is done again to ensure there where no new SEB Client
|
||||
* connection attempts in the meantime. If there where, the procedure will stop and rollback all
|
||||
* changes so far.
|
||||
*
|
||||
* <p>
|
||||
* If everything is locked the changes to the SEB Exam Configuration will be saved to the data-base
|
||||
* and all involved caches will be flushed to ensure the changed will take effect on next request.
|
||||
*
|
||||
* <p>
|
||||
* The last step is to update the SEB restriction on the LMS for every involved exam if it is running
|
||||
* and the feature is switched on. If something goes wrong during the update for an exam here, no
|
||||
* rollback of the entire procedure is applied. Instead the error is logged and the update can be
|
||||
* later triggered manually by an administrator.
|
||||
*
|
||||
* <p>
|
||||
* If there is any other error during the procedure the changes are rolled back and a force release of
|
||||
* the update-locks is applied to ensure all involved Exams are not locked anymore.
|
||||
*
|
||||
|
|
|
@ -33,9 +33,9 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamSessionCac
|
|||
/** A Service to handle running exam sessions */
|
||||
public interface ExamSessionService {
|
||||
|
||||
public static final Predicate<ClientConnection> ACTIVE_CONNECTION_FILTER =
|
||||
Predicate<ClientConnection> ACTIVE_CONNECTION_FILTER =
|
||||
cc -> cc.status == ConnectionStatus.ACTIVE;
|
||||
public static final Predicate<ClientConnectionData> ACTIVE_CONNECTION_DATA_FILTER =
|
||||
Predicate<ClientConnectionData> ACTIVE_CONNECTION_DATA_FILTER =
|
||||
ccd -> ccd.clientConnection.status == ConnectionStatus.ACTIVE;
|
||||
|
||||
/** Get the underling ExamDAO service.
|
||||
|
|
|
@ -13,6 +13,8 @@ import java.util.Collections;
|
|||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
@ -46,6 +48,8 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
|
|||
private final ExamSessionService examSessionService;
|
||||
private final ExamUpdateHandler examUpdateHandler;
|
||||
private final ExamAdminService examAdminService;
|
||||
private final ExamConfigurationValueService examConfigurationValueService;
|
||||
|
||||
|
||||
protected ExamConfigUpdateServiceImpl(
|
||||
final ExamDAO examDAO,
|
||||
|
@ -53,7 +57,8 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
|
|||
final ExamConfigurationMapDAO examConfigurationMapDAO,
|
||||
final ExamSessionService examSessionService,
|
||||
final ExamUpdateHandler examUpdateHandler,
|
||||
final ExamAdminService examAdminService) {
|
||||
final ExamAdminService examAdminService,
|
||||
final ExamConfigurationValueService examConfigurationValueService) {
|
||||
|
||||
this.examDAO = examDAO;
|
||||
this.configurationDAO = configurationDAO;
|
||||
|
@ -61,13 +66,15 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
|
|||
this.examSessionService = examSessionService;
|
||||
this.examUpdateHandler = examUpdateHandler;
|
||||
this.examAdminService = examAdminService;
|
||||
this.examConfigurationValueService = examConfigurationValueService;
|
||||
}
|
||||
|
||||
// processing:
|
||||
// check running exam integrity (No running exam with active SEB client-connection available)
|
||||
// if OK, create an update-id and for each exam, create an update-lock on DB (This also prevents new SEB client connection attempts during update)
|
||||
// check running exam integrity again after lock to ensure there where no SEB Client connection attempts in the meantime
|
||||
// check running exam integrity again after lock to ensure there were no SEB Client connection attempts in the meantime
|
||||
// store the new configuration values (into history) so that they take effect
|
||||
// check if quit password has changed and if so set it too for to (SEBSERV-482)
|
||||
// generate the new Config Key and update the Config Key within the LMSSetup API for each exam (delete old Key and add new Key)
|
||||
// evict each Exam from cache and release the update-lock on DB
|
||||
@Override
|
||||
|
@ -129,6 +136,10 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
|
|||
|
||||
// generate the new Config Key and update the Config Key within the LMSSetup API for each exam (delete old Key and add new Key)
|
||||
for (final Exam exam : exams) {
|
||||
|
||||
// check if quit password has changed and if so set it for the exam (SEBSERV-482)
|
||||
examDAO.updateQuitPassword(exam, examConfigurationValueService.getQuitPassword(exam.id));
|
||||
|
||||
if (exam.getStatus() == ExamStatus.RUNNING && exam.lmsSetupId != null) {
|
||||
|
||||
this.examUpdateHandler
|
||||
|
@ -202,6 +213,15 @@ public class ExamConfigUpdateServiceImpl implements ExamConfigUpdateService {
|
|||
mapping.configurationNodeId))
|
||||
.getOrThrow();
|
||||
|
||||
// update quit password if needed (SEBSERV-482)
|
||||
if (StringUtils.isBlank(exam.quitPassword)) {
|
||||
// copy quit password from config to exam
|
||||
examDAO.updateQuitPassword(exam, examConfigurationValueService.getQuitPassword(exam.id));
|
||||
} else {
|
||||
// copy quit password from exam to config
|
||||
examConfigurationValueService.applyQuitPasswordToConfigs(exam.id, exam.quitPassword);
|
||||
}
|
||||
|
||||
// update seb client restriction if the feature is activated for the exam
|
||||
this.examUpdateHandler
|
||||
.getSEBRestrictionService()
|
||||
|
|
|
@ -339,7 +339,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
return;
|
||||
}
|
||||
|
||||
// for distributed setups check if cached config is still up to date. Flush and reload if not.
|
||||
// for distributed setups check if cached config is still up-to-date. Flush and reload if not.
|
||||
if (this.distributedSetup && !this.examSessionCacheService.isUpToDate(sebConfigForExam)) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
|
@ -530,9 +530,8 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
@Override
|
||||
public Result<Exam> updateExamCache(final Long examId) {
|
||||
|
||||
// TODO check how often this is called in distributed environments
|
||||
//System.out.println("************** performance check: updateExamCache");
|
||||
|
||||
// TODO make interval access. this should only check when the last check was more then 5 seconds ago
|
||||
// TODO is this really needed?
|
||||
try {
|
||||
final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM);
|
||||
final ValueWrapper valueWrapper = cache.get(examId);
|
||||
|
@ -554,7 +553,6 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
.getOr(false);
|
||||
|
||||
if (!BooleanUtils.toBoolean(isUpToDate)) {
|
||||
// TODO this should only flush the exam cache but not the SEB connection cache
|
||||
return flushCache(exam);
|
||||
} else {
|
||||
return Result.of(exam);
|
||||
|
@ -603,7 +601,7 @@ public class ExamSessionServiceImpl implements ExamSessionService {
|
|||
.collect(Collectors.toSet());
|
||||
|
||||
this.clientConnectionDAO.getClientConnectionsOutOfSyc(examId, timestamps)
|
||||
.getOrElse(() -> Collections.emptySet())
|
||||
.getOrElse(Collections::emptySet)
|
||||
.stream()
|
||||
.forEach(this.examSessionCacheService::evictClientConnection);
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import java.util.stream.Collectors;
|
|||
|
||||
import javax.validation.Valid;
|
||||
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
|
@ -652,6 +653,8 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
return entity;
|
||||
});
|
||||
|
||||
this.examAdminService.applyQuitPassword(entity);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
errors.add(0, ErrorMessage.EXAM_IMPORT_ERROR_AUTO_SETUP.of(
|
||||
entity.getModelId(),
|
||||
|
@ -669,6 +672,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
protected Result<Exam> notifySaved(final Exam entity) {
|
||||
return Result.tryCatch(() -> {
|
||||
this.examAdminService.notifyExamSaved(entity);
|
||||
this.examAdminService.applyQuitPassword(entity);
|
||||
this.examSessionService.flushCache(entity);
|
||||
return entity;
|
||||
});
|
||||
|
@ -684,7 +688,8 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
protected Result<Exam> validForSave(final Exam entity) {
|
||||
return super.validForSave(entity)
|
||||
.map(this::checkExamSupporterRole)
|
||||
.map(ExamAdminService::noLMSFieldValidation);
|
||||
.map(ExamAdminService::noLMSFieldValidation)
|
||||
.map(this::checkQuitPasswordChange);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -702,6 +707,22 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
return checkNoActiveSEBClientConnections(entity);
|
||||
}
|
||||
|
||||
private Exam checkQuitPasswordChange(final Exam exam) {
|
||||
if (this.examSessionService.isExamRunning(exam.id) &&
|
||||
examSessionService.hasActiveSEBClientConnections(exam.id)) {
|
||||
|
||||
final Exam oldExam = this.examDAO.byPK(exam.id).getOrThrow();
|
||||
if (!oldExam.quitPassword.equals(exam.quitPassword)) {
|
||||
throw new APIMessageException(APIMessage.fieldValidationError(
|
||||
new FieldError(
|
||||
EXAM.ATTR_QUIT_PASSWORD,
|
||||
EXAM.ATTR_QUIT_PASSWORD,
|
||||
"exam:quitPassword:changeDenied:")));
|
||||
}
|
||||
}
|
||||
return exam;
|
||||
}
|
||||
|
||||
private Exam checkExamSupporterRole(final Exam exam) {
|
||||
final Set<String> examSupporter = this.userDAO.all(
|
||||
this.authorization.getUserService().getCurrentUser().getUserInfo().institutionId,
|
||||
|
|
|
@ -8,18 +8,17 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.weblayer.api;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.Valid;
|
||||
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.mybatis.dynamic.sql.SqlTable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
@ -60,8 +59,11 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe
|
|||
@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.EXAM_TEMPLATE_ENDPOINT)
|
||||
public class ExamTemplateController extends EntityController<ExamTemplate, ExamTemplate> {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ExamTemplateController.class);
|
||||
|
||||
private final ExamTemplateDAO examTemplateDAO;
|
||||
private final ProctoringAdminService proctoringServiceSettingsService;
|
||||
private final ExamConfigurationValueService examConfigurationValueService;
|
||||
|
||||
protected ExamTemplateController(
|
||||
final AuthorizationService authorization,
|
||||
|
@ -70,7 +72,8 @@ public class ExamTemplateController extends EntityController<ExamTemplate, ExamT
|
|||
final UserActivityLogDAO userActivityLogDAO,
|
||||
final PaginationService paginationService,
|
||||
final BeanValidationService beanValidationService,
|
||||
final ProctoringAdminService proctoringServiceSettingsService) {
|
||||
final ProctoringAdminService proctoringServiceSettingsService,
|
||||
final ExamConfigurationValueService examConfigurationValueService) {
|
||||
|
||||
super(
|
||||
authorization,
|
||||
|
@ -82,6 +85,7 @@ public class ExamTemplateController extends EntityController<ExamTemplate, ExamT
|
|||
|
||||
this.examTemplateDAO = entityDAO;
|
||||
this.proctoringServiceSettingsService = proctoringServiceSettingsService;
|
||||
this.examConfigurationValueService = examConfigurationValueService;
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
|
@ -101,6 +105,45 @@ public class ExamTemplateController extends EntityController<ExamTemplate, ExamT
|
|||
.getOrThrow();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<ExamTemplate> validForCreate(final ExamTemplate entity) {
|
||||
return super.validForCreate(entity)
|
||||
.map(this::applyQuitPasswordIfNeeded);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<ExamTemplate> validForSave(final ExamTemplate entity) {
|
||||
return super.validForSave(entity)
|
||||
.map(this::applyQuitPasswordIfNeeded);
|
||||
}
|
||||
|
||||
private ExamTemplate applyQuitPasswordIfNeeded(final ExamTemplate entity) {
|
||||
if (entity.configTemplateId != null) {
|
||||
try {
|
||||
final String quitPassword = this.examConfigurationValueService
|
||||
.getQuitPasswordFromConfigTemplate(entity.configTemplateId);
|
||||
final HashMap<String, String> attributes = new HashMap<>(entity.examAttributes);
|
||||
attributes.put(ExamTemplate.ATTR_QUIT_PASSWORD, quitPassword);
|
||||
return new ExamTemplate(
|
||||
entity.id,
|
||||
entity.institutionId,
|
||||
entity.name,
|
||||
entity.description,
|
||||
entity.examType,
|
||||
entity.supporter,
|
||||
entity.configTemplateId,
|
||||
entity.institutionalDefault,
|
||||
entity.indicatorTemplates,
|
||||
entity.clientGroupTemplates,
|
||||
attributes
|
||||
);
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to apply quit password to Exam Template.", e);
|
||||
}
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// **** Indicator Templates
|
||||
|
||||
|
@ -466,7 +509,7 @@ public class ExamTemplateController extends EntityController<ExamTemplate, ExamT
|
|||
|
||||
final String sortBy = PageSortOrder.decode(sort);
|
||||
return indicators -> {
|
||||
final List<IndicatorTemplate> list = indicators.stream().collect(Collectors.toList());
|
||||
final List<IndicatorTemplate> list = new ArrayList<>(indicators);
|
||||
if (StringUtils.isBlank(sort)) {
|
||||
return list;
|
||||
}
|
||||
|
@ -487,7 +530,7 @@ public class ExamTemplateController extends EntityController<ExamTemplate, ExamT
|
|||
|
||||
final String sortBy = PageSortOrder.decode(sort);
|
||||
return clientGroups -> {
|
||||
final List<ClientGroupTemplate> list = clientGroups.stream().collect(Collectors.toList());
|
||||
final List<ClientGroupTemplate> list = new ArrayList<>(clientGroups);
|
||||
if (StringUtils.isBlank(sort)) {
|
||||
return list;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
server.address=localhost
|
||||
server.port=8080
|
||||
server.port=8090
|
||||
|
||||
sebserver.gui.http.external.scheme=http
|
||||
sebserver.gui.entrypoint=/gui
|
||||
sebserver.gui.webservice.protocol=http
|
||||
sebserver.gui.webservice.address=localhost
|
||||
sebserver.gui.webservice.port=8080
|
||||
sebserver.gui.webservice.port=8090
|
||||
sebserver.gui.webservice.apipath=/admin-api/v1
|
||||
# defines the polling interval that is used to poll the webservice for client connection data on a monitored exam page
|
||||
sebserver.gui.webservice.poll-interval=1000
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
-- ----------------------------------------------------------------
|
||||
-- Add SEB Settings GUI additions (SEBSERV-414 and SEBSERV-465)
|
||||
-- ----------------------------------------------------------------
|
||||
|
|
@ -594,6 +594,8 @@ sebserver.exam.form.supporter.tooltip=A list of users that are allowed to suppor
|
|||
sebserver.exam.form.examTemplate=Exam Template
|
||||
sebserver.exam.form.examTemplate.tooltip=Select an exam template to automatically create the exam, exam configuration and indicators defined by the template.
|
||||
sebserver.exam.form.examTemplate.error=Failed to set type and supporter form template.<br/>Please make sure you have selected the right type and supporter or tray again.
|
||||
sebserver.exam.form.quitpwd=Quit Password
|
||||
sebserver.exam.form.quitpwd.tooltip=A password if set a SEB user must provide to be able to quit SEB<br/>This mirrors the Quit Password from SEB Settings.
|
||||
|
||||
sebserver.exam.form.export.config.popup.title=Export Connection Configuration for Starting the Exam
|
||||
sebserver.exam.form.export.config.name=Name
|
||||
|
|
|
@ -205,7 +205,7 @@ public class ModelObjectJSONGenerator {
|
|||
1L, 1L, 1L, "externalId", true, "name", DateTime.now(), DateTime.now(),
|
||||
ExamType.BYOD, "owner",
|
||||
Arrays.asList("user1", "user2"),
|
||||
ExamStatus.RUNNING, false, "browserExamKeys", true, null, null, null, null);
|
||||
ExamStatus.RUNNING, null, false, "browserExamKeys", true, null, null, null, null);
|
||||
System.out.println(domainObject.getClass().getSimpleName() + ":");
|
||||
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));
|
||||
|
||||
|
|
|
@ -904,6 +904,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
|
|||
null,
|
||||
Utils.immutableCollectionOf(userId),
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
true,
|
||||
|
|
|
@ -62,6 +62,7 @@ public class ExamAPITest extends AdministrationAPIIntegrationTester {
|
|||
exam.owner,
|
||||
Arrays.asList("user5"),
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
true,
|
||||
|
@ -91,6 +92,7 @@ public class ExamAPITest extends AdministrationAPIIntegrationTester {
|
|||
exam.owner,
|
||||
Arrays.asList("user2"),
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
true,
|
||||
|
|
|
@ -91,6 +91,7 @@ public class OlatLmsAPITemplateTest extends AdministrationAPIIntegrationTester {
|
|||
null,
|
||||
null,
|
||||
ExamStatus.FINISHED,
|
||||
null,
|
||||
Boolean.FALSE,
|
||||
null,
|
||||
Boolean.FALSE,
|
||||
|
|
|
@ -121,7 +121,8 @@ public class SEBClientEventCSVExporterTest {
|
|||
final ClientEventRecord event = new ClientEventRecord(0L, 1L, 2, 3L, 4L, new BigDecimal(5), "text");
|
||||
final Exam exam = new Exam(0L, 1L, 3L, "externalid", true, "name", new DateTime(1L),
|
||||
new DateTime(1L),
|
||||
Exam.ExamType.BYOD, "owner", new ArrayList<>(), Exam.ExamStatus.RUNNING, false, "bek", true,
|
||||
Exam.ExamType.BYOD, "owner", new ArrayList<>(), Exam.ExamStatus.RUNNING,
|
||||
null, false, "bek", true,
|
||||
"lastUpdate", 4L, null, attrs);
|
||||
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
final BufferedOutputStream output = new BufferedOutputStream(stream);
|
||||
|
@ -150,7 +151,8 @@ public class SEBClientEventCSVExporterTest {
|
|||
final ClientEventRecord event = new ClientEventRecord(0L, 1L, 2, 3L, 4L, new BigDecimal(5), "text");
|
||||
final Exam exam = new Exam(0L, 1L, 3L, "externalid", true, "name", new DateTime(1L),
|
||||
new DateTime(1L),
|
||||
Exam.ExamType.BYOD, "owner", new ArrayList<>(), Exam.ExamStatus.RUNNING, false, "bek", true,
|
||||
Exam.ExamType.BYOD, "owner", new ArrayList<>(), Exam.ExamStatus.RUNNING,
|
||||
null, false, "bek", true,
|
||||
"lastUpdate", 4L, null, attrs);
|
||||
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
final BufferedOutputStream output = new BufferedOutputStream(stream);
|
||||
|
|
|
@ -52,7 +52,8 @@ public class MoodlePluginCourseRestrictionTest {
|
|||
public void getNoneExistingRestriction() {
|
||||
final MoodlePluginCourseRestriction candidate = crateMockup();
|
||||
final Exam exam = new Exam(1L, 1L, 1L, "101:1:c1:i1",
|
||||
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
|
||||
null, null, null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null);
|
||||
|
||||
final Result<SEBRestriction> sebClientRestriction = candidate.getSEBClientRestriction(exam);
|
||||
|
||||
|
@ -67,7 +68,8 @@ public class MoodlePluginCourseRestrictionTest {
|
|||
public void getSetGetRestriction() {
|
||||
final MoodlePluginCourseRestriction candidate = crateMockup();
|
||||
final Exam exam = new Exam(1L, 1L, 1L, "101:1:c1:i1",
|
||||
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
|
||||
null, null, null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null);
|
||||
|
||||
final SEBRestriction restriction = new SEBRestriction(
|
||||
exam.id,
|
||||
|
@ -156,7 +158,7 @@ public class MoodlePluginCourseRestrictionTest {
|
|||
final ExamConfigurationValueService examConfigurationValueService =
|
||||
Mockito.mock(ExamConfigurationValueService.class);
|
||||
Mockito.when(examConfigurationValueService.getQuitLink(Mockito.anyLong())).thenReturn("quitLink");
|
||||
Mockito.when(examConfigurationValueService.getQuitSecret(Mockito.anyLong())).thenReturn("quitSecret");
|
||||
Mockito.when(examConfigurationValueService.getQuitPassword(Mockito.anyLong())).thenReturn("quitSecret");
|
||||
|
||||
return new MoodlePluginCourseRestriction(jsonMapper, moodleMockupRestTemplateFactory,
|
||||
examConfigurationValueService);
|
||||
|
|
Loading…
Reference in a new issue