From ac21400388c2520e26b963e38a2436788717d97b Mon Sep 17 00:00:00 2001 From: anhefti Date: Wed, 10 Jan 2024 16:38:56 +0100 Subject: [PATCH] SEBSERV-482 implementation no testing yet --- .../java/ch/ethz/seb/sebserver/SEBServer.java | 4 +- .../seb/sebserver/gbl/model/exam/Exam.java | 34 +++--- .../gbl/model/exam/ExamTemplate.java | 4 +- .../ethz/seb/sebserver/gbl/util/Cryptor.java | 15 +++ .../sebserver/gui/content/exam/ExamForm.java | 102 ++++++++-------- .../webservice/servicelayer/dao/ExamDAO.java | 1 + .../servicelayer/dao/impl/ExamDAOImpl.java | 8 ++ .../servicelayer/dao/impl/ExamRecordDAO.java | 106 +++++++++++------ .../dao/impl/ExamTemplateDAOImpl.java | 9 ++ .../servicelayer/exam/ExamAdminService.java | 3 +- .../exam/ExamConfigurationValueService.java | 24 +++- .../exam/impl/ExamAdminServiceImpl.java | 7 ++ .../ExamConfigurationValueServiceImpl.java | 109 ++++++++++++++++-- .../exam/impl/ExamTemplateServiceImpl.java | 1 - .../lms/impl/SEBRestrictionServiceImpl.java | 2 +- .../plugin/MoodlePluginCourseRestriction.java | 2 +- .../lms/impl/olat/OlatLmsAPITemplate.java | 2 +- .../session/ExamConfigUpdateService.java | 16 +-- .../session/ExamSessionService.java | 4 +- .../impl/ExamConfigUpdateServiceImpl.java | 24 +++- .../session/impl/ExamSessionServiceImpl.java | 10 +- .../api/ExamAdministrationController.java | 23 +++- .../weblayer/api/ExamTemplateController.java | 59 ++++++++-- .../config/application-dev-gui.properties | 4 +- .../V27__exam_config_gui_additions_v1_6.sql | 4 + src/main/resources/messages.properties | 2 + .../gbl/model/ModelObjectJSONGenerator.java | 2 +- .../integration/UseCasesIntegrationTest.java | 1 + .../integration/api/admin/ExamAPITest.java | 2 + .../api/admin/OlatLmsAPITemplateTest.java | 1 + .../impl/SEBClientEventCSVExporterTest.java | 6 +- .../MoodlePluginCourseRestrictionTest.java | 8 +- 32 files changed, 428 insertions(+), 171 deletions(-) create mode 100644 src/main/resources/config/sql/base/V27__exam_config_gui_additions_v1_6.sql 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);