diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index 8841ff5f..f6e0f993 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -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"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java index 37d5249e..25955d2f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java @@ -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 additionalAttributes = new HashMap<>(quizData.getAdditionalAttributes()); + String additionalQuizData = null; + try { + additionalQuizData = new JSONMapper().writeValueAsString(quizData.getAdditionalAttributes()); + } catch (final Exception ignored) {} + + final Map 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); diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ScreenProctoringSettings.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ScreenProctoringSettings.java index d14a466f..c270e597 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ScreenProctoringSettings.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ScreenProctoringSettings.java @@ -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) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/TeacherAccountService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/TeacherAccountService.java index b1ff53c1..7b167a36 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/TeacherAccountService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/TeacherAccountService.java @@ -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 createNewTeacherAccountForExam( Exam exam, - String userId, - String username, - String timezone); + final FullLmsIntegrationService.AdHocAccountData adHocAccountData); - Result deleteTeacherAccountsForExam(final Exam exam); + Result deactivateTeacherAccountsForExam(Exam exam); Result getOneTimeTokenForTeacherAccount( Exam exam, - String userId, - String username, - String timezone, - final boolean createIfNotExists); + FullLmsIntegrationService.AdHocAccountData adHocAccountData, + boolean createIfNotExists); Result verifyOneTimeTokenForTeacherAccount(String token); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/TeacherAccountServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/TeacherAccountServiceImpl.java index 5f3e680e..23de0e2e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/TeacherAccountServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/TeacherAccountServiceImpl.java @@ -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 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 deleteTeacherAccountsForExam(final Exam exam) { + public Result 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 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 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; } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/DeleteExamAction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/DeleteExamAction.java index 8fc840b4..e98db5f6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/DeleteExamAction.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/impl/DeleteExamAction.java @@ -100,7 +100,7 @@ public class DeleteExamAction implements BatchActionExec { } @Transactional - public Result deleteExamFromLMSIntegration(final Exam exam) { + public Result deleteExamInternal(final Exam exam) { return deleteExamDependencies(exam) .flatMap(this::deleteExamWithRefs) .map(Exam::getEntityKey) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java index 1877150b..b87503c3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java @@ -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, BulkActionSup * @return Result refer to all exams for LMS update or to an error when happened */ Result> allForLMSUpdate(); + Result> allActiveForLMSSetup(Collection 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, BulkActionSup void updateQuitPassword(Exam exam, String quitPassword); - } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java index e94391d9..927c1ead 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ExamDAOImpl.java @@ -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> allActiveForLMSSetup(final Collection 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> 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 additionalAttributes = new HashMap<>(quizData.getAdditionalAttributes()); + String additionalQuizData = null; + try { + additionalQuizData = jsonMapper.writeValueAsString(quizData.getAdditionalAttributes()); + } catch (final Exception ignored) {} + + final Map 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 { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ProctoringSettingsDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ProctoringSettingsDAOImpl.java index 4c8e6a4d..87d05d11 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ProctoringSettingsDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ProctoringSettingsDAOImpl.java @@ -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; }); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/SEBClientConfigDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/SEBClientConfigDAOImpl.java index 376c6787..f9f2b22d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/SEBClientConfigDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/SEBClientConfigDAOImpl.java @@ -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) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java index d2408ed8..86fa0d4c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java @@ -18,20 +18,12 @@ public interface ExamAdminService { ProctoringAdminService getProctoringAdminService(); - Result 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 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 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 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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamImportService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamImportService.java new file mode 100644 index 00000000..f16e4b16 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamImportService.java @@ -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 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 initAdditionalAttributes(final Exam exam); +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java index 3a79c76f..fc2cd92e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java @@ -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 applyExamImportInitialization(final Exam exam) { - final List 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 examForPK(final Long examId) { return this.examDAO.byPK(examId); } - @Override - public Result 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 saveSecurityKeySettings( final Long institutionId, @@ -207,44 +114,6 @@ public class ExamAdminServiceImpl implements ExamAdminService { }).flatMap(v -> this.examDAO.byPK(examId)); } - @Override - public Result 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 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 isRestricted(final Exam exam) { if (exam == null || exam.lmsSetupId == null) { @@ -283,7 +152,8 @@ public class ExamAdminServiceImpl implements ExamAdminService { @Override public Result 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 applyQuitPassword(final Exam exam) { + public Result 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 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 archiveExam(final Exam exam) { return Result.tryCatch(() -> { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamImportServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamImportServiceImpl.java new file mode 100644 index 00000000..e49b8916 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamImportServiceImpl.java @@ -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 applyExamImportInitialization(final Exam exam) { + final List 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 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 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 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 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; + }); + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ProctoringAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ProctoringAdminServiceImpl.java index 053be789..e3850635 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ProctoringAdminServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ProctoringAdminServiceImpl.java @@ -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; }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/CourseAccessAPI.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/CourseAccessAPI.java index 85a398f9..713c10c4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/CourseAccessAPI.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/CourseAccessAPI.java @@ -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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationAPI.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationAPI.java index 3ce9ec63..7e16f245 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationAPI.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationAPI.java @@ -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 applyConnectionDetails(IntegrationData data); + Result applyExamData(ExamData examData); + + Result applyConnectionConfiguration(Exam exam, byte[] configData); + Result deleteConnectionDetails(); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationService.java index e2d1592a..da2d7d30 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationService.java @@ -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 applyExamDataToLMS(Exam exam); Result applyFullLmsIntegration(Long lmsSetupId); @@ -48,8 +61,7 @@ public interface FullLmsIntegrationService { String courseId, String quizId); - @EventListener(ExamDeletionEvent.class) - void notifyExamDeletion(ExamDeletionEvent event); + Result 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; + } } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java index daae1fe0..95fa0368 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java @@ -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; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java index 0e687436..4f4b1b18 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java @@ -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 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 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 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 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 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 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; } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java index ee26a13f..96f3b69b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java @@ -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 accountDetailRequest; private final CircuitBreaker restrictionRequest; - private final CircuitBreaker releaseRestrictionRequest; + private final CircuitBreaker examRequest; private final CircuitBreaker lmsAccessRequest; + private final CircuitBreaker applyExamDataRequest; private final CircuitBreaker 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 protectedRun = this.releaseRestrictionRequest.protectedRun(() -> this.sebRestrictionAPI + final Result 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 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 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 deleteConnectionDetails() { if (this.lmsIntegrationAPI == null) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java index 8b86bc02..8c4b5880 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java @@ -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 applyExamData(final FullLmsIntegrationService.ExamData examData) { + return Result.ofRuntimeError("Not Supported"); + } + + @Override + public Result applyConnectionConfiguration(final Exam exam, final byte[] configData) { + return Result.ofRuntimeError("Not Supported"); + } + @Override public Result deleteConnectionDetails() { return Result.ofRuntimeError("Not Supported"); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupFullIntegration.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupFullIntegration.java index a04f4c79..a5105c15 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupFullIntegration.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupFullIntegration.java @@ -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 applyExamData(final FullLmsIntegrationService.ExamData examData) { + return Result.ofRuntimeError("Not Supported"); + } + + @Override + public Result applyConnectionConfiguration(final Exam exam, final byte[] configData) { + return Result.ofRuntimeError("Not Supported"); + } + @Override public Result deleteConnectionDetails() { return Result.ofRuntimeError("TODO"); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleAPIRestTemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleAPIRestTemplate.java index ae5b184b..46d7ae6d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleAPIRestTemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleAPIRestTemplate.java @@ -40,13 +40,18 @@ public interface MoodleAPIRestTemplate { String postToMoodleAPIFunction(String functionName, String body); String callMoodleAPIFunction( - final String functionName, - final MultiValueMap queryAttributes); + String functionName, + MultiValueMap queryAttributes); String callMoodleAPIFunction( - final String functionName, - final MultiValueMap queryParams, - final MultiValueMap queryAttributes); + String functionName, + MultiValueMap queryParams, + MultiValueMap queryAttributes); + + String uploadMultiPart( + String uploadEndpoint, + MultiValueMap multiPartAttributes); + /** This maps a Moodle warning JSON object */ @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactoryImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactoryImpl.java index 0b5c4e31..2c059db1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactoryImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactoryImpl.java @@ -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 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 httpEntity = new HttpEntity<>(body, headers); + final HttpEntity 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 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 functions; @JsonCreator - protected WebserviceInfo( + private WebserviceInfo( @JsonProperty(value = "username") final String username, @JsonProperty(value = "userid") final String userid, @JsonProperty(value = "functions") final Collection 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) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java index 1046d7a5..370e95a1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java @@ -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. - * + *

