SEBSERV-376 back-end implementation
This commit is contained in:
parent
a6f7c501ca
commit
504c2a0843
16 changed files with 543 additions and 17 deletions
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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 */
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue