diff --git a/src/main/java/ch/ethz/seb/sebserver/SEBServer.java b/src/main/java/ch/ethz/seb/sebserver/SEBServer.java
index da12f52d..52041c9b 100644
--- a/src/main/java/ch/ethz/seb/sebserver/SEBServer.java
+++ b/src/main/java/ch/ethz/seb/sebserver/SEBServer.java
@@ -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)
- *
+ *
* 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.
diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java
index d4cefce4..b7aef066 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java
@@ -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 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;
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ExamTemplate.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ExamTemplate.java
index ef049bb1..32c8884a 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ExamTemplate.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ExamTemplate.java
@@ -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);
}
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Cryptor.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Cryptor.java
index 952a2b8c..bd9e1efe 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Cryptor.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Cryptor.java
@@ -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 encryptCheckAlreadyEncrypted(final CharSequence text) {
+ return Result.tryCatch(() -> {
+
+ // try to decrypt to check if it is already encrypted
+ final Result decryption = this.decrypt(text);
+ if (decryption.hasError()) {
+ return encrypt(text).getOrThrow();
+ } else {
+ return text;
+ }
+ });
+ }
+
public static Result decrypt(final CharSequence cipher, final CharSequence secret) {
return Result.tryCatch(() -> {
if (cipher == null) {
@@ -183,4 +197,5 @@ public class Cryptor {
});
}
+
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java
index 61435176..f43917eb 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java
@@ -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 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);
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java
index f6a056ae..a1767302 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java
@@ -227,4 +227,5 @@ public interface ExamDAO extends ActivatableEntityDAO, 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);
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java
index 161313e3..4b8be0f6 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java
@@ -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 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()),
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java
index 505a2077..618405ef 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamRecordDAO.java
@@ -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 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 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 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;
+ }
+
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamTemplateDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamTemplateDAOImpl.java
index 82b1622f..25795cdb 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamTemplateDAOImpl.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamTemplateDAOImpl.java
@@ -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;
})
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java
index 2ed1a012..fe51b753 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java
@@ -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 {
}
}
-
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamConfigurationValueService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamConfigurationValueService.java
index 15cf3c68..21032fa8 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamConfigurationValueService.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamConfigurationValueService.java
@@ -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 applyQuitPasswordToConfigs(Long examId, String quitPassword);
/** Get the quitLink SEB Setting from the Exam Configuration that is applied to the given exam.
*
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java
index 8e9935d6..0f4cb7b8 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java
@@ -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 initAdditionalAttributesForMoodleExams(final Exam exam) {
return Result.tryCatch(() -> {
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamConfigurationValueServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamConfigurationValueServiceImpl.java
index 55234517..3c5017c5 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamConfigurationValueServiceImpl.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamConfigurationValueServiceImpl.java
@@ -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 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);
+ }
+
}
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamTemplateServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamTemplateServiceImpl.java
index c9e1978c..c484f7dc 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamTemplateServiceImpl.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamTemplateServiceImpl.java
@@ -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()) {
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java
index 74ed6d38..6845185b 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java
@@ -165,7 +165,6 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService {
}
@Override
- @Transactional
public Result 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,
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestriction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestriction.java
index e93bfedb..bbc38041 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestriction.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestriction.java
@@ -136,7 +136,7 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
final ArrayList beks = new ArrayList<>(sebRestrictionData.browserExamKeys);
final ArrayList 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);
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java
index 99b375eb..e4ab7f7b 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java
@@ -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);
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamConfigUpdateService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamConfigUpdateService.java
index 37c79037..c628597a 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamConfigUpdateService.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamConfigUpdateService.java
@@ -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.
- *
+ *
* 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.
- *
+ *
* 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.
- *
+ *
* 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.
- *
+ *
* 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.
- *
+ *
* 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.
- *
+ *
* 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.
- *
+ *
* 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.
*
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java
index 7d181e16..602f3e0d 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java
@@ -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 ACTIVE_CONNECTION_FILTER =
+ Predicate ACTIVE_CONNECTION_FILTER =
cc -> cc.status == ConnectionStatus.ACTIVE;
- public static final Predicate ACTIVE_CONNECTION_DATA_FILTER =
+ Predicate ACTIVE_CONNECTION_DATA_FILTER =
ccd -> ccd.clientConnection.status == ConnectionStatus.ACTIVE;
/** Get the underling ExamDAO service.
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamConfigUpdateServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamConfigUpdateServiceImpl.java
index b3ff107f..8b4e218a 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamConfigUpdateServiceImpl.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamConfigUpdateServiceImpl.java
@@ -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()
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java
index a3155dda..3165a5eb 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java
@@ -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 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);
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java
index 8ec76573..082f41aa 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java
@@ -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 {
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 {
protected Result 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 {
protected Result 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 {
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 examSupporter = this.userDAO.all(
this.authorization.getUserService().getCurrentUser().getUserInfo().institutionId,
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamTemplateController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamTemplateController.java
index 2c6acfa1..377934a1 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamTemplateController.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamTemplateController.java
@@ -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 {
+ 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 validForCreate(final ExamTemplate entity) {
+ return super.validForCreate(entity)
+ .map(this::applyQuitPasswordIfNeeded);
+ }
+
+ @Override
+ protected Result 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 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 {
- final List list = indicators.stream().collect(Collectors.toList());
+ final List list = new ArrayList<>(indicators);
if (StringUtils.isBlank(sort)) {
return list;
}
@@ -487,7 +530,7 @@ public class ExamTemplateController extends EntityController {
- final List list = clientGroups.stream().collect(Collectors.toList());
+ final List list = new ArrayList<>(clientGroups);
if (StringUtils.isBlank(sort)) {
return list;
}
diff --git a/src/main/resources/config/application-dev-gui.properties b/src/main/resources/config/application-dev-gui.properties
index 58cfaa5b..a0194445 100644
--- a/src/main/resources/config/application-dev-gui.properties
+++ b/src/main/resources/config/application-dev-gui.properties
@@ -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
diff --git a/src/main/resources/config/sql/base/V27__exam_config_gui_additions_v1_6.sql b/src/main/resources/config/sql/base/V27__exam_config_gui_additions_v1_6.sql
new file mode 100644
index 00000000..7dccdec4
--- /dev/null
+++ b/src/main/resources/config/sql/base/V27__exam_config_gui_additions_v1_6.sql
@@ -0,0 +1,4 @@
+-- ----------------------------------------------------------------
+-- Add SEB Settings GUI additions (SEBSERV-414 and SEBSERV-465)
+-- ----------------------------------------------------------------
+
diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties
index 41b65184..4785337b 100644
--- a/src/main/resources/messages.properties
+++ b/src/main/resources/messages.properties
@@ -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.
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
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
diff --git a/src/test/java/ch/ethz/seb/sebserver/gbl/model/ModelObjectJSONGenerator.java b/src/test/java/ch/ethz/seb/sebserver/gbl/model/ModelObjectJSONGenerator.java
index 3e0ab14d..e2f50695 100644
--- a/src/test/java/ch/ethz/seb/sebserver/gbl/model/ModelObjectJSONGenerator.java
+++ b/src/test/java/ch/ethz/seb/sebserver/gbl/model/ModelObjectJSONGenerator.java
@@ -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));
diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java
index 8e3e981f..bfee60ad 100644
--- a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java
+++ b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java
@@ -904,6 +904,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
null,
Utils.immutableCollectionOf(userId),
null,
+ null,
false,
null,
true,
diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamAPITest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamAPITest.java
index 5bd8c9b1..f46b3de3 100644
--- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamAPITest.java
+++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamAPITest.java
@@ -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,
diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/OlatLmsAPITemplateTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/OlatLmsAPITemplateTest.java
index 2431220e..d43c0616 100644
--- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/OlatLmsAPITemplateTest.java
+++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/OlatLmsAPITemplateTest.java
@@ -91,6 +91,7 @@ public class OlatLmsAPITemplateTest extends AdministrationAPIIntegrationTester {
null,
null,
ExamStatus.FINISHED,
+ null,
Boolean.FALSE,
null,
Boolean.FALSE,
diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventCSVExporterTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventCSVExporterTest.java
index 840e87f7..4667244c 100644
--- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventCSVExporterTest.java
+++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/SEBClientEventCSVExporterTest.java
@@ -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);
diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestrictionTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestrictionTest.java
index 42daa7a8..3e8191ae 100644
--- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestrictionTest.java
+++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestrictionTest.java
@@ -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 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);