From 6bf7da76193bda74ddd00d351c9ce98f36acf232 Mon Sep 17 00:00:00 2001 From: Matti Lupari Date: Fri, 5 Jan 2024 14:23:56 +0200 Subject: [PATCH] CSCEXAM-1240 Examination event specific quit password --- .../ExaminationEventController.java | 73 ++++++++--- app/controllers/StudentActionsController.java | 7 +- app/impl/EmailComposerImpl.java | 7 +- app/models/ExaminationEventConfiguration.java | 34 +++++ app/sanitizers/Attrs.java | 1 + app/sanitizers/ExaminationEventSanitizer.java | 6 +- app/util/config/ByodConfigHandler.scala | 4 +- app/util/config/ByodConfigHandlerImpl.scala | 13 +- app/util/config/ConfigReader.java | 1 - app/util/config/ConfigReaderImpl.java | 5 - build.sbt | 123 ++---------------- conf/application.conf | 6 - conf/dev.conf | 1 - conf/evolutions/default/132.sql | 6 + test/functional/ByodConfigHandlerTest.java | 6 +- .../examination-event-dialog.component.html | 32 ++++- .../examination-event-dialog.component.ts | 18 ++- ui/src/app/exam/exam.model.ts | 1 + ui/src/app/exam/exam.service.ts | 1 + .../logout/examination-logout.component.ts | 22 ++-- ui/src/assets/i18n/en.json | 3 +- ui/src/assets/i18n/fi.json | 3 +- ui/src/assets/i18n/sv.json | 3 +- 23 files changed, 189 insertions(+), 187 deletions(-) create mode 100644 conf/evolutions/default/132.sql diff --git a/app/controllers/ExaminationEventController.java b/app/controllers/ExaminationEventController.java index 5b9bf81777..ade59497f7 100644 --- a/app/controllers/ExaminationEventController.java +++ b/app/controllers/ExaminationEventController.java @@ -136,9 +136,13 @@ public Result insertExaminationEvent(Long eid, Http.Request request) { if (capacity + ub > configReader.getMaxByodExaminationParticipantCount()) { return forbidden("i18n_error_max_capacity_exceeded"); } - String password = request.attrs().get(Attrs.SETTINGS_PASSWORD); - if (exam.getImplementation() == Exam.Implementation.CLIENT_AUTH && password == null) { - return forbidden("no password provided"); + String quitPassword = request.attrs().get(Attrs.QUIT_PASSWORD); + if (exam.getImplementation() == Exam.Implementation.CLIENT_AUTH && quitPassword == null) { + return forbidden("no quit password provided"); + } + String settingsPassword = request.attrs().get(Attrs.SETTINGS_PASSWORD); + if (exam.getImplementation() == Exam.Implementation.CLIENT_AUTH && settingsPassword == null) { + return forbidden("no settings password provided"); } ee.setStart(start); ee.setDescription(request.attrs().get(Attrs.DESCRIPTION)); @@ -147,12 +151,14 @@ public Result insertExaminationEvent(Long eid, Http.Request request) { eec.setExaminationEvent(ee); eec.setExam(exam); eec.setHash(UUID.randomUUID().toString()); - if (password != null) { - encryptSettingsPassword(eec, password); + if (quitPassword != null && settingsPassword != null) { + encryptQuitPassword(eec, quitPassword); + encryptSettingsPassword(eec, settingsPassword, quitPassword); + // Pass back the plaintext password, so it can be shown to user + eec.setQuitPassword(quitPassword); + eec.setSettingsPassword(settingsPassword); } eec.save(); - // Pass back the plaintext password, so it can be shown to user - eec.setSettingsPassword(request.attrs().get(Attrs.SETTINGS_PASSWORD)); return ok(eec); } @@ -172,9 +178,13 @@ public Result updateExaminationEvent(Long eid, Long eecid, Http.Request request) ExaminationEventConfiguration eec = oeec.get(); boolean hasEnrolments = !eec.getExamEnrolments().isEmpty(); ExaminationEvent ee = eec.getExaminationEvent(); - String password = request.attrs().get(Attrs.SETTINGS_PASSWORD); - if (eec.getExam().getImplementation() == Exam.Implementation.CLIENT_AUTH && password == null) { - return forbidden("no password provided"); + String quitPassword = request.attrs().get(Attrs.QUIT_PASSWORD); + if (eec.getExam().getImplementation() == Exam.Implementation.CLIENT_AUTH && quitPassword == null) { + return forbidden("no quit password provided"); + } + String settingsPassword = request.attrs().get(Attrs.SETTINGS_PASSWORD); + if (eec.getExam().getImplementation() == Exam.Implementation.CLIENT_AUTH && settingsPassword == null) { + return forbidden("no settings password provided"); } DateTime start = request.attrs().get(Attrs.START_DATE); if (!hasEnrolments) { @@ -194,19 +204,23 @@ public Result updateExaminationEvent(Long eid, Long eecid, Http.Request request) } ee.setCapacity(capacity); ee.setDescription(request.attrs().get(Attrs.DESCRIPTION)); - ee.update(); - if (password == null) { + if (quitPassword == null || settingsPassword == null) { return ok(eec); - } else if (!hasEnrolments) { - encryptSettingsPassword(eec, password); + } + if (!hasEnrolments) { + encryptQuitPassword(eec, settingsPassword); + encryptSettingsPassword(eec, settingsPassword, quitPassword); eec.save(); - // Pass back the plaintext password, so it can be shown to user - eec.setSettingsPassword(request.attrs().get(Attrs.SETTINGS_PASSWORD)); + // Pass back the plaintext passwords, so they can be shown to user + eec.setQuitPassword(quitPassword); + eec.setSettingsPassword(settingsPassword); } else { // Disallow changing password if enrolments exist for this event - // TODO: check how this could be made possible. Would need resending seb-files with new encryption - // Send back the original (unchanged password) + // Pass back the original unchanged passwords + eec.setQuitPassword( + byodConfigHandler.getPlaintextPassword(eec.getEncryptedQuitPassword(), eec.getQuitPasswordSalt()) + ); eec.setSettingsPassword( byodConfigHandler.getPlaintextPassword( eec.getEncryptedSettingsPassword(), @@ -246,7 +260,7 @@ public Result removeExaminationEvent(Long eid, Long eeid) { return ok(); } - private void encryptSettingsPassword(ExaminationEventConfiguration eec, String password) { + private void encryptSettingsPassword(ExaminationEventConfiguration eec, String password, String quitPassword) { try { String oldPwd = eec.getEncryptedSettingsPassword() != null ? byodConfigHandler.getPlaintextPassword( @@ -260,7 +274,26 @@ private void encryptSettingsPassword(ExaminationEventConfiguration eec, String p eec.setEncryptedSettingsPassword(byodConfigHandler.getEncryptedPassword(password, newSalt)); eec.setSettingsPasswordSalt(newSalt); // Pre-calculate config key, so we don't need to do it each time a check is needed - eec.setConfigKey(byodConfigHandler.calculateConfigKey(eec.getHash())); + eec.setConfigKey(byodConfigHandler.calculateConfigKey(eec.getHash(), quitPassword)); + } + } catch (Exception e) { + logger.error("unable to set settings password", e); + throw new RuntimeException(e); + } + } + + private void encryptQuitPassword(ExaminationEventConfiguration eec, String password) { + try { + String oldPwd = eec.getEncryptedQuitPassword() != null + ? byodConfigHandler.getPlaintextPassword(eec.getEncryptedQuitPassword(), eec.getQuitPasswordSalt()) + : null; + + if (!password.equals(oldPwd)) { + String newSalt = UUID.randomUUID().toString(); + eec.setEncryptedQuitPassword(byodConfigHandler.getEncryptedPassword(password, newSalt)); + eec.setQuitPasswordSalt(newSalt); + // Pre-calculate config key, so we don't need to do it each time a check is needed + eec.setConfigKey(byodConfigHandler.calculateConfigKey(eec.getHash(), password)); } } catch (Exception e) { logger.error("unable to set settings password", e); diff --git a/app/controllers/StudentActionsController.java b/app/controllers/StudentActionsController.java index 8730f3043e..19585ab2a6 100644 --- a/app/controllers/StudentActionsController.java +++ b/app/controllers/StudentActionsController.java @@ -344,6 +344,10 @@ public Result getExamConfigFile(Long enrolmentId, Http.Request request) { String examName = oee.get().getExam().getName(); ExaminationEventConfiguration eec = oee.get().getExaminationEventConfiguration(); String fileName = examName.replace(" ", "-"); + String quitPassword = byodConfigHandler.getPlaintextPassword( + eec.getEncryptedQuitPassword(), + eec.getQuitPasswordSalt() + ); File file; try { file = File.createTempFile(fileName, ".seb"); @@ -351,7 +355,8 @@ public Result getExamConfigFile(Long enrolmentId, Http.Request request) { byte[] data = byodConfigHandler.getExamConfig( eec.getHash(), eec.getEncryptedSettingsPassword(), - eec.getSettingsPasswordSalt() + eec.getSettingsPasswordSalt(), + quitPassword ); fos.write(data); fos.close(); diff --git a/app/impl/EmailComposerImpl.java b/app/impl/EmailComposerImpl.java index 6d78dccf36..5631bd47f1 100644 --- a/app/impl/EmailComposerImpl.java +++ b/app/impl/EmailComposerImpl.java @@ -348,6 +348,10 @@ public void composeExaminationEventNotification(User recipient, ExamEnrolment en if (exam.getImplementation() == Exam.Implementation.CLIENT_AUTH) { // Attach a SEB config file + String quitPassword = byodConfigHandler.getPlaintextPassword( + config.getEncryptedQuitPassword(), + config.getQuitPasswordSalt() + ); String fileName = exam.getName().replace(" ", "-"); File file; try { @@ -356,7 +360,8 @@ public void composeExaminationEventNotification(User recipient, ExamEnrolment en byte[] data = byodConfigHandler.getExamConfig( config.getHash(), config.getEncryptedSettingsPassword(), - config.getSettingsPasswordSalt() + config.getSettingsPasswordSalt(), + quitPassword ); fos.write(data); fos.close(); diff --git a/app/models/ExaminationEventConfiguration.java b/app/models/ExaminationEventConfiguration.java index bf3f212437..15aa3a3208 100644 --- a/app/models/ExaminationEventConfiguration.java +++ b/app/models/ExaminationEventConfiguration.java @@ -48,9 +48,16 @@ public class ExaminationEventConfiguration extends GeneratedIdentityModel { @JsonIgnore private byte[] encryptedSettingsPassword; + @Lob + @JsonIgnore + private byte[] encryptedQuitPassword; + @JsonIgnore private String settingsPasswordSalt; + @JsonIgnore + private String quitPasswordSalt; + @JsonIgnore private String configKey; @@ -60,6 +67,9 @@ public class ExaminationEventConfiguration extends GeneratedIdentityModel { @Transient private String settingsPassword; + @Transient + private String quitPassword; + public Exam getExam() { return exam; } @@ -92,10 +102,26 @@ public void setEncryptedSettingsPassword(byte[] encryptedSettingsPassword) { this.encryptedSettingsPassword = encryptedSettingsPassword; } + public byte[] getEncryptedQuitPassword() { + return encryptedQuitPassword; + } + + public void setEncryptedQuitPassword(byte[] encryptedQuitPassword) { + this.encryptedQuitPassword = encryptedQuitPassword; + } + public String getSettingsPasswordSalt() { return settingsPasswordSalt; } + public String getQuitPasswordSalt() { + return quitPasswordSalt; + } + + public void setQuitPasswordSalt(String quitPasswordSalt) { + this.quitPasswordSalt = quitPasswordSalt; + } + public void setSettingsPasswordSalt(String settingsPasswordSalt) { this.settingsPasswordSalt = settingsPasswordSalt; } @@ -124,6 +150,14 @@ public void setSettingsPassword(String settingsPassword) { this.settingsPassword = settingsPassword; } + public String getQuitPassword() { + return quitPassword; + } + + public void setQuitPassword(String quitPassword) { + this.quitPassword = quitPassword; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/app/sanitizers/Attrs.java b/app/sanitizers/Attrs.java index 687893e1e2..059cff03c0 100644 --- a/app/sanitizers/Attrs.java +++ b/app/sanitizers/Attrs.java @@ -60,6 +60,7 @@ public enum Attrs { public static final TypedKey COURSE_CODE = TypedKey.create("code"); public static final TypedKey ANONYMOUS = TypedKey.create("anonymous"); public static final TypedKey SETTINGS_PASSWORD = TypedKey.create("settingsPassword"); + public static final TypedKey QUIT_PASSWORD = TypedKey.create("quitPassword"); public static final TypedKey QUESTION_TEXT = TypedKey.create("question"); public static final TypedKey ANSWER_INSTRUCTIONS = TypedKey.create("answerInstructions"); public static final TypedKey EVALUATION_CRITERIA = TypedKey.create("evaluationCriteria"); diff --git a/app/sanitizers/ExaminationEventSanitizer.java b/app/sanitizers/ExaminationEventSanitizer.java index 68a247ff79..a35931b7d4 100644 --- a/app/sanitizers/ExaminationEventSanitizer.java +++ b/app/sanitizers/ExaminationEventSanitizer.java @@ -26,7 +26,8 @@ public class ExaminationEventSanitizer extends BaseSanitizer { protected Http.Request sanitize(Http.Request req, JsonNode body) throws SanitizingException { if (body.has("config")) { JsonNode configNode = body.get("config"); - String pwd = configNode.path("settingsPassword").asText(null); + String settingsPassword = configNode.path("settingsPassword").asText(null); + String quitPassword = configNode.path("quitPassword").asText(null); JsonNode eventNode = configNode.get("examinationEvent"); DateTime dateTime = DateTime.parse(eventNode.get("start").asText(), ISODateTimeFormat.dateTime()); String description = eventNode.get("description").asText(); @@ -35,7 +36,8 @@ protected Http.Request sanitize(Http.Request req, JsonNode body) throws Sanitizi .addAttr(Attrs.START_DATE, dateTime) .addAttr(Attrs.DESCRIPTION, description) .addAttr(Attrs.CAPACITY, capacity) - .addAttr(Attrs.SETTINGS_PASSWORD, pwd); + .addAttr(Attrs.SETTINGS_PASSWORD, settingsPassword) + .addAttr(Attrs.QUIT_PASSWORD, quitPassword); } else { throw new SanitizingException("missing required data"); } diff --git a/app/util/config/ByodConfigHandler.scala b/app/util/config/ByodConfigHandler.scala index 3cd86c97e5..19c2d4e0d2 100644 --- a/app/util/config/ByodConfigHandler.scala +++ b/app/util/config/ByodConfigHandler.scala @@ -5,8 +5,8 @@ import java.util.Optional import play.mvc.{Http, Result} trait ByodConfigHandler: - def getExamConfig(hash: String, pwd: Array[Byte], salt: String): Array[Byte] - def calculateConfigKey(hash: String): String + def getExamConfig(hash: String, pwd: Array[Byte], salt: String, quitPwd: String): Array[Byte] + def calculateConfigKey(hash: String, quitPwd: String): String def getPlaintextPassword(pwd: Array[Byte], salt: String): String def getEncryptedPassword(pwd: String, salt: String): Array[Byte] def checkUserAgent(request: Http.RequestHeader, examConfigKey: String): Optional[Result] diff --git a/app/util/config/ByodConfigHandlerImpl.scala b/app/util/config/ByodConfigHandlerImpl.scala index 1d9719c89b..80749f0c7d 100644 --- a/app/util/config/ByodConfigHandlerImpl.scala +++ b/app/util/config/ByodConfigHandlerImpl.scala @@ -39,14 +39,13 @@ class ByodConfigHandlerImpl @Inject() (configReader: ConfigReader, env: Environm /* FIXME: have Apache provide us with X-Forwarded-Proto header so we can resolve this automatically */ private val protocol = URI.create(configReader.getHostName).toURL.getProtocol - private def getTemplate(hash: String): String = + private def getTemplate(hash: String, quitPwdPlain: String): String = val path = s"${env.rootPath.getAbsolutePath}/conf/seb.template.plist" val startUrl = s"${configReader.getHostName}?exam=$hash" val quitLink = configReader.getQuitExaminationLink val adminPwd = DigestUtils.sha256Hex(configReader.getExaminationAdminPassword) - val quitPwdPlain = configReader.getQuitPassword val quitPwd = DigestUtils.sha256Hex(quitPwdPlain) - val allowQuitting = if (quitPwdPlain.isEmpty) "" else "" + val allowQuitting = if quitPwdPlain.isEmpty then "" else "" val source = Source.fromFile(path) val template = source.mkString .replace(StartUrlPlaceholder, startUrl) @@ -98,8 +97,8 @@ class ByodConfigHandlerImpl @Inject() (configReader: ConfigReader, env: Environm .sortBy(_._1.toLowerCase) Some(JsObject(json)) - override def getExamConfig(hash: String, pwd: Array[Byte], salt: String): Array[Byte] = - val template = getTemplate(hash) + override def getExamConfig(hash: String, pwd: Array[Byte], salt: String, quitPwd: String): Array[Byte] = + val template = getTemplate(hash, quitPwd) val templateGz = compress(template.getBytes(StandardCharsets.UTF_8)) // Decrypt user defined setting password val plaintextPwd = getPlaintextPassword(pwd, salt) @@ -128,14 +127,14 @@ class ByodConfigHandlerImpl @Inject() (configReader: ConfigReader, env: Environm Some(Results.unauthorized("Wrong configuration key digest")).toJava } - override def calculateConfigKey(hash: String): String = + override def calculateConfigKey(hash: String, quitPwd: String): String = // Override the DTD setting. We need it with PLIST format and in order to integrate with SBT val parser = XML.withSAXParser { val factory = SAXParserFactory.newInstance() factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false) factory.newSAXParser() } - val plist: Node = parser.loadString(getTemplate(hash)) + val plist: Node = parser.loadString(getTemplate(hash, quitPwd)) // Construct a Json-like structure out of .plist and create a digest over it // See SEB documentation for details dictToJson((plist \ "dict").head) match diff --git a/app/util/config/ConfigReader.java b/app/util/config/ConfigReader.java index ae2c6e7635..5b1b56eac3 100644 --- a/app/util/config/ConfigReader.java +++ b/app/util/config/ConfigReader.java @@ -33,7 +33,6 @@ public interface ConfigReader { String getQuitExaminationLink(); String getExaminationAdminPassword(); String getSettingsPasswordEncryptionKey(); - String getQuitPassword(); String getHomeOrganisationRef(); Integer getMaxByodExaminationParticipantCount(); String getCourseCodePrefix(); diff --git a/app/util/config/ConfigReaderImpl.java b/app/util/config/ConfigReaderImpl.java index 39a1038401..698ad1c03a 100644 --- a/app/util/config/ConfigReaderImpl.java +++ b/app/util/config/ConfigReaderImpl.java @@ -197,11 +197,6 @@ public String getSettingsPasswordEncryptionKey() { return config.getString("exam.exam.seb.settingsPwd.encryption.key"); } - @Override - public String getQuitPassword() { - return config.getString("exam.exam.seb.quitPwd"); - } - @Override public String getHomeOrganisationRef() { return config.getString("exam.integration.iop.organisationRef"); diff --git a/build.sbt b/build.sbt index ef1cce5bf6..e42efd1bdb 100644 --- a/build.sbt +++ b/build.sbt @@ -1,8 +1,3 @@ -import play.sbt.PlayRunHook - -import scala.sys.process.Process -import scala.util.Properties - name := "exam" version := "6.2.0" @@ -17,7 +12,7 @@ lazy val root = (project in file(".")).enablePlugins(PlayJava, PlayEbean) libraryDependencies ++= Seq(javaJdbc, ws, evolutions, filters, guice) -libraryDependencies += "be.objectify" %% "deadbolt-java" % "3.0.0" +libraryDependencies += "be.objectify" %% "deadbolt-java" % "3.0.0" libraryDependencies += "com.networknt" % "json-schema-validator" % "1.0.82" libraryDependencies += "com.google.code.gson" % "gson" % "2.10.1" libraryDependencies += "com.opencsv" % "opencsv" % "5.7.1" @@ -31,14 +26,14 @@ libraryDependencies += "org.cryptonode.jncryptor" % "jncryptor" % "1 libraryDependencies += "joda-time" % "joda-time" % "2.12.5" libraryDependencies += "org.jsoup" % "jsoup" % "1.15.4" libraryDependencies += "org.postgresql" % "postgresql" % "42.5.4" -libraryDependencies += "com.icegreen" % "greenmail" % "2.1.0-alpha-3" % "test" -libraryDependencies += "com.icegreen" % "greenmail-junit4" % "2.0.0" % "test" -libraryDependencies += "com.jayway.jsonpath" % "json-path" % "2.7.0" % "test" -libraryDependencies += "net.jodah" % "concurrentunit" % "0.4.6" % "test" -libraryDependencies += "org.eclipse.jetty" % "jetty-server" % "11.0.14" % "test" -libraryDependencies += "org.eclipse.jetty" % "jetty-servlet" % "11.0.14" % "test" -libraryDependencies += "org.easytesting" % "fest-assert" % "1.4" % "test" -libraryDependencies += "org.yaml" % "snakeyaml" % "2.0" % "test" +libraryDependencies += "com.icegreen" % "greenmail" % "2.1.0-alpha-3" % "test" +libraryDependencies += "com.icegreen" % "greenmail-junit4" % "2.0.0" % "test" +libraryDependencies += "com.jayway.jsonpath" % "json-path" % "2.7.0" % "test" +libraryDependencies += "net.jodah" % "concurrentunit" % "0.4.6" % "test" +libraryDependencies += "org.eclipse.jetty" % "jetty-server" % "11.0.14" % "test" +libraryDependencies += "org.eclipse.jetty" % "jetty-servlet" % "11.0.14" % "test" +libraryDependencies += "org.easytesting" % "fest-assert" % "1.4" % "test" +libraryDependencies += "org.yaml" % "snakeyaml" % "2.0" % "test" //dependencyOverrides += "com.sun.mail" % "javax.mail" % "1.6.2" % "test" @@ -52,103 +47,5 @@ Test / testOptions += Tests.Argument(TestFrameworks.JUnit, "-a", "-v") Test / javaOptions += "-Dconfig.resource=integrationtest.conf" -Compile / doc / sources := Seq.empty +Compile / doc / sources := Seq.empty Compile / packageDoc / publishArtifact := false - -lazy val frontendDirectory = baseDirectory { - _ / "ui" -} - -lazy val protractorDirectory = baseDirectory { - _ / "ui/protractor" -} - -/** - * Webpack dev server task - */ -def withoutWebpackServer = Properties.propOrEmpty("withoutWebpackServer") - -def webpackTask = Def.taskDyn[PlayRunHook] { - if (withoutWebpackServer.equals("true")) - Def.task { - NoOp() - } else { - val webpackBuild = taskKey[Unit]("Webpack build task.") - - webpackBuild := { - Process("npm start", frontendDirectory.value).run - } - - Universal / packageBin := (Universal / packageBin dependsOn webpackBuild).value - - Def.task { - frontendDirectory.map(WebpackServer(_)).value - } - } -} - -PlayKeys.playRunHooks += webpackTask.value - -/** - * Karma test task. - */ -def skipUiTests = Properties.propOrEmpty("skipUiTests") - -def protractorConf = Properties.propOrEmpty("config.file") - -lazy val npmInstall = taskKey[Option[Process]]("Npm install task") -npmInstall := { - Some(Process("npm install", frontendDirectory.value).run()) -} - -lazy val protractorInstall = taskKey[Option[Process]]("Protractor install task") -protractorInstall := { - Some(Process("npm install", protractorDirectory.value).run()) -} - -lazy val karmaTest = taskKey[Option[Process]]("Karma test task") -karmaTest := { - Some( - Process("node_modules/karma/bin/karma start ./test/karma.conf.ci.js", frontendDirectory.value) - .run()) -} - -lazy val webDriverUpdate = taskKey[Option[Process]]("Web driver update task") -webDriverUpdate := { - Some( - Process("node_modules/protractor/bin/webdriver-manager update", protractorDirectory.value) - .run()) -} - -/*test in Test := { - if (karmaTest.value.get.exitValue() != 0) - sys.error("Karma tests failed!") - (test in Test).value -}*/ - -def uiTestTask = Def.taskDyn[Seq[PlayRunHook]] { - if (!skipUiTests.equals("true") && npmInstall.value.get - .exitValue() == 0 && protractorInstall.value.get.exitValue() == 0) { - def bdval = baseDirectory.value - - def fdval = frontendDirectory.value - - Def.task { - Seq( - if (protractorConf - .equals("conf/protractor.conf") && webDriverUpdate.value.get.exitValue() == 0) - Protractor(bdval, - Properties.propOrElse("protractor.config", "conf.js"), - Properties.propOrElse("protractor.args", " ")) - else { - Karma(fdval) - }) - } - } else { - Def.task { - Seq(NoOp()) - } - } -} - -// PlayKeys.playRunHooks ++= uiTestTask.value diff --git a/conf/application.conf b/conf/application.conf index f44224b182..9e98e57815 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -206,12 +206,6 @@ exam.byod.maxConcurrentParticipants = 100000 exam.exam.seb.settingsPwd.encryption.key = "changeme" # Link for quitting SEB after having returned the exam. Displayed for students on the EXAM UI. exam.exam.seb.quitLink = "http://quit.seb.now" -# SEB client quit password. Define one if you insist that supervisors can enter it for quitting student's SEB client -# during exam. Please bear in mind that if a student gets access to this password, then nothing technically prevents her -# from quitting SEB, leaving the examination event, restarting SEB and finishing the exam elsewhere without supervision. -# Regardless of this password there's always the above quit link (exam.exam.seb.quitLink) for quitting SEB in a -# controlled manner. -exam.exam.seb.quitPwd = "" # SEB configuration admin password. Used to protect generated SEB configuration files so that regular users can not view # the contents using SEB configuration tool. Even if they could, modifications would not work because exam verifies that # client's configuration is unaltered. You may also choose to use randomized admin passwords in case you wish that SEB diff --git a/conf/dev.conf b/conf/dev.conf index 93c0f950cf..275f449f61 100644 --- a/conf/dev.conf +++ b/conf/dev.conf @@ -32,4 +32,3 @@ play.evolutions.db.default.autocommit = false exam.byod.seb.active = true exam.byod.home.active = true -exam.exam.seb.quitPwd = "quit now" diff --git a/conf/evolutions/default/132.sql b/conf/evolutions/default/132.sql new file mode 100644 index 0000000000..6ad3979bd3 --- /dev/null +++ b/conf/evolutions/default/132.sql @@ -0,0 +1,6 @@ +# --- !Ups +ALTER TABLE examination_event_configuration ADD encrypted_quit_password BYTEA NULL; +ALTER TABLE examination_event_configuration ADD quit_password_salt VARCHAR(36) NULL; +# --- !Downs +ALTER TABLE examination_event_configuration DROP encrypted_quit_password; +ALTER TABLE examination_event_configuration DROP quit_password_salt; diff --git a/test/functional/ByodConfigHandlerTest.java b/test/functional/ByodConfigHandlerTest.java index ad098d2f6f..aa74d938a5 100644 --- a/test/functional/ByodConfigHandlerTest.java +++ b/test/functional/ByodConfigHandlerTest.java @@ -15,8 +15,8 @@ public void testCalculateConfigKey() { app, () -> { ByodConfigHandler bch = app.injector().instanceOf(ByodConfigHandler.class); - String key = bch.calculateConfigKey("123456"); - assertThat(key).isEqualTo("e3c8007ac6b9f8a70b20b9696eb38a13097b22806a85e13f356a6eb45d7800d3"); + String key = bch.calculateConfigKey("123456", "quit"); + assertThat(key).isEqualTo("50ea3844757d2c915284d1f9ded0d6c9ceb41930105ac32530c4c1eb8623053a"); } ); } @@ -28,7 +28,7 @@ public void testCreateConfigFile() { () -> { ByodConfigHandler bch = app.injector().instanceOf(ByodConfigHandler.class); byte[] pwd = bch.getEncryptedPassword("password", "salt"); - byte[] data = bch.getExamConfig("123456", pwd, "salt"); + byte[] data = bch.getExamConfig("123456", pwd, "salt", "quit"); // sanity check that we actually have a reasonably sized file content assertThat(data.length).isGreaterThan(1000); } diff --git a/ui/src/app/exam/editor/events/examination-event-dialog.component.html b/ui/src/app/exam/editor/events/examination-event-dialog.component.html index c4d66d9364..cf3b47b38f 100644 --- a/ui/src/app/exam/editor/events/examination-event-dialog.component.html +++ b/ui/src/app/exam/editor/events/examination-event-dialog.component.html @@ -53,19 +53,37 @@

/>
- -
+ +
- +
+
+
+
+ +
+ +
+
diff --git a/ui/src/app/exam/editor/events/examination-event-dialog.component.ts b/ui/src/app/exam/editor/events/examination-event-dialog.component.ts index ce6ea3d906..11297c6da6 100644 --- a/ui/src/app/exam/editor/events/examination-event-dialog.component.ts +++ b/ui/src/app/exam/editor/events/examination-event-dialog.component.ts @@ -41,9 +41,11 @@ export class ExaminationEventDialogComponent implements OnInit { start = new Date(new Date().getTime() + 60 * 1000); description = ''; capacity = 0; - password?: string; + quitPassword?: string; + settingsPassword?: string; hasEnrolments = false; - pwdInputType = 'password'; + settingsPasswordInputType = 'password'; + quitPasswordInputType = 'password'; now = new Date(); maxDateValidator?: Date; @@ -59,7 +61,8 @@ export class ExaminationEventDialogComponent implements OnInit { this.start = new Date(this.config.examinationEvent.start); this.description = this.config.examinationEvent.description; this.capacity = this.config.examinationEvent.capacity; - this.password = this.config.settingsPassword; + this.quitPassword = this.config.quitPassword; + this.settingsPassword = this.config.settingsPassword; this.hasEnrolments = this.config.examEnrolments.length > 0; } else { this.start.setMinutes(60); @@ -70,7 +73,11 @@ export class ExaminationEventDialogComponent implements OnInit { } } - togglePasswordInputType = () => (this.pwdInputType = this.pwdInputType === 'text' ? 'password' : 'text'); + toggleSettingsPasswordInputType = () => + (this.settingsPasswordInputType = this.settingsPasswordInputType === 'text' ? 'password' : 'text'); + toggleQuitPasswordInputType = () => + (this.quitPasswordInputType = this.quitPasswordInputType === 'text' ? 'password' : 'text'); + onStartDateChange = (event: { date: Date }) => { if (this.maxDateValidator && this.maxDateValidator < event.date) { this.toast.error( @@ -100,7 +107,8 @@ export class ExaminationEventDialogComponent implements OnInit { description: this.description, capacity: this.capacity, }, - settingsPassword: this.password, + settingsPassword: this.settingsPassword, + quitPassword: this.quitPassword, }, }; if (!this.config) { diff --git a/ui/src/app/exam/exam.model.ts b/ui/src/app/exam/exam.model.ts index 96b9f9e021..059170fb43 100644 --- a/ui/src/app/exam/exam.model.ts +++ b/ui/src/app/exam/exam.model.ts @@ -245,6 +245,7 @@ export interface ExaminationEvent { export interface ExaminationEventConfiguration { id?: number; + quitPassword?: string; settingsPassword?: string; exam: Exam; examinationEvent: ExaminationEvent; diff --git a/ui/src/app/exam/exam.service.ts b/ui/src/app/exam/exam.service.ts index 21fceb6a11..75afc50783 100644 --- a/ui/src/app/exam/exam.service.ts +++ b/ui/src/app/exam/exam.service.ts @@ -42,6 +42,7 @@ export type ExaminationEventConfigurationInput = { description: string; capacity: number; }; + quitPassword?: string; settingsPassword?: string; }; }; diff --git a/ui/src/app/examination/logout/examination-logout.component.ts b/ui/src/app/examination/logout/examination-logout.component.ts index 631c9e8dbe..221903b828 100644 --- a/ui/src/app/examination/logout/examination-logout.component.ts +++ b/ui/src/app/examination/logout/examination-logout.component.ts @@ -22,16 +22,18 @@ import { ExaminationStatusService } from '../examination-status.service'; @Component({ selector: 'xm-examination-logout', template: ` -
-

{{ 'i18n_end_of_exam' | translate }}

-

- {{ reasonPhrase | translate }} -

-

- - {{ 'i18n_quit_seb' | translate }} - -

+
+
+
+
+

{{ 'i18n_end_of_exam' | translate }}

+

{{ reasonPhrase | translate }}

+ {{ 'i18n_quit_seb' | translate }} + +
+
+
`, standalone: true, diff --git a/ui/src/assets/i18n/en.json b/ui/src/assets/i18n/en.json index 93c0dfad4c..cdb6f44cc4 100644 --- a/ui/src/assets/i18n/en.json +++ b/ui/src/assets/i18n/en.json @@ -1158,5 +1158,6 @@ "i18n_preview_question": "Question Preview", "i18n_button_preview": "Preview question", "i18n_no_preview_available": "No preview available", - "i18n_used_in_exams": "Used in exams" + "i18n_used_in_exams": "Used in exams", + "i18n_quit_password": "SEB-poistumissalasana EN" } diff --git a/ui/src/assets/i18n/fi.json b/ui/src/assets/i18n/fi.json index 728799d342..f711b9ba1a 100644 --- a/ui/src/assets/i18n/fi.json +++ b/ui/src/assets/i18n/fi.json @@ -1158,5 +1158,6 @@ "i18n_preview_question": "Kysymyksen esikatselu", "i18n_button_preview": "Esikatsele kysymys", "i18n_no_preview_available": "Kysymyksen esikatselu ei ole saatavilla", - "i18n_used_in_exams": "Käytössä tenteissä" + "i18n_used_in_exams": "Käytössä tenteissä", + "i18n_quit_password": "SEB-poistumissalasana" } diff --git a/ui/src/assets/i18n/sv.json b/ui/src/assets/i18n/sv.json index 5a7ea3deb0..fb763b1f8c 100644 --- a/ui/src/assets/i18n/sv.json +++ b/ui/src/assets/i18n/sv.json @@ -1158,5 +1158,6 @@ "i18n_preview_question": "Question Preview SV", "i18n_button_preview": "Preview question SV", "i18n_no_preview_available": "No preview available SV", - "i18n_used_in_exams": "Käytössä tenteissä SV" + "i18n_used_in_exams": "Käytössä tenteissä SV", + "i18n_quit_password": "SEB-poistumissalasana SV" }