diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index 8db6c7ab..757c434d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -155,7 +155,8 @@ public final class API { public static final String EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported"; public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_CHAPTERS_PATH_SEGMENT = "/chapters"; public static final String EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT = "/proctoring"; - public static final String EXAM_ADMINISTRATION_SEB_SECURITY_KEY_GRANTS_PATH_SEGMENT = "/seb-grants"; + public static final String EXAM_ADMINISTRATION_SEB_SECURITY_KEY_GRANTS_PATH_SEGMENT = "/grant"; + public static final String EXAM_ADMINISTRATION_SEB_SECURITY_AS_KEYS_PATH_SEGMENT = "/signature-key"; public static final String EXAM_INDICATOR_ENDPOINT = "/indicator"; public static final String EXAM_CLIENT_GROUP_ENDPOINT = "/client-group"; @@ -200,7 +201,7 @@ public final class API { public static final String EXAM_MONITORING_INSTRUCTION_ENDPOINT = "/instruction"; public static final String EXAM_MONITORING_NOTIFICATION_ENDPOINT = "/notification"; public static final String EXAM_MONITORING_DISABLE_CONNECTION_ENDPOINT = "/disable-connection"; - public static final String EXAM_MONITORING_GRANT_APP_SIGNATURE_KEY_ENDPOINT = "/apply-grant"; + public static final String EXAM_MONITORING_SIGNATURE_KEY_ENDPOINT = "/signature"; public static final String EXAM_MONITORING_STATE_FILTER = "hidden-states"; public static final String EXAM_MONITORING_CLIENT_GROUP_FILTER = "hidden-client-group"; public static final String EXAM_MONITORING_FINISHED_ENDPOINT = "/finishedexams"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/AppSignatureKeyInfo.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/AppSignatureKeyInfo.java new file mode 100644 index 00000000..82a487f7 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/AppSignatureKeyInfo.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2022 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.gbl.model.institution; + +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import javax.validation.constraints.NotNull; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import ch.ethz.seb.sebserver.gbl.model.Domain.SEB_SECURITY_KEY_REGISTRY; +import ch.ethz.seb.sebserver.gbl.util.Utils; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class AppSignatureKeyInfo { + + public static final String ATTR_KEY_CONNECTION_MAPPING = "kcMapping"; + + @NotNull + @JsonProperty(SEB_SECURITY_KEY_REGISTRY.ATTR_INSTITUTION_ID) + public final Long institutionId; + + @NotNull + @JsonProperty(SEB_SECURITY_KEY_REGISTRY.ATTR_EXAM_ID) + public final Long examId; + + @JsonProperty(ATTR_KEY_CONNECTION_MAPPING) + public final Map> keyConnectionMapping; + + @JsonCreator + public AppSignatureKeyInfo( + @JsonProperty(SEB_SECURITY_KEY_REGISTRY.ATTR_INSTITUTION_ID) final Long institutionId, + @JsonProperty(SEB_SECURITY_KEY_REGISTRY.ATTR_EXAM_ID) final Long examId, + @JsonProperty(ATTR_KEY_CONNECTION_MAPPING) final Map> keyConnectionMapping) { + + this.institutionId = institutionId; + this.examId = examId; + this.keyConnectionMapping = Utils.immutableMapOf(keyConnectionMapping); + } + + public Long getInstitutionId() { + return this.institutionId; + } + + public Long getExamId() { + return this.examId; + } + + public Map> getKeyConnectionMapping() { + return this.keyConnectionMapping; + } + + @Override + public int hashCode() { + return Objects.hash(this.examId, this.institutionId); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + final AppSignatureKeyInfo other = (AppSignatureKeyInfo) obj; + return Objects.equals(this.examId, other.examId) && Objects.equals(this.institutionId, other.institutionId); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("AppSignatureKeyInfo [institutionId="); + builder.append(this.institutionId); + builder.append(", examId="); + builder.append(this.examId); + builder.append(", keyConnectionMapping="); + builder.append(this.keyConnectionMapping); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java index c071df70..d82845fc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java @@ -55,7 +55,7 @@ public final class LmsSetup implements GrantEntity, Activatable { * Also defines the supports feature(s) for each type of LMS binding. */ public enum LmsType { /** Mockup LMS type used to create test setups */ - MOCKUP(Features.COURSE_API), + MOCKUP(Features.COURSE_API, Features.SEB_RESTRICTION), /** The Open edX LMS binding features both APIs, course access as well as SEB restriction */ OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION), /** The Moodle binding features only the course access API so far */ 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 7db3fdd4..b29a6d42 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 @@ -415,6 +415,17 @@ public enum ActionDefinition { PageStateDefinitionImpl.EXAM_VIEW, ActionCategory.FORM), + EXAM_SECURITY_KEY_ENABLED( + new LocTextKey("sebserver.exam.signaturekey.action.edit"), + ImageIcon.SHIELD, + PageStateDefinitionImpl.SECURITY_KEY_EDIT, + ActionCategory.FORM), + EXAM_SECURITY_KEY_DISABLED( + new LocTextKey("sebserver.exam.signaturekey.action.edit"), + ImageIcon.NO_SHIELD, + PageStateDefinitionImpl.SECURITY_KEY_EDIT, + ActionCategory.FORM), + EXAM_SEB_CLIENT_CONFIG_EXPORT( new LocTextKey("sebserver.exam.action.createClientToStartExam"), ImageIcon.EXPORT, @@ -849,6 +860,11 @@ public enum ActionDefinition { ImageIcon.YES, PageStateDefinitionImpl.MONITORING_CLIENT_CONNECTION, ActionCategory.EXAM_MONITORING_NOTIFICATION_LIST), + MONITOR_EXAM_CLIENT_CONNECTION_GRANT_SIGNATURE_KEY( + new LocTextKey("sebserver.monitoring.exam.connection.action.grant.signaturekey"), + ImageIcon.VERIFY, + PageStateDefinitionImpl.MONITORING_CLIENT_CONNECTION, + ActionCategory.FORM), MONITOR_EXAM_QUIT_SELECTED( new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.selected"), diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/PageStateDefinitionImpl.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/PageStateDefinitionImpl.java index 40261eb9..cf84fa6d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/PageStateDefinitionImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/PageStateDefinitionImpl.java @@ -28,6 +28,7 @@ import ch.ethz.seb.sebserver.gui.content.exam.ClientGroupForm; import ch.ethz.seb.sebserver.gui.content.exam.ClientGroupTemplateForm; import ch.ethz.seb.sebserver.gui.content.exam.ExamForm; import ch.ethz.seb.sebserver.gui.content.exam.ExamList; +import ch.ethz.seb.sebserver.gui.content.exam.ExamSignatureKeyForm; import ch.ethz.seb.sebserver.gui.content.exam.ExamTemplateForm; import ch.ethz.seb.sebserver.gui.content.exam.ExamTemplateList; import ch.ethz.seb.sebserver.gui.content.exam.IndicatorForm; @@ -68,6 +69,7 @@ public enum PageStateDefinitionImpl implements PageStateDefinition { EXAM_EDIT(Type.FORM_EDIT, ExamForm.class, ActivityDefinition.EXAM), INDICATOR_EDIT(Type.FORM_EDIT, IndicatorForm.class, ActivityDefinition.EXAM), CLIENT_GROUP_EDIT(Type.FORM_EDIT, ClientGroupForm.class, ActivityDefinition.EXAM), + SECURITY_KEY_EDIT(Type.FORM_EDIT, ExamSignatureKeyForm.class, ActivityDefinition.EXAM), EXAM_TEMPLATE_LIST(Type.LIST_VIEW, ExamTemplateList.class, ActivityDefinition.EXAM_TEMPLATE), EXAM_TEMPLATE_VIEW(Type.LIST_VIEW, ExamTemplateForm.class, ActivityDefinition.EXAM_TEMPLATE), 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 7503a416..3dc911ac 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 @@ -252,6 +252,8 @@ public class ExamForm implements TemplateComposer { final ExamStatus examStatus = exam.getStatus(); final boolean editable = modifyGrant && (examStatus == ExamStatus.UP_COMING || examStatus == ExamStatus.RUNNING); + final boolean signatureKeyCheckEnabled = BooleanUtils.toBoolean( + exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED)); final boolean sebRestrictionAvailable = testSEBRestrictionAPI(exam); final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService @@ -465,6 +467,14 @@ public class ExamForm implements TemplateComposer { .publishIf(() -> sebRestrictionAvailable && readonly && modifyGrant && !importFromQuizData && BooleanUtils.isTrue(isRestricted)) + .newAction(ActionDefinition.EXAM_SECURITY_KEY_ENABLED) + .withEntityKey(entityKey) + .publishIf(() -> signatureKeyCheckEnabled && readonly) + + .newAction(ActionDefinition.EXAM_SECURITY_KEY_DISABLED) + .withEntityKey(entityKey) + .publishIf(() -> !signatureKeyCheckEnabled && readonly) + .newAction(ActionDefinition.EXAM_PROCTORING_ON) .withEntityKey(entityKey) .withExec(this.proctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable)) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamSignatureKeyForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamSignatureKeyForm.java new file mode 100644 index 00000000..438ae2ff --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamSignatureKeyForm.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2022 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.content.exam; + +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.ResourceService; +import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +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.TemplateComposer; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; + +@Lazy +@Component +@GuiProfile +public class ExamSignatureKeyForm implements TemplateComposer { + + private static final LocTextKey TILE = + new LocTextKey("sebserver.exam.signaturekey.title"); + + private static final LocTextKey FORM_ENABLED = + new LocTextKey("sebserver.exam.signaturekey.form.enabled"); + private static final LocTextKey FORM_STAT_GRANT_THRESHOLD = + new LocTextKey("sebserver.exam.signaturekey.form.grant.threshold"); + + private static final LocTextKey GRANT_LIST_TITLE = + new LocTextKey("sebserver.exam.signaturekey.grantlist.title"); + private static final LocTextKey GRANT_LIST_KEY = + new LocTextKey("sebserver.exam.signaturekey.grantlist.key"); + private static final LocTextKey GRANT_LIST_TAG = + new LocTextKey("sebserver.exam.signaturekey.grantlist.tag"); + + private static final LocTextKey APP_SIG_KEY_LIST_TITLE = + new LocTextKey("sebserver.exam.signaturekey.keylist.title"); + private static final LocTextKey APP_SIG_KEY_LIST_KEY = + new LocTextKey("sebserver.exam.signaturekey.keylist.key"); + private static final LocTextKey APP_SIG_KEY_LIST_NUM_CLIENTS = + new LocTextKey("sebserver.exam.signaturekey.keylist.clients"); + + private final PageService pageService; + private final ResourceService resourceService; + private final I18nSupport i18nSupport; + + public ExamSignatureKeyForm( + final PageService pageService, + final ResourceService resourceService, + final I18nSupport i18nSupport) { + + this.pageService = pageService; + this.resourceService = resourceService; + this.i18nSupport = i18nSupport; + } + + @Override + public void compose(final PageContext pageContext) { + final RestService restService = this.resourceService.getRestService(); + final WidgetFactory widgetFactory = this.pageService.getWidgetFactory(); + final EntityKey entityKey = pageContext.getEntityKey(); + + // TODO Auto-generated method stub + + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringClientConnection.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringClientConnection.java index 5a9aea6b..0e4a8f92 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringClientConnection.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringClientConnection.java @@ -31,6 +31,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringFeature; +import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; @@ -61,6 +62,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.indicator.Ge import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs.GetExtendedClientEventPage; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.ConfirmPendingClientNotification; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionData; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionSecurityKey; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetPendingClientNotifications; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionDetails; @@ -124,6 +126,7 @@ public class MonitoringClientConnection implements TemplateComposer { private final InstructionProcessor instructionProcessor; private final SEBClientEventDetailsPopup sebClientLogDetailsPopup; private final SEBSendLockPopup sebSendLockPopup; + private final SignatureKeyGrantPopup signatureKeyGrantPopup; private final MonitoringProctoringService monitoringProctoringService; private final long pollInterval; private final int pageSize; @@ -139,6 +142,7 @@ public class MonitoringClientConnection implements TemplateComposer { final SEBClientEventDetailsPopup sebClientLogDetailsPopup, final MonitoringProctoringService monitoringProctoringService, final SEBSendLockPopup sebSendLockPopup, + final SignatureKeyGrantPopup signatureKeyGrantPopup, @Value("${sebserver.gui.webservice.poll-interval:500}") final long pollInterval, @Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) { @@ -151,6 +155,7 @@ public class MonitoringClientConnection implements TemplateComposer { this.pollInterval = pollInterval; this.sebClientLogDetailsPopup = sebClientLogDetailsPopup; this.sebSendLockPopup = sebSendLockPopup; + this.signatureKeyGrantPopup = signatureKeyGrantPopup; this.pageSize = pageSize; this.typeFilter = new TableFilterAttribute( @@ -395,6 +400,26 @@ public class MonitoringClientConnection implements TemplateComposer { .publishIf(() -> isExamSupporter.getAsBoolean() && connectionData.clientConnection.status.clientActiveStatus); + if (clientConnectionDetails.checkSecurityGrant) { + final SecurityKey securityKey = this.pageService + .getRestService() + .getBuilder(GetClientConnectionSecurityKey.class) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, parentEntityKey.modelId) + .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) + .call() + .getOrThrow(); + + if (securityKey.id < 0) { + actionBuilder + .newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_GRANT_SIGNATURE_KEY) + .withParentEntityKey(parentEntityKey) + .withEntityKey(entityKey) + .withExec(action -> this.signatureKeyGrantPopup.showGrantPopup(action, securityKey)) + .noEventPropagation() + .publishIf(isExamSupporter); + } + } + if (connectionData.clientConnection.status == ConnectionStatus.ACTIVE) { final ProctoringServiceSettings proctoringSettings = restService .getBuilder(GetExamProctoringSettings.class) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/SignatureKeyGrantPopup.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/SignatureKeyGrantPopup.java new file mode 100644 index 00000000..36cc5e2e --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/SignatureKeyGrantPopup.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2022 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.content.monitoring; + +import java.util.function.Predicate; +import java.util.function.Supplier; + +import org.eclipse.swt.widgets.Composite; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gbl.util.Utils; +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.impl.ModalInputDialog; +import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GrantClientConnectionSecurityKey; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; + +@Lazy +@Component +@GuiProfile +public class SignatureKeyGrantPopup { + + private static final LocTextKey TITLE_TEXT_KEY = + new LocTextKey("sebserver.monitoring.signaturegrant.title"); + private static final LocTextKey TITLE_TEXT_INFO = + new LocTextKey("sebserver.monitoring.signaturegrant.info"); + + private static final LocTextKey TITLE_TEXT_FORM_SIGNATURE = + new LocTextKey("sebserver.monitoring.signaturegrant.signature"); + private static final LocTextKey TITLE_TEXT_FORM_TAG = + new LocTextKey("sebserver.monitoring.signaturegrant.tag"); + + private final PageService pageService; + + protected SignatureKeyGrantPopup(final PageService pageService) { + this.pageService = pageService; + } + + public PageAction showGrantPopup(final PageAction action, final SecurityKey securityKey) { + final PageContext pageContext = action.pageContext(); + final PopupComposer popupComposer = new PopupComposer(this.pageService, pageContext, securityKey); + try { + final ModalInputDialog> dialog = + new ModalInputDialog<>( + action.pageContext().getParent().getShell(), + this.pageService.getWidgetFactory()); + dialog.setDialogWidth(700); + + final Predicate> applyGrant = formHandle -> applyGrant( + pageContext, + formHandle); + + dialog.open( + TITLE_TEXT_KEY, + applyGrant, + Utils.EMPTY_EXECUTION, + popupComposer); + + } catch (final Exception e) { + action.pageContext().notifyUnexpectedError(e); + } + return action; + } + + private final class PopupComposer implements ModalInputDialogComposer> { + + private final PageService pageService; + private final PageContext pageContext; + private final SecurityKey securityKey; + + protected PopupComposer( + final PageService pageService, + final PageContext pageContext, + final SecurityKey securityKey) { + + this.pageService = pageService; + this.pageContext = pageContext; + this.securityKey = securityKey; + } + + @Override + public Supplier> compose(final Composite parent) { + final WidgetFactory widgetFactory = this.pageService.getWidgetFactory(); + widgetFactory.addFormSubContextHeader(parent, TITLE_TEXT_INFO, null); + + //final Composite defaultPageLayout = widgetFactory.defaultPageLayout(parent, TITLE_TEXT_INFO); + final PageContext formContext = this.pageContext.copyOf(parent); + + final FormHandle form = this.pageService.formBuilder(formContext) + .addField(FormBuilder.text( + Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG, + TITLE_TEXT_FORM_TAG, + this.securityKey.tag)) + .addField(FormBuilder.text( + Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_KEY_VALUE, + TITLE_TEXT_FORM_SIGNATURE, + String.valueOf(this.securityKey.key)) + .readonly(true)) + .build(); + + return () -> form; + } + } + + private boolean applyGrant( + final PageContext pageContext, + final FormHandle formHandle) { + + final EntityKey examKey = pageContext.getParentEntityKey(); + final EntityKey connectionKey = pageContext.getEntityKey(); + + return this.pageService + .getRestService() + .getBuilder(GrantClientConnectionSecurityKey.class) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, examKey.modelId) + .withURIVariable(API.PARAM_MODEL_ID, connectionKey.modelId) + .withFormBinding(formHandle.getFormBinding()) + .call() + .onError(formHandle::handleError) + .hasValue(); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java index d408bdf9..2e23f3e2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/ResourceService.java @@ -60,7 +60,6 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.SEBClientConfig.VDIType; import ch.ethz.seb.sebserver.gbl.model.sebconfig.TemplateAttribute; import ch.ethz.seb.sebserver.gbl.model.sebconfig.View; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; -import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification; @@ -87,7 +86,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.Ge import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetViews; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.useraccount.GetUserAccountNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; -import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable.MonitoringEntry; +import ch.ethz.seb.sebserver.gui.service.session.MonitoringEntry; @Lazy @Service @@ -626,27 +625,38 @@ public class ResourceService { .getText(ResourceService.EXAMCONFIG_STATUS_PREFIX + config.configStatus.name()); } - public Function localizedClientConnectionStatusNameFunction() { - - // Memoizing - final String missing = this.i18nSupport.getText( - SEB_CONNECTION_STATUS_KEY_PREFIX + MISSING_CLIENT_PING_NAME_KEY, - MISSING_CLIENT_PING_NAME_KEY); - final EnumMap localizedNames = new EnumMap<>(ConnectionStatus.class); - Arrays.asList(ConnectionStatus.values()).stream().forEach(state -> localizedNames.put(state, this.i18nSupport - .getText(SEB_CONNECTION_STATUS_KEY_PREFIX + state.name(), state.name()))); - - return connectionData -> { - if (connectionData == null) { - localizedNames.get(ConnectionStatus.UNDEFINED); - } - if (connectionData.missingPing && connectionData.clientConnection.status.establishedStatus) { - return missing; - } else { - return localizedNames.get(connectionData.clientConnection.status); - } - }; - } +// public Function localizedClientConnectionStatusNameFunction() { +// +// // Memoizing +// final String missing = this.i18nSupport.getText( +// SEB_CONNECTION_STATUS_KEY_PREFIX + MISSING_CLIENT_PING_NAME_KEY, +// MISSING_CLIENT_PING_NAME_KEY); +// final String missingGrant = this.i18nSupport.getText( +// SEB_CONNECTION_STATUS_KEY_PREFIX + MISSING_CLIENT_SEC_GRANT_NAME_KEY, +// MISSING_CLIENT_SEC_GRANT_NAME_KEY); +// final EnumMap localizedNames = new EnumMap<>(ConnectionStatus.class); +// Arrays.asList(ConnectionStatus.values()).stream().forEach(state -> localizedNames.put(state, this.i18nSupport +// .getText(SEB_CONNECTION_STATUS_KEY_PREFIX + state.name(), state.name()))); +// +// return connectionData -> { +// if (connectionData == null) { +// localizedNames.get(ConnectionStatus.UNDEFINED); +// } +// if (connectionData.clientConnection.status.establishedStatus) { +// if (connectionData.c !connectionData.clientConnection.securityCheckGranted) { +// return missingGrant; +// } +// if (connectionData.missingPing) { +// return missing; +// } +// } +// if (connectionData.missingPing && connectionData.clientConnection.status.establishedStatus) { +// return missing; +// } else { +// return localizedNames.get(connectionData.clientConnection.status); +// } +// }; +// } public Function localizedClientMonitoringStatusNameFunction() { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnectionSecurityKey.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnectionSecurityKey.java new file mode 100644 index 00000000..ea7c7479 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetClientConnectionSecurityKey.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 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.session; + +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.institution.SecurityKey; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class GetClientConnectionSecurityKey extends RestCall { + + public GetClientConnectionSecurityKey() { + super(new TypeKey<>( + CallType.GET_SINGLE, + EntityType.SEB_SECURITY_KEY_REGISTRY, + new TypeReference() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_MONITORING_ENDPOINT + + API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_MONITORING_SIGNATURE_KEY_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GrantClientConnectionSecurityKey.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GrantClientConnectionSecurityKey.java new file mode 100644 index 00000000..6dfd14c7 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GrantClientConnectionSecurityKey.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 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.session; + +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.institution.SecurityKey; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class GrantClientConnectionSecurityKey extends RestCall { + + public GrantClientConnectionSecurityKey() { + super(new TypeKey<>( + CallType.GET_SINGLE, + EntityType.SEB_SECURITY_KEY_REGISTRY, + new TypeReference() { + }), + HttpMethod.POST, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_MONITORING_ENDPOINT + + API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_MONITORING_SIGNATURE_KEY_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionDetails.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionDetails.java index b8383662..7daeff7f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionDetails.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionDetails.java @@ -28,6 +28,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification; import ch.ethz.seb.sebserver.gbl.monitoring.IndicatorValue; @@ -47,7 +48,7 @@ import ch.ethz.seb.sebserver.gui.service.session.IndicatorData.ThresholdColor; import ch.ethz.seb.sebserver.gui.table.EntityTable; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; -public class ClientConnectionDetails { +public class ClientConnectionDetails implements MonitoringEntry { private static final Logger log = LoggerFactory.getLogger(ClientConnectionDetails.class); @@ -72,7 +73,8 @@ public class ClientConnectionDetails { private final RestCall.RestCallBuilder restCallBuilder; private final FormHandle formHandle; private final ColorData colorData; - private final Function localizedClientConnectionStatusNameFunction; + private final Function localizedClientConnectionStatusNameFunction; + public final boolean checkSecurityGrant; private ClientConnectionData connectionData = null; private boolean statusChanged = true; @@ -94,6 +96,8 @@ public class ClientConnectionDetails { this.resourceService = pageService.getResourceService(); this.restCallBuilder = restCallBuilder; this.colorData = new ColorData(display); + this.checkSecurityGrant = BooleanUtils.toBoolean( + exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED)); this.indicatorMapping = IndicatorData.createFormIndicators( indicators, display, @@ -146,7 +150,26 @@ public class ClientConnectionDetails { this.formHandle = formBuilder.build(); this.localizedClientConnectionStatusNameFunction = - this.resourceService.localizedClientConnectionStatusNameFunction(); + this.resourceService.localizedClientMonitoringStatusNameFunction(); + } + + @Override + public ConnectionStatus getStatus() { + if (this.connectionData == null) { + return ConnectionStatus.UNDEFINED; + } + return this.connectionData.clientConnection.status; + } + + @Override + public boolean hasMissingPing() { + return (this.connectionData != null) ? this.connectionData.missingPing : false; + } + + @Override + public boolean hasMissingGrant() { + return (this.connectionData != null) + ? this.checkSecurityGrant && !this.connectionData.clientConnection.securityCheckGranted : false; } public void setStatusChangeListener(final Consumer statusChangeListener) { @@ -210,8 +233,8 @@ public class ClientConnectionDetails { // update status form.setFieldValue( Domain.CLIENT_CONNECTION.ATTR_STATUS, - this.localizedClientConnectionStatusNameFunction.apply(this.connectionData)); - final Color statusColor = this.colorData.getStatusColor(this.connectionData); + this.localizedClientConnectionStatusNameFunction.apply(this)); + final Color statusColor = this.colorData.getStatusColor(this); final Color statusTextColor = this.colorData.getStatusTextColor(statusColor); form.setFieldColor(Domain.CLIENT_CONNECTION.ATTR_STATUS, statusColor); form.setFieldTextColor(Domain.CLIENT_CONNECTION.ATTR_STATUS, statusTextColor); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionTable.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionTable.java index ed832d11..0a3cf91d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionTable.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionTable.java @@ -449,14 +449,6 @@ public final class ClientConnectionTable implements FullPageMonitoringGUIUpdate // TODO if right click get selected item and show additional information (notification) } - public interface MonitoringEntry { - ConnectionStatus getStatus(); - - boolean hasMissingPing(); - - boolean hasMissingGrant(); - } - private final class UpdatableTableItem implements Comparable, MonitoringEntry { final Long connectionId; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ColorData.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ColorData.java index 9f813cec..65a1eb61 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ColorData.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ColorData.java @@ -16,7 +16,6 @@ import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.util.Utils; -import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable.MonitoringEntry; public class ColorData { @@ -36,19 +35,6 @@ public class ColorData { this.lightColor = new Color(display, Constants.WHITE_RGB); } - Color getStatusColor(final ClientConnectionData connectionData) { - if (connectionData == null || connectionData.clientConnection == null) { - return this.defaultColor; - } - - switch (connectionData.clientConnection.status) { - case ACTIVE: - return (connectionData.missingPing) ? this.color2 : this.color1; - default: - return this.defaultColor; - } - } - Color getStatusColor(final MonitoringEntry entry) { final ConnectionStatus status = entry.getStatus(); if (status == null) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringEntry.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringEntry.java new file mode 100644 index 00000000..087454f3 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringEntry.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 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.session; + +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; + +public interface MonitoringEntry { + + ConnectionStatus getStatus(); + + boolean hasMissingPing(); + + boolean hasMissingGrant(); +} \ No newline at end of file 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 7a9a15b1..73eb50a2 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 @@ -144,7 +144,10 @@ public class WidgetFactory { RESTRICTION("restriction.png"), VISIBILITY("visibility.png"), VISIBILITY_OFF("visibility_off.png"), - NOTIFICATION("notification.png"); + NOTIFICATION("notification.png"), + VERIFY("verify.png"), + SHIELD("shield.png"), + NO_SHIELD("no_shield.png"); public String fileName; private ImageData image = null; @@ -466,6 +469,7 @@ public class WidgetFactory { final LocTextKey locToolTextKey) { final Label label = new Label(parent, SWT.NONE); + label.setData(RWT.MARKUP_ENABLED, true); this.polyglotPageService.injectI18n(label, locTextKey, locToolTextKey); return label; } @@ -477,6 +481,7 @@ public class WidgetFactory { final LocTextKey locToolTextKey) { final Label label = new Label(parent, SWT.NONE); + label.setData(RWT.MARKUP_ENABLED, true); this.polyglotPageService.injectI18n(label, locTextKey, locToolTextKey); label.setData(RWT.CUSTOM_VARIANT, variant.key); return label; @@ -484,6 +489,7 @@ public class WidgetFactory { public Label labelLocalizedTitle(final Composite content, final LocTextKey locTextKey) { final Label labelLocalized = labelLocalized(content, CustomVariant.TEXT_H1, locTextKey); + labelLocalized.setData(RWT.MARKUP_ENABLED, true); final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false); labelLocalized.setLayoutData(gridData); return labelLocalized; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java index 7dd2c2dd..88f74416 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java @@ -167,6 +167,12 @@ public interface ClientConnectionDAO extends * * @param examId the exam identifier * @return Result refer to a collection of client connection records or to an error when happened */ - Result> getAllConnectionIdsForExam(Long examId); + Result> getAllConnectionRecordsForExam(Long examId); + + /** Get all client connection identifiers for an exam. + * + * @param examId the exam identifier + * @return Result refer to a collection of client connection identifiers or to an error when happened */ + Result> getAllConnectionIdsForExam(Long examId); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/SecurityKeyRegistryDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/SecurityKeyRegistryDAO.java index 92b384f6..f29963a0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/SecurityKeyRegistryDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/SecurityKeyRegistryDAO.java @@ -36,4 +36,11 @@ public interface SecurityKeyRegistryDAO extends EntityDAO getGrantOr(final SecurityKey key); + } 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 237ec944..f4cb96e5 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 @@ -764,7 +764,7 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { @Override @Transactional(readOnly = true) - public Result> getAllConnectionIdsForExam(final Long examId) { + public Result> getAllConnectionRecordsForExam(final Long examId) { return Result.tryCatch(() -> this.clientConnectionRecordMapper .selectByExample() .where( @@ -774,6 +774,18 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { .execute()); } + @Override + @Transactional(readOnly = true) + public Result> getAllConnectionIdsForExam(final Long examId) { + return Result.tryCatch(() -> this.clientConnectionRecordMapper + .selectIdsByExample() + .where( + ClientConnectionRecordDynamicSqlSupport.examId, + SqlBuilder.isEqualTo(examId)) + .build() + .execute()); + } + private Result recordById(final Long id) { return Result.tryCatch(() -> { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/SecurityKeyRegistryDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/SecurityKeyRegistryDAOImpl.java index 14b36fb0..58d86b0a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/SecurityKeyRegistryDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/SecurityKeyRegistryDAOImpl.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -30,6 +31,7 @@ import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey; import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey.KeyType; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Cryptor; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.SecurityKeyRegistryRecordDynamicSqlSupport; @@ -46,9 +48,14 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO { private final SecurityKeyRegistryRecordMapper securityKeyRegistryRecordMapper; + private final Cryptor cryptor; + + public SecurityKeyRegistryDAOImpl( + final SecurityKeyRegistryRecordMapper securityKeyRegistryRecordMapper, + final Cryptor cryptor) { - public SecurityKeyRegistryDAOImpl(final SecurityKeyRegistryRecordMapper securityKeyRegistryRecordMapper) { this.securityKeyRegistryRecordMapper = securityKeyRegistryRecordMapper; + this.cryptor = cryptor; } @Override @@ -123,7 +130,7 @@ public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO { public Result createNew(final SecurityKey data) { return Result.tryCatch(() -> { - checkUniqueTag(data); + checkUniqueKey(data); final SecurityKeyRegistryRecord newRecord = new SecurityKeyRegistryRecord( null, @@ -146,8 +153,6 @@ public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO { public Result save(final SecurityKey data) { return Result.tryCatch(() -> { - checkUniqueTag(data); - final SecurityKeyRegistryRecord newRecord = new SecurityKeyRegistryRecord( null, data.institutionId, @@ -312,6 +317,32 @@ public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO { }); } + @Override + @Transactional(readOnly = true) + public Result getGrantOr(final SecurityKey key) { + return Result.tryCatch(() -> { + final CharSequence signature = this.cryptor.decrypt(key.key).get(); + return this.securityKeyRegistryRecordMapper + .selectByExample() + .where( + SecurityKeyRegistryRecordDynamicSqlSupport.institutionId, + SqlBuilder.isEqualTo(key.institutionId)) + .and( + SecurityKeyRegistryRecordDynamicSqlSupport.examId, + isEqualToWhenPresent(key.examId)) + .and( + SecurityKeyRegistryRecordDynamicSqlSupport.keyType, + isEqualToWhenPresent((key.keyType == null) ? null : key.keyType.name())) + .build() + .execute() + .stream() + .filter(other -> Objects.equals(signature, this.cryptor.decrypt(other.getKeyValue()).getOr(null))) + .findFirst() + .map(this::toDomainModel) + .orElse(key); + }); + } + @Override public void notifyExamDeletion(final ExamDeletionEvent event) { try { @@ -367,18 +398,11 @@ public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO { rec.getExamTemplateId()); } - private void checkUniqueTag(final SecurityKey securityKeyRegistry) { - final Long count = this.securityKeyRegistryRecordMapper - .countByExample() - .where(SecurityKeyRegistryRecordDynamicSqlSupport.tag, isEqualTo(securityKeyRegistry.tag)) - .and(SecurityKeyRegistryRecordDynamicSqlSupport.id, isNotEqualToWhenPresent(securityKeyRegistry.id)) - .build() - .execute(); - - if (count != null && count > 0) { + private void checkUniqueKey(final SecurityKey key) { + if (getGrantOr(key).getOr(key) != key) { throw new FieldValidationException( Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG, - "institution:name:tag.notunique"); + "securityKey:keyValue:alreadyGranted"); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/SecurityKeyService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/SecurityKeyService.java index f15ce55f..6f32f9af 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/SecurityKeyService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/SecurityKeyService.java @@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.institution; import java.util.Collection; import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.institution.AppSignatureKeyInfo; import ch.ethz.seb.sebserver.gbl.model.institution.SecurityCheckResult; import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; @@ -21,7 +22,9 @@ public interface SecurityKeyService { /** This attribute name is used to store the App-Signature-Key given by a SEB Client */ public static final String ADDITIONAL_ATTR_APP_SIGNATURE_KEY = "APP_SIGNATURE_KEY"; - Result> getPlainGrants(Long institutionId, Long examId); + Result getSecurityKeyOfConnection(Long institutionId, Long connectionId); + + Result getAppSignaturesInfo(Long institutionId, Long examId); Result> getPlainAppSignatureKeyGrants(Long institutionId, Long examId); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/impl/SecurityKeyServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/impl/SecurityKeyServiceImpl.java index 0e5304d2..00dd20cc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/impl/SecurityKeyServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/impl/SecurityKeyServiceImpl.java @@ -10,8 +10,12 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.institution.impl; import java.security.cert.Certificate; import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import org.apache.commons.lang3.BooleanUtils; @@ -24,6 +28,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.institution.AppSignatureKeyInfo; import ch.ethz.seb.sebserver.gbl.model.institution.SecurityCheckResult; import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey; import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey.KeyType; @@ -69,10 +74,35 @@ public class SecurityKeyServiceImpl implements SecurityKeyService { } @Override - public Result> getPlainGrants(final Long institutionId, final Long examId) { - return this.securityKeyRegistryDAO - .getAll(institutionId, examId, null) - .map(this::decryptAll); + public Result getSecurityKeyOfConnection(final Long institutionId, final Long connectionId) { + return this.clientConnectionDAO.byPK(connectionId) + .map(connection -> new SecurityKey( + null, + institutionId, + KeyType.APP_SIGNATURE_KEY, + decryptStoredSignatureForConnection(connection), + connection.sebVersion, + null, null)) + .flatMap(this.securityKeyRegistryDAO::getGrantOr); + } + + @Override + public Result getAppSignaturesInfo(final Long institutionId, final Long examId) { + return Result.tryCatch(() -> { + final Map> keyMapping = new HashMap<>(); + + this.clientConnectionDAO + .getAllConnectionRecordsForExam(examId) + .getOrThrow() + .stream() + .forEach(rec -> keyMapping.computeIfAbsent( + this.decryptStoredSignatureForConnection( + rec.getId(), + rec.getConnectionToken()), + s -> new HashSet<>()).add(rec.getId())); + + return new AppSignatureKeyInfo(institutionId, examId, keyMapping); + }); } @Override @@ -386,6 +416,15 @@ public class SecurityKeyServiceImpl implements SecurityKeyService { return decryptSignatureWithConnectionToken(cc.connectionToken, signatureKey); } + private String decryptStoredSignatureForConnection(final Long cId, final String cToken) { + final String signatureKey = getSignatureKeyForConnection(cId); + if (StringUtils.isBlank(signatureKey)) { + return null; + } + + return decryptSignatureWithConnectionToken(cToken, signatureKey); + } + private void saveSignatureKeyForConnection(final ClientConnection clientConnection, final String appSignatureKey) { this.additionalAttributesDAO .saveAdditionalAttribute( diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockSEBRestrictionAPI.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockSEBRestrictionAPI.java index a2443385..34fd34ee 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockSEBRestrictionAPI.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockSEBRestrictionAPI.java @@ -25,7 +25,7 @@ public class MockSEBRestrictionAPI implements SEBRestrictionAPI { @Override public LmsSetupTestResult testCourseRestrictionAPI() { - return LmsSetupTestResult.ofQuizRestrictionAPIError(LmsType.MOCKUP, "unsupported"); + return LmsSetupTestResult.ofOkay(LmsType.MOCKUP); } @Override 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 0c428bea..808eff7f 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 @@ -50,6 +50,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType; 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.SEBRestriction; +import ch.ethz.seb.sebserver.gbl.model.institution.AppSignatureKeyInfo; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features; import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey; @@ -190,7 +191,7 @@ public class ExamAdministrationController extends EntityController { } // **************************************************************************** - // **** SEB Security Key + // **** SEB Security Key and App Signature Key @RequestMapping( path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT @@ -258,6 +259,24 @@ public class ExamAdministrationController extends EntityController { .getOrThrow(); } + @RequestMapping( + path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_ADMINISTRATION_SEB_SECURITY_AS_KEYS_PATH_SEGMENT, + method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_VALUE) + public AppSignatureKeyInfo getAppSignatureKeyInfo( + @PathVariable(name = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT, required = true) final Long examId, + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) { + + return this.examDAO.byPK(examId) + .flatMap(this::checkReadAccess) + .flatMap(exam -> this.securityKeyService.getAppSignaturesInfo(institutionId, examId)) + .getOrThrow(); + } + // **** SEB Security Key // **************************************************************************** diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java index 7b4994f0..74e79d35 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java @@ -46,6 +46,7 @@ import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig; import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction; @@ -463,29 +464,50 @@ public class ExamMonitoringController { @RequestMapping( path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + - API.EXAM_MONITORING_GRANT_APP_SIGNATURE_KEY_ENDPOINT + + API.EXAM_MONITORING_SIGNATURE_KEY_ENDPOINT + API.MODEL_ID_VAR_PATH_SEGMENT, method = RequestMethod.POST, - consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) - public void grantAppSignatureKey( + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public SecurityKey grantAppSignatureKey( @RequestParam( name = API.PARAM_INSTITUTION_ID, required = true, defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId, - @PathVariable(name = API.MODEL_ID_VAR_PATH_SEGMENT, required = true) final Long connectionId, - @RequestParam( - name = Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN, - required = false) final String connectionToken) { + @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long connectionId, + @RequestParam(name = Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG, required = true) final String tagName) { checkPrivileges(institutionId, examId); - this.securityKeyService - .registerExamAppSignatureKey(institutionId, examId, connectionId, null) + return this.securityKeyService + .registerExamAppSignatureKey(institutionId, examId, connectionId, tagName) .onSuccess(key -> this.securityKeyService.updateAppSignatureKeyGrants(examId)) .getOrThrow(); } + @RequestMapping( + path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_MONITORING_SIGNATURE_KEY_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT, + method = RequestMethod.GET, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public SecurityKey getAppSignatureKey( + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId, + @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long connectionId) { + + checkPrivileges(institutionId, examId); + return this.securityKeyService + .getSecurityKeyOfConnection(institutionId, connectionId) + .getOrThrow(); + + } + private Exam checkPrivileges(final Long institutionId, final Long examId) { // check overall privilege this.authorization.checkRole( diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 7f694be7..6f041114 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -794,6 +794,8 @@ 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.signaturekey.action.edit=App Signature Key + ################################ # Connection Configuration @@ -1984,6 +1986,7 @@ sebserver.monitoring.exam.connection.actions.group3=  sebserver.monitoring.exam.connection.notificationlist.actions= sebserver.monitoring.exam.connection.action.confirm.notification=Confirm Notification sebserver.monitoring.exam.connection.action.confirm.notification.text=Are you sure you want to confirm this pending notification?

Note that this will send a notification confirmation instruction to the SEB client and remove this notification from the pending list. +sebserver.monitoring.exam.connection.action.grant.signaturekey=Grant App Signature Key sebserver.monitoring.exam.connection.notificationlist.pleaseSelect=At first please select a notification form the Pending Notification list sebserver.monitoring.exam.connection.notificationlist.title=Pending Notification @@ -2037,6 +2040,11 @@ sebserver.monitoring.lock.list.name=SEB User Session Identifier sebserver.monitoring.lock.list.info=SEB Connection Info sebserver.monitoring.lock.noselection=Please select at least one active SEB client connection. +sebserver.monitoring.signaturegrant.title=Grant App Signature Key +sebserver.monitoring.signaturegrant.info=Mark this App Signature Key as granted. Please also choose a meaningful tag. +sebserver.monitoring.signaturegrant.signature=App Signature Key +sebserver.monitoring.signaturegrant.tag=Tag + ################################ # Finished Exams ################################ diff --git a/src/main/resources/static/images/no_shield.png b/src/main/resources/static/images/no_shield.png new file mode 100644 index 00000000..e3285cd7 Binary files /dev/null and b/src/main/resources/static/images/no_shield.png differ diff --git a/src/main/resources/static/images/shield.png b/src/main/resources/static/images/shield.png new file mode 100644 index 00000000..825454e4 Binary files /dev/null and b/src/main/resources/static/images/shield.png differ diff --git a/src/main/resources/static/images/verify.png b/src/main/resources/static/images/verify.png new file mode 100644 index 00000000..0e8453ed Binary files /dev/null and b/src/main/resources/static/images/verify.png differ