SEBSP-129 and SEBSERV-418

This commit is contained in:
anhefti 2024-05-29 09:39:30 +02:00
parent 9618b942fb
commit 41b056edce
42 changed files with 1047 additions and 515 deletions

View file

@ -178,8 +178,12 @@ public final class API {
public static final String LMS_FULL_INTEGRATION_EXAM_TEMPLATE_ID = "exam_template_id";
public static final String LMS_FULL_INTEGRATION_QUIT_PASSWORD = "quit_password";
public static final String LMS_FULL_INTEGRATION_QUIT_LINK = "quit_link";
public static final String LMS_FULL_INTEGRATION_USER_ID = "user_id";
public static final String LMS_FULL_INTEGRATION_USER_NAME = "user_name";
public static final String LMS_FULL_INTEGRATION_USER_ID = "userid_id";
public static final String LMS_FULL_INTEGRATION_USER_NAME = "userid_username ";
public static final String LMS_FULL_INTEGRATION_USER_EMAIL = "userid_email";
public static final String LMS_FULL_INTEGRATION_USER_FIRST_NAME = "user_firstname";
public static final String LMS_FULL_INTEGRATION_USER_LAST_NAME = "user_lastname";
public static final String LMS_FULL_INTEGRATION_TIME_ZONE = "account_time_zone";
public static final String USER_ACCOUNT_ENDPOINT = "/useraccount";

View file

@ -18,8 +18,7 @@ import java.util.Map;
import javax.validation.constraints.NotNull;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
@ -65,6 +64,7 @@ public final class Exam implements GrantEntity {
public static final String ADDITIONAL_ATTR_ALLOWED_SEB_VERSIONS = "ALLOWED_SEB_VERSIONS";
public static final String ADDITIONAL_ATTR_DEFAULT_CONNECTION_CONFIGURATION = "DEFAULT_CONNECTION_CONFIGURATION";
public static final String ADDITIONAL_ATTR_QUIZ_ATTRIBUTES = "ADDITIONAL_QUIZ_ATTRIBUTES";
public enum ExamStatus {
UP_COMING,
@ -239,7 +239,15 @@ public final class Exam implements GrantEntity {
}
public Exam(final String modelId, final QuizData quizData, final POSTMapper mapper) {
final Map<String, String> additionalAttributes = new HashMap<>(quizData.getAdditionalAttributes());
String additionalQuizData = null;
try {
additionalQuizData = new JSONMapper().writeValueAsString(quizData.getAdditionalAttributes());
} catch (final Exception ignored) {}
final Map<String, String> additionalAttributes = new HashMap<>();
if (additionalQuizData != null) {
additionalAttributes.put(ADDITIONAL_ATTR_QUIZ_ATTRIBUTES, additionalQuizData);
}
additionalAttributes.put(QuizData.QUIZ_ATTR_DESCRIPTION, quizData.description);
additionalAttributes.put(QuizData.QUIZ_ATTR_START_URL, quizData.startURL);

View file

@ -25,8 +25,6 @@ public class ScreenProctoringSettings implements SPSAPIAccessData {
public static final String ATTR_ENABLE_SCREEN_PROCTORING = "enableScreenProctoring";
public static final String ATTR_SPS_SERVICE_URL = "spsServiceURL";
public static final String ATTR_COLLECTING_STRATEGY = "spsCollectingStrategy";
public static final String ATTR_COLLECTING_GROUP_SIZE = "collectingGroupSize";
public static final String ATTR_SPS_API_KEY = "spsAPIKey";
public static final String ATTR_SPS_API_SECRET = "spsAPISecret";
@ -34,6 +32,9 @@ public class ScreenProctoringSettings implements SPSAPIAccessData {
public static final String ATTR_SPS_ACCOUNT_ID = "spsAccountId";
public static final String ATTR_SPS_ACCOUNT_PASSWORD = "spsAccountPassword";
public static final String ATTR_COLLECTING_STRATEGY = "spsCollectingStrategy";
public static final String ATTR_COLLECTING_GROUP_SIZE = "spsCollectingGroupSize";
public static final String ATTR_SPS_BUNDLED = "bundled";
@JsonProperty(Domain.EXAM.ATTR_ID)

View file

@ -8,27 +8,33 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.authorization;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.user.TokenLoginInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
public interface TeacherAccountService {
default String getTeacherAccountIdentifier(
final Exam exam,
final FullLmsIntegrationService.AdHocAccountData adHocAccountData) {
return getTeacherAccountIdentifier(exam.getModelId(), adHocAccountData.userId);
}
String getTeacherAccountIdentifier(String examId, String userId);
Result<UserInfo> createNewTeacherAccountForExam(
Exam exam,
String userId,
String username,
String timezone);
final FullLmsIntegrationService.AdHocAccountData adHocAccountData);
Result<Exam> deleteTeacherAccountsForExam(final Exam exam);
Result<Exam> deactivateTeacherAccountsForExam(Exam exam);
Result<String> getOneTimeTokenForTeacherAccount(
Exam exam,
String userId,
String username,
String timezone,
final boolean createIfNotExists);
FullLmsIntegrationService.AdHocAccountData adHocAccountData,
boolean createIfNotExists);
Result<TokenLoginInfo> verifyOneTimeTokenForTeacherAccount(String token);

View file

@ -13,8 +13,6 @@ import java.util.*;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.user.TokenLoginInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
@ -28,8 +26,8 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttribut
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.TeacherAccountService;
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.UserDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService;
import ch.ethz.seb.sebserver.webservice.weblayer.oauth.AdminAPIClientDetails;
import io.jsonwebtoken.Claims;
@ -45,7 +43,6 @@ import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Service;
@Lazy
@ -87,35 +84,41 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
this.adminAPIClientDetails = adminAPIClientDetails;
}
@Override
public String getTeacherAccountIdentifier(final String examId, final String userId) {
if (examId == null || userId == null) {
throw new RuntimeException("examId and/or userId cannot be null");
}
return userId + Constants.UNDERLINE + examId;
}
@Override
public Result<UserInfo> createNewTeacherAccountForExam(
final Exam exam,
final String userId,
final String username,
final String timezone) {
final FullLmsIntegrationService.AdHocAccountData adHocAccountData) {
return Result.tryCatch(() -> {
final String uuid = UUID.randomUUID().toString();
DateTimeZone dtz = DateTimeZone.UTC;
if (StringUtils.isNotBlank(timezone)) {
if (StringUtils.isNotBlank(adHocAccountData.timezone)) {
try {
dtz = DateTimeZone.forID(timezone);
dtz = DateTimeZone.forID(adHocAccountData.timezone);
} catch (final Exception e) {
log.warn("Failed to set requested time zone for ad-hoc teacher account: {}", timezone);
log.warn("Failed to set requested time zone for ad-hoc teacher account: {}", adHocAccountData.timezone);
}
}
final UserMod adHocTeacherUser = new UserMod(
uuid,
getTeacherAccountIdentifier(exam, adHocAccountData),
exam.institutionId,
userId,
getTeacherAccountIdentifier(exam),
username,
adHocAccountData.firstName != null ? adHocAccountData.firstName : adHocAccountData.userId,
adHocAccountData.lastName != null ? adHocAccountData.lastName : adHocAccountData.userId,
adHocAccountData.username != null ? adHocAccountData.username : adHocAccountData.userId,
uuid,
uuid,
null,
adHocAccountData.userMail,
Locale.ENGLISH,
dtz,
true,
@ -130,27 +133,14 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
}
@Override
public Result<Exam> deleteTeacherAccountsForExam(final Exam exam) {
public Result<Exam> deactivateTeacherAccountsForExam(final Exam exam) {
return Result.tryCatch(() -> {
final String externalId = exam.externalId;
final FilterMap filter = new FilterMap();
filter.putIfAbsent(Domain.USER.ATTR_SURNAME, getTeacherAccountIdentifier(exam));
final Collection<UserInfo> accounts = userDAO.allMatching(filter).getOrThrow();
if (accounts.isEmpty()) {
return exam;
}
if (accounts.size() > 1) {
log.error("Too many accounts found!?... ad-hoc teacher account mapping: {}", externalId);
return exam;
}
userDAO.delete(Utils.immutableSetOf(new EntityKey(
accounts.iterator().next().uuid,
EntityType.USER)))
.getOrThrow();
exam.supporter.stream()
.map(userUUID -> userDAO.byModelId(userUUID).getOr(null))
.filter(user -> user != null && user.roles.contains(UserRole.TEACHER.name()))
.filter( user -> user.roles.size() == 1)
.forEach( user -> userDAO.setActive(user, false));
return exam;
});
@ -159,19 +149,14 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
@Override
public Result<String> getOneTimeTokenForTeacherAccount(
final Exam exam,
final String userId,
final String username,
final String timezone,
final FullLmsIntegrationService.AdHocAccountData adHocAccountData,
final boolean createIfNotExists) {
return this.userDAO
.byModelId(userId)
.onErrorDo(error -> handleAccountDoesNotExistYet(createIfNotExists, exam, userId, username, timezone))
.byModelId(getTeacherAccountIdentifier(exam, adHocAccountData))
.onErrorDo(error -> handleAccountDoesNotExistYet(createIfNotExists, exam, adHocAccountData))
.map(account -> applySupporter(account, exam))
.map(account -> {
this.screenProctoringService.synchronizeSPSUserForExam(exam.id);
return account;
})
.map(account -> synchronizeSPSUserForExam(account, exam.id))
.map(account -> this.createOneTimeToken(account, exam.id));
}
@ -209,20 +194,26 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
private UserInfo handleAccountDoesNotExistYet(
final boolean createIfNotExists,
final Exam exam,
final String userId,
final String username,
final String timezone) {
final FullLmsIntegrationService.AdHocAccountData adHocAccountData) {
if (createIfNotExists) {
return this
.createNewTeacherAccountForExam(exam, userId, username, timezone)
.createNewTeacherAccountForExam(exam, adHocAccountData)
.getOrThrow();
} else {
throw new RuntimeException("Teacher Account with userId "+ userId + " and username "+username+" does not exist.");
throw new RuntimeException("Teacher Account with user "+ adHocAccountData + " does not exist.");
}
}
private UserInfo applySupporter(final UserInfo account, final Exam exam) {
// activate ad-hoc account if not active
if (!account.isActive()) {
userDAO.setActive(account, true)
.onError(error -> log.error(
"Failed to activate ad-hoc teacher account: {}, exam: {}, error {}",
account.uuid, exam.externalId, error.getMessage()));
}
if (!exam.supporter.contains(account.uuid)) {
this.examDAO.applySupporter(exam, account.uuid)
.onError(error -> log.error(
@ -317,7 +308,10 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
.getOrElse(null);
}
private static String getTeacherAccountIdentifier(final Exam exam) {
return "AdHoc-Teacher-Account-" + exam.id;
private UserInfo synchronizeSPSUserForExam(final UserInfo account, final Long examId) {
if (this.screenProctoringService.isScreenProctoringEnabled(examId)) {
this.screenProctoringService.synchronizeSPSUserForExam(examId);
}
return account;
}
}

View file

@ -100,7 +100,7 @@ public class DeleteExamAction implements BatchActionExec {
}
@Transactional
public Result<EntityKey> deleteExamFromLMSIntegration(final Exam exam) {
public Result<EntityKey> deleteExamInternal(final Exam exam) {
return deleteExamDependencies(exam)
.flatMap(this::deleteExamWithRefs)
.map(Exam::getEntityKey)

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;
import org.springframework.cache.annotation.CacheEvict;
@ -109,6 +110,8 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
* @return Result refer to all exams for LMS update or to an error when happened */
Result<Collection<Exam>> allForLMSUpdate();
Result<Collection<Exam>> allActiveForLMSSetup(Collection<Long> lmsId);
/** This is used to get all Exams that potentially needs a state change.
* Checks if the stored running time frame of the exam is not in sync with the current state and return
* all exams for this is the case.
@ -243,5 +246,4 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
void updateQuitPassword(Exam exam, String quitPassword);
}

View file

@ -12,7 +12,6 @@ import static ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecord
import static ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport.examRecord;
import static org.mybatis.dynamic.sql.SqlBuilder.*;
import java.sql.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@ -25,6 +24,7 @@ import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.*;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
@ -67,17 +67,20 @@ public class ExamDAOImpl implements ExamDAO {
private final ExamRecordDAO examRecordDAO;
private final ApplicationEventPublisher applicationEventPublisher;
private final AdditionalAttributesDAO additionalAttributesDAO;
private final JSONMapper jsonMapper;
public ExamDAOImpl(
final ExamRecordMapper examRecordMapper,
final ExamRecordDAO examRecordDAO,
final ApplicationEventPublisher applicationEventPublisher,
final AdditionalAttributesDAO additionalAttributesDAO) {
final AdditionalAttributesDAO additionalAttributesDAO,
final JSONMapper jsonMapper) {
this.examRecordMapper = examRecordMapper;
this.examRecordDAO = examRecordDAO;
this.applicationEventPublisher = applicationEventPublisher;
this.additionalAttributesDAO = additionalAttributesDAO;
this.jsonMapper = jsonMapper;
}
@Override
@ -363,6 +366,25 @@ public class ExamDAOImpl implements ExamDAO {
.flatMap(this::toDomainModel);
}
@Override
@Transactional(readOnly = true)
public Result<Collection<Exam>> allActiveForLMSSetup(final Collection<Long> lmsIds) {
return Result.tryCatch(() -> {
return this.examRecordMapper.selectByExample()
.where(
ExamRecordDynamicSqlSupport.active,
isEqualTo(BooleanUtils.toInteger(true)))
.and(
ExamRecordDynamicSqlSupport.lmsSetupId,
isIn(lmsIds))
.and(
ExamRecordDynamicSqlSupport.status,
isNotEqualTo(ExamStatus.ARCHIVED.name()))
.build()
.execute();
}).flatMap(this::toDomainModel);
}
@Override
public Result<Collection<Exam>> allThatNeedsStatusUpdate(final long leadTime, final long followupTime) {
return this.examRecordDAO
@ -862,7 +884,15 @@ public class ExamDAOImpl implements ExamDAO {
}
private QuizData saveAdditionalQuizAttributes(final Long examId, final QuizData quizData) {
final Map<String, String> additionalAttributes = new HashMap<>(quizData.getAdditionalAttributes());
String additionalQuizData = null;
try {
additionalQuizData = jsonMapper.writeValueAsString(quizData.getAdditionalAttributes());
} catch (final Exception ignored) {}
final Map<String, String> additionalAttributes = new HashMap<>();
if (additionalQuizData != null) {
additionalAttributes.put(Exam.ADDITIONAL_ATTR_QUIZ_ATTRIBUTES, additionalQuizData);
}
if (StringUtils.isNotBlank(quizData.description)) {
additionalAttributes.put(QuizData.QUIZ_ATTR_DESCRIPTION, quizData.description);
} else {

View file

@ -16,6 +16,7 @@ import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
@ -50,15 +51,18 @@ public class ProctoringSettingsDAOImpl implements ProctoringSettingsDAO {
private final AdditionalAttributesDAO additionalAttributesDAO;
private final RemoteProctoringRoomDAO remoteProctoringRoomDAO;
private final WebserviceInfo.ScreenProctoringServiceBundle screenProctoringServiceBundle;
private final Cryptor cryptor;
public ProctoringSettingsDAOImpl(
final AdditionalAttributesDAO additionalAttributesDAO,
final RemoteProctoringRoomDAO remoteProctoringRoomDAO,
final WebserviceInfo webserviceInfo,
final Cryptor cryptor) {
this.additionalAttributesDAO = additionalAttributesDAO;
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
this.screenProctoringServiceBundle = webserviceInfo.getScreenProctoringServiceBundle();
this.cryptor = cryptor;
}
@ -165,8 +169,6 @@ public class ProctoringSettingsDAOImpl implements ProctoringSettingsDAO {
return Result.tryCatch(() -> {
final Long entityId = Long.parseLong(entityKey.modelId);
//checkType(parentEntityKey);
return this.additionalAttributesDAO
.getAdditionalAttributes(entityKey.entityType, entityId)
.map(attrs -> attrs.stream()
@ -174,16 +176,31 @@ public class ProctoringSettingsDAOImpl implements ProctoringSettingsDAO {
AdditionalAttributeRecord::getName,
Function.identity())))
.map(mapping -> {
return new ScreenProctoringSettings(
entityId,
getScreenproctoringEnabled(mapping),
getString(mapping, ScreenProctoringSettings.ATTR_SPS_SERVICE_URL),
getString(mapping, ScreenProctoringSettings.ATTR_SPS_API_KEY),
getString(mapping, ScreenProctoringSettings.ATTR_SPS_API_SECRET),
getString(mapping, ScreenProctoringSettings.ATTR_SPS_ACCOUNT_ID),
getString(mapping, ScreenProctoringSettings.ATTR_SPS_ACCOUNT_PASSWORD),
getScreenProctoringCollectingStrategy(mapping),
getScreenProctoringCollectingSize(mapping));
if (screenProctoringServiceBundle.bundled) {
return new ScreenProctoringSettings(
entityId,
getScreenproctoringEnabled(mapping),
screenProctoringServiceBundle.serviceURL,
screenProctoringServiceBundle.clientId,
screenProctoringServiceBundle.clientSecret.toString(),
screenProctoringServiceBundle.apiAccountName,
screenProctoringServiceBundle.apiAccountPassword.toString(),
getScreenProctoringCollectingStrategy(mapping),
getScreenProctoringCollectingSize(mapping),
true);
} else {
return new ScreenProctoringSettings(
entityId,
getScreenproctoringEnabled(mapping),
getString(mapping, ScreenProctoringSettings.ATTR_SPS_SERVICE_URL),
getString(mapping, ScreenProctoringSettings.ATTR_SPS_API_KEY),
getString(mapping, ScreenProctoringSettings.ATTR_SPS_API_SECRET),
getString(mapping, ScreenProctoringSettings.ATTR_SPS_ACCOUNT_ID),
getString(mapping, ScreenProctoringSettings.ATTR_SPS_ACCOUNT_PASSWORD),
getScreenProctoringCollectingStrategy(mapping),
getScreenProctoringCollectingSize(mapping),
false);
}
})
.getOrThrow();
});
@ -203,33 +220,43 @@ public class ProctoringSettingsDAOImpl implements ProctoringSettingsDAO {
attributes.put(
ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING,
String.valueOf(screenProctoringSettings.enableScreenProctoring));
attributes.put(
ScreenProctoringSettings.ATTR_SPS_SERVICE_URL,
StringUtils.trim(screenProctoringSettings.spsServiceURL));
// we need to store this only if it is an unbundled setup otherwise attributes are known by the service
if (!screenProctoringServiceBundle.bundled) {
attributes.put(
ScreenProctoringSettings.ATTR_SPS_SERVICE_URL,
StringUtils.trim(screenProctoringSettings.spsServiceURL));
attributes.put(
ScreenProctoringSettings.ATTR_SPS_API_KEY,
StringUtils.trim(screenProctoringSettings.spsAPIKey));
attributes.put(
ScreenProctoringSettings.ATTR_SPS_API_SECRET,
encryptSecret(Utils.trim(screenProctoringSettings.spsAPISecret)));
attributes.put(
ScreenProctoringSettings.ATTR_SPS_ACCOUNT_ID,
StringUtils.trim(screenProctoringSettings.spsAccountId));
attributes.put(
ScreenProctoringSettings.ATTR_SPS_ACCOUNT_PASSWORD,
encryptSecret(Utils.trim(screenProctoringSettings.spsAccountPassword)));
}
attributes.put(
ScreenProctoringSettings.ATTR_COLLECTING_STRATEGY,
String.valueOf(screenProctoringSettings.collectingStrategy));
attributes.put(
ScreenProctoringSettings.ATTR_COLLECTING_GROUP_SIZE,
String.valueOf(screenProctoringSettings.collectingGroupSize));
attributes.put(
ScreenProctoringSettings.ATTR_SPS_API_KEY,
StringUtils.trim(screenProctoringSettings.spsAPIKey));
attributes.put(
ScreenProctoringSettings.ATTR_SPS_API_SECRET,
encryptSecret(Utils.trim(screenProctoringSettings.spsAPISecret)));
attributes.put(
ScreenProctoringSettings.ATTR_SPS_ACCOUNT_ID,
StringUtils.trim(screenProctoringSettings.spsAccountId));
attributes.put(
ScreenProctoringSettings.ATTR_SPS_ACCOUNT_PASSWORD,
encryptSecret(Utils.trim(screenProctoringSettings.spsAccountPassword)));
this.additionalAttributesDAO.saveAdditionalAttributes(
entityKey.entityType,
entityId,
attributes,
true);
true)
.onError(error -> log.warn(
"Failed to store SPS attributes for Exam: {} error: {}",
entityKey,
error.getMessage()));
return screenProctoringSettings;
});

View file

@ -59,7 +59,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationService;
import ch.ethz.seb.sebserver.webservice.weblayer.oauth.RevokeTokenEndpoint.RevokeExamTokenEvent;
@Lazy
@ -722,7 +722,7 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
// clear cache
this.cacheManager
.getCache(ClientConfigService.EXAM_CLIENT_DETAILS_CACHE)
.getCache(ConnectionConfigurationService.EXAM_CLIENT_DETAILS_CACHE)
.evictIfPresent(rec.getClientName());
} catch (final Exception e) {

View file

@ -18,20 +18,12 @@ public interface ExamAdminService {
ProctoringAdminService getProctoringAdminService();
Result<Exam> applyExamImportInitialization(Exam exam);
/** Get the exam domain object for the exam identifier (PK).
*
* @param examId the exam identifier
* @return Result refer to the domain object or to an error when happened */
Result<Exam> examForPK(Long examId);
/** Initializes initial additional attributes for a yet created exam.
*
* @param exam The exam that has been created
* @return The exam with the initial additional attributes */
Result<Exam> initAdditionalAttributes(final Exam exam);
/** Saves the security key settings for an specific exam.
*
* @param institutionId The institution identifier
@ -45,13 +37,6 @@ public interface ExamAdminService {
Boolean enabled,
Integer numThreshold);
/** Applies all additional SEB restriction attributes that are defined by the
* type of the LMS of a given Exam to this given Exam.
*
* @param exam the Exam to apply all additional SEB restriction attributes
* @return Result refer to the created exam or to an error when happened */
Result<Exam> applyAdditionalSEBRestrictions(Exam exam);
/** Indicates whether a specific exam is being restricted with SEB restriction feature on the LMS or not.
*
* @param exam The exam instance

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2019 ETH Zürich, IT Services
*
* 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.exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.util.Result;
public interface ExamImportService {
Result<Exam> applyExamImportInitialization(Exam exam);
/** Initializes initial additional attributes for a yet created exam.
*
* @param exam The exam that has been created
* @return The exam with the initial additional attributes */
Result<Exam> initAdditionalAttributes(final Exam exam);
}

View file

@ -8,46 +8,31 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.exam.*;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.*;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateService;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
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.ProctoringAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.RemoteProctoringService;
@Lazy
@ -63,13 +48,10 @@ public class ExamAdminServiceImpl implements ExamAdminService {
private final ConfigurationNodeDAO configurationNodeDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
private final LmsAPIService lmsAPIService;
private final boolean appSignatureKeyEnabled;
private final int defaultNumericalTrustThreshold;
private final ExamConfigurationValueService examConfigurationValueService;
private final SEBRestrictionService sebRestrictionService;
private final ExamTemplateService examTemplateService;
protected ExamAdminServiceImpl(
final ExamDAO examDAO,
final ProctoringAdminService proctoringAdminService,
@ -78,10 +60,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
final ExamConfigurationMapDAO examConfigurationMapDAO,
final LmsAPIService lmsAPIService,
final ExamConfigurationValueService examConfigurationValueService,
final SEBRestrictionService sebRestrictionService,
final ExamTemplateService examTemplateService,
final @Value("${sebserver.webservice.api.admin.exam.app.signature.key.enabled:false}") boolean appSignatureKeyEnabled,
final @Value("${sebserver.webservice.api.admin.exam.app.signature.key.numerical.threshold:2}") int defaultNumericalTrustThreshold) {
final SEBRestrictionService sebRestrictionService) {
this.examDAO = examDAO;
this.proctoringAdminService = proctoringAdminService;
@ -90,10 +69,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
this.examConfigurationMapDAO = examConfigurationMapDAO;
this.lmsAPIService = lmsAPIService;
this.examConfigurationValueService = examConfigurationValueService;
this.appSignatureKeyEnabled = appSignatureKeyEnabled;
this.defaultNumericalTrustThreshold = defaultNumericalTrustThreshold;
this.sebRestrictionService = sebRestrictionService;
this.examTemplateService = examTemplateService;
}
@Override
@ -101,80 +77,11 @@ public class ExamAdminServiceImpl implements ExamAdminService {
return this.proctoringAdminService;
}
@Override
public Result<Exam> applyExamImportInitialization(final Exam exam) {
final List<APIMessage> errors = new ArrayList<>();
this.initAdditionalAttributes(exam)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_ATTRIBUTES.of(error)))
.flatMap(this.examTemplateService::addDefinedIndicators)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_INDICATOR.of(error)))
.flatMap(this.examTemplateService::addDefinedClientGroups)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_CLIENT_GROUPS.of(error)))
.flatMap(this.examTemplateService::initAdditionalTemplateAttributes)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_ATTRIBUTES.of(error)))
.flatMap(this.examTemplateService::initExamConfiguration)
.onError(error -> {
if (error instanceof APIMessageException) {
errors.addAll(((APIMessageException) error).getAPIMessages());
} else {
errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_CONFIG.of(error));
}
})
.flatMap(this::applyAdditionalSEBRestrictions)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_RESTRICTION.of(error)))
.flatMap(this::applyQuitPassword)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_QUIT_PASSWORD.of(error)))
.flatMap(examTemplateService::applyScreenProctoringSettingsForExam)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_SCREEN_PROCTORING_SETTINGS.of(error)));
if (!errors.isEmpty()) {
errors.add(0, APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_SETUP.of(
exam.getModelId(),
API.PARAM_MODEL_ID + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + exam.getModelId()));
log.warn("Exam successfully created but some initialization did go wrong: {}", errors);
throw new APIMessageException(errors);
} else {
return this.examDAO.byPK(exam.id);
}
}
@Override
public Result<Exam> examForPK(final Long examId) {
return this.examDAO.byPK(examId);
}
@Override
public Result<Exam> initAdditionalAttributes(final Exam exam) {
return Result.tryCatch(() -> {
final Long examId = exam.getId();
// initialize App-Signature-Key feature attributes
this.additionalAttributesDAO.initAdditionalAttribute(
EntityType.EXAM,
examId,
Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED,
String.valueOf(this.appSignatureKeyEnabled));
this.additionalAttributesDAO.initAdditionalAttribute(
EntityType.EXAM,
examId,
Exam.ADDITIONAL_ATTR_NUMERICAL_TRUST_THRESHOLD,
String.valueOf(this.defaultNumericalTrustThreshold));
this.additionalAttributesDAO.initAdditionalAttribute(
EntityType.EXAM,
examId,
Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_SALT,
KeyGenerators.string().generateKey());
return exam;
}).flatMap(this::initAdditionalAttributesForMoodleExams);
}
@Override
public Result<Exam> saveSecurityKeySettings(
final Long institutionId,
@ -207,44 +114,6 @@ public class ExamAdminServiceImpl implements ExamAdminService {
}).flatMap(v -> this.examDAO.byPK(examId));
}
@Override
public Result<Exam> applyAdditionalSEBRestrictions(final Exam exam) {
return Result.tryCatch(() -> {
// this only applies to exams that are attached to an LMS
if (exam.lmsSetupId == null) {
return exam;
}
if (log.isDebugEnabled()) {
log.debug("Apply additional SEB restrictions for exam: {}",
exam.externalId);
}
final LmsSetup lmsSetup = this.lmsAPIService
.getLmsSetup(exam.lmsSetupId)
.getOrThrow();
if (lmsSetup.lmsType == LmsType.OPEN_EDX) {
final List<String> permissions = Arrays.asList(
OpenEdxSEBRestriction.PermissionComponent.ALWAYS_ALLOW_STAFF.key,
OpenEdxSEBRestriction.PermissionComponent.CHECK_CONFIG_KEY.key);
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
exam.id,
SEBRestrictionService.SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX +
OpenEdxSEBRestriction.ATTR_PERMISSION_COMPONENTS,
StringUtils.join(permissions, Constants.LIST_SEPARATOR_CHAR))
.getOrThrow();
}
return this.examDAO
.byPK(exam.id)
.getOrThrow();
});
}
@Override
public Result<Boolean> isRestricted(final Exam exam) {
if (exam == null || exam.lmsSetupId == null) {
@ -283,7 +152,8 @@ public class ExamAdminServiceImpl implements ExamAdminService {
@Override
public Result<ProctoringServiceSettings> getProctoringServiceSettings(final Long examId) {
return this.proctoringAdminService.getProctoringSettings(new EntityKey(examId, EntityType.EXAM));
return this.proctoringAdminService
.getProctoringSettings(new EntityKey(examId, EntityType.EXAM));
}
@Override
@ -377,60 +247,27 @@ public class ExamAdminServiceImpl implements ExamAdminService {
}
@Override
public Result<Exam> applyQuitPassword(final Exam exam) {
public Result<Exam> applyQuitPassword(final Exam exam) {
return this.examConfigurationValueService
.applyQuitPasswordToConfigs(exam.id, exam.quitPassword)
.flatMap(id -> this.sebRestrictionService.applySEBClientRestriction(exam))
.flatMap(e -> this.examDAO.setSEBRestriction(e.id, true))
.onError(t -> log.error("Failed to update SEB Client restriction for Exam: {}", exam, t));
.map(id -> applySEBRestrictionIfExamRunning(exam))
.onError(t -> log.error("Failed to quit password for Exam: {}", exam, t));
}
private Result<Exam> initAdditionalAttributesForMoodleExams(final Exam exam) {
return Result.tryCatch(() -> {
if (exam.lmsSetupId == null) {
return exam;
}
final LmsAPITemplate lmsTemplate = this.lmsAPIService
.getLmsAPITemplate(exam.lmsSetupId)
.getOrThrow();
// TODO check if this is still needed
if (lmsTemplate.lmsSetup().lmsType == LmsType.MOODLE) {
lmsTemplate.getQuiz(exam.externalId)
.flatMap(quizData -> this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
exam.id,
QuizData.QUIZ_ATTR_NAME,
quizData.name))
.onError(error -> log.error("Failed to create additional moodle quiz name attribute: ", error));
}
if (lmsTemplate.lmsSetup().lmsType == LmsType.MOODLE_PLUGIN) {
// Save additional Browser Exam Key for Moodle plugin integration SEBSERV-372
try {
final String moodleBEKUUID = UUID.randomUUID().toString();
final MessageDigest hasher = MessageDigest.getInstance(Constants.SHA_256);
hasher.update(Utils.toByteArray(moodleBEKUUID));
final String moodleBEK = Hex.toHexString(hasher.digest());
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
exam.id,
SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK,
moodleBEK)
.getOrThrow();
} catch (final Exception e) {
log.error("Failed to create additional moodle SEB BEK attribute: ", e);
}
}
private Exam applySEBRestrictionIfExamRunning(final Exam exam) {
if (exam.status != ExamStatus.RUNNING) {
return exam;
});
}
return this.sebRestrictionService
.applySEBClientRestriction(exam)
.flatMap(e -> this.examDAO.setSEBRestriction(e.id, true))
.onError(t -> log.error("Failed to update SEB Client restriction for Exam: {}", exam, t))
.getOr(exam);
}
@Override
public Result<Exam> archiveExam(final Exam exam) {
return Result.tryCatch(() -> {

View file

@ -0,0 +1,233 @@
/*
* Copyright (c) 2019 ETH Zürich, IT Services
*
* 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.exam.impl;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
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.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamImportService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.crypto.keygen.KeyGenerators;
import org.springframework.stereotype.Service;
@Lazy
@Service
@WebServiceProfile
public class ExamImportServiceImpl implements ExamImportService {
private static final Logger log = LoggerFactory.getLogger(ExamImportServiceImpl.class);
private final ExamDAO examDAO;
// private final FullLmsIntegrationService fullLmsIntegrationService;
private final ExamTemplateService examTemplateService;
private final ExamAdminService examAdminService;
private final boolean appSignatureKeyEnabled;
private final int defaultNumericalTrustThreshold;
public ExamImportServiceImpl(
final ExamDAO examDAO,
final ExamTemplateService examTemplateService,
final ExamAdminService examAdminService,
final AdditionalAttributesDAO additionalAttributesDAO,
final LmsAPIService lmsAPIService,
final @Value("${sebserver.webservice.api.admin.exam.app.signature.key.enabled:false}") boolean appSignatureKeyEnabled,
final @Value("${sebserver.webservice.api.admin.exam.app.signature.key.numerical.threshold:2}") int defaultNumericalTrustThreshold) {
this.examDAO = examDAO;
this.examTemplateService = examTemplateService;
this.examAdminService = examAdminService;
this.additionalAttributesDAO = additionalAttributesDAO;
this.lmsAPIService = lmsAPIService;
this.appSignatureKeyEnabled = appSignatureKeyEnabled;
this.defaultNumericalTrustThreshold = defaultNumericalTrustThreshold;
}
private final AdditionalAttributesDAO additionalAttributesDAO;
private final LmsAPIService lmsAPIService;
@Override
public Result<Exam> applyExamImportInitialization(final Exam exam) {
final List<APIMessage> errors = new ArrayList<>();
this.initAdditionalAttributes(exam)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_ATTRIBUTES.of(error)))
.flatMap(this.examTemplateService::addDefinedIndicators)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_INDICATOR.of(error)))
.flatMap(this.examTemplateService::addDefinedClientGroups)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_CLIENT_GROUPS.of(error)))
.flatMap(this.examTemplateService::initAdditionalTemplateAttributes)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_ATTRIBUTES.of(error)))
.flatMap(this.examTemplateService::initExamConfiguration)
.onError(error -> {
if (error instanceof APIMessage.APIMessageException) {
errors.addAll(((APIMessage.APIMessageException) error).getAPIMessages());
} else {
errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_CONFIG.of(error));
}
})
.flatMap(this::applyAdditionalSEBRestrictions)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_RESTRICTION.of(error)))
.flatMap(examAdminService::applyQuitPassword)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_QUIT_PASSWORD.of(error)))
.flatMap(examTemplateService::applyScreenProctoringSettingsForExam)
.onError(error -> errors.add(APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_SCREEN_PROCTORING_SETTINGS.of(error)));
if (!errors.isEmpty()) {
errors.add(0, APIMessage.ErrorMessage.EXAM_IMPORT_ERROR_AUTO_SETUP.of(
exam.getModelId(),
API.PARAM_MODEL_ID + Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR + exam.getModelId()));
log.warn("Exam successfully created but some initialization did go wrong: {}", errors);
throw new APIMessage.APIMessageException(errors);
} else {
return this.examDAO.byPK(exam.id);
}
}
@Override
public Result<Exam> initAdditionalAttributes(final Exam exam) {
return Result.tryCatch(() -> {
final Long examId = exam.getId();
// initialize App-Signature-Key feature attributes
this.additionalAttributesDAO.initAdditionalAttribute(
EntityType.EXAM,
examId,
Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED,
String.valueOf(this.appSignatureKeyEnabled));
this.additionalAttributesDAO.initAdditionalAttribute(
EntityType.EXAM,
examId,
Exam.ADDITIONAL_ATTR_NUMERICAL_TRUST_THRESHOLD,
String.valueOf(this.defaultNumericalTrustThreshold));
this.additionalAttributesDAO.initAdditionalAttribute(
EntityType.EXAM,
examId,
Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_SALT,
KeyGenerators.string().generateKey());
return exam;
}).flatMap(this::initAdditionalAttributesForMoodleExams);
}
private Result<Exam> applyAdditionalSEBRestrictions(final Exam exam) {
return Result.tryCatch(() -> {
// this only applies to exams that are attached to an LMS
if (exam.lmsSetupId == null) {
return exam;
}
if (log.isDebugEnabled()) {
log.debug("Apply additional SEB restrictions for exam: {}",
exam.externalId);
}
final LmsSetup lmsSetup = this.lmsAPIService
.getLmsSetup(exam.lmsSetupId)
.getOrThrow();
if (lmsSetup.lmsType == LmsSetup.LmsType.OPEN_EDX) {
final List<String> permissions = Arrays.asList(
OpenEdxSEBRestriction.PermissionComponent.ALWAYS_ALLOW_STAFF.key,
OpenEdxSEBRestriction.PermissionComponent.CHECK_CONFIG_KEY.key);
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
exam.id,
SEBRestrictionService.SEB_RESTRICTION_ADDITIONAL_PROPERTY_NAME_PREFIX +
OpenEdxSEBRestriction.ATTR_PERMISSION_COMPONENTS,
StringUtils.join(permissions, Constants.LIST_SEPARATOR_CHAR))
.getOrThrow();
}
return this.examDAO
.byPK(exam.id)
.getOrThrow();
});
}
private Result<Exam> initAdditionalAttributesForMoodleExams(final Exam exam) {
return Result.tryCatch(() -> {
if (exam.lmsSetupId == null) {
return exam;
}
final LmsAPITemplate lmsTemplate = this.lmsAPIService
.getLmsAPITemplate(exam.lmsSetupId)
.getOrThrow();
if (lmsTemplate.lmsSetup().lmsType == LmsSetup.LmsType.MOODLE) {
lmsTemplate.getQuiz(exam.externalId)
.flatMap(quizData -> this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
exam.id,
QuizData.QUIZ_ATTR_NAME,
quizData.name))
.onError(error -> log.error("Failed to create additional moodle quiz name attribute: ", error));
}
if (lmsTemplate.lmsSetup().lmsType == LmsSetup.LmsType.MOODLE_PLUGIN) {
// Save additional Browser Exam Key for Moodle plugin integration SEBSERV-372
try {
final String moodleBEKUUID = UUID.randomUUID().toString();
final MessageDigest hasher = MessageDigest.getInstance(Constants.SHA_256);
hasher.update(Utils.toByteArray(moodleBEKUUID));
final String moodleBEK = Hex.toHexString(hasher.digest());
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.EXAM,
exam.id,
SEBRestrictionService.ADDITIONAL_ATTR_ALTERNATIVE_SEB_BEK,
moodleBEK)
.getOrThrow();
} catch (final Exception e) {
log.error("Failed to create additional moodle SEB BEK attribute: ", e);
}
}
return exam;
});
}
}

View file

@ -100,25 +100,25 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
checkType(parentEntityKey);
ScreenProctoringSettings settings = this.proctoringSettingsDAO
return this.proctoringSettingsDAO
.getScreenProctoringSettings(parentEntityKey)
.getOrThrow();
if (this.screenProctoringServiceBundle.bundled) {
settings = new ScreenProctoringSettings(
settings.examId,
settings.enableScreenProctoring,
this.screenProctoringServiceBundle.serviceURL,
this.screenProctoringServiceBundle.clientId,
null,
this.screenProctoringServiceBundle.apiAccountName,
null,
settings.collectingStrategy,
settings.collectingGroupSize,
true);
}
return settings;
// if (this.screenProctoringServiceBundle.bundled) {
// settings = new ScreenProctoringSettings(
// settings.examId,
// settings.enableScreenProctoring,
// this.screenProctoringServiceBundle.serviceURL,
// this.screenProctoringServiceBundle.clientId,
// null,
// this.screenProctoringServiceBundle.apiAccountName,
// null,
// settings.collectingStrategy,
// settings.collectingGroupSize,
// true);
// }
//
// return settings;
});
}

View file

@ -51,6 +51,14 @@ public interface CourseAccessAPI {
}
}
default String getCourseIdFromExam(final Exam exam) {
return exam.externalId;
}
default String getQuizIdFromExam(final Exam exam) {
return exam.externalId;
}
void fetchQuizzes(FilterMap filterMap, AsyncQuizFetchBuffer asyncQuizFetchBuffer);
/** Get all {@link QuizData } for the set of {@link QuizData } identifiers from LMS API in a collection

View file

@ -8,10 +8,12 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService.IntegrationData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService.ExamData;
public interface FullLmsIntegrationAPI {
@ -24,6 +26,10 @@ public interface FullLmsIntegrationAPI {
Result<IntegrationData> applyConnectionDetails(IntegrationData data);
Result<ExamData> applyExamData(ExamData examData);
Result<Exam> applyConnectionConfiguration(Exam exam, byte[] configData);
Result<String> deleteConnectionDetails();
}

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import java.io.OutputStream;
import java.util.Collection;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.util.Result;
@ -18,10 +19,12 @@ import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateChangeEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsSetupChangeEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationChangeEvent;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.RequestParam;
public interface FullLmsIntegrationService {
@ -30,6 +33,16 @@ public interface FullLmsIntegrationService {
@EventListener
void notifyExamTemplateChange(final ExamTemplateChangeEvent event);
@EventListener(ConnectionConfigurationChangeEvent.class)
void notifyConnectionConfigurationChange(ConnectionConfigurationChangeEvent event);
@EventListener(ExamDeletionEvent.class)
void notifyExamDeletion(ExamDeletionEvent event);
/** Applies the exam data to LMS to inform the LMS that the exam exists on SEB Server site.
* @param exam The Exam
*/
Result<Exam> applyExamDataToLMS(Exam exam);
Result<IntegrationData> applyFullLmsIntegration(Long lmsSetupId);
@ -48,8 +61,7 @@ public interface FullLmsIntegrationService {
String courseId,
String quizId);
@EventListener(ExamDeletionEvent.class)
void notifyExamDeletion(ExamDeletionEvent event);
Result<Void> streamConnectionConfiguration(
String lmsUUID,
@ -61,9 +73,68 @@ public interface FullLmsIntegrationService {
String lmsUUId,
String courseId,
String quizId,
String userId,
String username,
String timezone);
AdHocAccountData adHocAccountData);
final class AdHocAccountData {
public final String userId;
public final String username;
public final String userMail;
public final String firstName;
public final String lastName;
public final String timezone;
public AdHocAccountData(
final String userId,
final String username,
final String userMail,
final String firstName,
final String lastName,
final String timezone) {
this.userId = userId;
this.username = username;
this.userMail = userMail;
this.firstName = firstName;
this.lastName = lastName;
this.timezone = timezone;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
final class ExamData {
@JsonProperty("id")
public final String id;
@JsonProperty("course_id")
public final String course_id;
@JsonProperty("quiz_id")
public final String quiz_id;
@JsonProperty("exam_created")
public final Boolean exam_created;
@JsonProperty("template_id")
public final String template_id;
@JsonProperty("show_quit_link")
public final Boolean show_quit_link;
@JsonProperty("quit_password")
public final String quit_password;
public ExamData(
final String id,
final String course_id,
final String quiz_id,
final Boolean exam_created,
final String template_id,
final Boolean show_quit_link,
final String quit_password) {
this.id = id;
this.course_id = course_id;
this.quiz_id = quiz_id;
this.exam_created = exam_created;
this.template_id = template_id;
this.show_quit_link = show_quit_link;
this.quit_password = quit_password;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
final class IntegrationData {
@ -75,7 +146,6 @@ public interface FullLmsIntegrationService {
public final String url;
@JsonProperty("autologin_url")
public final String autoLoginURL;
@JsonProperty("access_token")
public final String access_token;
@JsonProperty("exam_templates")
@ -117,6 +187,20 @@ public interface FullLmsIntegrationService {
this.template_name = template_name;
this.template_description = template_description;
}
}
final class TokenLoginResponse {
@JsonProperty("id")
public final String id;
@JsonProperty("login_link")
public final String loginLink;
public TokenLoginResponse(
final String id,
final String loginLink) {
this.id = id;
this.loginLink = loginLink;
}
}
}

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess;

View file

@ -8,14 +8,17 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.function.Function;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
@ -35,13 +38,15 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.TeacherA
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.DeleteExamAction;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.*;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent;
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.ExamImportService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateChangeEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationChangeEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService;
import org.apache.commons.lang3.StringUtils;
@ -66,11 +71,12 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
private final UserActivityLogDAO userActivityLogDAO;
private final TeacherAccountServiceImpl teacherAccountServiceImpl;
private final SEBClientConfigDAO sebClientConfigDAO;
private final ClientConfigService clientConfigService;
private final ConnectionConfigurationService connectionConfigurationService;
private final DeleteExamAction deleteExamAction;
private final LmsAPIService lmsAPIService;
private final ExamAdminService examAdminService;
private final ExamImportService examImportService;
private final ExamSessionService examSessionService;
private final ExamConfigurationValueService examConfigurationValueService;
private final ExamDAO examDAO;
private final ExamTemplateDAO examTemplateDAO;
private final WebserviceInfo webserviceInfo;
@ -85,12 +91,13 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
final UserDAO userDAO,
final SEBClientConfigDAO sebClientConfigDAO,
final ScreenProctoringService screenProctoringService,
final ClientConfigService clientConfigService,
final ConnectionConfigurationService connectionConfigurationService,
final DeleteExamAction deleteExamAction,
final LmsAPIService lmsAPIService,
final ExamAdminService examAdminService,
final ExamSessionService examSessionService,
final ExamConfigurationValueService examConfigurationValueService,
final ExamDAO examDAO,
final ExamImportService examImportService,
final ExamTemplateDAO examTemplateDAO,
final WebserviceInfo webserviceInfo,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
@ -104,16 +111,17 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
this.userActivityLogDAO = userActivityLogDAO;
this.teacherAccountServiceImpl = teacherAccountServiceImpl;
this.sebClientConfigDAO = sebClientConfigDAO;
this.clientConfigService = clientConfigService;
this.connectionConfigurationService = connectionConfigurationService;
this.deleteExamAction = deleteExamAction;
this.lmsAPIService = lmsAPIService;
this.examAdminService = examAdminService;
this.examSessionService = examSessionService;
this.examDAO = examDAO;
this.examTemplateDAO = examTemplateDAO;
this.webserviceInfo = webserviceInfo;
this.lmsAPIEndpoint = lmsAPIEndpoint;
this.userService = userService;
this.examConfigurationValueService = examConfigurationValueService;
this.examImportService = examImportService;
resource = new ClientCredentialsResourceDetails();
resource.setAccessTokenUri(webserviceInfo.getOAuthTokenURI());
@ -133,13 +141,18 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
.add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
}
@Override
public Result<Exam> applyExamDataToLMS(final Exam exam) {
return Result.tryCatch(() -> this.applyExamData(exam, false));
}
@Override
public void notifyExamDeletion(final ExamDeletionEvent event) {
event.ids.forEach( examId -> {
this.examDAO.byPK(examId)
.map(this.teacherAccountServiceImpl::deleteTeacherAccountsForExam)
.onError(error -> log.warn("Failed delete teacher accounts for exam: {}", examId));
});
event.ids.forEach( examId -> this.examDAO.byPK(examId)
.flatMap(this.teacherAccountServiceImpl::deactivateTeacherAccountsForExam)
.map(exam -> applyExamData(exam, true))
.onError(error -> log.warn("Failed delete teacher accounts for exam: {}", examId))
);
}
@Override
@ -170,17 +183,37 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
lmsSetupDAO.idsOfActiveWithFullIntegration(examTemplate.institutionId)
.onSuccess(all -> all.stream()
.map(this::applyFullLmsIntegration)
.forEach(res -> {
.forEach(res ->
res.onError(error -> log.warn(
"Failed to update LMS Full Integration: {}",
error.getMessage()) );
}))
error.getMessage()) )
))
.onError(error -> log.warn(
"Failed to apply LMS Full Integration change caused by Exam Template: {}",
examTemplate,
error));
}
@Override
public void notifyConnectionConfigurationChange(final ConnectionConfigurationChangeEvent event) {
lmsSetupDAO.idsOfActiveWithFullIntegration(event.institutionId)
.flatMap(examDAO::allActiveForLMSSetup)
.onError(error -> log.error("Failed to notifyConnectionConfigurationChange: {}", error.getMessage()))
.getOr(Collections.emptyList())
.stream()
.filter(exam -> this.needsConnectionConfigurationChange(exam, event.configId))
.forEach(this::applyConnectionConfiguration);
}
private boolean needsConnectionConfigurationChange(final Exam exam, final Long ccId) {
if (exam.status == Exam.ExamStatus.ARCHIVED) {
return false;
}
final String configId = getConnectionConfigurationId(exam);
return StringUtils.isNotBlank(configId) && configId.equals(String.valueOf(ccId));
}
@Override
public Result<IntegrationData> applyFullLmsIntegration(final Long lmsSetupId) {
return lmsSetupDAO
@ -261,7 +294,9 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
.getLmsSetupIdByConnectionId(lmsUUID)
.flatMap(lmsAPIService::getLmsAPITemplate)
.map(findQuizData(courseId, quizId))
.map(createExam(examTemplateId, quitPassword));
.map(createExam(examTemplateId, quitPassword))
.map(exam -> applyExamData(exam, false))
.map(this::applyConnectionConfiguration);
}
@Override
@ -277,25 +312,11 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
.flatMap(this::findExam)
.map(this::checkDeletion)
.map(this::logExamDeleted)
.flatMap(teacherAccountServiceImpl::deleteTeacherAccountsForExam)
.flatMap(deleteExamAction::deleteExamFromLMSIntegration);
.flatMap(teacherAccountServiceImpl::deactivateTeacherAccountsForExam)
.map(exam -> applyExamData(exam, true))
.flatMap(deleteExamAction::deleteExamInternal);
}
private Exam checkDeletion(final Exam exam) {
// TODO check if Exam can be deleted according to the Spec
// check if there are no active SEB client connections
if (this.examSessionService.hasActiveSEBClientConnections(exam.id)) {
throw new APIMessage.APIMessageException(
APIMessage.ErrorMessage.INTEGRITY_VALIDATION
.of("Exam currently has active SEB Client connections."));
}
return exam;
}
@Override
public Result<Void> streamConnectionConfiguration(
final String lmsUUID,
@ -317,22 +338,12 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
final Exam exam = examResult.get();
String connectionConfigId = exam.getAdditionalAttribute(Exam.ADDITIONAL_ATTR_DEFAULT_CONNECTION_CONFIGURATION);
if (StringUtils.isBlank(connectionConfigId)) {
connectionConfigId = this.sebClientConfigDAO
.all(exam.institutionId, true)
.map(all -> all.stream().filter(config -> config.configPurpose == SEBClientConfig.ConfigPurpose.START_EXAM)
.findFirst()
.orElseThrow(() -> new APIMessage.APIMessageException(
APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT.of("No active Connection Configuration found"))))
.map(SEBClientConfig::getModelId)
.getOr(null);
}
final String connectionConfigId = getConnectionConfigurationId(exam);
if (StringUtils.isBlank(connectionConfigId)) {
throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT.of("No active Connection Configuration found"));
}
this.clientConfigService.exportSEBClientConfiguration(out, connectionConfigId, exam.id);
this.connectionConfigurationService.exportSEBClientConfiguration(out, connectionConfigId, exam.id);
return Result.EMPTY;
} catch (final Exception e) {
@ -340,14 +351,27 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
}
}
private String getConnectionConfigurationId(final Exam exam) {
String connectionConfigId = exam.getAdditionalAttribute(Exam.ADDITIONAL_ATTR_DEFAULT_CONNECTION_CONFIGURATION);
if (StringUtils.isBlank(connectionConfigId)) {
connectionConfigId = this.sebClientConfigDAO
.all(exam.institutionId, true)
.map(all -> all.stream().filter(config -> config.configPurpose == SEBClientConfig.ConfigPurpose.START_EXAM)
.findFirst()
.orElseThrow(() -> new APIMessage.APIMessageException(
APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT.of("No active Connection Configuration found"))))
.map(SEBClientConfig::getModelId)
.getOr(null);
}
return connectionConfigId;
}
@Override
public Result<String> getOneTimeLoginToken(
final String lmsUUID,
final String courseId,
final String quizId,
final String userId,
final String username,
final String timezone) {
final AdHocAccountData adHocAccountData) {
return lmsSetupDAO
.getLmsSetupIdByConnectionId(lmsUUID)
@ -355,7 +379,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
.map(findQuizData(courseId, quizId))
.flatMap(this::findExam)
.flatMap(exam -> this.teacherAccountServiceImpl
.getOneTimeTokenForTeacherAccount(exam, userId, username, timezone, true));
.getOneTimeTokenForTeacherAccount(exam, adHocAccountData, true));
}
@ -408,9 +432,19 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
return existingExam.get();
}
final ExamTemplate examTemplate = examTemplateDAO
.byModelId(examTemplateId)
.getOrThrow();
// import exam
final POSTMapper post = new POSTMapper(null, null);
post.putIfAbsent(Domain.EXAM.ATTR_EXAM_TEMPLATE_ID, examTemplateId);
post.putIfAbsent(Domain.EXAM.ATTR_OWNER, userService.getCurrentUser().uuid());
post.putIfAbsent(
Domain.EXAM.ATTR_SUPPORTER,
StringUtils.join(examTemplate.supporter, Constants.LIST_SEPARATOR));
post.putIfAbsent(Domain.EXAM.ATTR_TYPE, examTemplate.examType.name());
if (StringUtils.isNotBlank(quitPassword)) {
post.putIfAbsent(Domain.EXAM.ATTR_QUIT_PASSWORD, quitPassword);
}
@ -418,87 +452,24 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
final Exam exam = new Exam(null, quizData, post);
return examDAO
.createNew(exam)
.flatMap(examAdminService::applyExamImportInitialization)
.flatMap(examImportService::applyExamImportInitialization)
.map(this::logExamCreated)
.getOrThrow();
};
}
// private UserInfo createNewAdHocAccount(
// final Exam exam,
// final String userId,
// final String username,
// final String timezone) {
// try {
//
// final String uuid = UUID.randomUUID().toString();
// DateTimeZone dtz = DateTimeZone.UTC;
// if (StringUtils.isNotBlank(timezone)) {
// try {
// dtz = DateTimeZone.forID(timezone);
// } catch (final Exception e) {
// log.warn("Failed to set requested time zone for ad-hoc teacher account: {}", timezone);
// }
// }
//
// final UserMod adHocTeacherUser = new UserMod(
// uuid,
// exam.institutionId,
// userId,
// getTeacherAccountIdentifier(exam),
// username,
// uuid,
// uuid,
// null,
// Locale.ENGLISH,
// dtz,
// true,
// false,
// Utils.immutableSetOf(UserRole.TEACHER.name()));
//
// return userDAO.createNew(adHocTeacherUser)
// .flatMap(account -> userDAO.setActive(account, true))
// .getOrThrow();
//
// } catch (final Exception e) {
// log.error("Failed to create ad-hoc teacher account for importing exam: {}", exam, e);
// return null;
// }
// }
// private Exam deleteAdHocAccounts(final Exam exam) {
// try {
//
// final String externalId = exam.externalId;
// final FilterMap filter = new FilterMap();
// filter.putIfAbsent(Domain.USER.ATTR_SURNAME, getTeacherAccountIdentifier(exam));
// final Collection<UserInfo> accounts = userDAO.allMatching(filter).getOrThrow();
//
// if (accounts.isEmpty()) {
// return exam;
// }
//
// if (accounts.size() > 1) {
// log.error("Too many accounts found!?... ad-hoc teacher account mapping: {}", externalId);
// return exam;
// }
//
// userDAO.delete(Utils.immutableSetOf(new EntityKey(
// accounts.iterator().next().uuid,
// EntityType.USER)))
// .getOrThrow();
//
// } catch (final Exception e) {
// log.error("Failed to delete ad-hoc account for exam: {}", exam, e);
// }
//
// return exam;
// }
private Exam checkDeletion(final Exam exam) {
// TODO check if Exam can be deleted according to the Spec
// check if there are no active SEB client connections
if (this.examSessionService.hasActiveSEBClientConnections(exam.id)) {
throw new APIMessage.APIMessageException(
APIMessage.ErrorMessage.INTEGRITY_VALIDATION
.of("Exam currently has active SEB Client connections."));
}
return exam;
}
private Collection<ExamTemplateSelection> getIntegrationTemplates(final Long institutionId) {
@ -514,6 +485,54 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
.getOrThrow();
}
private Exam applyExamData(final Exam exam, final boolean deletion) {
try {
final LmsAPITemplate lmsAPITemplate = lmsAPIService
.getLmsAPITemplate(exam.lmsSetupId)
.getOrThrow();
final String lmsUUID = lmsAPITemplate.lmsSetup().connectionId;
final String courseId = lmsAPITemplate.getCourseIdFromExam(exam);
final String quizId = lmsAPITemplate.getQuizIdFromExam(exam);
final String templateId = deletion ? null : String.valueOf(exam.examTemplateId);
final String quitPassword = deletion ? null : examConfigurationValueService.getQuitPassword(exam.id);
final Boolean quitLink = deletion ? null : StringUtils.isNotBlank(examConfigurationValueService.getQuitLink(exam.id));
final ExamData examData = new ExamData(
lmsUUID,
courseId,
quizId,
!deletion,
templateId,
quitLink,
quitPassword);
lmsAPITemplate.applyExamData(examData).getOrThrow();
} catch (final Exception e) {
log.warn("Failed to apply exam data to LMS for exam: {} error: {}", exam, e.getMessage());
}
return exam;
}
private Exam applyConnectionConfiguration(final Exam exam) {
return lmsAPIService
.getLmsAPITemplate(exam.lmsSetupId)
.flatMap(template -> {
final String connectionConfigId = getConnectionConfigurationId(exam);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
this.connectionConfigurationService
.exportSEBClientConfiguration(out, connectionConfigId, exam.id);
// TODO check if this works as expected
return template.applyConnectionConfiguration(exam, out.toByteArray());
})
.onError(error -> log.error("Failed to apply ConnectionConfiguration for exam: {} error: {}", exam, error.getMessage()))
.getOr(exam);
}
private String getAPIRootURL() {
return webserviceInfo.getExternalServerURL() + lmsAPIEndpoint;
}

View file

@ -13,6 +13,7 @@ import java.util.Set;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.*;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService.IntegrationData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService.ExamData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
@ -55,8 +56,9 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
private final CircuitBreaker<ExamineeAccountDetails> accountDetailRequest;
private final CircuitBreaker<SEBRestriction> restrictionRequest;
private final CircuitBreaker<Exam> releaseRestrictionRequest;
private final CircuitBreaker<Exam> examRequest;
private final CircuitBreaker<IntegrationData> lmsAccessRequest;
private final CircuitBreaker<ExamData> applyExamDataRequest;
private final CircuitBreaker<String> deleteLmsAccessRequest;
public LmsAPITemplateAdapter(
@ -88,15 +90,29 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
lmsAccessRequest = asyncService.createCircuitBreaker(
environment.getProperty(
"sebserver.webservice.circuitbreaker.lmsTestRequest.attempts",
"sebserver.webservice.circuitbreaker.lmsAccessRequest.attempts",
Integer.class,
2),
environment.getProperty(
"sebserver.webservice.circuitbreaker.lmsTestRequest.blockingTime",
"sebserver.webservice.circuitbreaker.lmsAccessRequest.blockingTime",
Long.class,
Constants.SECOND_IN_MILLIS * 20),
environment.getProperty(
"sebserver.webservice.circuitbreaker.lmsTestRequest.timeToRecover",
"sebserver.webservice.circuitbreaker.lmsAccessRequest.timeToRecover",
Long.class,
0L));
applyExamDataRequest = asyncService.createCircuitBreaker(
environment.getProperty(
"sebserver.webservice.circuitbreaker.applyExamDataRequest.attempts",
Integer.class,
2),
environment.getProperty(
"sebserver.webservice.circuitbreaker.applyExamDataRequest.blockingTime",
Long.class,
Constants.SECOND_IN_MILLIS * 20),
environment.getProperty(
"sebserver.webservice.circuitbreaker.applyExamDataRequest.timeToRecover",
Long.class,
0L));
@ -198,17 +214,17 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
Long.class,
0L));
this.releaseRestrictionRequest = asyncService.createCircuitBreaker(
this.examRequest = asyncService.createCircuitBreaker(
environment.getProperty(
"sebserver.webservice.circuitbreaker.sebrestriction.attempts",
"sebserver.webservice.circuitbreaker.examRequest.attempts",
Integer.class,
2),
environment.getProperty(
"sebserver.webservice.circuitbreaker.sebrestriction.blockingTime",
"sebserver.webservice.circuitbreaker.examRequest.blockingTime",
Long.class,
Constants.SECOND_IN_MILLIS * 10),
environment.getProperty(
"sebserver.webservice.circuitbreaker.sebrestriction.timeToRecover",
"sebserver.webservice.circuitbreaker.examRequest.timeToRecover",
Long.class,
0L));
}
@ -456,7 +472,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
log.debug("Release course restriction: {} for LMSSetup: {}", exam.externalId, lmsSetup());
}
final Result<Exam> protectedRun = this.releaseRestrictionRequest.protectedRun(() -> this.sebRestrictionAPI
final Result<Exam> protectedRun = this.examRequest.protectedRun(() -> this.sebRestrictionAPI
.releaseSEBClientRestriction(exam)
.onError(error -> log.error(
"Failed to release SEB restrictions: {}",
@ -498,6 +514,39 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
.getOrThrow());
}
@Override
public Result<ExamData> applyExamData(final ExamData examData) {
if (this.lmsIntegrationAPI == null) {
return Result.ofError(
new UnsupportedOperationException("LMS Integration API Not Supported For: " + getType().name()));
}
if (log.isDebugEnabled()) {
log.debug("Apply exam data: {} for LMSSetup: {}", examData, lmsSetup());
}
return this.applyExamDataRequest.protectedRun(() -> this.lmsIntegrationAPI
.applyExamData(examData)
.getOrThrow());
}
@Override
public Result<Exam> applyConnectionConfiguration(final Exam exam, final byte[] configData) {
if (this.lmsIntegrationAPI == null) {
return Result.ofError(
new UnsupportedOperationException("LMS Integration API Not Supported For: " + getType().name()));
}
if (log.isDebugEnabled()) {
log.debug("Apply Connection Configuration for exam: {} for LMSSetup: {}", exam, lmsSetup());
}
return this.examRequest.protectedRun(() -> this.lmsIntegrationAPI
.applyConnectionConfiguration(exam, configData)
.getOrThrow());
}
@Override
public Result<String> deleteConnectionDetails() {
if (this.lmsIntegrationAPI == null) {

View file

@ -22,6 +22,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
@ -431,6 +432,16 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
return Result.ofRuntimeError("Not Supported");
}
@Override
public Result<FullLmsIntegrationService.ExamData> applyExamData(final FullLmsIntegrationService.ExamData examData) {
return Result.ofRuntimeError("Not Supported");
}
@Override
public Result<Exam> applyConnectionConfiguration(final Exam exam, final byte[] configData) {
return Result.ofRuntimeError("Not Supported");
}
@Override
public Result<String> deleteConnectionDetails() {
return Result.ofRuntimeError("Not Supported");

View file

@ -8,10 +8,12 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService.IntegrationData;
public class MockupFullIntegration implements FullLmsIntegrationAPI {
@ -26,6 +28,16 @@ public class MockupFullIntegration implements FullLmsIntegrationAPI {
return Result.ofRuntimeError("TODO");
}
@Override
public Result<FullLmsIntegrationService.ExamData> applyExamData(final FullLmsIntegrationService.ExamData examData) {
return Result.ofRuntimeError("Not Supported");
}
@Override
public Result<Exam> applyConnectionConfiguration(final Exam exam, final byte[] configData) {
return Result.ofRuntimeError("Not Supported");
}
@Override
public Result<String> deleteConnectionDetails() {
return Result.ofRuntimeError("TODO");

View file

@ -40,13 +40,18 @@ public interface MoodleAPIRestTemplate {
String postToMoodleAPIFunction(String functionName, String body);
String callMoodleAPIFunction(
final String functionName,
final MultiValueMap<String, String> queryAttributes);
String functionName,
MultiValueMap<String, String> queryAttributes);
String callMoodleAPIFunction(
final String functionName,
final MultiValueMap<String, String> queryParams,
final MultiValueMap<String, String> queryAttributes);
String functionName,
MultiValueMap<String, String> queryParams,
MultiValueMap<String, String> queryAttributes);
String uploadMultiPart(
String uploadEndpoint,
MultiValueMap<String, Object> multiPartAttributes);
/** This maps a Moodle warning JSON object */
@JsonIgnoreProperties(ignoreUnknown = true)

View file

@ -149,14 +149,13 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
this. activeRestTemplate = this.knownTokenAccessPaths
.stream()
.map(path -> this.createRestTemplate(service, path))
.map(result -> {
.peek(result -> {
if (result.hasError()) {
log.warn("Failed to get access token for LMS: {}({}), error {}",
lmsSetup.name,
lmsSetup.id,
result.getError().getMessage());
}
return result;
})
.filter(Result::hasValue)
.findFirst()
@ -284,7 +283,7 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
final List<String> missingAPIFunctions = Arrays.stream(functions)
.filter(f -> !webserviceInfo.functions.containsKey(f))
.collect(Collectors.toList());
.toList();
if (!missingAPIFunctions.isEmpty()) {
throw new RuntimeException("Missing Moodle Webservice API functions: " + missingAPIFunctions);
@ -328,7 +327,7 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
headers.set(
HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_JSON_VALUE);
HttpEntity<String> httpEntity = new HttpEntity<>(body, headers);
final HttpEntity<String> httpEntity = new HttpEntity<>(body, headers);
return doRequest(functionName, queryParam, true, httpEntity);
}
@ -369,6 +368,22 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
return doRequest(functionName, queryParam, usePOST, functionReqEntity);
}
@Override
public String uploadMultiPart(
final String uploadEndpoint,
final MultiValueMap<String, Object> multiPartAttributes) {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final String uri = lmsSetup.lmsApiUrl + uploadEndpoint;
getAccessToken();
multiPartAttributes.add("token", this.accessToken);
return super.postForObject(
uploadEndpoint,
multiPartAttributes,
String.class);
}
private String doRequest(
final String functionName,
final UriComponentsBuilder queryParam,
@ -474,7 +489,7 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
final String privatetoken;
@JsonCreator
protected MoodleToken(
private MoodleToken(
@JsonProperty(value = "token") final String token,
@JsonProperty(value = "privatetoken", required = false) final String privatetoken) {
@ -491,7 +506,7 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
Map<String, FunctionInfo> functions;
@JsonCreator
protected WebserviceInfo(
private WebserviceInfo(
@JsonProperty(value = "username") final String username,
@JsonProperty(value = "userid") final String userid,
@JsonProperty(value = "functions") final Collection<FunctionInfo> functions) {
@ -513,7 +528,7 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
String version;
@JsonCreator
protected FunctionInfo(
private FunctionInfo(
@JsonProperty(value = "name") final String name,
@JsonProperty(value = "version") final String version) {

View file

@ -61,9 +61,9 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils.MoodleUserDetails;
/** Implements the LmsAPITemplate for Open edX LMS Course API access.
*
* <p>
* See also: https://docs.moodle.org/dev/Web_service_API_functions
*
* <p>
* NOTE: Because of the missing integration on Moodle side so far the MoodleCourseAccess
* needs to deal with Moodle's standard API functions that don't allow to filter and page course/quiz data
* in an easy and proper way. Therefore we have to fetch all course and quiz data from Moodle before
@ -141,6 +141,16 @@ public class MoodleCourseAccess implements CourseAccessAPI {
return this.restTemplateFactory.getApiTemplateDataSupplier();
}
@Override
public String getCourseIdFromExam(final Exam exam) {
return MoodleUtils.getCourseId(exam.externalId);
}
@Override
public String getQuizIdFromExam(final Exam exam) {
return MoodleUtils.getQuizId(exam.externalId);
}
@Override
public LmsSetupTestResult testCourseAccessAPI() {
final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test();

View file

@ -144,6 +144,16 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
return this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup().id;
}
@Override
public String getCourseIdFromExam(final Exam exam) {
return MoodleUtils.getCourseId(exam.externalId);
}
@Override
public String getQuizIdFromExam(final Exam exam) {
return MoodleUtils.getQuizId(exam.externalId);
}
@Override
public LmsSetupTestResult testCourseAccessAPI() {
final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test();

View file

@ -10,17 +10,21 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService.IntegrationData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService.ExamData;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationAPI;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleAPIRestTemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleResponseException;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@ -30,6 +34,10 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI {
private static final String FUNCTION_NAME_SEBSERVER_CONNECTION = "quizaccess_sebserver_connection";
private static final String FUNCTION_NAME_SEBSERVER_CONNECTION_DELETE = "quizaccess_sebserver_connection_delete";
private static final String FUNCTION_NAME_SET_EXAM_DATA = "quizaccess_sebserver_set_exam_data";
private static final String UPLOAD_ENDPOINT = "/mod/quiz/accessrule/sebserver/uploadconfig.php";
private final JSONMapper jsonMapper;
private final MoodleRestTemplateFactory restTemplateFactory;
@ -60,7 +68,8 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI {
final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get();
restTemplate.testAPIConnection(
FUNCTION_NAME_SEBSERVER_CONNECTION,
FUNCTION_NAME_SEBSERVER_CONNECTION_DELETE);
FUNCTION_NAME_SEBSERVER_CONNECTION_DELETE,
FUNCTION_NAME_SET_EXAM_DATA);
} catch (final RuntimeException e) {
return LmsSetupTestResult.ofQuizAccessAPIError(LmsSetup.LmsType.MOODLE_PLUGIN, e.getMessage());
@ -112,6 +121,70 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI {
});
}
@Override
public Result<ExamData> applyExamData(final ExamData examData) {
return Result.tryCatch(() -> {
// validation
if (StringUtils.isBlank( examData.id)) {
throw new APIMessage.FieldValidationException("ExamData:id", "id is mandatory");
}
if (StringUtils.isBlank( examData.course_id)) {
throw new APIMessage.FieldValidationException("ExamData:course_id", "course_id is mandatory");
}
if (StringUtils.isBlank( examData.quiz_id)) {
throw new APIMessage.FieldValidationException("ExamData:quiz_id", "quiz_id is mandatory");
}
final LmsSetup lmsSetup = this.restTemplateFactory.getApiTemplateDataSupplier().getLmsSetup();
final String jsonPayload = jsonMapper.writeValueAsString(examData);
final MoodleAPIRestTemplate rest = getRestTemplate().getOrThrow();
final String response = rest.postToMoodleAPIFunction(FUNCTION_NAME_SET_EXAM_DATA, jsonPayload);
if (response != null && (response.startsWith("{\"exception\":") || response.startsWith("0"))) {
log.warn("Failed to apply Exam data to moodle: {}", examData);
}
return examData;
});
}
@Override
public Result<Exam> applyConnectionConfiguration(final Exam exam, final byte[] configData) {
return Result.tryCatch(() -> {
final String quizId = MoodleUtils.getQuizId(exam.externalId);
final String fileName = getConnectionConfigFileName(exam);
final MultiValueMap<String, Object> multiPartAttributes = new LinkedMultiValueMap<>();
multiPartAttributes.add("quizid", quizId);
multiPartAttributes.add("name", fileName);
multiPartAttributes.add("filename", fileName);
final ByteArrayResource contentsAsResource = new ByteArrayResource(configData) {
@Override
public String getFilename() {
return fileName; // Filename has to be returned in order to be able to post.
}
};
multiPartAttributes.add("file", contentsAsResource);
final MoodleAPIRestTemplate rest = getRestTemplate().getOrThrow();
final String response = rest.uploadMultiPart(UPLOAD_ENDPOINT, multiPartAttributes);
if (response != null && (response.startsWith("{\"exception\":") || response.startsWith("0"))) {
log.warn("Failed to apply Connection Configuration to LMS for Exam: {}", exam.externalId);
}
return exam;
});
}
private String getConnectionConfigFileName(final Exam exam) {
return "SEBServerConnectionConfiguration-" + exam.id + ".seb";
}
@Override
public Result<String> deleteConnectionDetails() {
return Result.tryCatch(() -> {

View file

@ -19,6 +19,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService.IntegrationData;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
@ -418,6 +419,16 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
return Result.ofRuntimeError("Not Supported");
}
@Override
public Result<FullLmsIntegrationService.ExamData> applyExamData(final FullLmsIntegrationService.ExamData examData) {
return Result.ofRuntimeError("Not Supported");
}
@Override
public Result<Exam> applyConnectionConfiguration(final Exam exam, final byte[] configData) {
return Result.ofRuntimeError("Not Supported");
}
@Override
public Result<String> deleteConnectionDetails() {
return Result.ofRuntimeError("Not Supported");

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2019 ETH Zürich, IT Services
*
* 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.sebconfig;
import org.springframework.context.ApplicationEvent;
public class ConnectionConfigurationChangeEvent extends ApplicationEvent {
public final Long institutionId;
public final Long configId;
public ConnectionConfigurationChangeEvent(final Long institutionId, final Long configId) {
super(configId);
this.institutionId = institutionId;
this.configId = configId;
}
}

View file

@ -20,9 +20,9 @@ import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.SEBClientConfig;
import ch.ethz.seb.sebserver.gbl.util.Result;
public interface ClientConfigService {
public interface ConnectionConfigurationService {
Logger log = LoggerFactory.getLogger(ClientConfigService.class);
Logger log = LoggerFactory.getLogger(ConnectionConfigurationService.class);
/** The cache name of ClientDetails */
String EXAM_CLIENT_DETAILS_CACHE = "EXAM_CLIENT_DETAILS_CACHE";

View file

@ -18,7 +18,7 @@ import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SEBConfigEncryptionService.Strategy;
/** Interface for a SEB Configuration encryption and decryption strategy.
*
* <p>
* To support a new SEB Configuration encryption and decryption strategy use this interface
* to implement a concrete strategy for encryption and decryption of SEB configurations */
public interface SEBConfigCryptor {

View file

@ -57,7 +57,7 @@ import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.CertificateDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SEBConfigEncryptionContext;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SEBConfigEncryptionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ZipService;
@ -67,9 +67,9 @@ import ch.ethz.seb.sebserver.webservice.weblayer.oauth.WebserviceResourceConfigu
@Lazy
@Service
@WebServiceProfile
public class ClientConfigServiceImpl implements ClientConfigService {
public class ConnectionConfigurationServiceImpl implements ConnectionConfigurationService {
private static final Logger log = LoggerFactory.getLogger(ClientConfigServiceImpl.class);
private static final Logger log = LoggerFactory.getLogger(ConnectionConfigurationServiceImpl.class);
//@formatter:off
private static final String SEB_CLIENT_CONFIG_EXAM_PROP_NAME = "exam";
@ -171,7 +171,7 @@ public class ClientConfigServiceImpl implements ClientConfigService {
private final long defaultPingInterval;
private final int examAPITokenValiditySeconds;
protected ClientConfigServiceImpl(
protected ConnectionConfigurationServiceImpl(
final SEBClientConfigDAO sebClientConfigDAO,
final ClientCredentialService clientCredentialService,
final SEBConfigEncryptionService sebConfigEncryptionService,

View file

@ -33,6 +33,8 @@ public interface ScreenProctoringService extends SessionUpdateTask {
updateClientConnections();
}
boolean isScreenProctoringEnabled(Long examId);
/** This is testing the given ScreenProctoringSettings on integrity and if we can
* connect to the given SEB screen proctoring service.
*

View file

@ -31,7 +31,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.institution.SecurityKeyService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
@ -87,7 +87,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
private final SecurityKeyService securityKeyService;
private final SEBClientEventBatchService sebClientEventBatchService;
private final SEBClientInstructionService sebClientInstructionService;
private final ClientConfigService clientConfigService;
private final ConnectionConfigurationService connectionConfigurationService;
private final JSONMapper jsonMapper;
private final boolean isDistributedSetup;
@ -101,7 +101,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
final WebserviceInfo webserviceInfo,
final SEBClientEventBatchService sebClientEventBatchService,
final SEBClientInstructionService sebClientInstructionService,
final ClientConfigService clientConfigService,
final ConnectionConfigurationService connectionConfigurationService,
final JSONMapper jsonMapper) {
this.examSessionService = examSessionService;
@ -115,7 +115,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
this.isDistributedSetup = webserviceInfo.isDistributed();
this.sebClientEventBatchService = sebClientEventBatchService;
this.sebClientInstructionService = sebClientInstructionService;
this.clientConfigService = clientConfigService;
this.connectionConfigurationService = connectionConfigurationService;
this.jsonMapper = jsonMapper;
}
@ -644,7 +644,7 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
pout = new PipedOutputStream();
pin = new PipedInputStream(pout);
this.clientConfigService.exportSEBClientConfiguration(
this.connectionConfigurationService.exportSEBClientConfiguration(
pout,
modelId,
null);

View file

@ -71,6 +71,7 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
private final SEBClientInstructionService sebInstructionService;
private final ExamSessionCacheService examSessionCacheService;
private final WebserviceInfo webserviceInfo;
private final WebserviceInfo.ScreenProctoringServiceBundle screenProctoringServiceBundle;
public ScreenProctoringServiceImpl(
final Cryptor cryptor,
@ -94,6 +95,8 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
this.examSessionCacheService = examSessionCacheService;
this.proctoringSettingsDAO = proctoringSettingsDAO;
this.webserviceInfo = webserviceInfo;
this.screenProctoringServiceBundle = webserviceInfo.getScreenProctoringServiceBundle();
this.screenProctoringAPIBinding = new ScreenProctoringAPIBinding(
userDAO,
cryptor,
@ -104,6 +107,11 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
webserviceInfo);
}
@Override
public boolean isScreenProctoringEnabled(final Long examId) {
return this.proctoringSettingsDAO.isScreenProctoringEnabled(examId);
}
@Override
public Result<ScreenProctoringSettings> testSettings(final ScreenProctoringSettings screenProctoringSettings) {
return Result.tryCatch(() -> {
@ -488,7 +496,9 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
}
final SPSData spsData = this.screenProctoringAPIBinding.getSPSData(exam.id);
final String url = exam.additionalAttributes.get(ScreenProctoringSettings.ATTR_SPS_SERVICE_URL);
final String url = screenProctoringServiceBundle.bundled
? screenProctoringServiceBundle.serviceURL
: exam.additionalAttributes.get(ScreenProctoringSettings.ATTR_SPS_SERVICE_URL);
final Map<String, String> attributes = new HashMap<>();
attributes.put(SERVICE_TYPE, SERVICE_TYPE_NAME);

View file

@ -15,6 +15,7 @@ import java.util.stream.Collectors;
import javax.validation.Valid;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamImportService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamUtils;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException;
import org.apache.commons.lang3.StringUtils;
@ -81,11 +82,10 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
private static final Logger log = LoggerFactory.getLogger(ExamAdministrationController.class);
// TODO reduce dependencies here.
// Move SecurityKeyService, SEBRestrictionService RemoteProctoringRoomService into ExamAdminService
private final ExamDAO examDAO;
private final UserDAO userDAO;
private final ExamAdminService examAdminService;
private final ExamImportService examImportService;
private final RemoteProctoringRoomService remoteProctoringRoomService;
private final LmsAPIService lmsAPIService;
private final ExamSessionService examSessionService;
@ -103,6 +103,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
final LmsAPIService lmsAPIService,
final UserDAO userDAO,
final ExamAdminService examAdminService,
final ExamImportService examImportService,
final RemoteProctoringRoomService remoteProctoringRoomService,
final ExamSessionService examSessionService,
final SEBRestrictionService sebRestrictionService,
@ -119,6 +120,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
this.examDAO = examDAO;
this.userDAO = userDAO;
this.examAdminService = examAdminService;
this.examImportService = examImportService;
this.remoteProctoringRoomService = remoteProctoringRoomService;
this.lmsAPIService = lmsAPIService;
this.examSessionService = examSessionService;
@ -609,7 +611,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
@Override
protected Result<Exam> notifyCreated(final Exam entity) {
return examAdminService.applyExamImportInitialization(entity);
return examImportService.applyExamImportInitialization(entity);
}
@Override

View file

@ -20,6 +20,7 @@ import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
@ -39,9 +40,14 @@ public class LmsIntegrationController {
private static final Logger log = LoggerFactory.getLogger(LmsIntegrationController.class);
private final FullLmsIntegrationService fullLmsIntegrationService;
private final WebserviceInfo webserviceInfo;
public LmsIntegrationController(
final FullLmsIntegrationService fullLmsIntegrationService,
final WebserviceInfo webserviceInfo) {
public LmsIntegrationController(final FullLmsIntegrationService fullLmsIntegrationService) {
this.fullLmsIntegrationService = fullLmsIntegrationService;
this.webserviceInfo = webserviceInfo;
}
@RequestMapping(
@ -49,10 +55,10 @@ public class LmsIntegrationController {
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void createExam(
@RequestParam(name = API.LMS_FULL_INTEGRATION_LMS_UUID, required = true) final String lmsUUId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_COURSE_ID, required = true) final String courseId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIZ_ID, required = true) final String quizId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_EXAM_TEMPLATE_ID, required = true) final String templateId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_LMS_UUID) final String lmsUUId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_COURSE_ID) final String courseId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIZ_ID) final String quizId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_EXAM_TEMPLATE_ID) final String templateId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIT_PASSWORD, required = false) final String quitPassword,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIT_LINK, required = false) final String quitLink,
final HttpServletResponse response) {
@ -77,9 +83,9 @@ public class LmsIntegrationController {
method = RequestMethod.DELETE,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void deleteExam(
@RequestParam(name = API.LMS_FULL_INTEGRATION_LMS_UUID, required = true) final String lmsUUId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_COURSE_ID, required = true) final String courseId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIZ_ID, required = true) final String quizId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_LMS_UUID) final String lmsUUId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_COURSE_ID) final String courseId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIZ_ID) final String quizId,
final HttpServletResponse response) {
final EntityKey examID = fullLmsIntegrationService.deleteExam(lmsUUId, courseId, quizId)
@ -97,11 +103,13 @@ public class LmsIntegrationController {
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void getConnectionConfiguration(
@RequestParam(name = API.LMS_FULL_INTEGRATION_LMS_UUID, required = true) final String lmsUUId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_COURSE_ID, required = true) final String courseId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIZ_ID, required = true) final String quizId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_LMS_UUID) final String lmsUUId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_COURSE_ID) final String courseId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIZ_ID) final String quizId,
final HttpServletResponse response) throws IOException {
// TODO change this according to the outcome of discussion about Moodle Connection Configuration handling
final ServletOutputStream outputStream = response.getOutputStream();
final PipedOutputStream pout;
final PipedInputStream pin;
@ -137,17 +145,34 @@ public class LmsIntegrationController {
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public String getOneTimeLoginToken(
@RequestParam(name = API.LMS_FULL_INTEGRATION_LMS_UUID, required = true) final String lmsUUId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_COURSE_ID, required = true) final String courseId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIZ_ID, required = true) final String quizId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_USER_ID, required = true) final String userId,
public FullLmsIntegrationService.TokenLoginResponse getOneTimeLoginToken(
@RequestParam(name = API.LMS_FULL_INTEGRATION_LMS_UUID) final String lmsUUId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_COURSE_ID) final String courseId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIZ_ID) final String quizId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_USER_ID) final String userId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_USER_NAME, required = false) final String username,
@RequestParam(name = API.LMS_FULL_INTEGRATION_USER_EMAIL, required = false) final String userMail,
@RequestParam(name = API.LMS_FULL_INTEGRATION_USER_FIRST_NAME, required = false) final String firstName,
@RequestParam(name = API.LMS_FULL_INTEGRATION_USER_LAST_NAME, required = false) final String lastName,
@RequestParam(name = API.LMS_FULL_INTEGRATION_TIME_ZONE, required = false) final String timezone,
final HttpServletResponse response) throws IOException {
final HttpServletResponse response) {
return this.fullLmsIntegrationService
.getOneTimeLoginToken(lmsUUId, courseId, quizId, userId, username, timezone)
final FullLmsIntegrationService.AdHocAccountData adHocAccountData = new FullLmsIntegrationService.AdHocAccountData(
userId,
username,
userMail,
firstName,
lastName,
timezone
);
final String token = this.fullLmsIntegrationService
.getOneTimeLoginToken(lmsUUId, courseId, quizId, adHocAccountData)
.onError(error -> log.error("Failed to create ad-hoc account with one time login token: ", error))
.getOrThrow();
return new FullLmsIntegrationService.TokenLoginResponse(
lmsUUId,
webserviceInfo.getExternalServerURL() + API.LMS_FULL_INTEGRATION_LOGIN_TOKEN_ENDPOINT + "?jwt=" + token);
}
}

View file

@ -52,7 +52,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
@WebServiceProfile
@ -61,7 +61,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe
@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.SEB_CLIENT_CONFIG_ENDPOINT)
public class SEBClientConfigController extends ActivatableEntityController<SEBClientConfig, SEBClientConfig> {
private final ClientConfigService sebClientConfigService;
private final ConnectionConfigurationService sebConnectionConfigurationService;
public SEBClientConfigController(
final SEBClientConfigDAO sebClientConfigDAO,
@ -70,7 +70,7 @@ public class SEBClientConfigController extends ActivatableEntityController<SEBCl
final BulkActionService bulkActionService,
final PaginationService paginationService,
final BeanValidationService beanValidationService,
final ClientConfigService sebClientConfigService) {
final ConnectionConfigurationService sebConnectionConfigurationService) {
super(authorization,
bulkActionService,
@ -79,7 +79,7 @@ public class SEBClientConfigController extends ActivatableEntityController<SEBCl
paginationService,
beanValidationService);
this.sebClientConfigService = sebClientConfigService;
this.sebConnectionConfigurationService = sebConnectionConfigurationService;
}
@RequestMapping(
@ -127,7 +127,7 @@ public class SEBClientConfigController extends ActivatableEntityController<SEBCl
pout = new PipedOutputStream();
pin = new PipedInputStream(pout);
this.sebClientConfigService.exportSEBClientConfiguration(
this.sebConnectionConfigurationService.exportSEBClientConfiguration(
pout,
modelId,
examId);
@ -182,7 +182,7 @@ public class SEBClientConfigController extends ActivatableEntityController<SEBCl
protected Result<SEBClientConfig> notifySaved(final SEBClientConfig entity) {
if (entity.isActive()) {
// try to get access token for SEB client
this.sebClientConfigService.initialCheckAccess(entity);
this.sebConnectionConfigurationService.initialCheckAccess(entity);
}
return super.notifySaved(entity);
}

View file

@ -18,7 +18,7 @@ import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationService;
/** A ClientDetailsService to manage different API clients of SEB Server webservice API.
* <p>
@ -32,17 +32,17 @@ public class WebClientDetailsService implements ClientDetailsService {
private static final Logger log = LoggerFactory.getLogger(WebClientDetailsService.class);
private final ClientConfigService sebClientConfigService;
private final ConnectionConfigurationService sebConnectionConfigurationService;
private final AdminAPIClientDetails adminClientDetails;
private final LmsAPIClientDetails lmsAPIClientDetails;
public WebClientDetailsService(
final AdminAPIClientDetails adminClientDetails,
final ClientConfigService sebClientConfigService,
final ConnectionConfigurationService sebConnectionConfigurationService,
final LmsAPIClientDetails lmsAPIClientDetails) {
this.adminClientDetails = adminClientDetails;
this.sebClientConfigService = sebClientConfigService;
this.sebConnectionConfigurationService = sebConnectionConfigurationService;
this.lmsAPIClientDetails = lmsAPIClientDetails;
}
@ -81,7 +81,7 @@ public class WebClientDetailsService implements ClientDetailsService {
}
protected Result<ClientDetails> getForExamClientAPI(final String clientId) {
return this.sebClientConfigService.getClientConfigDetails(clientId);
return this.sebConnectionConfigurationService.getClientConfigDetails(clientId);
}
}

View file

@ -53,7 +53,7 @@ import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.WebserviceInfoDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationService;
import ch.ethz.seb.sebserver.webservice.weblayer.oauth.AdminAPIClientDetails;
import ch.ethz.seb.sebserver.webservice.weblayer.oauth.WebserviceResourceConfiguration;
@ -313,7 +313,7 @@ public abstract class ExamAPIIntegrationTester {
@Autowired
AdminAPIClientDetails adminClientDetails;
@Autowired
ClientConfigService sebClientConfigService;
ConnectionConfigurationService sebConnectionConfigurationService;
@Autowired
@Qualifier(WebSecurityConfig.CLIENT_PASSWORD_ENCODER_BEAN_NAME)
private PasswordEncoder clientPasswordEncoder;

View file

@ -195,6 +195,13 @@ public class MoodleMockupRestTemplateFactory implements MoodleRestTemplateFactor
}
}
@Override
public String uploadMultiPart(
final String uploadEndpoint,
final MultiValueMap<String, Object> multiPartAttributes) {
throw new UnsupportedOperationException("Not supported yet");
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();