* See also: https://docs.moodle.org/dev/Web_service_API_functions - * + *

* 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(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java index 005e0ee0..d4656f7e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java @@ -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(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginFullIntegration.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginFullIntegration.java index 8478d896..f153f4ce 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginFullIntegration.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginFullIntegration.java @@ -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 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 applyConnectionConfiguration(final Exam exam, final byte[] configData) { + return Result.tryCatch(() -> { + + final String quizId = MoodleUtils.getQuizId(exam.externalId); + final String fileName = getConnectionConfigFileName(exam); + + + final MultiValueMap 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 deleteConnectionDetails() { return Result.tryCatch(() -> { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java index c7d1ee4d..317daa03 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java @@ -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 applyExamData(final FullLmsIntegrationService.ExamData examData) { + return Result.ofRuntimeError("Not Supported"); + } + + @Override + public Result applyConnectionConfiguration(final Exam exam, final byte[] configData) { + return Result.ofRuntimeError("Not Supported"); + } + @Override public Result deleteConnectionDetails() { return Result.ofRuntimeError("Not Supported"); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/ConnectionConfigurationChangeEvent.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/ConnectionConfigurationChangeEvent.java new file mode 100644 index 00000000..fb4288cf --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/ConnectionConfigurationChangeEvent.java @@ -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; + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/ClientConfigService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/ConnectionConfigurationService.java similarity index 95% rename from src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/ClientConfigService.java rename to src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/ConnectionConfigurationService.java index 8ee69163..d3582aed 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/ClientConfigService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/ConnectionConfigurationService.java @@ -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"; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SEBConfigCryptor.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SEBConfigCryptor.java index d538eecc..a3507bb7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SEBConfigCryptor.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SEBConfigCryptor.java @@ -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. - * + *

* 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 { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ClientConfigServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ConnectionConfigurationServiceImpl.java similarity index 98% rename from src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ClientConfigServiceImpl.java rename to src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ConnectionConfigurationServiceImpl.java index 2a7ed071..f544e810 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ClientConfigServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ConnectionConfigurationServiceImpl.java @@ -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, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ScreenProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ScreenProctoringService.java index dfde2a8a..b514bc3d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ScreenProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ScreenProctoringService.java @@ -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. * diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java index 190fd06a..a19e6ee8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/SEBClientConnectionServiceImpl.java @@ -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); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringServiceImpl.java index 6163b5e6..b5b0dbcb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringServiceImpl.java @@ -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 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 attributes = new HashMap<>(); attributes.put(SERVICE_TYPE, SERVICE_TYPE_NAME); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index 0a667869..8ac4e9cb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -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 { 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 { 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 { 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 { @Override protected Result notifyCreated(final Exam entity) { - return examAdminService.applyExamImportInitialization(entity); + return examImportService.applyExamImportInitialization(entity); } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsIntegrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsIntegrationController.java index 6a3950f6..d37ce358 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsIntegrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsIntegrationController.java @@ -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); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SEBClientConfigController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SEBClientConfigController.java index 85c8723d..16966ee4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SEBClientConfigController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SEBClientConfigController.java @@ -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 { - private final ClientConfigService sebClientConfigService; + private final ConnectionConfigurationService sebConnectionConfigurationService; public SEBClientConfigController( final SEBClientConfigDAO sebClientConfigDAO, @@ -70,7 +70,7 @@ public class SEBClientConfigController extends ActivatableEntityController 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); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java index 1da9a942..11bfc78b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java @@ -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. *

@@ -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 getForExamClientAPI(final String clientId) { - return this.sebClientConfigService.getClientConfigDetails(clientId); + return this.sebConnectionConfigurationService.getClientConfigDetails(clientId); } } diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java index ca57c791..c93f91eb 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java @@ -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; diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleMockupRestTemplateFactory.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleMockupRestTemplateFactory.java index 0df6fe1b..6e29d5eb 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleMockupRestTemplateFactory.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleMockupRestTemplateFactory.java @@ -195,6 +195,13 @@ public class MoodleMockupRestTemplateFactory implements MoodleRestTemplateFactor } } + @Override + public String uploadMultiPart( + final String uploadEndpoint, + final MultiValueMap multiPartAttributes) { + throw new UnsupportedOperationException("Not supported yet"); + } + @Override public String toString() { final StringBuilder builder = new StringBuilder();