diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java b/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java index 6e3242fe..1a166cbc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java @@ -77,6 +77,7 @@ public final class Constants { public static final Character LIST_SEPARATOR_CHAR = COMMA; public static final Character COMPLEX_VALUE_SEPARATOR = COLON; public static final Character HASH_TAG = '#'; + public static final Character DOT = '.'; public static final String NULL = "null"; public static final String PERCENTAGE_STRING = Constants.PERCENTAGE.toString(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/AllowedSEBVersion.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/AllowedSEBVersion.java new file mode 100644 index 00000000..cb6e83f4 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/AllowedSEBVersion.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gbl.model.exam; + +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; + +import ch.ethz.seb.sebserver.gbl.Constants; + +public class AllowedSEBVersion { + + public static final String OS_WINDOWS_IDENTIFIER = "Win"; + public static final String OS_MAC_IDENTIFIER = "Mac"; + public static final String OS_IOS_IDENTIFIER = "iOS"; + public static final String ALIANCE_EDITION_IDENTIFIER = "AE"; + public static final String MINIMAL_IDENTIFIER = "min"; + + public final String wholeVersionString; + public final String osTypeString; + public final int major; + public final int minor; + public final int patch; + public final boolean alianceEdition; + public final boolean minimal; + public final boolean isValidFormat; + + public AllowedSEBVersion(final String wholeVersionString) { + this.wholeVersionString = wholeVersionString; + + boolean valid = true; + final String[] split = StringUtils.split(wholeVersionString, Constants.DOT); + if (OS_WINDOWS_IDENTIFIER.equalsIgnoreCase(split[0])) { + this.osTypeString = OS_WINDOWS_IDENTIFIER; + } else if (OS_MAC_IDENTIFIER.equalsIgnoreCase(split[0])) { + this.osTypeString = OS_MAC_IDENTIFIER; + } else if (OS_IOS_IDENTIFIER.equalsIgnoreCase(split[0])) { + this.osTypeString = OS_IOS_IDENTIFIER; + } else { + this.osTypeString = null; + valid = false; + } + + int num = -1; + try { + num = Integer.valueOf(split[1]); + } catch (final Exception e) { + valid = false; + } + this.major = num; + + try { + num = Integer.valueOf(split[2]); + } catch (final Exception e) { + valid = false; + } + this.minor = num; + try { + num = Integer.valueOf(split[3]); + } catch (final Exception e) { + valid = false; + } + this.patch = num; + + if (split.length > 4 && ALIANCE_EDITION_IDENTIFIER.equalsIgnoreCase(split[4])) { + this.alianceEdition = true; + if (split.length > 5 && MINIMAL_IDENTIFIER.equalsIgnoreCase(split[5])) { + this.minimal = true; + } else { + this.minimal = false; + } + } else { + this.alianceEdition = false; + if (split.length > 4 && MINIMAL_IDENTIFIER.equalsIgnoreCase(split[4])) { + this.minimal = true; + } else { + this.minimal = false; + } + } + + this.isValidFormat = valid; + } + + public boolean match(final ClientVersion clientVersion) { + if (Objects.equals(this.osTypeString, clientVersion.osTypeString)) { + if (this.minimal) { + // check greater or equals minimum version + return this.major <= clientVersion.major || + this.minor <= clientVersion.minor || + this.patch <= clientVersion.patch; + } else { + // check exact match + return this.major == clientVersion.major && + this.minor == clientVersion.minor && + this.patch == clientVersion.patch; + } + } + return false; + } + + public static final class ClientVersion { + + public String osTypeString; + public final int major; + public final int minor; + public final int patch; + + public ClientVersion(final String osTypeString, final int major, final int minor, final int patch) { + this.osTypeString = osTypeString; + this.major = major; + this.minor = minor; + this.patch = patch; + } + } + +} 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 81fa9d0e..df0e389c 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 @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import javax.validation.constraints.NotNull; @@ -73,6 +74,8 @@ public final class Exam implements GrantEntity { public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_CERT_ALIAS = "SIGNATURE_KEY_CERT_ALIAS"; /** This attribute name is used to store the per exam generated app-signature-key encryption salt */ public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_SALT = "SIGNATURE_KEY_SALT"; + /** Comma separated String value that defines allowed SEB version from linked Exam Configuration */ + public static final String ADDITIONAL_ATTR_ALLOWED_SEB_VERSIONS = "ALLOWED_SEB_VERSIONS"; public enum ExamStatus { UP_COMING, @@ -149,6 +152,11 @@ public final class Exam implements GrantEntity { @JsonProperty(ATTR_ADDITIONAL_ATTRIBUTES) public final Map additionalAttributes; + @JsonIgnore + public final boolean checkASK; + @JsonIgnore + public final List allowedSEBVersions; + @JsonCreator public Exam( @JsonProperty(EXAM.ATTR_ID) final Long id, @@ -193,7 +201,11 @@ public final class Exam implements GrantEntity { ? Collections.unmodifiableCollection(supporter) : Collections.emptyList(); - this.additionalAttributes = additionalAttributes; + this.additionalAttributes = Utils.immutableMapOf(additionalAttributes); + + this.checkASK = BooleanUtils + .toBoolean(this.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED)); + this.allowedSEBVersions = initAllowedSEBVersions(); } public Exam(final String modelId, final QuizData quizData, final POSTMapper mapper) { @@ -225,6 +237,9 @@ public final class Exam implements GrantEntity { this.lastModified = null; this.additionalAttributes = Utils.immutableMapOf(additionalAttributes); + this.checkASK = BooleanUtils + .toBoolean(this.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED)); + this.allowedSEBVersions = initAllowedSEBVersions(); } public Exam(final QuizData quizData) { @@ -251,6 +266,25 @@ public final class Exam implements GrantEntity { this.examTemplateId = null; this.lastModified = null; this.additionalAttributes = null; + this.checkASK = false; + this.allowedSEBVersions = null; + } + + private List initAllowedSEBVersions() { + if (this.additionalAttributes.containsKey(ADDITIONAL_ATTR_ALLOWED_SEB_VERSIONS)) { + final String asvString = this.additionalAttributes.get(Exam.ADDITIONAL_ATTR_ALLOWED_SEB_VERSIONS); + final String[] split = StringUtils.split(asvString, Constants.LIST_SEPARATOR); + final List result = new ArrayList<>(); + for (int i = 0; i < split.length; i++) { + final AllowedSEBVersion allowedSEBVersion = new AllowedSEBVersion(split[i]); + if (allowedSEBVersion.isValidFormat) { + result.add(allowedSEBVersion); + } + } + return result; + } else { + return null; + } } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnection.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnection.java index a96d7729..48f8552e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnection.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnection.java @@ -13,6 +13,8 @@ import java.util.EnumSet; import java.util.List; import java.util.function.Predicate; +import org.apache.commons.lang3.StringUtils; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -216,11 +218,11 @@ public final class ClientConnection implements GrantEntity { : false; this.info = new StringBuilder() - .append((clientAddress != null) ? clientAddress : Constants.EMPTY_NOTE) + .append(getSEBInfo(seb_version)) .append(Constants.LIST_SEPARATOR) - .append((seb_os_name != null) ? seb_os_name : Constants.EMPTY_NOTE) + .append(getOSInfo(seb_os_name)) .append(Constants.LIST_SEPARATOR) - .append((seb_version != null) ? seb_version : Constants.EMPTY_NOTE) + .append((clientAddress != null) ? "IP: " + clientAddress : Constants.EMPTY_NOTE) .toString(); this.securityCheckGranted = securityCheckGranted; @@ -439,4 +441,16 @@ public final class ClientConnection implements GrantEntity { return connection -> states.contains(connection.status); } + private String getSEBInfo(final String seb_version) { + return (seb_version != null) ? "SEBV: " + seb_version : Constants.EMPTY_NOTE; + } + + private String getOSInfo(final String seb_os_name) { + if (seb_os_name != null) { + final String[] split = StringUtils.split(seb_os_name, Constants.LIST_SEPARATOR); + return "OSV: " + split[0]; + } + return Constants.EMPTY_NOTE; + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientMonitoringDataView.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientMonitoringDataView.java index 10e7e46e..0aab5f9a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientMonitoringDataView.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientMonitoringDataView.java @@ -61,6 +61,11 @@ public interface ClientMonitoringDataView { return notificationFlag != null && (notificationFlag & FLAG_GRANT_DENIED) > 0; } + default boolean isSEBVersionDenied() { + final Integer notificationFlag = notificationFlag(); + return notificationFlag != null && (notificationFlag & FLAG_INVALID_SEB_VERSION) > 0; + } + default boolean isPendingNotification() { final Integer notificationFlag = notificationFlag(); return notificationFlag != null && (notificationFlag & FLAG_PENDING_NOTIFICATION) > 0; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java index 29a97c4d..d67bb637 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java @@ -171,15 +171,27 @@ public interface ClientConnectionDAO extends * @return Result refer to a collection of client connection records or to an error when happened */ Result> getsecurityKeyConnectionRecords(Long examId); - /** Get all client connection records that don't have an security access grant yet + /** Get all client connection records that don't have a security access grant yet * and for specific exam. * * @param examId The exam identifier * @return Result refer to client connection records to the an error when happened */ Result> getAllActiveNotGranted(Long examId); + /** Count all known and matching ASK hashes for a given exam. + * + * @param examId The exam identifier + * @param signatureHash The signature hash the count + * @return Result refer to the signature hash count or to an result when happened */ Result countSignatureHashes(Long examId, String signatureHash); + /** Get all client connection records that don't have a SEB client version check yet + * and for specific exam. + * + * @param examId The exam identifier + * @return Result refer to client connection records to the an error when happened */ + Result> getAllActiveNoSEBVersionCheck(Long examId); + /** Get all client connection identifiers for an exam. * * @param examId the exam identifier @@ -188,9 +200,16 @@ public interface ClientConnectionDAO extends /** Saves the given security check status for specified client connection id * - * @param id the client connection identifier (PK) + * @param connectionId the client connection identifier (PK) * @param checkStatus The status to save * @return Result refer to the given check status or to an error when happened */ - Result saveSecurityCheckStatus(Long id, Boolean checkStatus); + Result saveSecurityCheckStatus(Long connectionId, Boolean checkStatus); + + /** Saves the given SEB version check status for specified client connection id + * + * @param connectionId the client connection identifier (PK) + * @param checkStatus The status to save + * @return Result refer to the given check status or to an error when happened */ + Result saveSEBClientVersionCheckStatus(Long connectionId, Boolean checkStatus); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java index 00f8b4a6..4a1e9b6f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java @@ -446,6 +446,19 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { .onError(TransactionHandler::rollback); } + @Override + @Transactional + public Result saveSEBClientVersionCheckStatus(final Long connectionId, final Boolean checkStatus) { + return Result.tryCatch(() -> { + this.clientConnectionRecordMapper.updateByPrimaryKeySelective(new ClientConnectionRecord( + connectionId, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, Utils.toByte(checkStatus))); + return checkStatus; + }) + .onError(TransactionHandler::rollback); + } + @Override @Transactional public Result assignToProctoringRoom( @@ -838,6 +851,25 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { .execute()); } + @Override + @Transactional(readOnly = true) + public Result> getAllActiveNoSEBVersionCheck(final Long examId) { + return Result.tryCatch(() -> this.clientConnectionRecordMapper + .selectByExample() + .where( + ClientConnectionRecordDynamicSqlSupport.status, + SqlBuilder.isIn(ClientConnection.SECURE_CHECK_STATES)) + .and( + ClientConnectionRecordDynamicSqlSupport.examId, + SqlBuilder.isEqualTo(examId)) + .and( + ClientConnectionRecordDynamicSqlSupport.clientVersionGranted, + SqlBuilder.isNull()) + .and(ClientConnectionRecordDynamicSqlSupport.ask, SqlBuilder.isNotNull()) + .build() + .execute()); + } + @Override @Transactional(readOnly = true) public Result> getAllConnectionIdsForExam(final Long examId) { 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 b4164829..c6456dd4 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 @@ -21,8 +21,22 @@ public interface ExamConfigurationValueService { * @return The current value of the above SEB settings attribute and given exam. */ String getMappedDefaultConfigAttributeValue(Long examId, String configAttributeName); + /** 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); + /** Get the quitLink SEB Setting from the Exam Configuration that is applied to the given exam. + * + * @param examId Exam identifier + * @return the value of the quitLink SEB Setting */ String getQuitLink(Long examId); + /** Get the allowedSEBVersions SEB Setting from the Exam Configuration that is applied to the given exam. + * + * @param examId Exam identifier + * @return the value of the allowedSEBVersions SEB Setting */ + String getAllowedSEBVersion(Long examId); + } 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 ebc0b085..f76e40fc 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 @@ -124,4 +124,18 @@ public class ExamConfigurationValueServiceImpl implements ExamConfigurationValue } } + @Override + public String getAllowedSEBVersion(final Long examId) { + try { + + return getMappedDefaultConfigAttributeValue( + examId, + CONFIG_ATTR_NAME_QUIT_LINK); + + } catch (final Exception e) { + log.error("Failed to get SEB restriction with quit link: ", e); + return null; + } + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/validation/SEBVersionValidator.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/validation/SEBVersionValidator.java index 2ef4e816..4e9ad8bf 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/validation/SEBVersionValidator.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/validation/SEBVersionValidator.java @@ -16,6 +16,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.model.exam.AllowedSEBVersion; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; @@ -61,8 +62,7 @@ public class SEBVersionValidator implements ConfigurationValueValidator { } private boolean isValidSEBVersionMarker(final String versionMarker) { - // TODO Auto-generated method stub - return false; + return new AllowedSEBVersion(versionMarker).isValidFormat; } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientVersionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientVersionService.java new file mode 100644 index 00000000..fc3ca6f3 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/SEBClientVersionService.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.session; + +import java.util.List; + +import ch.ethz.seb.sebserver.gbl.model.exam.AllowedSEBVersion; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord; + +public interface SEBClientVersionService { + + /** Use this to check if a given SEB Version from SEB client connections are valid + * + * @param clientOSName The client OS name sent by SEB client + * @param clientVersion The SEB version sent by SEB client + * @param allowedSEBVersions List of allowed SEB version conditions given from the Exam Configuration. + * @return True if the given SEB Version matches one of the defined AllowedSEBVersion conditions */ + boolean isAllowedVersion( + String clientOSName, + String clientVersion, + List allowedSEBVersions); + + void checkVersionAndUpdateClientConnection( + ClientConnectionRecord record, + List allowedSEBVersions); + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientConnectionDataInternal.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientConnectionDataInternal.java index f06413dd..f79556a7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientConnectionDataInternal.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientConnectionDataInternal.java @@ -45,6 +45,7 @@ public class ClientConnectionDataInternal extends ClientConnectionData { private final PendingNotificationIndication pendingNotificationIndication; private final Boolean grantDenied; + private final Boolean sebVersionDenied; public ClientConnectionDataInternal( final ClientConnection clientConnection, @@ -76,6 +77,12 @@ public class ClientConnectionDataInternal extends ClientConnectionData { } else { this.grantDenied = !clientConnection.securityCheckGranted; } + + if (clientConnection.clientVersionGranted == null) { + this.sebVersionDenied = null; + } else { + this.sebVersionDenied = !clientConnection.clientVersionGranted; + } } public final void notifyPing(final long timestamp, final int pingNumber) { @@ -175,6 +182,11 @@ public class ClientConnectionDataInternal extends ClientConnectionData { return BooleanUtils.isTrue(ClientConnectionDataInternal.this.grantDenied); } + @Override + @JsonIgnore + public boolean isSEBVersionDenied() { + return BooleanUtils.isTrue(ClientConnectionDataInternal.this.sebVersionDenied); + } }; /** This is a static monitoring connection data wrapper/holder */ diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java index a66375c0..c959ee4e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java @@ -108,6 +108,7 @@ public class ExamSessionControlTask implements DisposableBean { controlExamLMSUpdate(); controlExamState(updateId); this.examDAO.releaseAgedLocks(); + this.sebClientSessionService.cleanupInstructions(); } @Scheduled( @@ -127,7 +128,6 @@ public class ExamSessionControlTask implements DisposableBean { this.sebClientSessionService.updatePingEvents(); this.sebClientSessionService.updateASKGrants(); - this.sebClientSessionService.cleanupInstructions(); this.examProcotringRoomService.updateProctoringCollectingRooms(); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientSessionServiceImpl.java index d9971fec..7d668cb0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientSessionServiceImpl.java @@ -10,15 +10,15 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; import java.math.BigDecimal; import java.util.Collections; +import java.util.List; import java.util.Objects; -import org.apache.commons.lang3.BooleanUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; -import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.model.exam.AllowedSEBVersion; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; @@ -33,6 +33,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.EventHandlingStrate import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientSessionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientVersionService; @Lazy @Service @@ -49,6 +50,7 @@ public class SEBClientSessionServiceImpl implements SEBClientSessionService { private final ClientIndicatorFactory clientIndicatorFactory; private final InternalClientConnectionDataFactory internalClientConnectionDataFactory; private final SecurityKeyService securityKeyService; + private final SEBClientVersionService sebClientVersionService; public SEBClientSessionServiceImpl( final ClientConnectionDAO clientConnectionDAO, @@ -57,7 +59,8 @@ public class SEBClientSessionServiceImpl implements SEBClientSessionService { final SEBClientInstructionService sebInstructionService, final ClientIndicatorFactory clientIndicatorFactory, final InternalClientConnectionDataFactory internalClientConnectionDataFactory, - final SecurityKeyService securityKeyService) { + final SecurityKeyService securityKeyService, + final SEBClientVersionService sebClientVersionService) { this.clientConnectionDAO = clientConnectionDAO; this.examSessionService = examSessionService; @@ -67,6 +70,7 @@ public class SEBClientSessionServiceImpl implements SEBClientSessionService { this.clientIndicatorFactory = clientIndicatorFactory; this.internalClientConnectionDataFactory = internalClientConnectionDataFactory; this.securityKeyService = securityKeyService; + this.sebClientVersionService = sebClientVersionService; } @Override @@ -97,7 +101,7 @@ public class SEBClientSessionServiceImpl implements SEBClientSessionService { this.examSessionService .getExamDAO() .allRunningExamIds() - .onSuccess(ids -> ids.stream().forEach(examId -> updateASKGrant(examId))) + .onSuccess(ids -> ids.stream().forEach(examId -> updateGrants(examId))) .onError(error -> log.error("Unexpected error while trying to updateASKGrants: ", error)); } @@ -201,11 +205,23 @@ public class SEBClientSessionServiceImpl implements SEBClientSessionService { } } + private void updateGrants(final Long examId) { + try { + updateASKGrant(examId); + } catch (final Exception e) { + log.error("Failed to update ASK grant for exam: {}", examId, e); + } + try { + updateAllowedSEBVersionGrant(examId); + } catch (final Exception e) { + log.error("Failed to update SEB client version grant for exam: {}", examId, e); + } + } + private void updateASKGrant(final Long examId) { if (this.examSessionService .getRunningExam(examId) - .map(exam -> exam.getAdditionalAttribute(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED)) - .map(BooleanUtils::toBoolean) + .map(exam -> exam.checkASK) .getOr(true)) { this.clientConnectionDAO @@ -218,4 +234,22 @@ public class SEBClientSessionServiceImpl implements SEBClientSessionService { } } + private void updateAllowedSEBVersionGrant(final Long examId) { + final List allowedSEBVersions = this.examSessionService + .getRunningExam(examId) + .map(exam -> exam.allowedSEBVersions) + .getOr(Collections.emptyList()); + + if (allowedSEBVersions != null && !allowedSEBVersions.isEmpty()) { + this.clientConnectionDAO + .getAllActiveNoSEBVersionCheck(examId) + .onError(error -> log.error( + "Failed to get none SEB version checked active client connections: ", + error)) + .getOr(Collections.emptyList()) + .forEach(cc -> this.sebClientVersionService.checkVersionAndUpdateClientConnection( + cc, + allowedSEBVersions)); + } + } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientVersionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientVersionServiceImpl.java new file mode 100644 index 00000000..8f8bd08e --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientVersionServiceImpl.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.model.exam.AllowedSEBVersion; +import ch.ethz.seb.sebserver.gbl.model.exam.AllowedSEBVersion.ClientVersion; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientVersionService; + +@Lazy +@Service +@WebServiceProfile +public class SEBClientVersionServiceImpl implements SEBClientVersionService { + + private static final Logger log = LoggerFactory.getLogger(SEBClientVersionServiceImpl.class); + + private final ClientConnectionDAO clientConnectionDAO; + private final ExamSessionCacheService examSessionCacheService; + + public final Set knownWindowsOSTags; + public final Set knownMacOSTags; + public final Set knownIOSTags; + public final Set knownRestrictedVersions; + + public SEBClientVersionServiceImpl( + final ClientConnectionDAO clientConnectionDAO, + final ExamSessionCacheService examSessionCacheService, + @Value("${ebserver.webservice.config.knownWindowsOSTags:Win,Windows}") final String knownWindowsOSTags, + @Value("${ebserver.webservice.config.knownMacOSTags:macOS}") final String knownMacOSTags, + @Value("${ebserver.webservice.config.knownIOSTags:iOS,iPad,iPadOS}") final String knownIOSTags, + @Value("${ebserver.webservice.config.knownRestrictedVersions:BETA,rc,1.0.0.0}") final String knownRestrictedVersions) { + + this.clientConnectionDAO = clientConnectionDAO; + this.examSessionCacheService = examSessionCacheService; + this.knownWindowsOSTags = new HashSet<>(Arrays.asList(StringUtils.split( + knownWindowsOSTags, + Constants.LIST_SEPARATOR))); + this.knownMacOSTags = new HashSet<>(Arrays.asList(StringUtils.split( + knownMacOSTags, + Constants.LIST_SEPARATOR))); + this.knownIOSTags = new HashSet<>(Arrays.asList(StringUtils.split( + knownIOSTags, + Constants.LIST_SEPARATOR))); + this.knownRestrictedVersions = new HashSet<>(Arrays.asList(StringUtils.split( + knownRestrictedVersions, + Constants.LIST_SEPARATOR))); + } + + @Override + public boolean isAllowedVersion( + final String clientOSName, + final String clientVersion, + final List allowedSEBVersions) { + + // first check if this is a known restricted version + if (this.knownRestrictedVersions.stream().filter(clientVersion::contains).findFirst().isPresent()) { + if (log.isDebugEnabled()) { + log.debug("Found default restricted SEB client version: {}", clientVersion); + } + return false; + } + + final String osType = verifyOSType(clientOSName, clientVersion); + + if (StringUtils.isBlank(osType)) { + if (log.isDebugEnabled()) { + log.debug("No SEB client OS type tag found in : {} {}", clientOSName, clientVersion); + } + return false; + } + + try { + final String[] versionSplit = StringUtils.split(clientVersion, Constants.SPACE); + final String versioNumber = versionSplit[0]; + final String[] versionNumberSplit = StringUtils.split(versioNumber, Constants.DOT); + + final int major = extractVersionNumber(versionNumberSplit[0]); + final int minor = extractVersionNumber(versionNumberSplit[1]); + final int patch = extractVersionNumber(versionNumberSplit[2]); + + final ClientVersion version = new ClientVersion(osType, major, minor, patch); + return allowedSEBVersions + .stream() + .filter(v -> v.match(version)) + .findFirst() + .isPresent(); + } catch (final Exception e) { + log.warn("Unexpected error while trying to parse SEB version number in: {} {}", clientOSName, + clientVersion); + return false; + } + } + + @Override + public void checkVersionAndUpdateClientConnection( + final ClientConnectionRecord record, + final List allowedSEBVersions) { + + if (isAllowedVersion(record.getClientOsName(), record.getClientVersion(), allowedSEBVersions)) { + saveSecurityCheckState(record, true); + } else { + saveSecurityCheckState(record, false); + } + } + + private int extractVersionNumber(final String versionNumPart) { + try { + return Integer.parseInt(versionNumPart); + } catch (final NumberFormatException nfe) { + return Integer.parseInt(String.valueOf(versionNumPart.charAt(0))); + } + } + + private String verifyOSType(final String clientOSName, final String clientVersion) { + final char c = clientVersion.charAt(0); + final String osVersionText = (c >= 'A' && c <= 'Z') ? clientVersion : clientOSName; + if (StringUtils.isNotBlank(osVersionText)) { + if (this.knownWindowsOSTags.stream().filter(osVersionText::contains).findFirst().isPresent()) { + return AllowedSEBVersion.OS_WINDOWS_IDENTIFIER; + } + if (this.knownMacOSTags.stream().filter(osVersionText::contains).findFirst().isPresent()) { + return AllowedSEBVersion.OS_MAC_IDENTIFIER; + } + if (this.knownIOSTags.stream().filter(osVersionText::contains).findFirst().isPresent()) { + return AllowedSEBVersion.OS_IOS_IDENTIFIER; + } + } + + return null; + } + + private void saveSecurityCheckState(final ClientConnectionRecord record, final Boolean checkStatus) { + this.clientConnectionDAO + .saveSEBClientVersionCheckStatus(record.getId(), checkStatus) + .onError(error -> log.error("Failed to save ClientConnection grant: ", + error)) + .onSuccess(c -> this.examSessionCacheService.evictClientConnection(record.getConnectionToken())); + } +} 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 31a3f9d7..67086713 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 @@ -64,11 +64,13 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.Authorization import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser; import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; +import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamConfigurationValueService; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateService; import ch.ethz.seb.sebserver.webservice.servicelayer.institution.SecurityKeyService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; @@ -93,6 +95,8 @@ public class ExamAdministrationController extends EntityController { private final SEBRestrictionService sebRestrictionService; private final SecurityKeyService securityKeyService; private final ExamProctoringRoomService examProctoringRoomService; + private final ExamConfigurationValueService examConfigurationValueService; + private final AdditionalAttributesDAO additionalAttributesDAO; public ExamAdministrationController( final AuthorizationService authorization, @@ -108,7 +112,9 @@ public class ExamAdministrationController extends EntityController { final ExamSessionService examSessionService, final SEBRestrictionService sebRestrictionService, final SecurityKeyService securityKeyService, - final ExamProctoringRoomService examProctoringRoomService) { + final ExamProctoringRoomService examProctoringRoomService, + final ExamConfigurationValueService examConfigurationValueService, + final AdditionalAttributesDAO additionalAttributesDAO) { super(authorization, bulkActionService, @@ -126,6 +132,8 @@ public class ExamAdministrationController extends EntityController { this.sebRestrictionService = sebRestrictionService; this.securityKeyService = securityKeyService; this.examProctoringRoomService = examProctoringRoomService; + this.examConfigurationValueService = examConfigurationValueService; + this.additionalAttributesDAO = additionalAttributesDAO; } @Override @@ -599,6 +607,7 @@ public class ExamAdministrationController extends EntityController { @Override protected Result notifySaved(final Exam entity) { return Result.tryCatch(() -> { + this.saveAdditionalExamConfigAttributes(entity); this.examSessionService.flushCache(entity); return entity; }); @@ -702,6 +711,29 @@ public class ExamAdministrationController extends EntityController { }); } + private void saveAdditionalExamConfigAttributes(final Exam entity) { + try { + final String allowedSEBVersion = this.examConfigurationValueService + .getAllowedSEBVersion(entity.id); + + if (StringUtils.isNotBlank(allowedSEBVersion)) { + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.EXAM, + entity.id, Exam.ADDITIONAL_ATTR_ALLOWED_SEB_VERSIONS, + allowedSEBVersion) + .getOrThrow(); + } else { + this.additionalAttributesDAO.delete( + EntityType.EXAM, + entity.id, Exam.ADDITIONAL_ATTR_ALLOWED_SEB_VERSIONS); + } + + } catch (final Exception e) { + log.error("Unexpected error while trying to save additional Exam Configuration settings for exam: {}", + entity, e); + } + } + static Function, List> pageSort(final String sort) { final String sortBy = PageSortOrder.decode(sort);