SEBSERV-559 and SEBSERV-552

This commit is contained in:
anhefti 2024-07-03 16:29:00 +02:00
parent c0ead99e2b
commit 16b2c8deb4
12 changed files with 131 additions and 90 deletions

View file

@ -169,7 +169,6 @@ public class MonitoringProctoringService {
final ScreenProctoringSettings screenProctoringSettings) {
collectingRooms
.stream()
.forEach(room -> updateProctoringAction(
pageContext,
proctoringSettings,
@ -184,7 +183,6 @@ public class MonitoringProctoringService {
if (screenProctoringGroups != null) {
screenProctoringGroups
.stream()
.forEach(group -> updateScreenProctoringAction(
pageContext,
screenProctoringSettings,
@ -357,8 +355,12 @@ public class MonitoringProctoringService {
// Open SPS Gui redirect URL with login token (jwt token) in new browser tab
final String redirectLocation = redirect.getBody() + "/jwt?token=" + tokenRequest.getBody();
final String script = "window.open("+ redirectLocation + ", 'seb_screen_proctoring')";
final UrlLauncher launcher = RWT.getClient().getService(UrlLauncher.class);
launcher.openURL(redirectLocation);
// RWT.getClient()
// .getService(JavaScriptExecutor.class)
// .execute(script);
} catch (final Exception e) {
log.error("Failed to open screen proctoring service group gallery view: ", e);
_action.pageContext()

View file

@ -0,0 +1,34 @@
/*
* 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.authorization;
public 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;
}
}

View file

@ -27,7 +27,7 @@ public interface TeacherAccountService {
*/
Result<UserInfo> createNewTeacherAccountForExam(
Exam exam,
final FullLmsIntegrationService.AdHocAccountData adHocAccountData);
final AdHocAccountData adHocAccountData);
/** Get the identifier for certain Teacher account for specified Exam.
*
@ -37,7 +37,7 @@ public interface TeacherAccountService {
*/
default String getTeacherAccountIdentifier(
final Exam exam,
final FullLmsIntegrationService.AdHocAccountData adHocAccountData) {
final AdHocAccountData adHocAccountData) {
return getTeacherAccountIdentifier(
String.valueOf(exam.lmsSetupId),
@ -62,7 +62,7 @@ public interface TeacherAccountService {
*/
Result<String> getOneTimeTokenForTeacherAccount(
Exam exam,
FullLmsIntegrationService.AdHocAccountData adHocAccountData,
AdHocAccountData adHocAccountData,
boolean createIfNotExists);
/** Used to verify a given One Time Access JWT Token. This must have the expected claims and must not be expired

View file

@ -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";
/** UUID of the internal account that is used for LMS integration related remote call tasks */
String LMS_INTEGRATION_CLIENT_UUID = "LMS_INTEGRATION_CLIENT";
String LMS_INTEGRATION_CLIENT_NAME = "lmsIntegrationClient";

View file

@ -20,10 +20,10 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AdHocAccountData;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.TeacherAccountService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
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;
@ -91,7 +91,7 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
@Override
public Result<UserInfo> createNewTeacherAccountForExam(
final Exam exam,
final FullLmsIntegrationService.AdHocAccountData adHocAccountData) {
final AdHocAccountData adHocAccountData) {
return Result.tryCatch(() -> {
@ -130,7 +130,7 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
@Override
public Result<String> getOneTimeTokenForTeacherAccount(
final Exam exam,
final FullLmsIntegrationService.AdHocAccountData adHocAccountData,
final AdHocAccountData adHocAccountData,
final boolean createIfNotExists) {
return this.userDAO
@ -186,7 +186,7 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
private UserInfo handleAccountDoesNotExistYet(
final boolean createIfNotExists,
final Exam exam,
final FullLmsIntegrationService.AdHocAccountData adHocAccountData) {
final AdHocAccountData adHocAccountData) {
if (createIfNotExists) {
return this
@ -212,6 +212,7 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
"Failed to apply ad-hoc-teacher account to supporter list of exam: {} user: {}",
exam, account, error));
}
return account;
}
@ -219,7 +220,7 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
final String subjectClaim = UUID.randomUUID().toString();
userDAO.changePassword(account.uuid, subjectClaim);
synchronizeSPSUserForExam(account, examId);
this.screenProctoringService.updateExamOnScreenProctoringService(examId);
final Map<String, Object> claims = new HashMap<>();
claims.put(USER_CLAIM, account.uuid);
@ -275,10 +276,4 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
return claims;
}
private UserInfo synchronizeSPSUserForExam(final UserInfo account, final Long examId) {
if (this.screenProctoringService.isScreenProctoringEnabled(examId)) {
this.screenProctoringService.synchronizeSPSUserForExam(examId);
}
return account;
}
}

View file

@ -16,7 +16,6 @@ import java.util.Objects;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.*;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ProctoringAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
@ -299,7 +298,7 @@ public class ExamTemplateServiceImpl implements ExamTemplateService {
!Objects.equals(examConfig.templateId, examTemplate.configTemplateId)) {
final String newName = (examConfig != null && examConfig.name.equals(configName))
? examConfig.name + "_"
? examConfig.name + "_" + DateTime.now(DateTimeZone.UTC).toString(Constants.STANDARD_DATE_FORMATTER)
: configName;
final ConfigurationNode config = new ConfigurationNode(

View file

@ -11,12 +11,11 @@ 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.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AdHocAccountData;
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;
@ -26,7 +25,6 @@ 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 {
@ -79,30 +77,7 @@ public interface FullLmsIntegrationService {
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 {

View file

@ -32,6 +32,7 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.SEBClientConfig;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AdHocAccountData;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.TeacherAccountServiceImpl;
@ -455,14 +456,22 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
final String quizId,
final String examData) {
final String internalQuizId = MoodleUtils.getInternalQuizId(
quizId,
courseId,
null,
null);
if (StringUtils.isNotBlank(examData)) {
return lmsAPITemplate
.getQuizDataForRemoteImport(examData)
.getOrThrow();
} else {
return lmsAPITemplate.getQuizDataForRemoteImport(examData)
.getOrThrow();
final String internalQuizId = MoodleUtils.getInternalQuizId(
quizId,
courseId,
null,
null);
return lmsAPITemplate.getQuiz(internalQuizId)
.getOrThrow();
}
}

View file

@ -71,6 +71,9 @@ public interface ScreenProctoringService extends SessionUpdateTask {
@EventListener(ExamFinishedEvent.class)
void notifyExamFinished(ExamFinishedEvent event);
@EventListener(ExamResetEvent.class)
void notifyExamReset(ExamResetEvent event);
/** This is being called just before an Exam gets deleted on the permanent storage.
* This deactivates and dispose or deletes all exam relevant domain entities on the SPS service side.
*

View file

@ -102,6 +102,12 @@ class ScreenProctoringAPIBinding {
String ATTR_PRIVILEGES = "privileges";
}
interface PRIVILEGE_FLAGS {
String READ = "r";
String MODIFY = "m";
String WRITE = "w";
}
/** The screen proctoring service client-access API attribute names */
interface SEB_ACCESS {
String ATTR_UUID = "uuid";
@ -380,8 +386,6 @@ class ScreenProctoringAPIBinding {
void synchronizeUserAccounts(final Exam exam) {
try {
final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(exam.id);
final SPSData spsData = this.getSPSData(exam.id);
exam.supporter.forEach(userUUID -> synchronizeUserAccount(userUUID, apiTemplate));
if (exam.owner != null) {
@ -389,7 +393,7 @@ class ScreenProctoringAPIBinding {
}
} catch (final Exception e) {
log.error("Failed to synchronize user accounts with SPS for exam: {}", exam);
log.error("Failed to synchronize user accounts with SPS for exam: {}", exam, e);
}
}
void deleteSPSUser(final String userUUID) {
@ -433,11 +437,7 @@ class ScreenProctoringAPIBinding {
.build()
.toUriString();
final List<String> userIds = new ArrayList<>(exam.supporter);
if (exam.owner != null) {
userIds.add(exam.owner);
}
final List<String> supporterIds = getSupporterIds(exam);
final ExamUpdate examUpdate = new ExamUpdate(
exam.name,
exam.getDescription(),
@ -445,7 +445,7 @@ class ScreenProctoringAPIBinding {
exam.getType().name(),
exam.startTime != null ? exam.startTime.getMillis() : null,
exam.endTime != null ? exam.endTime.getMillis() : null,
userIds);
supporterIds);
final String jsonExamUpdate = this.jsonMapper.writeValueAsString(examUpdate);
@ -796,17 +796,15 @@ class ScreenProctoringAPIBinding {
if (activityRequest.getStatusCode() != HttpStatus.OK) {
final String body = activityRequest.getBody();
if (body != null && body.contains("Activation argument mismatch")) {
return;
if (body != null && !body.contains("Activation argument mismatch")) {
log.warn("Failed to synchronize activity for user account on SPS: {}", activityRequest);
}
log.warn("Failed to synchronize activity for user account on SPS: {}", activityRequest);
} else {
log.info("Successfully synchronize activity for user account on SPS for user: {}", userUUID);
}
} catch (final Exception e) {
log.error("Failed to synchronize user account with SPS for user: {}", userUUID);
log.error("Failed to synchronize user account with SPS for user: {}", userUUID, e);
}
}
@ -931,18 +929,14 @@ class ScreenProctoringAPIBinding {
try {
final List<String> userIds = new ArrayList<>(exam.supporter);
if (exam.owner != null) {
userIds.add(exam.owner);
}
final List<String> supporterIds = getSupporterIds(exam);
final String uri = UriComponentsBuilder
.fromUriString(apiTemplate.spsAPIAccessData.getSpsServiceURL())
.path(SPS_API.EXAM_ENDPOINT)
.build().toUriString();
final String uuid = createExamUUID(exam);
final MultiValueMap<String, String> params = createExamCreationParams(exam, uuid, userIds);
final MultiValueMap<String, String> params = createExamCreationParams(exam, uuid, supporterIds);
final String paramsFormEncoded = Utils.toAppFormUrlEncodedBodyForSPService(params);
final ResponseEntity<String> exchange = apiTemplate.exchange(uri, paramsFormEncoded);
@ -976,7 +970,7 @@ class ScreenProctoringAPIBinding {
private static MultiValueMap<String, String> createExamCreationParams(
final Exam exam,
final String uuid,
final List<String> userIds) {
final List<String> supporterIds) {
final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add(SPS_API.EXAM.ATTR_UUID, uuid);
@ -985,8 +979,8 @@ class ScreenProctoringAPIBinding {
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));
if (!supporterIds.isEmpty()) {
params.add(SPS_API.EXAM.ATTR_USER_IDS, StringUtils.join(supporterIds, 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()));
@ -1199,6 +1193,14 @@ class ScreenProctoringAPIBinding {
return this.apiTemplate;
}
private static List<String> getSupporterIds(final Exam exam) {
final List<String> supporterIds = new ArrayList<>(exam.supporter);
if (exam.owner != null && !UserService.LMS_INTEGRATION_CLIENT_UUID.equals(exam.owner)) {
supporterIds.add(exam.owner);
}
return supporterIds;
}
final static class ScreenProctoringServiceOAuthTemplate {
private static final String GRANT_TYPE = "password";

View file

@ -17,6 +17,7 @@ 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 ch.ethz.seb.sebserver.webservice.servicelayer.session.*;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
@ -47,10 +48,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ScreenProctoringGroupDA
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ScreenProctoringGroupDAOImpl.AllGroupsFullException;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamFinishedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamStartedEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamSessionCacheService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ScreenProctoringAPIBinding.SPSData;
@ -270,7 +267,8 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
@Override
public void synchronizeSPSUserForExam(final Long examId) {
this.examDAO.byPK(examId)
this.examDAO
.byPK(examId)
.onSuccess(this.screenProctoringAPIBinding::synchronizeUserAccounts)
.onError(error -> log.error("Failed to synchronize SPS user accounts for exam: {}", examId, error));
}
@ -299,14 +297,26 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
@Override
public void notifyExamFinished(final ExamFinishedEvent event) {
final Exam exam = event.exam;
if (!this.isScreenProctoringEnabled(exam.id) ||
!BooleanUtils.toBoolean(exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) {
if (!this.isScreenProctoringEnabled(event.exam.id) ||
!BooleanUtils.toBoolean(event.exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) {
return;
}
if (exam.status == Exam.ExamStatus.FINISHED) {
this.screenProctoringAPIBinding.deactivateScreenProctoring(exam);
if (event.exam.status != Exam.ExamStatus.UP_COMING) {
this.screenProctoringAPIBinding.deactivateScreenProctoring(event.exam);
}
}
@Override
public void notifyExamReset(final ExamResetEvent event) {
if (!this.isScreenProctoringEnabled(event.exam.id) ||
BooleanUtils.toBoolean(event.exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) {
return;
}
if (event.exam.status != Exam.ExamStatus.UP_COMING) {
this.screenProctoringAPIBinding.activateScreenProctoring(event.exam);
}
}

View file

@ -21,6 +21,8 @@ import ch.ethz.seb.sebserver.gbl.api.APIMessage;
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.authorization.AdHocAccountData;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.NoResourceFoundException;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.BooleanUtils;
@ -89,15 +91,23 @@ public class LmsIntegrationController {
examData)
.onError(e -> {
log.error(
"Failed to create/import exam: lmsId:{}, courseId: {}, quizId: {}, templateId: {} error: {}",
lmsUUId, courseId, quizId, templateId, e.getMessage());
"Failed to create/import exam: lmsId:{}, courseId: {}, quizId: {}, templateId: {} error: ",
lmsUUId, courseId, quizId, templateId, e);
log.info("Rollback Exam creation...");
fullLmsIntegrationService.deleteExam(lmsUUId, courseId, quizId)
.onError(error -> log.error("Failed to rollback auto Exam import: ", error));
.onError(error -> {
if (error instanceof NoResourceFoundException) {
log.info("No Exam found for rollback.");
} else {
log.error("Failed to rollback auto Exam import: ", error);
}
});
})
.getOrThrow();
.getOr(null);
log.info("Auto import of exam successful: {}", exam);
if (exam != null) {
log.info("Auto import of exam successful: {}", exam);
}
}
@RequestMapping(
@ -181,7 +191,7 @@ public class LmsIntegrationController {
@RequestParam(name = API.LMS_FULL_INTEGRATION_TIME_ZONE, required = false) final String timezone,
final HttpServletResponse response) {
final FullLmsIntegrationService.AdHocAccountData adHocAccountData = new FullLmsIntegrationService.AdHocAccountData(
final AdHocAccountData adHocAccountData = new AdHocAccountData(
userId,
username,
userMail,