From c161e3c5efee97bd48787a4cd1ecdb7cc6b178f5 Mon Sep 17 00:00:00 2001 From: anhefti Date: Wed, 12 Jun 2024 15:36:35 +0200 Subject: [PATCH] SEBSERV-417 and SEBSP-111 --- .../authorization/UserService.java | 2 + .../authorization/impl/UserServiceImpl.java | 9 +- .../servicelayer/dao/LmsSetupDAO.java | 5 + .../dao/impl/LmsSetupDAOImpl.java | 15 + .../lms/FullLmsIntegrationService.java | 2 +- .../lms/SEBRestrictionService.java | 2 +- .../impl/FullLmsIntegrationServiceImpl.java | 25 +- .../lms/impl/SEBRestrictionServiceImpl.java | 12 +- .../session/ScreenProctoringService.java | 6 + .../ScreenProctoringAPIBinding.java | 400 +++++++++++++----- .../ScreenProctoringServiceImpl.java | 50 ++- .../api/ExamAdministrationController.java | 1 + .../weblayer/api/LmsSetupController.java | 53 ++- 13 files changed, 414 insertions(+), 168 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/UserService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/UserService.java index d87ae358..6cb5d651 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/UserService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/UserService.java @@ -20,6 +20,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServe public interface UserService { String USERS_INSTITUTION_AS_DEFAULT = "USERS_INSTITUTION_AS_DEFAULT"; + String LMS_INTEGRATION_CLIENT_UUID = "LMS_INTEGRATION_CLIENT"; + String LMS_INTEGRATION_CLIENT_NAME = "lmsIntegrationClient"; /** Use this to get the current User within a request-response thread cycle. * diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/UserServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/UserServiceImpl.java index 29f95ca9..c7aafda2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/UserServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/UserServiceImpl.java @@ -38,6 +38,7 @@ public class UserServiceImpl implements UserService { private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class); + public interface ExtractUserFromAuthenticationStrategy { SEBServerUser extract(Principal principal); } @@ -137,7 +138,13 @@ public class UserServiceImpl implements UserService { if ("lmsClient".equals(name)) { return new SEBServerUser( -1L, - new UserInfo("LMS_INTEGRATION_CLIENT", -1L, null, "lmsIntegrationClient", "lmsIntegrationClient", "lmsIntegrationClient", null, + new UserInfo( + LMS_INTEGRATION_CLIENT_UUID, + -1L, + null, + LMS_INTEGRATION_CLIENT_NAME, + LMS_INTEGRATION_CLIENT_NAME, + LMS_INTEGRATION_CLIENT_NAME, null, false, false, true, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java index d57eb032..d971b04c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java @@ -52,7 +52,12 @@ public interface LmsSetupDAO extends ActivatableEntityDAO, B * @return Result refers to the specified LMS Setup or to en error when happened */ Result setIntegrationActive(Long lmsSetupId, boolean active); + boolean isIntegrationActive(Long lmsSetupId); + Result> idsOfActiveWithFullIntegration(Long institutionId); Result> allIdsFullIntegration(); + + + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java index 1e67f28f..63bdd221 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java @@ -317,6 +317,21 @@ public class LmsSetupDAOImpl implements LmsSetupDAO { .onError(TransactionHandler::rollback); } + @Override + @Transactional(readOnly = true) + public boolean isIntegrationActive(final Long lmsSetupId) { + try { + return this.lmsSetupRecordMapper.countByExample() + .where(LmsSetupRecordDynamicSqlSupport.id, isEqualTo(lmsSetupId)) + .and(integrationActive, isEqualTo(BooleanUtils.toInteger(true))) + .build() + .execute() > 0; + } catch (final Exception e) { + log.warn("Failed to verify if full LMS integration is active: {}", e.getMessage()); + return false; + } + } + @Override @Transactional(readOnly = true) public boolean isActive(final String modelId) { 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 519c5a14..065bdbfb 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 @@ -32,7 +32,7 @@ public interface FullLmsIntegrationService { @EventListener void notifyLmsSetupChange(final LmsSetupChangeEvent event); - Result applyLMSSetupDeactivation(LmsSetup lmsSetup); + //Result applyLMSSetupDeactivation(LmsSetup lmsSetup); @EventListener void notifyExamTemplateChange(final ExamTemplateChangeEvent event); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java index 656dc290..4521a286 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/SEBRestrictionService.java @@ -73,6 +73,6 @@ public interface SEBRestrictionService { @EventListener void notifyLmsSetupChange(final LmsSetupChangeEvent event); - Result applyLMSSetupDeactivation(LmsSetup lmsSetup); + Result releaseAllRestrictionsOf(LmsSetup lmsSetup); } 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 e2d3caf4..aaf24ce6 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 @@ -148,7 +148,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService return Result.tryCatch(() -> { final LmsSetup lmsSetup = lmsSetupDAO.byPK(exam.lmsSetupId).getOrThrow(); if (lmsSetup.lmsType.features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) { - return this.applyExamData(exam, false); + return this.applyExamData(exam, !exam.active); } return exam; @@ -183,17 +183,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService .map(data -> reapplyExistingExams(data,lmsSetup)) .onError(error -> log.warn("Failed to update LMS integration for: {} error {}", lmsSetup, error.getMessage())) .onSuccess(data -> log.debug("Successfully updated LMS integration for: {} data: {}", lmsSetup, data)); - } - } - - @Override - public Result applyLMSSetupDeactivation(final LmsSetup lmsSetup) { - if (!lmsSetup.getLmsType().features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) { - return Result.of(lmsSetup); - } - - return Result.tryCatch(() -> { - + } else if (event.activation == Activatable.ActivationAction.DEACTIVATE) { // remove all active exam data for involved exams before deactivate them this.examDAO .allActiveForLMSSetup(Arrays.asList(lmsSetup.id)) @@ -203,13 +193,10 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService .map(e -> applyExamData(e, true)) .onError(error -> log.warn("Failed delete teacher accounts for exam: {}", exam.name)); }); - - // delete full integration on Moodle side before deactivate LMS Setup + // delete full integration on Moodle side due to deactivation this.deleteFullLmsIntegration(lmsSetup.id) .getOrThrow(); - - return lmsSetup; - }); + } } @Override @@ -302,6 +289,10 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService return false; } + if (!lmsSetupDAO.isIntegrationActive(lmsSetupId)) { + return true; + } + lmsAPITemplateCacheService.getLmsAPITemplate(lmsSetupId) .getOrThrow() .deleteConnectionDetails() diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java index aed220ae..8bea0a64 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/SEBRestrictionServiceImpl.java @@ -114,6 +114,7 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { if (!lmsSetup.lmsType.features.contains(Features.SEB_RESTRICTION)) { return; } + try { if (event.activation == Activatable.ActivationAction.ACTIVATE) { examDAO.allActiveForLMSSetup(Arrays.asList(lmsSetup.id)) @@ -125,6 +126,11 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { log.warn("Failed to update SEB restriction for exam: {} error: {}", exam.name, e.getMessage()); } }); + } else if (event.activation == Activatable.ActivationAction.DEACTIVATE) { + releaseAllRestrictionsOf(lmsSetup) + .onError(error -> log.warn( + "Failed to remove all SEB Restrictions on LMS Setup deactivation: {}", + error.getMessage())); } } catch (final Exception e) { log.error("Failed to update SEB restriction for re-activated exams: {}", e.getMessage()); @@ -132,8 +138,7 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { } @Override - public Result applyLMSSetupDeactivation(final LmsSetup lmsSetup) { - + public Result releaseAllRestrictionsOf(final LmsSetup lmsSetup) { return Result.tryCatch(() -> { // only relevant for LMS Setups with SEB restriction feature if (!lmsSetup.lmsType.features.contains(Features.SEB_RESTRICTION)) { @@ -274,7 +279,6 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { // create new ones if needed sebRestriction.additionalProperties .entrySet() - .stream() .forEach(entry -> this.additionalAttributesDAO.saveAdditionalAttribute( EntityType.EXAM, exam.id, @@ -332,7 +336,7 @@ public class SEBRestrictionServiceImpl implements SEBRestrictionService { log.debug("ExamDeletionEvent received, process releaseSEBClientRestriction..."); } - event.ids.stream().forEach(this::processExamDeletion); + event.ids.forEach(this::processExamDeletion); } private Result processExamDeletion(final Long examId) { 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 b514bc3d..5cc3b049 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 @@ -12,6 +12,8 @@ import java.util.Collection; import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig; import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsSetupChangeEvent; import org.springframework.context.event.EventListener; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; @@ -76,6 +78,9 @@ public interface ScreenProctoringService extends SessionUpdateTask { @EventListener(ExamDeletionEvent.class) void notifyExamDeletion(final ExamDeletionEvent event); + @EventListener + void notifyLmsSetupChange(final LmsSetupChangeEvent event); + /** This is used to update the exam equivalent on the screen proctoring service side * if screen proctoring is enabled for the specified exam. * @@ -93,6 +98,7 @@ public interface ScreenProctoringService extends SessionUpdateTask { @Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) void synchronizeSPSUser(final String userUUID); + @Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) void synchronizeSPSUserForExam(final Long examId); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringAPIBinding.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringAPIBinding.java index 714b8bdb..62d9c741 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringAPIBinding.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringAPIBinding.java @@ -10,9 +10,11 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring; import java.util.*; +import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.model.exam.SPSAPIAccessData; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.webservice.WebserviceInfo; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -111,6 +113,7 @@ class ScreenProctoringAPIBinding { /** The screen proctoring service group API attribute names */ interface EXAM { + String ATTR_ID = "id"; String ATTR_UUID = "uuid"; String ATTR_SEB_SERVER_ID = "sebserverId"; String ATTR_NAME = "name"; @@ -269,7 +272,7 @@ class ScreenProctoringAPIBinding { SPSData.class); } catch (final Exception e) { - log.error("Failed to get local SPSData for exam: {}", examId); + log.warn("Failed to get local SPSData for exam: {}", examId); return null; } } @@ -290,7 +293,9 @@ class ScreenProctoringAPIBinding { } final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(exam.id); - + + // if we have an exam where SPS was initialized before but deactivated meanwhile + // reactivate on SPS site and synchronize if (exam.additionalAttributes.containsKey(SPSData.ATTR_SPS_ACTIVE)) { log.info("SPS Exam for SEB Server Exam: {} already exists. Try to re-activate", exam.externalId); @@ -298,51 +303,55 @@ class ScreenProctoringAPIBinding { final SPSData spsData = this.getSPSData(exam.id); // re-activate all needed entities on SPS side if (exam.status == Exam.ExamStatus.RUNNING) { - activation(exam, SPS_API.SEB_ACCESS_ENDPOINT, spsData.spsSEBAccessUUID, true, apiTemplate); - activation(exam, SPS_API.EXAM_ENDPOINT, spsData.spsExamUUID, true, apiTemplate); + activateScreenProctoring(exam).getOrThrow(); } + synchronizeUserAccounts(exam); - - // mark successfully activated on SPS side - this.additionalAttributesDAO.saveAdditionalAttribute( - EntityType.EXAM, - exam.id, - SPSData.ATTR_SPS_ACTIVE, - Constants.TRUE_STRING); - return Collections.emptyList(); } - final SPSData spsData = new SPSData(); - log.info( - "SPS Exam for SEB Server Exam: {} don't exists yet, create necessary structures on SPS", - exam.externalId); + // if we have a new Exam but Exam on SPS site for ExamUUID exists, reinitialize the exam and synchronize + if (existsExamOnSPS(exam)) { + return reinitializeScreenProctoring(exam); + } - synchronizeUserAccounts(exam); - createSEBAccess(exam, apiTemplate, spsData); - createExam(exam, apiTemplate, spsData); - final Collection initializeGroups = initializeGroups(exam, apiTemplate, spsData); - - // store encrypted spsData - final String spsDataJSON = this.jsonMapper.writeValueAsString(spsData); - this.additionalAttributesDAO.saveAdditionalAttribute( - EntityType.EXAM, - exam.id, - SPSData.ATTR_SPS_ACCESS_DATA, - this.cryptor.encrypt(spsDataJSON).getOrThrow().toString()); - - // mark successfully activated on SPS side - this.additionalAttributesDAO.saveAdditionalAttribute( - EntityType.EXAM, - exam.id, - SPSData.ATTR_SPS_ACTIVE, - Constants.TRUE_STRING); - - return initializeGroups; + // If this is a completely new exam with new SPS binding, initialize it + return initializeScreenProctoring(exam, apiTemplate); }); } + boolean existsExamOnSPS(final Exam exam) { + try { + + final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(exam.id); + final String uri = UriComponentsBuilder + .fromUriString(apiTemplate.spsAPIAccessData.getSpsServiceURL()) + .path(SPS_API.EXAM_ENDPOINT) + .pathSegment(createExamUUID(exam)) + .build().toUriString(); + + final ResponseEntity exchange = apiTemplate.exchange(uri, HttpMethod.GET); + + if (exchange.getStatusCode() != HttpStatus.NOT_FOUND) { + return false; + } else if (exchange.getStatusCode() != HttpStatus.OK) { + return true; + } else { + log.warn("Failed to verify if Exam on SPS already exists: {}", exchange.getBody()); + return false; + } + + } catch (final Exception e) { + log.error("Failed to verify if Exam exists already on SPS site: ", e); + return false; + } + } + void synchronizeUserAccount(final String userUUID) { + if (UserService.LMS_INTEGRATION_CLIENT_UUID.equals(userUUID)) { + return; + } + try { final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(null); @@ -446,22 +455,21 @@ class ScreenProctoringAPIBinding { log.error("Failed to update SPS exam data: {}", exchange); } - } catch (Exception e) { + } catch (final Exception e) { log.error("Failed to update exam on SPS service for exam: {}", exam, e); } } - /** This is called when an exam finishes and deactivates the Exam, SEB Client Access and the ad-hoc User-Account - * on Screen Proctoring Service side. + /** This is called when an exam finishes and deactivates the Exam, SEB Client Access on Screen Proctoring Service side. * * @param exam The exam * @return Result refer to the exam or to an error when happened */ - Result disposeScreenProctoring(final Exam exam) { + Result deactivateScreenProctoring(final Exam exam) { return Result.tryCatch(() -> { if (log.isDebugEnabled()) { - log.debug("Dispose active screen proctoring exam, groups and access on SPS for exam: {}", exam); + log.debug("Deactivate active screen proctoring exam, groups and access on SPS for exam: {}", exam.name); } final SPSData spsData = this.getSPSData(exam.id); @@ -469,7 +477,7 @@ class ScreenProctoringAPIBinding { activation(exam, SPS_API.SEB_ACCESS_ENDPOINT, spsData.spsSEBAccessUUID, false, apiTemplate); activation(exam, SPS_API.EXAM_ENDPOINT, spsData.spsExamUUID, false, apiTemplate); - // mark successfully dispose on SPS side + // mark local for successfully dispose on SPS side this.additionalAttributesDAO.saveAdditionalAttribute( EntityType.EXAM, exam.id, @@ -480,56 +488,197 @@ class ScreenProctoringAPIBinding { }); } - /** This is called on exam delete and deletes the SEB Client Access and the ad-hoc User-Account - * on Screen Proctoring Service side. - * Also sends a exam delete request where Exam on SPS gets deleted if there are no session data for the exam - * - * @param exam The exam - * @return Result refer to the exam or to an error when happened */ - Exam deleteScreenProctoring(final Exam exam) { - try { + Result activateScreenProctoring(final Exam exam) { - if (!BooleanUtils.toBoolean(exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) { + return Result.tryCatch(() -> { + + if (log.isDebugEnabled()) { + log.debug("Activate screen proctoring exam, groups and access on SPS for exam: {}", exam.name); + } + + final SPSData spsData = this.getSPSData(exam.id); + if (spsData == null) { return exam; } - if (log.isDebugEnabled()) { - log.debug("Deactivate exam and groups on SPS site and send deletion request for exam {}", exam); - } - final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(exam.id); - final SPSData spsData = this.getSPSData(exam.id); - deletion(SPS_API.SEB_ACCESS_ENDPOINT, spsData.spsSEBAccessUUID, apiTemplate); - activation(exam, SPS_API.EXAM_ENDPOINT, spsData.spsExamUUID, false, apiTemplate); + activation(exam, SPS_API.SEB_ACCESS_ENDPOINT, spsData.spsSEBAccessUUID, true, apiTemplate); + activation(exam, SPS_API.EXAM_ENDPOINT, spsData.spsExamUUID, true, apiTemplate); - // exam delete request on SPS - final String uri = UriComponentsBuilder - .fromUriString(apiTemplate.spsAPIAccessData.getSpsServiceURL()) - .path(SPS_API.EXAM_ENDPOINT) - .pathSegment(spsData.spsExamUUID) - .pathSegment(SPS_API.EXAM_DELETE_REQUEST_ENDPOINT) - .build() - .toUriString(); - - final ResponseEntity exchange = apiTemplate.exchange(uri, HttpMethod.DELETE); - if (exchange.getStatusCode() != HttpStatus.OK) { - log.error("Failed to request delete on SPS for Exam: {} with response: {}", exam, exchange); - } - - // mark successfully dispose on SPS side + // mark local for successfully activated on SPS side this.additionalAttributesDAO.saveAdditionalAttribute( EntityType.EXAM, exam.id, SPSData.ATTR_SPS_ACTIVE, - Constants.FALSE_STRING); + Constants.TRUE_STRING); + return exam; + }); + } + + private Collection initializeScreenProctoring( + final Exam exam, + final ScreenProctoringServiceOAuthTemplate apiTemplate) throws JsonProcessingException { + + final SPSData spsData = new SPSData(); + log.info( + "SPS Exam for SEB Server Exam: {} don't exists yet, create necessary structures on SPS", + exam.externalId); + + synchronizeUserAccounts(exam); + createSEBAccess(exam, apiTemplate, spsData); + createExam(exam, apiTemplate, spsData); + final Collection initializeGroups = initializeGroups(exam, apiTemplate, spsData); + + // store encrypted spsData + final String spsDataJSON = this.jsonMapper.writeValueAsString(spsData); + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.EXAM, + exam.id, + SPSData.ATTR_SPS_ACCESS_DATA, + this.cryptor.encrypt(spsDataJSON).getOrThrow().toString()); + + // mark successfully activated on SPS side + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.EXAM, + exam.id, + SPSData.ATTR_SPS_ACTIVE, + Constants.TRUE_STRING); + + return initializeGroups; + } + + Collection reinitializeScreenProctoring(final Exam exam) { + try { + + final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(exam.id); + + // get exam from SPS + final String examUUID = createExamUUID(exam); + final String uri = UriComponentsBuilder + .fromUriString(apiTemplate.spsAPIAccessData.getSpsServiceURL()) + .path(SPS_API.EXAM_ENDPOINT) + .pathSegment(examUUID) + .build().toUriString(); + final ResponseEntity exchange = apiTemplate.exchange(uri, HttpMethod.GET); + if (exchange.getStatusCode() != HttpStatus.OK) { + throw new RuntimeException("Failed to get Exam from SPS. local exam uuid: " + examUUID); + } + final JsonNode requestJSON = this.jsonMapper.readTree(exchange.getBody()); + final String spsExamId = requestJSON.get(SPS_API.EXAM.ATTR_ID).textValue(); + + + // check if Exam has SPSData, if not create and if check completeness + SPSData spsData = this.getSPSData(exam.id); + if (spsData == null) { + spsData = new SPSData(); + } + // create new SEB Account on SPS if needed + if (spsData.spsSEBAccessUUID == null) { + createSEBAccess(exam, apiTemplate, spsData); + } + + spsData.spsExamUUID = examUUID; + // store encrypted spsData + final String spsDataJSON = this.jsonMapper.writeValueAsString(spsData); + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.EXAM, + exam.id, + SPSData.ATTR_SPS_ACCESS_DATA, + this.cryptor.encrypt(spsDataJSON).getOrThrow().toString()); + + // reactivate exam on SPS + this.activateScreenProctoring(exam); + + // recreate groups on SEB Server if needed + try { + final Collection groups = new ArrayList<>(); + final String groupRequestURI = UriComponentsBuilder + .fromUriString(apiTemplate.spsAPIAccessData.getSpsServiceURL()) + .path(SPS_API.GROUP_ENDPOINT) + .queryParam(Page.ATTR_PAGE_SIZE, 100) + .queryParam(SPS_API.GROUP.ATTR_EXAM_ID, spsExamId) + .build() + .toUriString(); + + final JsonNode groupsJSON = this.jsonMapper.readTree(exchange.getBody()); + final JsonNode pageContent = groupsJSON.get("content"); + if (pageContent.isArray()) { + for (final JsonNode group : pageContent) { + groups.add(new ScreenProctoringGroup( + null, + exam.id, + group.get(SPS_API.GROUP.ATTR_UUID).textValue(), + group.get(SPS_API.GROUP.ATTR_NAME).textValue(), + 0, + null + )); + } + } + + return groups; + } catch (final Exception e) { + log.error("Failed to get exam groups from SPS due to reinitialization: ", e); + return initializeGroups(exam, apiTemplate, spsData); + } } catch (final Exception e) { - log.warn("Failed to apply SPS deletion of exam: {} error: {}", exam, e.getMessage()); + log.error("Failed to re-initialize Screen Proctoring: ", e); + return Collections.emptyList(); } - return exam; + } +// /** This is called on exam delete and deletes the SEB Client Access and the ad-hoc User-Account +// * on Screen Proctoring Service side. +// * Also sends a exam delete request where Exam on SPS gets deleted if there are no session data for the exam +// * +// * @param exam The exam +// * @return Result refer to the exam or to an error when happened */ +// Exam deleteScreenProctoring(final Exam exam) { +// try { +// +// if (!BooleanUtils.toBoolean(exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) { +// return exam; +// } +// +// if (log.isDebugEnabled()) { +// log.debug("Deactivate exam and groups on SPS site and send deletion request for exam {}", exam); +// } +// +// final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(exam.id); +// final SPSData spsData = this.getSPSData(exam.id); +// deletion(SPS_API.SEB_ACCESS_ENDPOINT, spsData.spsSEBAccessUUID, apiTemplate); +// activation(exam, SPS_API.EXAM_ENDPOINT, spsData.spsExamUUID, false, apiTemplate); +// +// // exam delete request on SPS +// final String uri = UriComponentsBuilder +// .fromUriString(apiTemplate.spsAPIAccessData.getSpsServiceURL()) +// .path(SPS_API.EXAM_ENDPOINT) +// .pathSegment(spsData.spsExamUUID) +// .pathSegment(SPS_API.EXAM_DELETE_REQUEST_ENDPOINT) +// .build() +// .toUriString(); +// +// final ResponseEntity exchange = apiTemplate.exchange(uri, HttpMethod.DELETE); +// if (exchange.getStatusCode() != HttpStatus.OK) { +// log.error("Failed to request delete on SPS for Exam: {} with response: {}", exam, exchange); +// } +// +// // mark successfully dispose on SPS side +// this.additionalAttributesDAO.saveAdditionalAttribute( +// EntityType.EXAM, +// exam.id, +// SPSData.ATTR_SPS_ACTIVE, +// Constants.FALSE_STRING); +// +// +// } catch (final Exception e) { +// log.warn("Failed to apply SPS deletion of exam: {} error: {}", exam, e.getMessage()); +// } +// return exam; +// } + Result createGroup( final String spsExamUUID, final int groupNumber, @@ -583,7 +732,7 @@ class ScreenProctoringAPIBinding { params.add(SPS_API.SESSION.ATTR_CLIENT_VERSION, clientConnection.getClientVersion()); final String paramsFormEncoded = Utils.toAppFormUrlEncodedBody(params); - final ResponseEntity exchange = apiTemplate.exchange(uri, paramsFormEncoded, HttpMethod.POST); + final ResponseEntity exchange = apiTemplate.exchange(uri, paramsFormEncoded); if (exchange.getStatusCode() != HttpStatus.OK) { throw new RuntimeException( "Failed to create SPS SEB session for SEB connection: " + token); @@ -592,17 +741,18 @@ class ScreenProctoringAPIBinding { return token; } - void activateSEBAccessOnSPS(final Exam exam, final boolean activate) { - try { - final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(exam.id); - final SPSData spsData = this.getSPSData(exam.id); - - activation(exam, SPS_API.SEB_ACCESS_ENDPOINT, spsData.spsSEBAccessUUID, activate, apiTemplate); - - } catch (final Exception e) { - log.error("Failed to de/activate SEB Access on SPS for exam: {}", exam); - } - } +// void activateExamOnSPS(final Exam exam, final boolean activate) { +// try { +// final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(exam.id); +// final SPSData spsData = this.getSPSData(exam.id); +// +// activation(exam, SPS_API.EXAM_ENDPOINT, spsData.spsSEBAccessUUID, activate, apiTemplate); +// activation(exam, SPS_API.SEB_ACCESS_ENDPOINT, spsData.spsSEBAccessUUID, activate, apiTemplate); +// +// } catch (final Exception e) { +// log.error("Failed to de/activate SEB Access on SPS for exam: {}", exam); +// } +// } private void synchronizeUserAccount( final String userUUID, @@ -758,17 +908,17 @@ class ScreenProctoringAPIBinding { params.add(SPS_API.GROUP.ATTR_EXAM_ID, spsExamUUID); final String paramsFormEncoded = Utils.toAppFormUrlEncodedBody(params); - final ResponseEntity exchange = apiTemplate.exchange(uri, paramsFormEncoded, HttpMethod.POST); + final ResponseEntity exchange = apiTemplate.exchange(uri, paramsFormEncoded); if (exchange.getStatusCode() != HttpStatus.OK) { throw new RuntimeException("Failed to create SPS SEB group for exam: " + spsExamUUID); } - final Map userAttributes = this.jsonMapper.readValue( + final Map groupAttributes = this.jsonMapper.readValue( exchange.getBody(), new TypeReference>() { }); - final String spsGroupUUID = userAttributes.get(SPS_API.GROUP.ATTR_UUID); + final String spsGroupUUID = groupAttributes.get(SPS_API.GROUP.ATTR_UUID); return new ScreenProctoringGroup(null, examId, spsGroupUUID, name, size, exchange.getBody()); } @@ -790,22 +940,11 @@ class ScreenProctoringAPIBinding { .path(SPS_API.EXAM_ENDPOINT) .build().toUriString(); - final MultiValueMap params = new LinkedMultiValueMap<>(); - params.add(SPS_API.EXAM.ATTR_NAME, exam.name); - params.add(SPS_API.EXAM.ATTR_DESCRIPTION, exam.getDescription()); - params.add(SPS_API.EXAM.ATTR_URL, exam.getStartURL()); - if (!userIds.isEmpty()) { - params.add(SPS_API.EXAM.ATTR_USER_IDS, StringUtils.join(userIds, Constants.LIST_SEPARATOR)); - } - params.add(SPS_API.EXAM.ATTR_TYPE, exam.getType().name()); - params.add(SPS_API.EXAM.ATTR_START_TIME, String.valueOf(exam.startTime.getMillis())); - - if (exam.endTime != null) { - params.add(SPS_API.EXAM.ATTR_END_TIME, String.valueOf(exam.endTime.getMillis())); - } + final String uuid = createExamUUID(exam); + final MultiValueMap params = createExamCreationParams(exam, uuid, userIds); final String paramsFormEncoded = Utils.toAppFormUrlEncodedBody(params); - final ResponseEntity exchange = apiTemplate.exchange(uri, paramsFormEncoded, HttpMethod.POST); + final ResponseEntity exchange = apiTemplate.exchange(uri, paramsFormEncoded); if (exchange.getStatusCode() != HttpStatus.OK) { throw new RuntimeException("Error response from Screen Proctoring Service: " + exchange.getStatusCodeValue() @@ -814,7 +953,14 @@ class ScreenProctoringAPIBinding { } final JsonNode requestJSON = this.jsonMapper.readTree(exchange.getBody()); - spsData.spsExamUUID = requestJSON.get(SPS_API.EXAM.ATTR_UUID).textValue(); + final String respondedUUID = requestJSON.get(SPS_API.EXAM.ATTR_UUID).textValue(); + if (!uuid.equals(respondedUUID)) { + log.warn("Detected Exam ({}) generation UUID mismatch. propagated UUID: {} responded UUID: {}", + exam.name, + uuid, + respondedUUID); + } + spsData.spsExamUUID = respondedUUID; } catch (final Exception e) { log.error( @@ -826,6 +972,32 @@ class ScreenProctoringAPIBinding { } } + private static MultiValueMap createExamCreationParams( + final Exam exam, + final String uuid, + final List userIds) { + + final MultiValueMap params = new LinkedMultiValueMap<>(); + params.add(SPS_API.EXAM.ATTR_UUID, uuid); + params.add(SPS_API.EXAM.ATTR_NAME, exam.name); + params.add(SPS_API.EXAM.ATTR_DESCRIPTION, exam.getDescription()); + params.add(SPS_API.EXAM.ATTR_URL, exam.getStartURL()); + if (!userIds.isEmpty()) { + params.add(SPS_API.EXAM.ATTR_USER_IDS, StringUtils.join(userIds, Constants.LIST_SEPARATOR)); + } + params.add(SPS_API.EXAM.ATTR_TYPE, exam.getType().name()); + params.add(SPS_API.EXAM.ATTR_START_TIME, String.valueOf(exam.startTime.getMillis())); + + if (exam.endTime != null) { + params.add(SPS_API.EXAM.ATTR_END_TIME, String.valueOf(exam.endTime.getMillis())); + } + return params; + } + + private String createExamUUID(final Exam exam) { + return exam.externalId; + } + private void createSEBAccess( final Exam exam, final ScreenProctoringServiceOAuthTemplate apiTemplate, @@ -846,7 +1018,7 @@ class ScreenProctoringAPIBinding { params.add(SPS_API.SEB_ACCESS.ATTR_DESCRIPTION, description); final String paramsFormEncoded = Utils.toAppFormUrlEncodedBody(params); - final ResponseEntity exchange = apiTemplate.exchange(uri, paramsFormEncoded, HttpMethod.POST); + final ResponseEntity exchange = apiTemplate.exchange(uri, paramsFormEncoded); if (exchange.getStatusCode() != HttpStatus.OK) { throw new RuntimeException("Failed to create SPS SEB access for exam: " + exam.externalId); } @@ -1099,10 +1271,9 @@ class ScreenProctoringAPIBinding { ResponseEntity exchange( final String url, - final String body, - final HttpMethod method) { + final String body) { - return exchange(url, method, body, getHeaders()); + return exchange(url, HttpMethod.POST, body, getHeaders()); } HttpHeaders getHeadersJSONRequest() { @@ -1160,8 +1331,6 @@ class ScreenProctoringAPIBinding { public static final String ATTR_SPS_ACTIVE = "spsExamActive"; public static final String ATTR_SPS_ACCESS_DATA = "spsAccessData"; - @JsonProperty("spsUserPWD") - String spsUserPWD = null; @JsonProperty("spsSEBAccessUUID") String spsSEBAccessUUID = null; @JsonProperty("spsSEBAccessName") @@ -1175,16 +1344,13 @@ class ScreenProctoringAPIBinding { } @JsonCreator - public SPSData( - @JsonProperty("spsUserPWD") final String spsUserPWD, - @JsonProperty("spsSEBAccessUUID") final String spsSEBAccessUUID, + public SPSData(@JsonProperty("spsSEBAccessUUID") final String spsSEBAccessUUID, // NOTE: this is only for compatibility reasons, TODO as soon as possible @JsonProperty("spsSEBAccesUUID") final String spsSEBAccesUUID, @JsonProperty("spsSEBAccessName") final String spsSEBAccessName, @JsonProperty("spsSEBAccessPWD") final String spsSEBAccessPWD, @JsonProperty("psExamUUID") final String spsExamUUID) { - this.spsUserPWD = spsUserPWD; this.spsSEBAccessUUID = StringUtils.isNotBlank(spsSEBAccesUUID) ? spsSEBAccesUUID : spsSEBAccessUUID; this.spsSEBAccessName = spsSEBAccessName; this.spsSEBAccessPWD = spsSEBAccessPWD; 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 b5b0dbcb..1f8ca115 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 @@ -10,14 +10,13 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring; import static ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_SCREEN_PROCTORING.*; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig; +import ch.ethz.seb.sebserver.gbl.model.Activatable; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.webservice.WebserviceInfo; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.LmsSetupChangeEvent; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -184,7 +183,7 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { final boolean isEnabling = this.proctoringSettingsDAO.isScreenProctoringEnabled(exam.id); if (isEnabling && !isSPSActive) { - + // if screen proctoring has been enabled this.screenProctoringAPIBinding .startScreenProctoring(exam) .onError(error -> log.error( @@ -197,9 +196,9 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { this.examDAO.markUpdate(exam.id); } else if (!isEnabling && isSPSActive) { - + // if screen proctoring has been disabled... this.screenProctoringAPIBinding - .disposeScreenProctoring(exam) + .deactivateScreenProctoring(exam) .onError(error -> log.error("Failed to dispose screen proctoring for exam: {}", exam, error)) @@ -301,7 +300,7 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { return; } - this.screenProctoringAPIBinding.activateSEBAccessOnSPS(exam, true); + this.screenProctoringAPIBinding.activateScreenProctoring(exam); } @Override @@ -311,7 +310,7 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { return; } - this.screenProctoringAPIBinding.activateSEBAccessOnSPS(exam, false); + this.screenProctoringAPIBinding.deactivateScreenProctoring(exam); } @Override @@ -330,6 +329,37 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { }); } + @Override + public void notifyLmsSetupChange(final LmsSetupChangeEvent event) { + try { + + if (event.activation == Activatable.ActivationAction.NONE) { + return; + } + + examDAO.allActiveForLMSSetup(Arrays.asList(event.getLmsSetup().id)) + .getOrThrow() + .forEach(exam -> { + if (screenProctoringAPIBinding.isSPSActive(exam)) { + if (event.activation == Activatable.ActivationAction.ACTIVATE) { + this.screenProctoringAPIBinding.activateScreenProctoring(exam) + .onError(error -> log.warn("Failed to re-activate SPS for exam: {} error: {}", + exam.name, + error.getMessage())); + } else if (event.activation == Activatable.ActivationAction.DEACTIVATE) { + this.screenProctoringAPIBinding.deactivateScreenProctoring(exam) + .onError(error -> log.warn("Failed to deactivate SPS for exam: {} error: {}", + exam.name, + error.getMessage())); + } + } + }); + + } catch (final Exception e) { + log.error("Failed to apply LMSSetup change activation/deactivation to Screen Proctoring: ", e); + } + } + private void applyScreenProctoringSession(final ClientConnectionRecord ccRecord) { Long placeReservedInGroup = null; @@ -457,7 +487,7 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { private Result deleteForExam(final Long examId) { return this.examDAO .byPK(examId) - .map(this.screenProctoringAPIBinding::deleteScreenProctoring) + .flatMap(this.screenProctoringAPIBinding::deactivateScreenProctoring) .map(this::cleanupAllLocalGroups) .onError(error -> log.error("Failed to delete SPS integration for exam: {}", examId, error)); } 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 f2f6ff18..9ac4b36f 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 @@ -14,6 +14,7 @@ import java.util.stream.Collectors; import javax.validation.Valid; +import ch.ethz.seb.sebserver.gbl.model.Activatable; 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; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java index 96d71a0f..31c822d7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java @@ -14,6 +14,7 @@ import ch.ethz.seb.sebserver.gbl.model.Activatable; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsTestService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService; import org.mybatis.dynamic.sql.SqlTable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -61,7 +62,8 @@ public class LmsSetupController extends ActivatableEntityController validForActivation(final LmsSetup entity, final boolean activation) { - return super.validForActivation(entity, activation) - .map(lmsSetup -> { - if (!activation) { - // on deactivation remove all SEB restrictions and delete full integration if in place - return sebRestrictionService - .applyLMSSetupDeactivation(lmsSetup) - .flatMap(fullLmsIntegrationService::applyLMSSetupDeactivation) - .getOrThrow(); - } - return entity; - }); - } +// @Override +// protected Result validForActivation(final LmsSetup entity, final boolean activation) { +// return super.validForActivation(entity, activation) +// .map(lmsSetup -> { +// if (!activation) { +// // on deactivation remove all SEB restrictions and delete full integration if in place +// return sebRestrictionService +// .applyLMSSetupDeactivation(lmsSetup) +// .onErrorDo(error -> { +// log.warn("Failed to apply LMSSetup deactivation for SEB Restriction: ", error); +// return lmsSetup; +// }) +// .flatMap(fullLmsIntegrationService::applyLMSSetupDeactivation) +// .onErrorDo(error -> { +// log.warn("Failed to apply LMSSetup deactivation for LMS full integration: ", error); +// return lmsSetup; +// }) +// .flatMap(screenProctoringService::applyLMSSetupDeactivation) +// .onErrorDo(error -> { +// log.warn("Failed to apply LMSSetup deactivation for Screen Proctoring: ", error); +// return lmsSetup; +// }) +// .getOrThrow(); +// } else { +// +// } +// return entity; +// }); +// } @Override protected Result validForDelete(final LmsSetup entity) { @@ -187,7 +206,7 @@ public class LmsSetupController extends ActivatableEntityController