SEBSERV-376 back-end implementation

This commit is contained in:
anhefti 2023-02-06 16:31:07 +01:00
parent a6f7c501ca
commit 504c2a0843
16 changed files with 543 additions and 17 deletions

View file

@ -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();

View file

@ -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;
}
}
}

View file

@ -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<String, String> additionalAttributes;
@JsonIgnore
public final boolean checkASK;
@JsonIgnore
public final List<AllowedSEBVersion> 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<AllowedSEBVersion> 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<AllowedSEBVersion> 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

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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<Collection<ClientConnectionRecord>> 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<Collection<ClientConnectionRecord>> 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<Long> 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<Collection<ClientConnectionRecord>> 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<Boolean> saveSecurityCheckStatus(Long id, Boolean checkStatus);
Result<Boolean> 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<Boolean> saveSEBClientVersionCheckStatus(Long connectionId, Boolean checkStatus);
}

View file

@ -446,6 +446,19 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
.onError(TransactionHandler::rollback);
}
@Override
@Transactional
public Result<Boolean> 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<Void> assignToProctoringRoom(
@ -838,6 +851,25 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
.execute());
}
@Override
@Transactional(readOnly = true)
public Result<Collection<ClientConnectionRecord>> 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<Collection<Long>> getAllConnectionIdsForExam(final Long examId) {

View file

@ -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);
}

View file

@ -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;
}
}
}

View file

@ -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

View file

@ -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<AllowedSEBVersion> allowedSEBVersions);
void checkVersionAndUpdateClientConnection(
ClientConnectionRecord record,
List<AllowedSEBVersion> allowedSEBVersions);
}

View file

@ -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 */

View file

@ -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();
}

View file

@ -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<AllowedSEBVersion> 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));
}
}
}

View file

@ -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<String> knownWindowsOSTags;
public final Set<String> knownMacOSTags;
public final Set<String> knownIOSTags;
public final Set<String> 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<AllowedSEBVersion> 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<AllowedSEBVersion> 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()));
}
}

View file

@ -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<Exam, Exam> {
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<Exam, Exam> {
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<Exam, Exam> {
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<Exam, Exam> {
@Override
protected Result<Exam> 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<Exam, Exam> {
});
}
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<Collection<Exam>, List<Exam>> pageSort(final String sort) {
final String sortBy = PageSortOrder.decode(sort);