diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/FeatureService.java b/src/main/java/ch/ethz/seb/sebserver/gbl/FeatureService.java index 06a9ad98..0152e9ef 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/FeatureService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/FeatureService.java @@ -22,4 +22,6 @@ public interface FeatureService { boolean isEnabled(CollectingStrategy collectingRoomStrategy); + boolean isScreenProcteringEnabled(); + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/FeatureServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/gbl/FeatureServiceImpl.java index 804833a9..15e46045 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/FeatureServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/FeatureServiceImpl.java @@ -59,4 +59,12 @@ public class FeatureServiceImpl implements FeatureService { return key.replaceAll("_", "-"); } + @Override + public boolean isScreenProcteringEnabled() { + return this.environment.getProperty(toConfigName( + FEATURE_SETTINGS_PREFIX + "seb.screenProctoring"), + Boolean.class, + Boolean.FALSE); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/client/ClientCredentialServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/gbl/client/ClientCredentialServiceImpl.java index caa7b297..e1cef59f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/client/ClientCredentialServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/client/ClientCredentialServiceImpl.java @@ -90,6 +90,10 @@ public class ClientCredentialServiceImpl implements ClientCredentialService { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^*()-_=+[{]}?" .toCharArray(); + private final static char[] possibleLess = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + .toCharArray(); + public static CharSequence generateClientId() { return RandomStringUtils.random( 16, 0, possibleCharacters.length - 1, false, false, @@ -102,6 +106,12 @@ public class ClientCredentialServiceImpl implements ClientCredentialService { possibleCharacters, new SecureRandom()); } + public static CharSequence generateClientSecretLess() { + return RandomStringUtils.random( + 16, 0, possibleLess.length - 1, false, false, + possibleLess, new SecureRandom()); + } + public static void clearChars(final CharSequence sequence) { if (sequence == null) { return; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java index 44ad452f..52b83fd7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java @@ -343,6 +343,16 @@ public enum ActionDefinition { ImageIcon.VISIBILITY_OFF, PageStateDefinitionImpl.EXAM_VIEW, ActionCategory.EXAM_SECURITY), + SCREEN_PROCTORING_ON( + new LocTextKey("sebserver.exam.sps.actions.open"), + ImageIcon.SCREEN_PROC_ON, + PageStateDefinitionImpl.EXAM_VIEW, + ActionCategory.EXAM_SECURITY), + SCREEN_PROCTORING_OFF( + new LocTextKey("sebserver.exam.sps.actions.open"), + ImageIcon.SCREEN_PROC_OFF, + PageStateDefinitionImpl.EXAM_VIEW, + ActionCategory.EXAM_SECURITY), EXAM_CONFIGURATION_NEW( new LocTextKey("sebserver.exam.configuration.action.list.new"), diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java index 81fc88cf..fb1c8cb8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamForm.java @@ -27,6 +27,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.FeatureService; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage.ErrorMessage; @@ -38,6 +39,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus; import ch.ethz.seb.sebserver.gbl.model.exam.ExamTemplate; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; +import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult.ErrorType; @@ -66,6 +68,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckExamCon import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.CheckSEBRestriction; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamProctoringSettings; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetScreenProctoringSettings; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetDefaultExamTemplate; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetExamTemplate; @@ -149,6 +152,7 @@ public class ExamForm implements TemplateComposer { private final ResourceService resourceService; private final ExamSEBRestrictionSettings examSEBRestrictionSettings; private final ProctoringSettingsPopup proctoringSettingsPopup; + private final ScreenProctoringSettingsPopup screenProctoringSettingsPopup; private final WidgetFactory widgetFactory; private final RestService restService; private final ExamDeletePopup examDeletePopup; @@ -156,22 +160,26 @@ public class ExamForm implements TemplateComposer { private final ExamIndicatorsList examIndicatorsList; private final ExamClientGroupList examClientGroupList; private final ExamCreateClientConfigPopup examCreateClientConfigPopup; + private final FeatureService featureService; protected ExamForm( final PageService pageService, final ExamSEBRestrictionSettings examSEBRestrictionSettings, final ProctoringSettingsPopup proctoringSettingsPopup, + final ScreenProctoringSettingsPopup screenProctoringSettingsPopup, final ExamToConfigBindingForm examToConfigBindingForm, final DownloadService downloadService, final ExamDeletePopup examDeletePopup, final ExamFormConfigs examFormConfigs, final ExamIndicatorsList examIndicatorsList, final ExamClientGroupList examClientGroupList, - final ExamCreateClientConfigPopup examCreateClientConfigPopup) { + final ExamCreateClientConfigPopup examCreateClientConfigPopup, + final FeatureService featureService) { this.pageService = pageService; this.resourceService = pageService.getResourceService(); this.examSEBRestrictionSettings = examSEBRestrictionSettings; + this.screenProctoringSettingsPopup = screenProctoringSettingsPopup; this.proctoringSettingsPopup = proctoringSettingsPopup; this.widgetFactory = pageService.getWidgetFactory(); this.restService = this.resourceService.getRestService(); @@ -180,6 +188,7 @@ public class ExamForm implements TemplateComposer { this.examIndicatorsList = examIndicatorsList; this.examClientGroupList = examClientGroupList; this.examCreateClientConfigPopup = examCreateClientConfigPopup; + this.featureService = featureService; this.consistencyMessageMapping = new HashMap<>(); this.consistencyMessageMapping.put( @@ -410,6 +419,13 @@ public class ExamForm implements TemplateComposer { .map(ProctoringServiceSettings::getEnableProctoring) .getOr(false); + final boolean screenProctoringEnabled = importFromQuizData ? false : this.restService + .getBuilder(GetScreenProctoringSettings.class) + .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) + .call() + .map(ScreenProctoringSettings::getEnableScreenProctoring) + .getOr(false); + final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(formContext .clearEntityKeys() .removeAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA)); @@ -491,7 +507,24 @@ public class ExamForm implements TemplateComposer { .withEntityKey(entityKey) .withExec(this.proctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable)) .noEventPropagation() - .publishIf(() -> !proctoringEnabled && readonly); + .publishIf(() -> !proctoringEnabled && readonly) + + .newAction(ActionDefinition.SCREEN_PROCTORING_ON) + .withEntityKey(entityKey) + .withExec( + this.screenProctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable)) + .noEventPropagation() + .publishIf(() -> this.featureService.isScreenProcteringEnabled() && screenProctoringEnabled && readonly) + + .newAction(ActionDefinition.SCREEN_PROCTORING_OFF) + .withEntityKey(entityKey) + .withExec( + this.screenProctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable)) + .noEventPropagation() + .publishIf( + () -> this.featureService.isScreenProcteringEnabled() && !screenProctoringEnabled && readonly) + + ; // additional data in read-only view if (readonly && !importFromQuizData) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ProctoringSettingsPopup.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ProctoringSettingsPopup.java index 9b947755..c65449b1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ProctoringSettingsPopup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ProctoringSettingsPopup.java @@ -486,8 +486,11 @@ public class ProctoringSettingsPopup { } - private FormBuilder buildHeader(final ProctoringServiceSettings proctoringSettings, - final FormHandleAnchor formHandleAnchor, final boolean isReadonly) { + private FormBuilder buildHeader( + final ProctoringServiceSettings proctoringSettings, + final FormHandleAnchor formHandleAnchor, + final boolean isReadonly) { + final FormBuilder formBuilder = this.pageService.formBuilder( formHandleAnchor.formContext) .withDefaultSpanInput(5) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ScreenProctoringSettingsPopup.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ScreenProctoringSettingsPopup.java index 1060d08c..8b12658f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ScreenProctoringSettingsPopup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ScreenProctoringSettingsPopup.java @@ -8,19 +8,297 @@ package ch.ethz.seb.sebserver.gui.content.exam; -import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.RowData; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.Entity; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.exam.CollectingStrategy; +import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; +import ch.ethz.seb.sebserver.gui.form.Form; +import ch.ethz.seb.sebserver.gui.form.FormBuilder; +import ch.ethz.seb.sebserver.gui.form.FormHandle; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.page.ModalInputDialogComposer; +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.PageService; +import ch.ethz.seb.sebserver.gui.service.page.event.ActionEvent; +import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog; +import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetScreenProctoringSettings; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveScreenProctoringSettings; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetExamTemplateScreenProctoringSettings; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.SaveExamTemplateScreenProctoringSettings; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; + +@Lazy +@Component +@GuiProfile public class ScreenProctoringSettingsPopup { - private final static LocTextKey SEB_PROCTORING_FORM_APPKEY_SPS = - new LocTextKey("sebserver.exam.proctoring.form.appkey.sps"); - private final static LocTextKey SEB_PROCTORING_FORM_SECRET_SPS = - new LocTextKey("sebserver.exam.proctoring.form.appsecret.sps"); - private final static LocTextKey SEB_PROCTORING_FORM_ACCOUNT_ID_SPS = - new LocTextKey("sebserver.exam.proctoring.form.accountId.sps"); - private final static LocTextKey SEB_PROCTORING_FORM_ACCOUNT_SECRET_SPS = - new LocTextKey("sebserver.exam.proctoring.form.accountSecret.sps"); - private final static LocTextKey SEB_PROCTORING_FORM_COLLECT_STRATEGY = - new LocTextKey("sebserver.exam.proctoring.form.collect.strategy"); + private static final Logger log = LoggerFactory.getLogger(ScreenProctoringSettingsPopup.class); + + private final static LocTextKey FORM_TITLE = + new LocTextKey("sebserver.exam.sps.form.title"); + private final static LocTextKey FORM_INFO_TITLE = + new LocTextKey("sebserver.exam.sps.form.info.title"); + private final static LocTextKey FORM_INFO = + new LocTextKey("sebserver.exam.sps.form.info"); + private final static LocTextKey FORM_ENABLE = + new LocTextKey("sebserver.exam.sps.form.enable"); + private final static LocTextKey FORM_URL = + new LocTextKey("sebserver.exam.sps.form.url"); + private final static LocTextKey FORM_APPKEY_SPS = + new LocTextKey("sebserver.exam.sps.form.appkey"); + private final static LocTextKey FORM_APPSECRET_SPS = + new LocTextKey("sebserver.exam.sps.form.appsecret"); + private final static LocTextKey FORM_ACCOUNT_ID_SPS = + new LocTextKey("sebserver.exam.sps.form.accountId"); + private final static LocTextKey FORM_ACCOUNT_SECRET_SPS = + new LocTextKey("sebserver.exam.sps.form.accountSecret"); + private final static LocTextKey FORM_COLLECT_STRATEGY = + new LocTextKey("sebserver.exam.sps.form.collect.strategy"); + + private final static LocTextKey SAVE_TEXT_KEY = + new LocTextKey("sebserver.exam.sps.form.saveSettings"); + + Function settingsFunction(final PageService pageService, final boolean modifyGrant) { + return action -> { + + final PageContext pageContext = action.pageContext() + .withAttribute( + PageContext.AttributeKeys.FORCE_READ_ONLY, + (modifyGrant) ? Constants.FALSE_STRING : Constants.TRUE_STRING); + + final ModalInputDialog> dialog = + new ModalInputDialog>( + action.pageContext().getParent().getShell(), + pageService.getWidgetFactory()) + .setDialogWidth(860); + + if (modifyGrant) { + + final BiConsumer>> actionComposer = (composite, handle) -> { + final WidgetFactory widgetFactory = pageService.getWidgetFactory(); + + final Button save = widgetFactory.buttonLocalized(composite, SAVE_TEXT_KEY); + save.setLayoutData(new RowData()); + save.addListener(SWT.Selection, event -> { + if (doSaveSettings(pageService, pageContext, handle.get())) { + dialog.close(); + } + }); + + }; + + final ScreenProctoringPropertiesForm bindFormContext = new ScreenProctoringPropertiesForm( + pageService, + pageContext); + + dialog.openWithActions( + FORM_TITLE, + actionComposer, + Utils.EMPTY_EXECUTION, + bindFormContext); + } else { + dialog.open( + FORM_TITLE, + pageContext, + pc -> new ScreenProctoringPropertiesForm( + pageService, + pageContext) + .compose(pc.getParent())); + } + + return action; + }; + } + + private boolean doSaveSettings( + final PageService pageService, + final PageContext pageContext, + final FormHandle formHandle) { + + final boolean isReadonly = BooleanUtils.toBoolean( + pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY)); + if (isReadonly) { + return true; + } + + final EntityKey entityKey = pageContext.getEntityKey(); + ScreenProctoringSettings settings = null; + try { + final Form form = formHandle.getForm(); + form.clearErrors(); + + final boolean enabled = BooleanUtils.toBoolean( + form.getFieldValue(ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING)); + final String groupSizeString = form.getFieldValue(ScreenProctoringSettings.ATTR_COLLECTING_GROUP_SIZE); + final int groupSize = StringUtils.isNotBlank(groupSizeString) ? Integer.parseInt(groupSizeString) : 0; + + settings = new ScreenProctoringSettings( + Long.parseLong(entityKey.modelId), + enabled, + form.getFieldValue(ScreenProctoringSettings.ATTR_SPS_SERVICE_URL), + form.getFieldValue(ScreenProctoringSettings.ATTR_SPS_API_KEY), + form.getFieldValue(ScreenProctoringSettings.ATTR_SPS_API_SECRET), + form.getFieldValue(ScreenProctoringSettings.ATTR_SPS_ACCOUNT_ID), + form.getFieldValue(ScreenProctoringSettings.ATTR_SPS_ACCOUNT_PASSWORD), + CollectingStrategy.EXAM, + groupSize); + + } catch (final Exception e) { + log.error("Unexpected error while trying to get settings from form: ", e); + } + + if (settings == null) { + return false; + } + + final Result saveRequest = pageService + .getRestService() + .getBuilder( + entityKey.entityType == EntityType.EXAM + ? SaveScreenProctoringSettings.class + : SaveExamTemplateScreenProctoringSettings.class) + .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) + .withBody(settings) + .call(); + + final boolean saveOk = !saveRequest + .onError(formHandle::handleError) + .hasError(); + + if (saveOk) { + final PageAction action = pageService.pageActionBuilder(pageContext) + .newAction( + entityKey.entityType == EntityType.EXAM + ? ActionDefinition.EXAM_VIEW_FROM_LIST + : ActionDefinition.EXAM_TEMPLATE_VIEW_FROM_LIST) + .create(); + + pageService.firePageEvent( + new ActionEvent(action), + action.pageContext()); + return true; + } + return false; + } + + private final class ScreenProctoringPropertiesForm + implements ModalInputDialogComposer> { + + private final PageService pageService; + private final PageContext pageContext; + + protected ScreenProctoringPropertiesForm( + final PageService pageService, + final PageContext pageContext) { + + this.pageService = pageService; + this.pageContext = pageContext; + } + + @Override + public Supplier> compose(final Composite parent) { + final RestService restService = this.pageService.getRestService(); + final EntityKey entityKey = this.pageContext.getEntityKey(); + + final Composite content = this.pageService + .getWidgetFactory() + .createPopupScrollComposite(parent); + + final PageContext formContext = this.pageContext + .copyOf(content) + .clearEntityKeys(); + + final ScreenProctoringSettings settings = restService + .getBuilder( + entityKey.entityType == EntityType.EXAM + ? GetScreenProctoringSettings.class + : GetExamTemplateScreenProctoringSettings.class) + .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) + .call() + .getOrThrow(); + + final boolean isReadonly = BooleanUtils.toBoolean( + this.pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY)); + + final FormHandle form = this.pageService.formBuilder(formContext) + .withDefaultSpanInput(5) + .withEmptyCellSeparation(true) + .withDefaultSpanEmptyCell(1) + .readonly(isReadonly) + + .addField(FormBuilder.text( + "Info", + FORM_INFO_TITLE, + this.pageService.getI18nSupport().getText(FORM_INFO)) + .asArea(80) + .asHTML() + .readonly(true)) + + .addField(FormBuilder.checkbox( + ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING, + FORM_ENABLE, + String.valueOf(settings.enableScreenProctoring))) + + .addField(FormBuilder.text( + ScreenProctoringSettings.ATTR_SPS_SERVICE_URL, + FORM_URL, + settings.spsServiceURL) + .mandatory()) + + .addField(FormBuilder.text( + ScreenProctoringSettings.ATTR_SPS_API_KEY, + FORM_APPKEY_SPS, + settings.spsAPIKey)) + .withEmptyCellSeparation(false) + + .addField(FormBuilder.password( + ScreenProctoringSettings.ATTR_SPS_API_SECRET, + FORM_APPSECRET_SPS, + (settings.spsAPISecret != null) + ? String.valueOf(settings.spsAPISecret) + : null)) + + .addField(FormBuilder.text( + ScreenProctoringSettings.ATTR_SPS_ACCOUNT_ID, + FORM_ACCOUNT_ID_SPS, + settings.spsAccountId)) + .withEmptyCellSeparation(false) + + .addField(FormBuilder.password( + ScreenProctoringSettings.ATTR_SPS_ACCOUNT_PASSWORD, + FORM_ACCOUNT_SECRET_SPS, + (settings.spsAccountPassword != null) + ? String.valueOf(settings.spsAccountPassword) + : null)) + + .build(); + + return () -> form; + } + } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetScreenProctoringSettings.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetScreenProctoringSettings.java new file mode 100644 index 00000000..f954159a --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetScreenProctoringSettings.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class GetScreenProctoringSettings extends RestCall { + + public GetScreenProctoringSettings() { + super(new TypeKey<>( + CallType.GET_SINGLE, + EntityType.EXAM_PROCTOR_DATA, + new TypeReference() { + }), + HttpMethod.GET, + MediaType.APPLICATION_JSON, + API.EXAM_ADMINISTRATION_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_ADMINISTRATION_SCREEN_PROCTORING_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/SaveScreenProctoringSettings.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/SaveScreenProctoringSettings.java new file mode 100644 index 00000000..2ee9d24e --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/SaveScreenProctoringSettings.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class SaveScreenProctoringSettings extends RestCall { + + public SaveScreenProctoringSettings() { + super(new TypeKey<>( + CallType.SAVE, + EntityType.EXAM_PROCTOR_DATA, + new TypeReference() { + }), + HttpMethod.POST, + MediaType.APPLICATION_JSON, + API.EXAM_ADMINISTRATION_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_ADMINISTRATION_SCREEN_PROCTORING_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/template/GetExamTemplateScreenProctoringSettings.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/template/GetExamTemplateScreenProctoringSettings.java new file mode 100644 index 00000000..2653c40c --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/template/GetExamTemplateScreenProctoringSettings.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class GetExamTemplateScreenProctoringSettings extends RestCall { + + public GetExamTemplateScreenProctoringSettings() { + super(new TypeKey<>( + CallType.GET_SINGLE, + EntityType.EXAM_PROCTOR_DATA, + new TypeReference() { + }), + HttpMethod.GET, + MediaType.APPLICATION_JSON, + API.EXAM_TEMPLATE_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_ADMINISTRATION_SCREEN_PROCTORING_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/template/SaveExamTemplateScreenProctoringSettings.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/template/SaveExamTemplateScreenProctoringSettings.java new file mode 100644 index 00000000..583fdf7c --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/template/SaveExamTemplateScreenProctoringSettings.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class SaveExamTemplateScreenProctoringSettings extends RestCall { + + public SaveExamTemplateScreenProctoringSettings() { + super(new TypeKey<>( + CallType.SAVE, + EntityType.EXAM_PROCTOR_DATA, + new TypeReference() { + }), + HttpMethod.POST, + MediaType.APPLICATION_JSON, + API.EXAM_TEMPLATE_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_ADMINISTRATION_SCREEN_PROCTORING_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java index e4046ce8..e39d0ed1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java @@ -148,7 +148,9 @@ public class WidgetFactory { VERIFY("verify.png"), SHIELD("shield.png"), NO_SHIELD("no_shield.png"), - BACK("back.png"); + BACK("back.png"), + SCREEN_PROC_ON("screen_proc_on.png"), + SCREEN_PROC_OFF("screen_proc_off.png"); public String fileName; private ImageData image = null; 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 2ca332e7..5578eb0a 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 @@ -211,6 +211,16 @@ public interface ExamDAO extends ActivatableEntityDAO, BulkActionSup key = "#examId") Result updateQuizData(Long examId, QuizData quizData, String updateId); + /** Marks the given exam by setting the last-update-time to current time. + * This provokes a cache update on distributed setup. + * Additionally this flushes the local cache immediately. + * + * @param examId the Exam identifier */ + @CacheEvict( + cacheNames = ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM, + key = "#examId") + void markUpdate(Long examId); + /** This is used by the internal update process to mark exams for which the LMS related data availability * * @param externalQuizId The exams external UUID or quiz id of the exam to mark diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ProctoringSettingsDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ProctoringSettingsDAO.java index 43f973cf..011ccaa5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ProctoringSettingsDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ProctoringSettingsDAO.java @@ -27,4 +27,8 @@ public interface ProctoringSettingsDAO { final EntityKey entityKey, final ScreenProctoringSettings screenProctoringSettings); + void disableScreenProctoring(Long examId); + + boolean isScreenProctoringEnabled(Long examId); + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java index 8a54828d..8969e753 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java @@ -238,7 +238,7 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { .build() .execute(); - final List execute = this.clientConnectionRecordMapper + this.clientConnectionRecordMapper .selectByExample() .build() .execute(); @@ -340,8 +340,6 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { final long millisecondsNow = Utils.getMillisecondsNow(); // NOTE: we use nanoseconds here to get a better precision to better avoid // same value of real concurrent calls on distributed systems - // TODO: Better solution for the future would be to count this value and - // isolation is done via DB transaction final long nanosecondsNow = System.nanoTime(); final ClientConnectionRecord newRecord = new ClientConnectionRecord( null, 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 809fe6ba..cf4148cd 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 @@ -159,6 +159,28 @@ public class ExamDAOImpl implements ExamDAO { .map(rec -> saveAdditionalQuizAttributes(examId, quizData)); } + @Override + @Transactional + public void markUpdate(final Long examId) { + + try { + + final long millisecondsNow = Utils.getMillisecondsNow(); + + UpdateDSL.updateWithMapper( + this.examRecordMapper::update, + ExamRecordDynamicSqlSupport.examRecord) + .set(ExamRecordDynamicSqlSupport.lastModified) + .equalTo(millisecondsNow) + .where(ExamRecordDynamicSqlSupport.id, isEqualTo(examId)) + .build() + .execute(); + + } catch (final Exception e) { + log.error("Failed to mark exam for update on distributed setup. exam: {}", examId, e); + } + } + @Override public void markLMSAvailability(final String externalQuizId, final boolean available, final String updateId) { 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 47acbade..cfaeb2ea 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 @@ -235,6 +235,30 @@ public class ProctoringSettingsDAOImpl implements ProctoringSettingsDAO { }); } + @Override + @Transactional + public void disableScreenProctoring(final Long examId) { + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.EXAM, + examId, + ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING, + Constants.FALSE_STRING) + .onError(error -> log.warn( + "Failed to disable screen proctoring for exam: {} error: {}", + examId, + error.getMessage())); + } + + @Override + @Transactional(readOnly = true) + public boolean isScreenProctoringEnabled(final Long examId) { + return this.additionalAttributesDAO.getAdditionalAttribute(EntityType.EXAM, + examId, + ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING) + .map(attrRec -> BooleanUtils.toBoolean(attrRec.getValue())) + .getOr(false); + } + private Boolean getEnabled(final Map mapping) { if (mapping.containsKey(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING)) { return BooleanUtils.toBoolean(mapping.get(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING).getValue()); 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 dea8a095..1daf89a1 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 @@ -136,6 +136,11 @@ public interface ExamAdminService { * @return Result refer to the archived exam or to an error when happened */ Result archiveExam(Exam exam); + /** Gets invoked after an exam has been changed and saved. + * + * @param exam the exam that has been changed and saved */ + void notifyExamSaved(Exam exam); + /** Used to check threshold consistency for a given list of thresholds. * Checks if all values are present (none null value) * Checks if there are duplicates diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ProctoringAdminService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ProctoringAdminService.java index 3f3dbc70..46931f77 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ProctoringAdminService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ProctoringAdminService.java @@ -20,6 +20,7 @@ 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; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType; import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; @@ -70,6 +71,11 @@ public interface ProctoringAdminService { * @return ExamProctoringService instance */ Result getExamProctoringService(final ProctoringServerType type); + /** Gets invoked after an exam has been changed and saved. + * + * @param exam the exam that has been changed and saved */ + void notifyExamSaved(Exam exam); + /** Use this to test the proctoring service settings against the remote proctoring server. * * @param proctoringSettings the settings to test 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 256b3f75..721ac440 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 @@ -299,6 +299,12 @@ public class ExamAdminServiceImpl implements ExamAdminService { .map(settings -> exam); } + @Override + public void notifyExamSaved(final Exam exam) { + updateAdditionalExamConfigAttributes(exam.id); + this.proctoringAdminService.notifyExamSaved(exam); + } + private Result initAdditionalAttributesForMoodleExams(final Exam exam) { return Result.tryCatch(() -> { final LmsAPITemplate lmsTemplate = this.lmsAPIService 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 e4c4c262..6ae0dde6 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 @@ -13,6 +13,7 @@ import org.springframework.stereotype.Service; 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; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType; import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; @@ -92,8 +93,6 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService { checkType(parentEntityKey); - final boolean notifyChangesToService = notifyChangesToService(parentEntityKey, screenProctoringSettings); - this.screenProctoringService .testSettings(screenProctoringSettings) .flatMap(settings -> this.proctoringSettingsDAO.storeScreenProctoringSettings( @@ -101,9 +100,12 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService { screenProctoringSettings)) .getOrThrow(); - if (notifyChangesToService) { + if (parentEntityKey.entityType == EntityType.EXAM) { + this.screenProctoringService - .applyScreenProctoingForExam(screenProctoringSettings) + .applyScreenProctoingForExam(screenProctoringSettings.examId) + .onError(error -> this.proctoringSettingsDAO + .disableScreenProctoring(screenProctoringSettings.examId)) .getOrThrow(); } @@ -111,26 +113,17 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService { }); } - private boolean notifyChangesToService( - final EntityKey entityKey, - final ScreenProctoringSettings newSettings) { - - if (entityKey.entityType != EntityType.EXAM) { - return false; - } - - return this.proctoringSettingsDAO - .getScreenProctoringSettings(entityKey) - .map(oldSettings -> oldSettings.enableScreenProctoring != newSettings.enableScreenProctoring) - .getOr(true); - } - @Override public Result getExamProctoringService(final ProctoringServerType type) { return this.remoteProctoringServiceFactory .getExamProctoringService(type); } + @Override + public void notifyExamSaved(final Exam exam) { + this.screenProctoringService.notifyExamSaved(exam); + } + private void checkType(final EntityKey parentEntityKey) { if (!SUPPORTED_PARENT_ENTITES.contains(parentEntityKey.entityType)) { throw new UnsupportedOperationException( diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java index 458242bc..c78a69bd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockCourseAccessAPI.java @@ -40,6 +40,11 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; public class MockCourseAccessAPI implements CourseAccessAPI { + private static final String startTime10 = DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS) + .toString(Constants.DEFAULT_DATE_TIME_FORMAT); + private static final String endTime10 = DateTime.now(DateTimeZone.UTC).plus(6 * Constants.MINUTE_IN_MILLIS) + .toString(Constants.DEFAULT_DATE_TIME_FORMAT); + private final Collection mockups; private final WebserviceInfo webserviceInfo; private final APITemplateDataSupplier apiTemplateDataSupplier; @@ -85,10 +90,8 @@ public class MockCourseAccessAPI implements CourseAccessAPI { this.mockups.add(new QuizData( "quiz10", institutionId, lmsSetupId, lmsType, "Demo Quiz 10 (MOCKUP)", "Starts in a minute and ends after five minutes", - DateTime.now(DateTimeZone.UTC).plus(Constants.MINUTE_IN_MILLIS) - .toString(Constants.DEFAULT_DATE_TIME_FORMAT), - DateTime.now(DateTimeZone.UTC).plus(6 * Constants.MINUTE_IN_MILLIS) - .toString(Constants.DEFAULT_DATE_TIME_FORMAT), + MockCourseAccessAPI.startTime10, + MockCourseAccessAPI.endTime10, "http://lms.mockup.com/api/")); this.mockups.add(new QuizData( "quiz11", institutionId, lmsSetupId, lmsType, "Demo Quiz 11 (MOCKUP)", 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 9f613a42..5cf5f3f7 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 @@ -34,17 +34,26 @@ public interface ScreenProctoringService extends SessionUpdateTask { * @return Result refer to the settings or to an error when happened */ Result testSettings(ScreenProctoringSettings settings); - //Result saveSettingsForExam(ScreenProctoringSettings settings); - - /** This applies the screen proctoring for the given exam. + /** This applies the stored screen proctoring for the given exam. * If screen proctoring for the exam is enabled, this initializes or re-activate all * needed things for screen proctoring for the given exam. * If screen proctoring is set to disable, this disables the whole service for the * given exam also on SPS side. * - * @param settings the actual ScreenProctoringSettings of the exam - * @return Result refer to the given ScreenProctoringSettings or to an error when happened */ - Result applyScreenProctoingForExam(ScreenProctoringSettings settings); + * @param examId use the screen proctoring settings of the exam with the given exam id + * @return Result refer to the given Exam or to an error when happened */ + Result applyScreenProctoingForExam(Long examId); + + /** Gets invoked after an exam has been changed and saved. + * + * @param exam the exam that has been changed and saved */ + void notifyExamSaved(Exam exam); + + @EventListener(ExamStartedEvent.class) + void notifyExamStarted(ExamStartedEvent event); + + @EventListener(ExamFinishedEvent.class) + void notifyExamFinished(ExamFinishedEvent event); /** This is been 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. diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java index 9d209435..67531192 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionCacheService.java @@ -18,7 +18,6 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; -import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -52,7 +51,6 @@ public class ExamSessionCacheService { private final ClientConnectionDAO clientConnectionDAO; private final InternalClientConnectionDataFactory internalClientConnectionDataFactory; private final ExamConfigService sebExamConfigService; - private final ExamUpdateHandler examUpdateHandler; protected ExamSessionCacheService( final ExamDAO examDAO, @@ -60,7 +58,6 @@ public class ExamSessionCacheService { final ClientConnectionDAO clientConnectionDAO, final InternalClientConnectionDataFactory internalClientConnectionDataFactory, final ExamConfigService sebExamConfigService, - final ExamUpdateHandler examUpdateHandler, final RemoteProctoringRoomDAO remoteProctoringRoomDAO) { this.examDAO = examDAO; @@ -68,7 +65,6 @@ public class ExamSessionCacheService { this.clientConnectionDAO = clientConnectionDAO; this.internalClientConnectionDataFactory = internalClientConnectionDataFactory; this.sebExamConfigService = sebExamConfigService; - this.examUpdateHandler = examUpdateHandler; } @Cacheable( @@ -132,9 +128,11 @@ public class ExamSessionCacheService { } case UP_COMING: case FINISHED: { - return this.examUpdateHandler.updateRunning(exam.id) - .map(e -> e.status == ExamStatus.RUNNING) - .getOr(false); + return false; + // TODO do we really need to double-check here? +// return this.examUpdateHandler.updateRunning(exam.id) +// .map(e -> e.status == ExamStatus.RUNNING) +// .getOr(false); } default: { return false; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java index f84127e7..35821f08 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java @@ -532,6 +532,9 @@ public class ExamSessionServiceImpl implements ExamSessionService { @Override public Result updateExamCache(final Long examId) { + // TODO check how often this is called in distributed environments + System.out.println("************** performance check: updateExamCache"); + try { final Cache cache = this.cacheManager.getCache(ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM); final ValueWrapper valueWrapper = cache.get(examId); @@ -547,7 +550,8 @@ public class ExamSessionServiceImpl implements ExamSessionService { return Result.ofEmpty(); } - final Boolean isUpToDate = this.examDAO.upToDate(exam) + final Boolean isUpToDate = this.examDAO + .upToDate(exam) .onError(t -> log.error("Failed to verify if cached exam is up to date: {}", exam, t)) .getOr(false); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateEvent.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateEvent.java new file mode 100644 index 00000000..5740e800 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateEvent.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; + +import org.springframework.context.ApplicationEvent; + +public class ExamUpdateEvent extends ApplicationEvent { + + private static final long serialVersionUID = -367779249954953708L; + + public final Long examId; + + public ExamUpdateEvent(final Long examId) { + super(examId); + this.examId = examId; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java index 506c7f21..3a78d902 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamUpdateHandler.java @@ -22,6 +22,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import ch.ethz.seb.sebserver.gbl.api.EntityType; @@ -216,18 +217,16 @@ class ExamUpdateHandler implements ExamUpdateTask { }); } - Result updateRunning(final Long examId) { - return this.examDAO.byPK(examId) - .map(exam -> { - final DateTime now = DateTime.now(DateTimeZone.UTC); - if (exam.getStatus() == ExamStatus.UP_COMING - && exam.endTime.plus(this.examTimeSuffix).isBefore(now)) { - return setRunning(exam, this.createUpdateId()) - .getOr(exam); - } else { - return exam; - } - }); + @EventListener(ExamUpdateEvent.class) + void updateRunning(final ExamUpdateEvent event) { + this.examDAO + .byPK(event.examId) + .onSuccess(exam -> updateState( + exam, + DateTime.now(DateTimeZone.UTC), + this.examTimePrefix, + this.examTimeSuffix, + this.createUpdateId())); } void updateState( 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 d50689b8..dc4f9c11 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 @@ -15,6 +15,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpEntity; @@ -27,12 +29,18 @@ import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails; import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClientResponseException; import org.springframework.web.util.UriComponentsBuilder; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException; @@ -40,7 +48,6 @@ import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; -import ch.ethz.seb.sebserver.gbl.client.ClientCredentialServiceImpl; import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.exam.CollectingStrategy; @@ -48,27 +55,34 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; +import ch.ethz.seb.sebserver.gbl.model.user.UserMod; 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.datalayer.batis.model.ClientConnectionRecord; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ProctoringSettingsDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ProctoringSettingsDAOImpl; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ScreenProctoringAPIBinding.SPS_API.ExamUpdate; class ScreenProctoringAPIBinding { private static final Logger log = LoggerFactory.getLogger(ScreenProctoringAPIBinding.class); - private static final String SEB_SERVER_SCREEN_PROCTORING_USER_PREFIX = "SEBServer_User_"; + //private static final String SEB_SERVER_SCREEN_PROCTORING_USER_PREFIX = "SEBServer_User_"; private static final String SEB_SERVER_SCREEN_PROCTORING_SEB_ACCESS_PREFIX = "SEBServer_SEB_Access_"; static interface SPS_API { + String PROCTOR_ROLE = "PROCTOR"; + String TOKEN_ENDPOINT = "/oauth/token"; String TEST_ENDPOINT = "/admin-api/v1/proctoring/group"; - String USER_ENDPOINT = "/admin-api/v1/useraccount"; - String ENTIY_PRIVILEGES_ENDPOINT = USER_ENDPOINT + "/entityprivilege"; + //String USER_ENDPOINT = "/admin-api/v1/useraccount"; + public static final String USERSYNC_SEBSERVER_ENDPOINT = "/admin-api/v1/useraccount/usersync/sebserver"; + String ENTIY_PRIVILEGES_ENDPOINT = "/admin-api/v1/useraccount/entityprivilege"; String EXAM_ENDPOINT = "/admin-api/v1/exam"; String SEB_ACCESS_ENDPOINT = "/admin-api/v1/clientaccess"; String GROUP_ENDPOINT = "/admin-api/v1/group"; @@ -76,16 +90,6 @@ class ScreenProctoringAPIBinding { String ACTIVE_PATH_SEGMENT = "/active"; String INACTIVE_PATH_SEGMENT = "/inactive"; - interface SEB_SERVER_EXAM_SETTINGS { - String ATTR_SPS_USER_UUID = "spsUserUUID"; - String ATTR_SPS_USER_NAME = "spsUserName"; - String ATTR_SPS_USER_PWD = "spsUserPWD"; - String ATTR_SPS_SEB_ACCESS_UUID = "spsSEBAccesUUID"; - String ATTR_SPS_SEB_ACCESS_NAME = "spsSEBAccessName"; - String ATTR_SPS_SEB_ACCESS_PWD = "spsSEBAccessPWD"; - String ATTR_SPS_EXAM_UUID = "spsExamUUID"; - } - interface PRIVILEGE_FLAGS { String READ = "r"; String MODIFY = "m"; @@ -98,7 +102,8 @@ class ScreenProctoringAPIBinding { String ATTR_NAME = "name"; String ATTR_SURNAME = "surname"; String ATTR_USERNAME = "username"; - String ATTR_PASSWORD = "password"; + String ATTR_PASSWORD = "newPassword"; + String ATTR_CONFIRM_PASSWORD = "confirmNewPassword"; String ATTR_LANGUAGE = "language"; String ATTR_TIMEZONE = "timeZone"; String ATTR_ROLES = "roles"; @@ -108,6 +113,7 @@ class ScreenProctoringAPIBinding { String ATTR_ENTITY_TYPE = "entityType"; String ATTR_ENTITY_ID = "entityId"; String ATTR_USER_UUID = "userUuid"; + String ATTR_USERNAME = "username"; String ATTR_PRIVILEGES = "privileges"; } @@ -149,13 +155,45 @@ class ScreenProctoringAPIBinding { String ATTR_CLIENT_OS_NAME = "clientOsName"; String ATTR_CLIENT_VERSION = "clientVersion"; } + + @JsonIgnoreProperties(ignoreUnknown = true) + static final class ExamUpdate { + @JsonProperty(EXAM.ATTR_NAME) + final String name; + @JsonProperty(EXAM.ATTR_DESCRIPTION) + final String description; + @JsonProperty(EXAM.ATTR_URL) + final String url; + @JsonProperty(EXAM.ATTR_TYPE) + final String type; + @JsonProperty(EXAM.ATTR_START_TIME) + final Long startTime; + @JsonProperty(EXAM.ATTR_END_TIME) + final Long endTime; + + public ExamUpdate( + final String name, + final String description, + final String url, + final String type, + final Long startTime, + final Long endTime) { + + this.name = name; + this.description = description; + this.url = url; + this.type = type; + this.startTime = startTime; + this.endTime = endTime; + } + } } private final UserDAO userDAO; private final Cryptor cryptor; private final AsyncService asyncService; private final JSONMapper jsonMapper; - private final ProctoringSettingsDAOImpl proctoringSettingsSupplier; + private final ProctoringSettingsDAO proctoringSettingsDAO; private final AdditionalAttributesDAO additionalAttributesDAO; ScreenProctoringAPIBinding( @@ -163,14 +201,14 @@ class ScreenProctoringAPIBinding { final Cryptor cryptor, final AsyncService asyncService, final JSONMapper jsonMapper, - final ProctoringSettingsDAOImpl proctoringSettingsSupplier, + final ProctoringSettingsDAO proctoringSettingsDAO, final AdditionalAttributesDAO additionalAttributesDAO) { this.userDAO = userDAO; this.cryptor = cryptor; this.asyncService = asyncService; this.jsonMapper = jsonMapper; - this.proctoringSettingsSupplier = proctoringSettingsSupplier; + this.proctoringSettingsDAO = proctoringSettingsDAO; this.additionalAttributesDAO = additionalAttributesDAO; } @@ -187,7 +225,7 @@ class ScreenProctoringAPIBinding { if (result.getStatusCode().is4xxClientError()) { throw new FieldValidationException( "serverURL", - "proctoringSettings:serverURL:url.noAccess"); + "screenProctoringSettings:spsServiceURL:url.noAccess"); } throw new RuntimeException("Invalid SEB Screen Proctoring Service response: " + result); } @@ -204,6 +242,42 @@ class ScreenProctoringAPIBinding { }); } + public boolean isSPSActive(final Exam exam) { + try { + final String active = this.additionalAttributesDAO + .getAdditionalAttribute( + EntityType.EXAM, + exam.id, + SPSData.ATTR_SPS_ACTIVE) + .getOrThrow() + .getValue(); + return BooleanUtils.toBoolean(active); + } catch (final Exception e) { + return false; + } + } + + private SPSData getSPSData(final Long examId) { + try { + + final String dataEncrypted = this.additionalAttributesDAO + .getAdditionalAttribute( + EntityType.EXAM, + examId, + SPSData.ATTR_SPS_ACCESS_DATA) + .getOrThrow() + .getValue(); + + return this.jsonMapper.readValue( + this.cryptor.decrypt(dataEncrypted).getOrThrow().toString(), + SPSData.class); + + } catch (final Exception e) { + log.error("Failed to get local SPSData for exam: {}", examId); + return null; + } + } + /** This is called when an exam goes in running state * If the needed resources on SPS side has been already created before, this just reactivates * all resources on SPS side. @@ -221,28 +295,67 @@ class ScreenProctoringAPIBinding { final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(exam.id); - if (existsExamOnSPS(exam, apiTemplate)) { + if (exam.additionalAttributes.containsKey(SPSData.ATTR_SPS_ACTIVE)) { log.info("SPS Exam for SEB Server Exam: {} already exists. Try to re-activate", exam.externalId); + final SPSData spsData = this.getSPSData(exam.id); // re-activate all needed entities on SPS side - activation(exam, SPS_API.USER_ENDPOINT, getSPSUserUUID(exam), true, apiTemplate); - activation(exam, SPS_API.SEB_ACCESS_ENDPOINT, getSPSSEBAccessUUID(exam), true, apiTemplate); - activation(exam, SPS_API.EXAM_ENDPOINT, getSPSExamUUID(exam), true, apiTemplate); + activation(exam, SPS_API.SEB_ACCESS_ENDPOINT, spsData.spsSEBAccesUUID, true, apiTemplate); + activation(exam, SPS_API.EXAM_ENDPOINT, spsData.spsExamUUID, true, apiTemplate); + + // mark successfully activated on SPS side + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.EXAM, + exam.id, + SPSData.ATTR_SPS_ACTIVE, + Constants.TRUE_STRING); return Collections.emptyList(); } - final String spsUserUUID = createExamUser(exam, apiTemplate); - createSEBAccess(exam, apiTemplate); - final String examUUID = createExam(exam, apiTemplate); + final SPSData spsData = new SPSData(); - createExamReadPrivilege(apiTemplate, spsUserUUID, examUUID); + log.info( + "SPS Exam for SEB Server Exam: {} don't exists yet, create necessary structures on SPS", + exam.externalId); - return initializeGroups(exam, examUUID, apiTemplate); + exam.supporter.forEach(userUUID -> synchronizeUserAccount(userUUID, apiTemplate, spsData)); + createSEBAccess(exam, apiTemplate, spsData); + createExam(exam, apiTemplate, spsData); + exam.supporter.forEach(userUUID -> createExamReadPrivilege(userUUID, spsData.spsExamUUID, apiTemplate)); + 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; }); } + public 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, spsData)); + } catch (final Exception e) { + log.error("Failed to synchronize user accounts with SPS for exam: {}", exam); + } + } + /** This is called when an exam has changed its parameter and needs data update on SPS side * * @param exam The exam @@ -250,27 +363,31 @@ class ScreenProctoringAPIBinding { public Result updateExam(final Exam exam) { return Result.tryCatch(() -> { - final String spsExamUUID = getSPSExamUUID(exam); + final SPSData spsData = this.getSPSData(exam.id); final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(exam.id); - UriComponentsBuilder uriBuilder = UriComponentsBuilder + final String uri = UriComponentsBuilder .fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL) .path(SPS_API.EXAM_ENDPOINT) - .path(spsExamUUID) - .queryParam(SPS_API.EXAM.ATTR_NAME, exam.name) - .queryParam(SPS_API.EXAM.ATTR_DESCRIPTION, exam.getDescription()) - .queryParam(SPS_API.EXAM.ATTR_URL, exam.getStartURL()) - .queryParam(SPS_API.EXAM.ATTR_TYPE, exam.getType().name()) - .queryParam(SPS_API.EXAM.ATTR_START_TIME, String.valueOf(exam.startTime.getMillis())); + .pathSegment(spsData.spsExamUUID) + .build() + .toUriString(); - if (exam.endTime != null) { - uriBuilder = - uriBuilder.queryParam(SPS_API.EXAM.ATTR_END_TIME, String.valueOf(exam.endTime.getMillis())); - } + final ExamUpdate examUpdate = new ExamUpdate( + exam.name, + exam.getDescription(), + exam.getStartURL(), + exam.getType().name(), + exam.startTime.getMillis(), + exam.endTime.getMillis()); - final String uri = uriBuilder.build().toUriString(); + final String jsonExamUpdate = this.jsonMapper.writeValueAsString(examUpdate); - final ResponseEntity exchange = apiTemplate.exchange(uri, HttpMethod.PUT); + final ResponseEntity exchange = apiTemplate.exchange( + uri, + HttpMethod.PUT, + jsonExamUpdate, + apiTemplate.getHeadersJSONRequest()); if (exchange.getStatusCode() != HttpStatus.OK) { log.error("Failed to update SPS exam data: {}", exchange); } @@ -292,9 +409,16 @@ class ScreenProctoringAPIBinding { log.debug("Dispose active screen proctoring exam, groups and access on SPS for exam: {}", exam); } - activation(exam, SPS_API.EXAM_ENDPOINT, getSPSExamUUID(exam), false, this.apiTemplate); - activation(exam, SPS_API.SEB_ACCESS_ENDPOINT, getSPSSEBAccessUUID(exam), false, this.apiTemplate); - activation(exam, SPS_API.USER_ENDPOINT, getSPSUserUUID(exam), false, this.apiTemplate); + final SPSData spsData = this.getSPSData(exam.id); + activation(exam, SPS_API.EXAM_ENDPOINT, spsData.spsExamUUID, false, this.apiTemplate); + activation(exam, SPS_API.SEB_ACCESS_ENDPOINT, spsData.spsSEBAccesUUID, false, this.apiTemplate); + + // mark successfully dispose on SPS side + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.EXAM, + exam.id, + SPSData.ATTR_SPS_ACTIVE, + Constants.FALSE_STRING); return exam; }); @@ -309,12 +433,24 @@ class ScreenProctoringAPIBinding { return Result.tryCatch(() -> { + if (!BooleanUtils.toBoolean(exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) { + return exam; + } + if (log.isDebugEnabled()) { log.debug("Delete screen proctoring exam, groups and access on SPS for exam: {}", exam); } - deletion(exam, SPS_API.SEB_ACCESS_ENDPOINT, getSPSSEBAccessUUID(exam), this.apiTemplate); - deletion(exam, SPS_API.USER_ENDPOINT, getSPSUserUUID(exam), this.apiTemplate); + final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(exam.id); + final SPSData spsData = this.getSPSData(exam.id); + deletion(SPS_API.SEB_ACCESS_ENDPOINT, spsData.spsSEBAccesUUID, apiTemplate); + + // mark successfully dispose on SPS side + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.EXAM, + exam.id, + SPSData.ATTR_SPS_ACTIVE, + Constants.FALSE_STRING); return exam; }); @@ -344,25 +480,6 @@ class ScreenProctoringAPIBinding { }); } - public Result getSEBClientCredentials(final Long examId) { - return Result.tryCatch(() -> { - - final String clientName = this.additionalAttributesDAO.getAdditionalAttribute( - EntityType.EXAM, - examId, - SPS_API.SEB_SERVER_EXAM_SETTINGS.ATTR_SPS_SEB_ACCESS_NAME) - .getOrThrow() - .getValue(); - final String clientSecret = this.additionalAttributesDAO.getAdditionalAttribute(EntityType.EXAM, - examId, - SPS_API.SEB_SERVER_EXAM_SETTINGS.ATTR_SPS_SEB_ACCESS_PWD) - .getOrThrow() - .getValue(); - - return new ClientCredentials(clientName, clientSecret); - }); - } - public String createSEBSession( final Long examId, final ScreenProctoringGroup localGroup, @@ -374,84 +491,198 @@ class ScreenProctoringAPIBinding { final String uri = UriComponentsBuilder .fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL) .path(SPS_API.SESSION_ENDPOINT) - .queryParam(SPS_API.SESSION.ATTR_UUID, token) - .queryParam(SPS_API.SESSION.ATTR_GROUP_ID, localGroup.uuid) - - .queryParam(SPS_API.SESSION.ATTR_CLIENT_IP, clientConnection.getClientAddress()) - .queryParam(SPS_API.SESSION.ATTR_CLIENT_NAME, clientConnection.getExamUserSessionId()) - .queryParam(SPS_API.SESSION.ATTR_CLIENT_MACHINE_NAME, clientConnection.getClientMachineName()) - .queryParam(SPS_API.SESSION.ATTR_CLIENT_OS_NAME, clientConnection.getClientOsName()) - .queryParam(SPS_API.SESSION.ATTR_CLIENT_VERSION, clientConnection.getClientVersion()) .build() .toUriString(); - final ResponseEntity exchange = apiTemplate.exchange(uri, HttpMethod.POST); + final MultiValueMap params = new LinkedMultiValueMap<>(); + params.add(SPS_API.SESSION.ATTR_UUID, token); + params.add(SPS_API.SESSION.ATTR_GROUP_ID, localGroup.uuid); + params.add(SPS_API.SESSION.ATTR_CLIENT_IP, clientConnection.getClientAddress()); + params.add(SPS_API.SESSION.ATTR_CLIENT_NAME, clientConnection.getExamUserSessionId()); + params.add(SPS_API.SESSION.ATTR_CLIENT_MACHINE_NAME, clientConnection.getClientMachineName()); + params.add(SPS_API.SESSION.ATTR_CLIENT_OS_NAME, clientConnection.getClientOsName()); + 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); if (exchange.getStatusCode() != HttpStatus.OK) { throw new RuntimeException( "Failed to create SPS SEB session for SEB connection: " + token); } return token; - } - private void createExamReadPrivilege(final ScreenProctoringServiceOAuthTemplate apiTemplate, - final String spsUserUUID, final String examUUID) { - final String uri = UriComponentsBuilder - .fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL) - .path(SPS_API.ENTIY_PRIVILEGES_ENDPOINT) - .queryParam(SPS_API.ENTITY_PRIVILEGE.ATTR_ENTITY_TYPE, EntityType.EXAM.name()) - .queryParam(SPS_API.ENTITY_PRIVILEGE.ATTR_ENTITY_ID, examUUID) - .queryParam(SPS_API.ENTITY_PRIVILEGE.ATTR_USER_UUID, spsUserUUID) - .queryParam(SPS_API.ENTITY_PRIVILEGE.ATTR_PRIVILEGES, SPS_API.PRIVILEGE_FLAGS.READ) - .build() - .toUriString(); + public void activateSEBAccessOnSPS(final Exam exam, final boolean activate) { + try { + final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(exam.id); + final SPSData spsData = this.getSPSData(exam.id); - final ResponseEntity exchange = apiTemplate.exchange(uri, HttpMethod.POST); - if (exchange.getStatusCode() != HttpStatus.OK) { - throw new RuntimeException("Failed to apply entity read privilege to SPS exam: " + examUUID); + activation(exam, SPS_API.SEB_ACCESS_ENDPOINT, spsData.spsSEBAccesUUID, activate, apiTemplate); + + } catch (final Exception e) { + log.error("Failed to de/activate SEB Access on SPS for exam: {}", exam); + } + } + + public void createExamReadPrivileges(final Exam exam) { + try { + final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(exam.id); + final SPSData spsData = this.getSPSData(exam.id); + + exam.supporter.forEach(userUUID -> createExamReadPrivilege(userUUID, spsData.spsExamUUID, apiTemplate)); + + } catch (final Exception e) { + log.error("Failed to synchronize user accounts exam privileges with SPS for exam: {}", exam); + } + } + + private void synchronizeUserAccount( + final String userUUID, + final ScreenProctoringServiceOAuthTemplate apiTemplate, + final SPSData spsData) { + + try { + + final UserInfo userInfo = this.userDAO + .byModelId(userUUID) + .getOrThrow(); + final SEBServerUser accountInfo = this.userDAO + .sebServerUserByUsername(userInfo.name) + .getOrThrow(); + + final UserMod userMod = new UserMod( + userInfo.uuid, + -1L, + userInfo.name, + userInfo.surname, + userInfo.username, + accountInfo.getPassword(), + accountInfo.getPassword(), + userInfo.email, + userInfo.language, + userInfo.timeZone, + userInfo.roles); + + final String uri = UriComponentsBuilder + .fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL) + .path(SPS_API.USERSYNC_SEBSERVER_ENDPOINT) + .build() + .toUriString(); + + final String jsonBody = this.jsonMapper.writeValueAsString(userMod); + + final ResponseEntity exchange = apiTemplate.exchange( + uri, + HttpMethod.POST, + jsonBody, + apiTemplate.getHeadersJSONRequest()); + if (exchange.getStatusCode() != HttpStatus.OK) { + log.warn("Failed to synchronize user account on SPS: {}", exchange); + } else { + log.info("Successfully synchronize user account on SPS for user: "); + + } + + } catch (final Exception e) { + log.error("Failed to synchronize user account with SPS for user: {}", userUUID); + } + } + + private void createExamReadPrivilege( + final String userUUID, + final String examUUID, + final ScreenProctoringServiceOAuthTemplate apiTemplate) { + + try { + + final UserInfo userInfo = this.userDAO + .byModelId(userUUID) + .getOrThrow(); + + final String uri = UriComponentsBuilder + .fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL) + .path(SPS_API.ENTIY_PRIVILEGES_ENDPOINT) + .build() + .toUriString(); + + final MultiValueMap params = new LinkedMultiValueMap<>(); + params.add(SPS_API.ENTITY_PRIVILEGE.ATTR_ENTITY_TYPE, EntityType.EXAM.name()); + params.add(SPS_API.ENTITY_PRIVILEGE.ATTR_ENTITY_ID, examUUID); + params.add(SPS_API.ENTITY_PRIVILEGE.ATTR_USERNAME, userInfo.username); + params.add(SPS_API.ENTITY_PRIVILEGE.ATTR_PRIVILEGES, SPS_API.PRIVILEGE_FLAGS.READ); + final String paramsFormEncoded = Utils.toAppFormUrlEncodedBody(params); + + final ResponseEntity exchange = apiTemplate.exchange(uri, paramsFormEncoded, HttpMethod.POST); + if (exchange.getStatusCode() != HttpStatus.OK) { + log.warn( + "Failed to apply exam read privilege on SPS side for exam: {} and user: {}", + examUUID, + userUUID); + } else { + log.info( + "Successfully apply exam read privilege on SPS side for exam: {} and user: {}", + examUUID, + userUUID); + } + } catch (final Exception e) { + log.error( + "Failed to apply exam read privilege on SPS side for exam: {} and user: {} error: {}", + examUUID, + userUUID, + e.getMessage()); } } private Collection initializeGroups( final Exam exam, - final String spsExamUUID, - final ScreenProctoringServiceOAuthTemplate apiTemplate) - throws JsonMappingException, JsonProcessingException { + final ScreenProctoringServiceOAuthTemplate apiTemplate, + final SPSData spsData) { - final List result = new ArrayList<>(); + try { - switch (apiTemplate.screenProctoringSettings.collectingStrategy) { + final List result = new ArrayList<>(); - case FIX_SIZE: { - result.add(createGroupOnSPS( - apiTemplate.screenProctoringSettings.collectingGroupSize, - exam.id, - "Group 1 : " + exam.getName(), - "Created by SEB Server", - spsExamUUID, - apiTemplate)); - break; - } - case SEB_GROUP: { - // TODO - throw new UnsupportedOperationException("SEB_GROUP based group collection is not supported yet"); - } - case EXAM: - default: { - result.add(createGroupOnSPS( - 0, - exam.id, - exam.getName(), - "Created by SEB Server", - spsExamUUID, - apiTemplate)); - break; + switch (apiTemplate.screenProctoringSettings.collectingStrategy) { + + case FIX_SIZE: { + result.add(createGroupOnSPS( + apiTemplate.screenProctoringSettings.collectingGroupSize, + exam.id, + "Group 1 : " + exam.getName(), + "Created by SEB Server", + spsData.spsExamUUID, + apiTemplate)); + break; + } + case SEB_GROUP: { + // TODO + throw new UnsupportedOperationException("SEB_GROUP based group collection is not supported yet"); + } + case EXAM: + default: { + result.add(createGroupOnSPS( + 0, + exam.id, + exam.getName(), + "Created by SEB Server", + spsData.spsExamUUID, + apiTemplate)); + break; + } } + + return result; + + } catch (final Exception e) { + log.error( + "Failed to initialize SPS Groups for screen proctoring. perform rollback. exam: {} error: {}", + exam, + e.getMessage()); + rollbackOnSPS(exam, spsData, apiTemplate); + throw new RuntimeException("Failed to apply screen proctoring:", e); } - - return result; } private ScreenProctoringGroup createGroupOnSPS( @@ -466,13 +697,15 @@ class ScreenProctoringAPIBinding { final String uri = UriComponentsBuilder .fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL) .path(SPS_API.GROUP_ENDPOINT) - .queryParam(SPS_API.GROUP.ATTR_NAME, name) - .queryParam(SPS_API.GROUP.ATTR_DESCRIPTION, description) - .queryParam(SPS_API.GROUP.ATTR_EXAM_ID, spsExamUUID) .build() .toUriString(); + final MultiValueMap params = new LinkedMultiValueMap<>(); + params.add(SPS_API.GROUP.ATTR_NAME, name); + params.add(SPS_API.GROUP.ATTR_DESCRIPTION, description); + params.add(SPS_API.GROUP.ATTR_EXAM_ID, spsExamUUID); + final String paramsFormEncoded = Utils.toAppFormUrlEncodedBody(params); - final ResponseEntity exchange = apiTemplate.exchange(uri, HttpMethod.POST); + final ResponseEntity exchange = apiTemplate.exchange(uri, paramsFormEncoded, HttpMethod.POST); if (exchange.getStatusCode() != HttpStatus.OK) { throw new RuntimeException("Failed to create SPS SEB group for exam: " + spsExamUUID); } @@ -487,162 +720,89 @@ class ScreenProctoringAPIBinding { return new ScreenProctoringGroup(null, examId, spsGroupUUID, name, size, exchange.getBody()); } - private String createExam( + private void createExam( final Exam exam, - final ScreenProctoringServiceOAuthTemplate apiTemplate) - throws JsonMappingException, JsonProcessingException { + final ScreenProctoringServiceOAuthTemplate apiTemplate, + final SPSData spsData) { - UriComponentsBuilder uriBuilder = UriComponentsBuilder - .fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL) - .path(SPS_API.EXAM_ENDPOINT) - .queryParam(SPS_API.EXAM.ATTR_NAME, exam.name) - .queryParam(SPS_API.EXAM.ATTR_DESCRIPTION, exam.getDescription()) - .queryParam(SPS_API.EXAM.ATTR_URL, exam.getStartURL()) - .queryParam(SPS_API.EXAM.ATTR_TYPE, exam.getType().name()) - .queryParam(SPS_API.EXAM.ATTR_START_TIME, String.valueOf(exam.startTime.getMillis())); + try { - if (exam.endTime != null) { - uriBuilder = - uriBuilder.queryParam(SPS_API.EXAM.ATTR_END_TIME, String.valueOf(exam.endTime.getMillis())); + final String uri = UriComponentsBuilder + .fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL) + .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()); + 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 paramsFormEncoded = Utils.toAppFormUrlEncodedBody(params); + + final ResponseEntity exchange = apiTemplate.exchange(uri, paramsFormEncoded, HttpMethod.POST); + if (exchange.getStatusCode() != HttpStatus.OK) { + log.error("Failed to update SPS exam data: {}", exchange); + } + + final JsonNode requestJSON = this.jsonMapper.readTree(exchange.getBody()); + spsData.spsExamUUID = requestJSON.get(SPS_API.EXAM.ATTR_UUID).textValue(); + + } catch (final Exception e) { + log.error( + "Failed to create ad-hoc SPS Exam for screen proctoring. perform rollback. exam: {} error: {}", + exam, + e.getMessage()); + rollbackOnSPS(exam, spsData, apiTemplate); + throw new RuntimeException("Failed to apply screen proctoring:", e); } - - final String uri = uriBuilder.build().toUriString(); - - final ResponseEntity exchange = apiTemplate.exchange(uri, HttpMethod.POST); - if (exchange.getStatusCode() != HttpStatus.OK) { - log.error("Failed to update SPS exam data: {}", exchange); - } - - final Map userAttributes = this.jsonMapper.readValue( - exchange.getBody(), - new TypeReference>() { - }); - - final String spsExamUUID = userAttributes.get(SPS_API.EXAM.ATTR_UUID); - this.additionalAttributesDAO.saveAdditionalAttribute( - EntityType.EXAM, - exam.id, - SPS_API.SEB_SERVER_EXAM_SETTINGS.ATTR_SPS_EXAM_UUID, - spsExamUUID); - - return spsExamUUID; } - private String createSEBAccess( + private void createSEBAccess( final Exam exam, - final ScreenProctoringServiceOAuthTemplate apiTemplate) - throws JsonMappingException, JsonProcessingException { + final ScreenProctoringServiceOAuthTemplate apiTemplate, + final SPSData spsData) { - final String name = SEB_SERVER_SCREEN_PROCTORING_SEB_ACCESS_PREFIX + exam.id; - final String description = "This SEB access was autogenerated by SEB Server"; + try { + final String name = SEB_SERVER_SCREEN_PROCTORING_SEB_ACCESS_PREFIX + exam.externalId; + final String description = "This SEB access was auto-generated by SEB Server"; - final String uri = UriComponentsBuilder - .fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL) - .path(SPS_API.SEB_ACCESS_ENDPOINT) - .queryParam(SPS_API.SEB_ACCESS.ATTR_NAME, name) - .queryParam(SPS_API.SEB_ACCESS.ATTR_DESCRIPTION, description) - .build() - .toUriString(); + final String uri = UriComponentsBuilder + .fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL) + .path(SPS_API.SEB_ACCESS_ENDPOINT) + .build() + .toUriString(); - final ResponseEntity exchange = apiTemplate.exchange(uri, HttpMethod.POST); - if (exchange.getStatusCode() != HttpStatus.OK) { - throw new RuntimeException("Failed to create SPS SEB access for exam: " + exam.externalId); + final MultiValueMap params = new LinkedMultiValueMap<>(); + params.add(SPS_API.SEB_ACCESS.ATTR_NAME, name); + params.add(SPS_API.SEB_ACCESS.ATTR_DESCRIPTION, description); + final String paramsFormEncoded = Utils.toAppFormUrlEncodedBody(params); + + final ResponseEntity exchange = apiTemplate.exchange(uri, paramsFormEncoded, HttpMethod.POST); + if (exchange.getStatusCode() != HttpStatus.OK) { + throw new RuntimeException("Failed to create SPS SEB access for exam: " + exam.externalId); + } + ; + + // store SEB access data for proctoring along with the exam + final JsonNode requestJSON = this.jsonMapper.readTree(exchange.getBody()); + spsData.spsSEBAccesUUID = requestJSON.get(SPS_API.SEB_ACCESS.ATTR_UUID).textValue(); + spsData.spsSEBAccessName = requestJSON.get(SPS_API.SEB_ACCESS.ATTR_CLIENT_NAME).textValue(); + spsData.spsSEBAccessPWD = requestJSON.get(SPS_API.SEB_ACCESS.ATTR_CLIENT_SECRET).textValue(); + + } catch (final Exception e) { + log.error( + "Failed to create ad-hoc SEB Access for screen proctoring. perform rollback. exam: {} error: {}", + exam, + e.getMessage()); + rollbackOnSPS(exam, spsData, apiTemplate); + throw new RuntimeException("Failed to apply screen proctoring:", e); } - final Map userAttributes = this.jsonMapper.readValue( - exchange.getBody(), - new TypeReference>() { - }); - - final String sebAccessUUID = userAttributes.get(SPS_API.SEB_ACCESS.ATTR_UUID); - - // store SEB access data for proctoring along with the exam - this.additionalAttributesDAO.saveAdditionalAttribute( - EntityType.EXAM, - exam.id, - SPS_API.SEB_SERVER_EXAM_SETTINGS.ATTR_SPS_SEB_ACCESS_UUID, - sebAccessUUID); - this.additionalAttributesDAO.saveAdditionalAttribute( - EntityType.EXAM, - exam.id, - SPS_API.SEB_SERVER_EXAM_SETTINGS.ATTR_SPS_SEB_ACCESS_NAME, - userAttributes.get(SPS_API.SEB_ACCESS.ATTR_CLIENT_NAME)); - this.additionalAttributesDAO.saveAdditionalAttribute( - EntityType.EXAM, - exam.id, - SPS_API.SEB_SERVER_EXAM_SETTINGS.ATTR_SPS_SEB_ACCESS_PWD, - this.cryptor.encrypt(userAttributes.get(SPS_API.SEB_ACCESS.ATTR_CLIENT_SECRET)).toString()); - - return sebAccessUUID; - } - - private String createExamUser( - final Exam exam, - final ScreenProctoringServiceOAuthTemplate apiTemplate) - throws JsonMappingException, JsonProcessingException { - - final UserInfo examOwner = this.userDAO.byModelId(exam.getOwnerId()).getOrThrow(); - final String userName = SEB_SERVER_SCREEN_PROCTORING_USER_PREFIX + exam.id; - final CharSequence secret = ClientCredentialServiceImpl.generateClientSecret(); - - final String uri = UriComponentsBuilder - .fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL) - .path(SPS_API.USER_ENDPOINT) - .queryParam(SPS_API.USER.ATTR_NAME, userName) - .queryParam(SPS_API.USER.ATTR_SURNAME, userName) - .queryParam(SPS_API.USER.ATTR_USERNAME, userName) - .queryParam(SPS_API.USER.ATTR_PASSWORD, secret.toString()) - .queryParam(SPS_API.USER.ATTR_LANGUAGE, examOwner.language.toLanguageTag()) - .queryParam(SPS_API.USER.ATTR_TIMEZONE, examOwner.timeZone.getID()) - .queryParam(SPS_API.USER.ATTR_ROLES, "ADMIN") // TODO adapt role when known - .build() - .toUriString(); - - final ResponseEntity exchange = apiTemplate.exchange(uri, HttpMethod.POST); - if (exchange.getStatusCode() != HttpStatus.OK) { - throw new RuntimeException("Failed to create SPS user account for exam: " + exam.externalId); - } - - final Map userAttributes = this.jsonMapper.readValue( - exchange.getBody(), - new TypeReference>() { - }); - - final String userUUID = userAttributes.get(SPS_API.USER.ATTR_UUID); - - // store user data for proctoring along with the exam - this.additionalAttributesDAO.saveAdditionalAttribute( - EntityType.EXAM, - exam.id, - SPS_API.SEB_SERVER_EXAM_SETTINGS.ATTR_SPS_USER_UUID, - userUUID); - this.additionalAttributesDAO.saveAdditionalAttribute( - EntityType.EXAM, - exam.id, - SPS_API.SEB_SERVER_EXAM_SETTINGS.ATTR_SPS_USER_NAME, - userName); - this.additionalAttributesDAO.saveAdditionalAttribute( - EntityType.EXAM, - exam.id, - SPS_API.SEB_SERVER_EXAM_SETTINGS.ATTR_SPS_USER_PWD, - this.cryptor.encrypt(secret).toString()); - - return userUUID; - } - - private boolean existsExamOnSPS( - final Exam exam, - final ScreenProctoringServiceOAuthTemplate apiTemplate) { - - final String uri = UriComponentsBuilder - .fromUriString(apiTemplate.screenProctoringSettings.spsServiceURL) - .path(SPS_API.EXAM_ENDPOINT) - .path(createSPSExamId(exam)) - .build() - .toUriString(); - - final ResponseEntity exchange = apiTemplate.exchange(uri, HttpMethod.GET); - return exchange.getStatusCode() == HttpStatus.OK; } private void activation( @@ -657,14 +817,14 @@ class ScreenProctoringAPIBinding { final String uri = UriComponentsBuilder .fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL) .path(domainPath) - .path(uuid) - .path(activate ? SPS_API.ACTIVE_PATH_SEGMENT : SPS_API.INACTIVE_PATH_SEGMENT) + .pathSegment(uuid) + .pathSegment(activate ? SPS_API.ACTIVE_PATH_SEGMENT : SPS_API.INACTIVE_PATH_SEGMENT) .build() .toUriString(); final ResponseEntity exchange = apiTemplate.exchange(uri, HttpMethod.POST); if (exchange.getStatusCode() != HttpStatus.OK) { - log.error("Failed to activate/deactivate on SPS: {} with response: ", uri, exchange); + log.error("Failed to activate/deactivate on SPS: {} with response: {}", uri, exchange); } } catch (final Exception e) { log.error("Failed to activate/deactivate on SPS: {}, {}, {}", domainPath, uuid, activate, e); @@ -672,7 +832,6 @@ class ScreenProctoringAPIBinding { } private void deletion( - final Exam exam, final String domainPath, final String uuid, final ScreenProctoringServiceOAuthTemplate apiTemplate) { @@ -682,7 +841,7 @@ class ScreenProctoringAPIBinding { final String uri = UriComponentsBuilder .fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL) .path(domainPath) - .path(uuid) + .pathSegment(uuid) .build() .toUriString(); @@ -695,41 +854,34 @@ class ScreenProctoringAPIBinding { } } - private String createSPSExamId(final Exam exam) { - return exam.getModelId(); - } + private void rollbackOnSPS( + final Exam exam, + final SPSData spsData, + final ScreenProctoringServiceOAuthTemplate apiTemplate) { - private String getSPSExamUUID(final Exam exam) { - final String spsExamUUID = this.additionalAttributesDAO - .getAdditionalAttribute( - EntityType.EXAM, - exam.id, - SPS_API.SEB_SERVER_EXAM_SETTINGS.ATTR_SPS_EXAM_UUID) - .getOrThrow() - .getValue(); - return spsExamUUID; - } + log.info("Try to rollback SPS binding for exam: {}", exam.externalId); - private String getSPSUserUUID(final Exam exam) { - final String spsExamUUID = this.additionalAttributesDAO - .getAdditionalAttribute( - EntityType.EXAM, - exam.id, - SPS_API.SEB_SERVER_EXAM_SETTINGS.ATTR_SPS_USER_UUID) - .getOrThrow() - .getValue(); - return spsExamUUID; - } + if (StringUtils.isNotBlank(spsData.spsExamUUID)) { - private String getSPSSEBAccessUUID(final Exam exam) { - final String spsExamUUID = this.additionalAttributesDAO - .getAdditionalAttribute( - EntityType.EXAM, - exam.id, - SPS_API.SEB_SERVER_EXAM_SETTINGS.ATTR_SPS_SEB_ACCESS_UUID) - .getOrThrow() - .getValue(); - return spsExamUUID; + // TODO delete entity privilege + + log.info( + "Try to rollback SPS Exam with UUID: {} for exam: {}", + spsData.spsExamUUID, + exam.externalId); + + deletion(SPS_API.EXAM_ENDPOINT, spsData.spsExamUUID, apiTemplate); + + } + + if (StringUtils.isNotBlank(spsData.spsSEBAccesUUID)) { + log.info( + "Try to rollback SPS SEB Access with UUID: {} for exam: {}", + spsData.spsSEBAccesUUID, + exam.externalId); + + deletion(SPS_API.SEB_ACCESS_ENDPOINT, spsData.spsSEBAccesUUID, apiTemplate); + } } private ScreenProctoringServiceOAuthTemplate apiTemplate = null; @@ -739,7 +891,7 @@ class ScreenProctoringAPIBinding { log.debug("Create new ScreenProctoringServiceOAuthTemplate for exam: {}", examId); - final ScreenProctoringSettings settings = this.proctoringSettingsSupplier + final ScreenProctoringSettings settings = this.proctoringSettingsDAO .getScreenProctoringSettings(new EntityKey(examId, EntityType.EXAM)) .getOrThrow(); @@ -873,8 +1025,24 @@ class ScreenProctoringAPIBinding { return exchange(url, method, null, getHeaders()); } + ResponseEntity exchange( + final String url, + final String body, + final HttpMethod method) { + + return exchange(url, method, body, getHeaders()); + } + + HttpHeaders getHeadersJSONRequest() { + final HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + httpHeaders.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + return httpHeaders; + } + HttpHeaders getHeaders() { final HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); httpHeaders.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); return httpHeaders; } @@ -914,4 +1082,48 @@ class ScreenProctoringAPIBinding { } } + @JsonIgnoreProperties(ignoreUnknown = true) + static final class SPSData { + + public static final String ATTR_SPS_ACTIVE = "spsExamActive"; + public static final String ATTR_SPS_ACCESS_DATA = "spsAccessData"; + + @JsonProperty("spsUserUUID") + String spsUserUUID = null; + @JsonProperty("spsUserName") + String spsUserName = null; + @JsonProperty("spsUserPWD") + String spsUserPWD = null; + @JsonProperty("spsSEBAccesUUID") + String spsSEBAccesUUID = null; + @JsonProperty("spsSEBAccessName") + String spsSEBAccessName = null; + @JsonProperty("spsSEBAccessPWD") + String spsSEBAccessPWD = null; + @JsonProperty("psExamUUID") + String spsExamUUID = null; + + private SPSData() { + } + + @JsonCreator + public SPSData( + @JsonProperty("spsUserUUID") final String spsUserUUID, + @JsonProperty("spsUserName") final String spsUserName, + @JsonProperty("spsUserPWD") final String spsUserPWD, + @JsonProperty("spsSEBAccesUUID") final String spsSEBAccesUUID, + @JsonProperty("spsSEBAccessName") final String spsSEBAccessName, + @JsonProperty("spsSEBAccessPWD") final String spsSEBAccessPWD, + @JsonProperty("psExamUUID") final String spsExamUUID) { + + this.spsUserUUID = spsUserUUID; + this.spsUserName = spsUserName; + this.spsUserPWD = spsUserPWD; + this.spsSEBAccesUUID = spsSEBAccesUUID; + this.spsSEBAccessName = spsSEBAccessName; + this.spsSEBAccessPWD = spsSEBAccessPWD; + this.spsExamUUID = spsExamUUID; + } + } + } 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 c5bc812a..64b16db0 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 @@ -24,12 +24,9 @@ import org.springframework.stereotype.Service; import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException; -import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException; -import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.async.AsyncService; -import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; -import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.exam.CollectingStrategy; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction; @@ -42,13 +39,17 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRe import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ProctoringSettingsDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ScreenProctoringGroupDAO; 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.ProctoringSettingsDAOImpl; 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; @Lazy @Service @@ -58,16 +59,18 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { private static final Logger log = LoggerFactory.getLogger(ScreenProctoringServiceImpl.class); private final Cryptor cryptor; + private final JSONMapper jsonMapper; private final ScreenProctoringAPIBinding screenProctoringAPIBinding; private final ScreenProctoringGroupDAO screenProctoringGroupDAO; + private final ProctoringSettingsDAO proctoringSettingsDAO; private final ClientConnectionDAO clientConnectionDAO; private final ExamDAO examDAO; - private final ProctoringSettingsDAOImpl proctoringSettingsSupplier; private final SEBClientInstructionService sebInstructionService; + private final ExamSessionCacheService examSessionCacheService; public ScreenProctoringServiceImpl( final Cryptor cryptor, - final ProctoringSettingsDAOImpl proctoringSettingsSupplier, + final ProctoringSettingsDAO proctoringSettingsDAO, final AsyncService asyncService, final JSONMapper jsonMapper, final UserDAO userDAO, @@ -75,21 +78,24 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { final ClientConnectionDAO clientConnectionDAO, final AdditionalAttributesDAO additionalAttributesDAO, final ScreenProctoringGroupDAO screenProctoringGroupDAO, - final SEBClientInstructionService sebInstructionService) { + final SEBClientInstructionService sebInstructionService, + final ExamSessionCacheService examSessionCacheService) { this.cryptor = cryptor; + this.jsonMapper = jsonMapper; this.examDAO = examDAO; - this.proctoringSettingsSupplier = proctoringSettingsSupplier; this.screenProctoringGroupDAO = screenProctoringGroupDAO; this.clientConnectionDAO = clientConnectionDAO; this.sebInstructionService = sebInstructionService; + this.examSessionCacheService = examSessionCacheService; + this.proctoringSettingsDAO = proctoringSettingsDAO; this.screenProctoringAPIBinding = new ScreenProctoringAPIBinding( userDAO, cryptor, asyncService, jsonMapper, - proctoringSettingsSupplier, + proctoringSettingsDAO, additionalAttributesDAO); } @@ -97,14 +103,19 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { public Result testSettings(final ScreenProctoringSettings screenProctoringSettings) { return Result.tryCatch(() -> { + final Collection fieldChecks = new ArrayList<>(); + if (StringUtils.isBlank(screenProctoringSettings.spsServiceURL)) { + fieldChecks.add(APIMessage.fieldValidationError( + "appKey", + "screenProctoringSettings:spsServiceURL:notNull")); + } if (screenProctoringSettings.spsServiceURL != null && screenProctoringSettings.spsServiceURL.contains("?")) { - throw new FieldValidationException( - "serverURL", - "screenProctoringSettings:spsServiceURL:invalidURL"); + fieldChecks.add(APIMessage.fieldValidationError( + "appKey", + "screenProctoringSettings:spsServiceURL:invalidURL")); } - final Collection fieldChecks = new ArrayList<>(); if (StringUtils.isBlank(screenProctoringSettings.spsAPIKey)) { fieldChecks.add(APIMessage.fieldValidationError( "appKey", @@ -150,27 +161,41 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { } @Override - public Result applyScreenProctoingForExam(final ScreenProctoringSettings settings) { + public Result applyScreenProctoingForExam(final Long examId) { return this.examDAO - .byPK(settings.examId) + .byPK(examId) .map(exam -> { - if (BooleanUtils.toBoolean(settings.enableScreenProctoring)) { + final boolean isSPSActive = this.screenProctoringAPIBinding.isSPSActive(exam); + final boolean isEnabling = this.proctoringSettingsDAO.isScreenProctoringEnabled(examId); + + if (isEnabling && !isSPSActive) { this.screenProctoringAPIBinding .startScreenProctoring(exam) + .onError(error -> log.error( + "Failed to apply screen proctoring for exam: {}", + exam, + error)) .getOrThrow() .stream() .forEach(newGroup -> createNewLocalGroup(exam, newGroup)); - } else { + + this.examDAO.markUpdate(exam.id); + + } else if (!isEnabling && isSPSActive) { this.screenProctoringAPIBinding .dispsoseScreenProctoring(exam) + .onError(error -> log.error("Failed to dispose screen proctoring for exam: {}", + exam, + error)) .getOrThrow(); - } - return settings; + this.examDAO.markUpdate(exam.id); + } + return exam; }); } @@ -204,20 +229,51 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { public void updateClientConnections() { try { - final Map examInfoCache = new HashMap<>(); - this.examDAO .allIdsOfRunningWithScreenProctoringEnabled() .flatMap(this.clientConnectionDAO::getAllForScreenProctoringUpdate) .getOrThrow() .stream() - .forEach(cc -> applyScreenProctoringSession(cc, examInfoCache)); + .forEach(cc -> applyScreenProctoringSession(cc)); } catch (final Exception e) { log.error("Failed to update active SEB connections for screen proctoring"); } } + @Override + public void notifyExamSaved(final Exam exam) { + final String enabeld = exam.additionalAttributes + .get(ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING); + + if (!BooleanUtils.toBoolean(enabeld)) { + return; + } + + this.screenProctoringAPIBinding.synchronizeUserAccounts(exam); + this.screenProctoringAPIBinding.createExamReadPrivileges(exam); + } + + @Override + public void notifyExamStarted(final ExamStartedEvent event) { + final Exam exam = event.exam; + if (!BooleanUtils.toBoolean(exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) { + return; + } + + this.screenProctoringAPIBinding.activateSEBAccessOnSPS(exam, true); + } + + @Override + public void notifyExamFinished(final ExamFinishedEvent event) { + final Exam exam = event.exam; + if (!BooleanUtils.toBoolean(exam.additionalAttributes.get(SPSData.ATTR_SPS_ACTIVE))) { + return; + } + + this.screenProctoringAPIBinding.activateSEBAccessOnSPS(exam, false); + } + @Override public void notifyExamDeletion(final ExamDeletionEvent event) { event.ids @@ -234,86 +290,42 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { }); } - private void applyScreenProctoringSession( - final ClientConnectionRecord ccRecord, - final Map examInfoCache) { + private void applyScreenProctoringSession(final ClientConnectionRecord ccRecord) { try { - final Long examId = ccRecord.getExamId(); - - // process based caching... - if (!examInfoCache.containsKey(examId)) { - final ScreenProctoringSettings settings = this.proctoringSettingsSupplier - .getScreenProctoringSettings(new EntityKey(examId, EntityType.EXAM)) - .getOrThrow(); - final ClientCredentials sebClientCredential = this.screenProctoringAPIBinding - .getSEBClientCredentials(examId) - .getOrThrow(); - - examInfoCache.put(examId, new ExamInfo(settings, sebClientCredential)); - } - - final ExamInfo examInfo = examInfoCache.get(examId); + final Exam runningExam = this.examSessionCacheService.getRunningExam(examId); // apply SEB connection to screen proctoring group final ScreenProctoringGroup group = applySEBConnectionToGroup( ccRecord, - examInfo.screenProctoringSettings); + runningExam); // create screen proctoring session for SEB connection on SPS service final String spsSessionToken = this.screenProctoringAPIBinding .createSEBSession(examId, group, ccRecord); // create instruction for SEB and add it to instruction queue for SEB connection - registerJoinInstruction(ccRecord, spsSessionToken, group, examInfo); + registerJoinInstruction(ccRecord, spsSessionToken, group, runningExam); } catch (final Exception e) { log.error("Failed to apply screen proctoring session to SEB with connection: ", ccRecord, e); } - - } - - private void registerJoinInstruction( - final ClientConnectionRecord ccRecord, - final String spsSessionToken, - final ScreenProctoringGroup group, - final ExamInfo examInfo) { - - if (log.isDebugEnabled()) { - log.debug("Register JOIN instruction for client "); - } - - final Long examId = examInfo.screenProctoringSettings.examId; - final Map attributes = new HashMap<>(); - attributes.put(SERVICE_TYPE, SERVICE_TYPE_NAME); - attributes.put(METHOD, ClientInstruction.ProctoringInstructionMethod.JOIN.name()); - attributes.put(URL, examInfo.screenProctoringSettings.getSpsServiceURL()); - attributes.put(CLIENT_ID, examInfo.sebClientCredential.clientIdAsString()); - attributes.put(CLIENT_SECRET, this.cryptor.decrypt(examInfo.sebClientCredential.secret).toString()); - attributes.put(GROUP_ID, group.uuid); - attributes.put(SESSION_ID, spsSessionToken); - - this.sebInstructionService - .registerInstruction( - examId, - InstructionType.SEB_PROCTORING, - attributes, - ccRecord.getConnectionToken(), - true) - .onError(error -> log.error( - "Failed to register screen proctoring join instruction for SEB connection: {}", - ccRecord, - error)); - } private ScreenProctoringGroup applySEBConnectionToGroup( final ClientConnectionRecord ccRecord, - final ScreenProctoringSettings settings) { + final Exam exam) { - final Long examId = ccRecord.getExamId(); - switch (settings.collectingStrategy) { + if (!exam.additionalAttributes.containsKey(ScreenProctoringSettings.ATTR_COLLECTING_STRATEGY)) { + log.warn("Can't verify collecting strategy for exam: {} use default group assignment.", exam.id); + return applyToDefaultGroup(ccRecord, exam); + } + + final CollectingStrategy strategy = CollectingStrategy.valueOf(exam.additionalAttributes + .get(ScreenProctoringSettings.ATTR_COLLECTING_STRATEGY)); + + switch (strategy) { case SEB_GROUP: { // TODO throw new UnsupportedOperationException("SEB_GROUP based group collection is not supported yet"); @@ -321,30 +333,43 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { case EXAM: case FIX_SIZE: default: { - - final ScreenProctoringGroup screenProctoringGroup = getProctoringGroup(settings); - this.clientConnectionDAO.assignToScreenProctoringGroup( - examId, - ccRecord.getConnectionToken(), - screenProctoringGroup.id) - .getOrThrow(); - - return screenProctoringGroup; + return applyToDefaultGroup(ccRecord, exam); } } + } - private ScreenProctoringGroup getProctoringGroup(final ScreenProctoringSettings settings) { + private ScreenProctoringGroup applyToDefaultGroup( + final ClientConnectionRecord ccRecord, + final Exam exam) { + + final ScreenProctoringGroup screenProctoringGroup = getProctoringGroup(exam); + this.clientConnectionDAO.assignToScreenProctoringGroup( + exam.id, + ccRecord.getConnectionToken(), + screenProctoringGroup.id) + .getOrThrow(); + + return screenProctoringGroup; + } + + private ScreenProctoringGroup getProctoringGroup(final Exam exam) { + + int collectingGroupSize = 0; + if (exam.additionalAttributes.containsKey(ScreenProctoringSettings.ATTR_COLLECTING_GROUP_SIZE)) { + collectingGroupSize = Integer.parseInt(exam.additionalAttributes + .get(ScreenProctoringSettings.ATTR_COLLECTING_GROUP_SIZE)); + } final Result reserve = this.screenProctoringGroupDAO .reservePlaceInCollectingGroup( - settings.examId, - settings.collectingGroupSize != null ? settings.collectingGroupSize : 0); + exam.id, + collectingGroupSize); ScreenProctoringGroup screenProctoringGroup = null; if (reserve.hasError()) { if (reserve.getError() instanceof AllGroupsFullException) { - screenProctoringGroup = applyNewGroup(settings.examId, settings.collectingGroupSize); + screenProctoringGroup = applyNewGroup(exam, collectingGroupSize); } else { throw new RuntimeException( "Failed to create new screen proctoring group: ", @@ -356,11 +381,9 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { return screenProctoringGroup; } - private ScreenProctoringGroup applyNewGroup(final Long examId, final Integer groupSize) { + private ScreenProctoringGroup applyNewGroup(final Exam exam, final Integer groupSize) { - final Exam exam = this.examDAO.byPK(examId).getOrThrow(); - final String spsExamUUID = exam.getAdditionalAttribute( - ScreenProctoringAPIBinding.SPS_API.SEB_SERVER_EXAM_SETTINGS.ATTR_SPS_EXAM_UUID); + final String spsExamUUID = this.getSPSData(exam).spsExamUUID; return this.screenProctoringGroupDAO .getCollectingGroups(exam.id) @@ -368,14 +391,16 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { .createGroup(spsExamUUID, count.size() + 1, "Created by SEB Server", exam)) .flatMap(this.screenProctoringGroupDAO::createNewGroup) .flatMap(group -> this.screenProctoringGroupDAO - .reservePlaceInCollectingGroup(examId, groupSize != null ? groupSize : 0)) + .reservePlaceInCollectingGroup(exam.id, groupSize != null ? groupSize : 0)) .getOrThrow(); } private Result deleteForExam(final Long examId) { - return this.examDAO.byPK(examId) + return this.examDAO + .byPK(examId) .flatMap(this.screenProctoringAPIBinding::deleteScreenProctoring) - .map(this::cleanupAllLocalGroups); + .map(this::cleanupAllLocalGroups) + .onError(error -> log.error("Failed to delete SPS integration for exam: {}", examId, error)); } private Exam cleanupAllLocalGroups(final Exam exam) { @@ -401,26 +426,54 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { newGroup, error)); } -// private Exam saveSettings(final Exam exam, final ScreenProctoringSettings settings) { -// -// this.proctoringAdminService -// .saveScreenProctoringSettings(exam.getEntityKey(), settings) -// .getOrThrow(); -// -// return exam; -// } + private void registerJoinInstruction( + final ClientConnectionRecord ccRecord, + final String spsSessionToken, + final ScreenProctoringGroup group, + final Exam exam) { - private static final class ExamInfo { + if (log.isDebugEnabled()) { + log.debug("Register JOIN instruction for client "); + } - final ScreenProctoringSettings screenProctoringSettings; - final ClientCredentials sebClientCredential; + final SPSData spsData = getSPSData(exam); + final String url = exam.additionalAttributes.get(ScreenProctoringSettings.ATTR_SPS_SERVICE_URL); + final Map attributes = new HashMap<>(); - public ExamInfo( - final ScreenProctoringSettings screenProctoringSettings, - final ClientCredentials sebClientCredential) { + attributes.put(SERVICE_TYPE, SERVICE_TYPE_NAME); + attributes.put(METHOD, ClientInstruction.ProctoringInstructionMethod.JOIN.name()); + attributes.put(URL, url); + attributes.put(CLIENT_ID, spsData.spsSEBAccessName); + attributes.put(CLIENT_SECRET, spsData.spsSEBAccessPWD); + attributes.put(GROUP_ID, group.uuid); + attributes.put(SESSION_ID, spsSessionToken); - this.screenProctoringSettings = screenProctoringSettings; - this.sebClientCredential = sebClientCredential; + this.sebInstructionService + .registerInstruction( + exam.id, + InstructionType.SEB_PROCTORING, + attributes, + ccRecord.getConnectionToken(), + true) + .onError(error -> log.error( + "Failed to register screen proctoring join instruction for SEB connection: {}", + ccRecord, + error)); + } + + // TODO make this with caching if performance is not good + private SPSData getSPSData(final Exam exam) { + try { + + final String dataEncrypted = exam.additionalAttributes.get(SPSData.ATTR_SPS_ACCESS_DATA); + + return this.jsonMapper.readValue( + this.cryptor.decrypt(dataEncrypted).getOrThrow().toString(), + SPSData.class); + + } catch (final Exception e) { + log.error("Failed to get local SPSData for exam: {}", exam); + return null; } } 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 ef24a718..9aeaa87f 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 @@ -85,6 +85,8 @@ 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; @@ -658,7 +660,7 @@ public class ExamAdministrationController extends EntityController { @Override protected Result notifySaved(final Exam entity) { return Result.tryCatch(() -> { - this.examAdminService.updateAdditionalExamConfigAttributes(entity.id); + this.examAdminService.notifyExamSaved(entity); this.examSessionService.flushCache(entity); return entity; }); diff --git a/src/main/resources/config/application-dev.properties b/src/main/resources/config/application-dev.properties index f207ba86..f2c01c1d 100644 --- a/src/main/resources/config/application-dev.properties +++ b/src/main/resources/config/application-dev.properties @@ -12,10 +12,10 @@ logging.level.ROOT=INFO logging.level.ch=INFO logging.level.ch.ethz.seb.sebserver.webservice.datalayer=INFO logging.level.org.springframework.cache=DEBUG -logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=DEBUG +logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=INFO logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG -logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=INFO -logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator=DEBUG +logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=DEBUG +logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.indicator=INFO logging.level.ch.ethz.seb.sebserver.webservice.weblayer.oauth=DEBUG #logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl=DEBUG #logging.level.ch.ethz.seb.sebserver.webservice.datalayer.batis=DEBUG diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index c52573ab..464e36c7 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -806,8 +806,7 @@ sebserver.exam.delete.report.list.empty=No dependencies will be deleted. sebserver.exam.proctoring.actions.open=Proctoring Settings sebserver.exam.proctoring.form.title=Exam Proctoring Settings sebserver.exam.proctoring.form.info.title=Remote Proctoring -sebserver.exam.proctoring.form.info=This allows to integrate a supported external proctoring service.
To integrate Jitsi Meet, JWT based authentication must be enabled.
To integrate Zoom a Zoom higher-plan account is needed and also JWT based authentication. -sebserver.exam.proctoring.form.enabled=Proctoring enabled +sebserver.exam.proctoring.form.sebserver.exam.proctoring.form.enabled=Proctoring enabled sebserver.exam.proctoring.form.enabled.tooltip=Indicates whether the exam proctoring feature is enabled for this exam or not. sebserver.exam.proctoring.form.type=Type sebserver.exam.proctoring.form.type.tooltip=The type and server type of the external proctoring service. @@ -828,17 +827,6 @@ sebserver.exam.proctoring.form.appkey.zoom.tooltip=Since Zoom no longer supports sebserver.exam.proctoring.form.secret.zoom=App Secret (Deprecated) sebserver.exam.proctoring.form.secret.zoom.tooltip=Since Zoom no longer supports JWT App, this is deprecated and the below Account ID, Client ID and Client Secret must be used -sebserver.exam.proctoring.form.appkey.sps=App Key -sebserver.exam.proctoring.form.appkey.sps.tooltip=The application key for witch SEB Server can connect to the proctoring service. This is defined by the screen proctoring service. -sebserver.exam.proctoring.form.appsecret.sps=App Secret -sebserver.exam.proctoring.form.appsecret.sps.tooltip=The secret used to access the proctoring service by SEB Server. -sebserver.exam.proctoring.form.accountId.sps=API Account Id -sebserver.exam.proctoring.form.accountId.sps.tooltip=This is the API account id on the proctoring service that SEB Server shall use to connect and manage the proctoring service -sebserver.exam.proctoring.form.accountSecret.sps=API Account Secret -sebserver.exam.proctoring.form.accountSecret.sps.tooltip=This is the secret for the above API account id - - - sebserver.exam.proctoring.form.accountId=Account ID sebserver.exam.proctoring.form.accountId.tooltip=This is the Account ID from the Zoom Server-to-Server OAuth application and needed by SEB server to automatically create meetings sebserver.exam.proctoring.form.clientId=Client ID @@ -887,6 +875,25 @@ sebserver.exam.proctoring.one.close.error=Failed to close the one-to-one room pr sebserver.exam.proctoring.collecting.open.error=Failed to open the collecting room. sebserver.exam.proctoring.collecting.close.error=Failed to close the collecting room properly. +sebserver.exam.sps.actions.open=Screen Recording Settings +sebserver.exam.sps.form.title=Screen Recording Settings +sebserver.exam.sps.form.info.title=Info +sebserver.exam.sps.form.info=This allows to integrate a SEB screen proctoring service for this exam to record screens and interaction-data for all SEBs applying the exam.
The SEB screen proctoring service must be available within the given URL and accessible as administrator with the given account credentials.
Also the service API credentials are needed to connect to the service securely. +sebserver.exam.sps.form.enable=Enable Screen Recording +sebserver.exam.sps.form.enable.tooltip=To enable screen recording with SEB and SEB Server you need to integrate an available SEB screen proctoring service +sebserver.exam.sps.form.url=Service URL +sebserver.exam.sps.form.url.tooltip=The root URL of the SEB screen proctoring service for integration +sebserver.exam.sps.form.appkey=API Key +sebserver.exam.sps.form.appkey.tooltip=The application key for witch SEB Server can connect to the proctoring service. This is defined by the screen proctoring service. +sebserver.exam.sps.form.appsecret=API Secret +sebserver.exam.sps.form.accountId=Account Name +sebserver.exam.sps.form.accountId.tooltip=This is the API account id on the proctoring service that SEB Server shall use to connect and manage the proctoring service +sebserver.exam.sps.form.accountSecret=Account Password +sebserver.exam.sps.form.accountSecret.tooltip=This is the secret/password for the above API account id +sebserver.exam.sps.form.collect.strategy=Grouping Strategy +sebserver.exam.sps.form.saveSettings=Save Settings + + sebserver.exam.signaturekey.action.edit=App Signature Key sebserver.exam.signaturekey.action.save=Save Settings sebserver.exam.signaturekey.action.cancel=Cancel and back to Exam @@ -1927,6 +1934,8 @@ sebserver.examconfig.props.label.screenProctoringMetadataURLEnabled=Enable URL R sebserver.examconfig.props.label.screenProctoringMetadataURLEnabled.tooltip=Enable the recording of browser URL input during a screen proctoring session sebserver.examconfig.props.label.screenProctoringMetadataWindowTitleEnabled=Enable Window Title Recording sebserver.examconfig.props.label.screenProctoringMetadataWindowTitleEnabled.tooltip=Enable the recording of the title of the active window during a screen proctoring session +sebserver.examconfig.props.label.screenProctoringMetadataActiveAppEnabled=Enable Active Application Recording +sebserver.examconfig.props.label.screenProctoringMetadataActiveAppEnabled.tooltip=Enable the recording of the name of the active application. The active application is the one with the actual user focus sebserver.examconfig.props.group.ScreenProctoring=SEB Screen Proctoring Settings sebserver.examconfig.props.group.screenshot=Screenshot Settings diff --git a/src/main/resources/static/images/screen_proc_off.png b/src/main/resources/static/images/screen_proc_off.png new file mode 100644 index 00000000..ffb6b0a0 Binary files /dev/null and b/src/main/resources/static/images/screen_proc_off.png differ diff --git a/src/main/resources/static/images/screen_proc_on.png b/src/main/resources/static/images/screen_proc_on.png new file mode 100644 index 00000000..7cd15467 Binary files /dev/null and b/src/main/resources/static/images/screen_proc_on.png differ