SEBSERV-335 implementation Exam

This commit is contained in:
anhefti 2022-11-17 12:56:50 +01:00
parent 949552bbf7
commit ea13bb6ca4
30 changed files with 712 additions and 86 deletions

View file

@ -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_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported";
public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_CHAPTERS_PATH_SEGMENT = "/chapters"; 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_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_INDICATOR_ENDPOINT = "/indicator";
public static final String EXAM_CLIENT_GROUP_ENDPOINT = "/client-group"; 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_INSTRUCTION_ENDPOINT = "/instruction";
public static final String EXAM_MONITORING_NOTIFICATION_ENDPOINT = "/notification"; 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_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_STATE_FILTER = "hidden-states";
public static final String EXAM_MONITORING_CLIENT_GROUP_FILTER = "hidden-client-group"; public static final String EXAM_MONITORING_CLIENT_GROUP_FILTER = "hidden-client-group";
public static final String EXAM_MONITORING_FINISHED_ENDPOINT = "/finishedexams"; public static final String EXAM_MONITORING_FINISHED_ENDPOINT = "/finishedexams";

View file

@ -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<String, Set<Long>> 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<String, Set<Long>> 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<String, Set<Long>> 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();
}
}

View file

@ -55,7 +55,7 @@ public final class LmsSetup implements GrantEntity, Activatable {
* Also defines the supports feature(s) for each type of LMS binding. */ * Also defines the supports feature(s) for each type of LMS binding. */
public enum LmsType { public enum LmsType {
/** Mockup LMS type used to create test setups */ /** 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 */ /** The Open edX LMS binding features both APIs, course access as well as SEB restriction */
OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION), OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION),
/** The Moodle binding features only the course access API so far */ /** The Moodle binding features only the course access API so far */

View file

@ -415,6 +415,17 @@ public enum ActionDefinition {
PageStateDefinitionImpl.EXAM_VIEW, PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM), 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( EXAM_SEB_CLIENT_CONFIG_EXPORT(
new LocTextKey("sebserver.exam.action.createClientToStartExam"), new LocTextKey("sebserver.exam.action.createClientToStartExam"),
ImageIcon.EXPORT, ImageIcon.EXPORT,
@ -849,6 +860,11 @@ public enum ActionDefinition {
ImageIcon.YES, ImageIcon.YES,
PageStateDefinitionImpl.MONITORING_CLIENT_CONNECTION, PageStateDefinitionImpl.MONITORING_CLIENT_CONNECTION,
ActionCategory.EXAM_MONITORING_NOTIFICATION_LIST), 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( MONITOR_EXAM_QUIT_SELECTED(
new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.selected"), new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.selected"),

View file

@ -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.ClientGroupTemplateForm;
import ch.ethz.seb.sebserver.gui.content.exam.ExamForm; 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.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.ExamTemplateForm;
import ch.ethz.seb.sebserver.gui.content.exam.ExamTemplateList; import ch.ethz.seb.sebserver.gui.content.exam.ExamTemplateList;
import ch.ethz.seb.sebserver.gui.content.exam.IndicatorForm; 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), EXAM_EDIT(Type.FORM_EDIT, ExamForm.class, ActivityDefinition.EXAM),
INDICATOR_EDIT(Type.FORM_EDIT, IndicatorForm.class, ActivityDefinition.EXAM), INDICATOR_EDIT(Type.FORM_EDIT, IndicatorForm.class, ActivityDefinition.EXAM),
CLIENT_GROUP_EDIT(Type.FORM_EDIT, ClientGroupForm.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_LIST(Type.LIST_VIEW, ExamTemplateList.class, ActivityDefinition.EXAM_TEMPLATE),
EXAM_TEMPLATE_VIEW(Type.LIST_VIEW, ExamTemplateForm.class, ActivityDefinition.EXAM_TEMPLATE), EXAM_TEMPLATE_VIEW(Type.LIST_VIEW, ExamTemplateForm.class, ActivityDefinition.EXAM_TEMPLATE),

View file

@ -252,6 +252,8 @@ public class ExamForm implements TemplateComposer {
final ExamStatus examStatus = exam.getStatus(); final ExamStatus examStatus = exam.getStatus();
final boolean editable = modifyGrant && final boolean editable = modifyGrant &&
(examStatus == ExamStatus.UP_COMING || examStatus == ExamStatus.RUNNING); (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 sebRestrictionAvailable = testSEBRestrictionAPI(exam);
final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService
@ -465,6 +467,14 @@ public class ExamForm implements TemplateComposer {
.publishIf(() -> sebRestrictionAvailable && readonly && modifyGrant && !importFromQuizData .publishIf(() -> sebRestrictionAvailable && readonly && modifyGrant && !importFromQuizData
&& BooleanUtils.isTrue(isRestricted)) && 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) .newAction(ActionDefinition.EXAM_PROCTORING_ON)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withExec(this.proctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable)) .withExec(this.proctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable))

View file

@ -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
}
}

View file

@ -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.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; 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.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.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; 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;
@ -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.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.ConfirmPendingClientNotification;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionData; 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.api.session.GetPendingClientNotifications;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionDetails; import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionDetails;
@ -124,6 +126,7 @@ public class MonitoringClientConnection implements TemplateComposer {
private final InstructionProcessor instructionProcessor; private final InstructionProcessor instructionProcessor;
private final SEBClientEventDetailsPopup sebClientLogDetailsPopup; private final SEBClientEventDetailsPopup sebClientLogDetailsPopup;
private final SEBSendLockPopup sebSendLockPopup; private final SEBSendLockPopup sebSendLockPopup;
private final SignatureKeyGrantPopup signatureKeyGrantPopup;
private final MonitoringProctoringService monitoringProctoringService; private final MonitoringProctoringService monitoringProctoringService;
private final long pollInterval; private final long pollInterval;
private final int pageSize; private final int pageSize;
@ -139,6 +142,7 @@ public class MonitoringClientConnection implements TemplateComposer {
final SEBClientEventDetailsPopup sebClientLogDetailsPopup, final SEBClientEventDetailsPopup sebClientLogDetailsPopup,
final MonitoringProctoringService monitoringProctoringService, final MonitoringProctoringService monitoringProctoringService,
final SEBSendLockPopup sebSendLockPopup, final SEBSendLockPopup sebSendLockPopup,
final SignatureKeyGrantPopup signatureKeyGrantPopup,
@Value("${sebserver.gui.webservice.poll-interval:500}") final long pollInterval, @Value("${sebserver.gui.webservice.poll-interval:500}") final long pollInterval,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) { @Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
@ -151,6 +155,7 @@ public class MonitoringClientConnection implements TemplateComposer {
this.pollInterval = pollInterval; this.pollInterval = pollInterval;
this.sebClientLogDetailsPopup = sebClientLogDetailsPopup; this.sebClientLogDetailsPopup = sebClientLogDetailsPopup;
this.sebSendLockPopup = sebSendLockPopup; this.sebSendLockPopup = sebSendLockPopup;
this.signatureKeyGrantPopup = signatureKeyGrantPopup;
this.pageSize = pageSize; this.pageSize = pageSize;
this.typeFilter = new TableFilterAttribute( this.typeFilter = new TableFilterAttribute(
@ -395,6 +400,26 @@ public class MonitoringClientConnection implements TemplateComposer {
.publishIf(() -> isExamSupporter.getAsBoolean() && .publishIf(() -> isExamSupporter.getAsBoolean() &&
connectionData.clientConnection.status.clientActiveStatus); 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) { if (connectionData.clientConnection.status == ConnectionStatus.ACTIVE) {
final ProctoringServiceSettings proctoringSettings = restService final ProctoringServiceSettings proctoringSettings = restService
.getBuilder(GetExamProctoringSettings.class) .getBuilder(GetExamProctoringSettings.class)

View file

@ -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<FormHandle<?>> dialog =
new ModalInputDialog<>(
action.pageContext().getParent().getShell(),
this.pageService.getWidgetFactory());
dialog.setDialogWidth(700);
final Predicate<FormHandle<?>> 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<FormHandle<?>> {
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<FormHandle<?>> 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();
}
}

View file

@ -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.TemplateAttribute;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.View; 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.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;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification; 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.seb.examconfig.GetViews;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.useraccount.GetUserAccountNames; 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.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable.MonitoringEntry; import ch.ethz.seb.sebserver.gui.service.session.MonitoringEntry;
@Lazy @Lazy
@Service @Service
@ -626,27 +625,38 @@ public class ResourceService {
.getText(ResourceService.EXAMCONFIG_STATUS_PREFIX + config.configStatus.name()); .getText(ResourceService.EXAMCONFIG_STATUS_PREFIX + config.configStatus.name());
} }
public Function<ClientConnectionData, String> localizedClientConnectionStatusNameFunction() { // public Function<ClientConnectionData, String> localizedClientConnectionStatusNameFunction() {
//
// Memoizing // // Memoizing
final String missing = this.i18nSupport.getText( // final String missing = this.i18nSupport.getText(
SEB_CONNECTION_STATUS_KEY_PREFIX + MISSING_CLIENT_PING_NAME_KEY, // SEB_CONNECTION_STATUS_KEY_PREFIX + MISSING_CLIENT_PING_NAME_KEY,
MISSING_CLIENT_PING_NAME_KEY); // MISSING_CLIENT_PING_NAME_KEY);
final EnumMap<ConnectionStatus, String> localizedNames = new EnumMap<>(ConnectionStatus.class); // final String missingGrant = this.i18nSupport.getText(
Arrays.asList(ConnectionStatus.values()).stream().forEach(state -> localizedNames.put(state, this.i18nSupport // SEB_CONNECTION_STATUS_KEY_PREFIX + MISSING_CLIENT_SEC_GRANT_NAME_KEY,
.getText(SEB_CONNECTION_STATUS_KEY_PREFIX + state.name(), state.name()))); // MISSING_CLIENT_SEC_GRANT_NAME_KEY);
// final EnumMap<ConnectionStatus, String> localizedNames = new EnumMap<>(ConnectionStatus.class);
return connectionData -> { // Arrays.asList(ConnectionStatus.values()).stream().forEach(state -> localizedNames.put(state, this.i18nSupport
if (connectionData == null) { // .getText(SEB_CONNECTION_STATUS_KEY_PREFIX + state.name(), state.name())));
localizedNames.get(ConnectionStatus.UNDEFINED); //
} // return connectionData -> {
if (connectionData.missingPing && connectionData.clientConnection.status.establishedStatus) { // if (connectionData == null) {
return missing; // localizedNames.get(ConnectionStatus.UNDEFINED);
} else { // }
return localizedNames.get(connectionData.clientConnection.status); // 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<MonitoringEntry, String> localizedClientMonitoringStatusNameFunction() { public Function<MonitoringEntry, String> localizedClientMonitoringStatusNameFunction() {

View file

@ -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<SecurityKey> {
public GetClientConnectionSecurityKey() {
super(new TypeKey<>(
CallType.GET_SINGLE,
EntityType.SEB_SECURITY_KEY_REGISTRY,
new TypeReference<SecurityKey>() {
}),
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);
}
}

View file

@ -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<SecurityKey> {
public GrantClientConnectionSecurityKey() {
super(new TypeKey<>(
CallType.GET_SINGLE,
EntityType.SEB_SECURITY_KEY_REGISTRY,
new TypeReference<SecurityKey>() {
}),
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);
}
}

View file

@ -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.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; 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;
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.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification; import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
import ch.ethz.seb.sebserver.gbl.monitoring.IndicatorValue; 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.table.EntityTable;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; 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); private static final Logger log = LoggerFactory.getLogger(ClientConnectionDetails.class);
@ -72,7 +73,8 @@ public class ClientConnectionDetails {
private final RestCall<ClientConnectionData>.RestCallBuilder restCallBuilder; private final RestCall<ClientConnectionData>.RestCallBuilder restCallBuilder;
private final FormHandle<?> formHandle; private final FormHandle<?> formHandle;
private final ColorData colorData; private final ColorData colorData;
private final Function<ClientConnectionData, String> localizedClientConnectionStatusNameFunction; private final Function<MonitoringEntry, String> localizedClientConnectionStatusNameFunction;
public final boolean checkSecurityGrant;
private ClientConnectionData connectionData = null; private ClientConnectionData connectionData = null;
private boolean statusChanged = true; private boolean statusChanged = true;
@ -94,6 +96,8 @@ public class ClientConnectionDetails {
this.resourceService = pageService.getResourceService(); this.resourceService = pageService.getResourceService();
this.restCallBuilder = restCallBuilder; this.restCallBuilder = restCallBuilder;
this.colorData = new ColorData(display); this.colorData = new ColorData(display);
this.checkSecurityGrant = BooleanUtils.toBoolean(
exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED));
this.indicatorMapping = IndicatorData.createFormIndicators( this.indicatorMapping = IndicatorData.createFormIndicators(
indicators, indicators,
display, display,
@ -146,7 +150,26 @@ public class ClientConnectionDetails {
this.formHandle = formBuilder.build(); this.formHandle = formBuilder.build();
this.localizedClientConnectionStatusNameFunction = 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<ClientConnectionData> statusChangeListener) { public void setStatusChangeListener(final Consumer<ClientConnectionData> statusChangeListener) {
@ -210,8 +233,8 @@ public class ClientConnectionDetails {
// update status // update status
form.setFieldValue( form.setFieldValue(
Domain.CLIENT_CONNECTION.ATTR_STATUS, Domain.CLIENT_CONNECTION.ATTR_STATUS,
this.localizedClientConnectionStatusNameFunction.apply(this.connectionData)); this.localizedClientConnectionStatusNameFunction.apply(this));
final Color statusColor = this.colorData.getStatusColor(this.connectionData); final Color statusColor = this.colorData.getStatusColor(this);
final Color statusTextColor = this.colorData.getStatusTextColor(statusColor); final Color statusTextColor = this.colorData.getStatusTextColor(statusColor);
form.setFieldColor(Domain.CLIENT_CONNECTION.ATTR_STATUS, statusColor); form.setFieldColor(Domain.CLIENT_CONNECTION.ATTR_STATUS, statusColor);
form.setFieldTextColor(Domain.CLIENT_CONNECTION.ATTR_STATUS, statusTextColor); form.setFieldTextColor(Domain.CLIENT_CONNECTION.ATTR_STATUS, statusTextColor);

View file

@ -449,14 +449,6 @@ public final class ClientConnectionTable implements FullPageMonitoringGUIUpdate
// TODO if right click get selected item and show additional information (notification) // 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<UpdatableTableItem>, MonitoringEntry { private final class UpdatableTableItem implements Comparable<UpdatableTableItem>, MonitoringEntry {
final Long connectionId; final Long connectionId;

View file

@ -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.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable.MonitoringEntry;
public class ColorData { public class ColorData {
@ -36,19 +35,6 @@ public class ColorData {
this.lightColor = new Color(display, Constants.WHITE_RGB); 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) { Color getStatusColor(final MonitoringEntry entry) {
final ConnectionStatus status = entry.getStatus(); final ConnectionStatus status = entry.getStatus();
if (status == null) { if (status == null) {

View file

@ -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();
}

View file

@ -144,7 +144,10 @@ public class WidgetFactory {
RESTRICTION("restriction.png"), RESTRICTION("restriction.png"),
VISIBILITY("visibility.png"), VISIBILITY("visibility.png"),
VISIBILITY_OFF("visibility_off.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; public String fileName;
private ImageData image = null; private ImageData image = null;
@ -466,6 +469,7 @@ public class WidgetFactory {
final LocTextKey locToolTextKey) { final LocTextKey locToolTextKey) {
final Label label = new Label(parent, SWT.NONE); final Label label = new Label(parent, SWT.NONE);
label.setData(RWT.MARKUP_ENABLED, true);
this.polyglotPageService.injectI18n(label, locTextKey, locToolTextKey); this.polyglotPageService.injectI18n(label, locTextKey, locToolTextKey);
return label; return label;
} }
@ -477,6 +481,7 @@ public class WidgetFactory {
final LocTextKey locToolTextKey) { final LocTextKey locToolTextKey) {
final Label label = new Label(parent, SWT.NONE); final Label label = new Label(parent, SWT.NONE);
label.setData(RWT.MARKUP_ENABLED, true);
this.polyglotPageService.injectI18n(label, locTextKey, locToolTextKey); this.polyglotPageService.injectI18n(label, locTextKey, locToolTextKey);
label.setData(RWT.CUSTOM_VARIANT, variant.key); label.setData(RWT.CUSTOM_VARIANT, variant.key);
return label; return label;
@ -484,6 +489,7 @@ public class WidgetFactory {
public Label labelLocalizedTitle(final Composite content, final LocTextKey locTextKey) { public Label labelLocalizedTitle(final Composite content, final LocTextKey locTextKey) {
final Label labelLocalized = labelLocalized(content, CustomVariant.TEXT_H1, 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); final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false);
labelLocalized.setLayoutData(gridData); labelLocalized.setLayoutData(gridData);
return labelLocalized; return labelLocalized;

View file

@ -167,6 +167,12 @@ public interface ClientConnectionDAO extends
* *
* @param examId the exam identifier * @param examId the exam identifier
* @return Result refer to a collection of client connection records or to an error when happened */ * @return Result refer to a collection of client connection records or to an error when happened */
Result<Collection<ClientConnectionRecord>> getAllConnectionIdsForExam(Long examId); Result<Collection<ClientConnectionRecord>> 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<Collection<Long>> getAllConnectionIdsForExam(Long examId);
} }

View file

@ -36,4 +36,11 @@ public interface SecurityKeyRegistryDAO extends EntityDAO<SecurityKey, SecurityK
@EventListener(ExamTemplateDeletionEvent.class) @EventListener(ExamTemplateDeletionEvent.class)
void notifyExamTemplateDeletion(ExamTemplateDeletionEvent event); void notifyExamTemplateDeletion(ExamTemplateDeletionEvent event);
/** This checks if there is already a grant for the given key and return it if available
* or the given key otherwise.
*
* @param key SecurityKey data to check if there is a grant registered
* @return Result refer to the grant if available or the the given key if not or to an error when happened */
Result<SecurityKey> getGrantOr(final SecurityKey key);
} }

View file

@ -764,7 +764,7 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Collection<ClientConnectionRecord>> getAllConnectionIdsForExam(final Long examId) { public Result<Collection<ClientConnectionRecord>> getAllConnectionRecordsForExam(final Long examId) {
return Result.tryCatch(() -> this.clientConnectionRecordMapper return Result.tryCatch(() -> this.clientConnectionRecordMapper
.selectByExample() .selectByExample()
.where( .where(
@ -774,6 +774,18 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
.execute()); .execute());
} }
@Override
@Transactional(readOnly = true)
public Result<Collection<Long>> getAllConnectionIdsForExam(final Long examId) {
return Result.tryCatch(() -> this.clientConnectionRecordMapper
.selectIdsByExample()
.where(
ClientConnectionRecordDynamicSqlSupport.examId,
SqlBuilder.isEqualTo(examId))
.build()
.execute());
}
private Result<ClientConnectionRecord> recordById(final Long id) { private Result<ClientConnectionRecord> recordById(final Long id) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {

View file

@ -14,6 +14,7 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; 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;
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey.KeyType; import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey.KeyType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; 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.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.SecurityKeyRegistryRecordDynamicSqlSupport; 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 { public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO {
private final SecurityKeyRegistryRecordMapper securityKeyRegistryRecordMapper; 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.securityKeyRegistryRecordMapper = securityKeyRegistryRecordMapper;
this.cryptor = cryptor;
} }
@Override @Override
@ -123,7 +130,7 @@ public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO {
public Result<SecurityKey> createNew(final SecurityKey data) { public Result<SecurityKey> createNew(final SecurityKey data) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
checkUniqueTag(data); checkUniqueKey(data);
final SecurityKeyRegistryRecord newRecord = new SecurityKeyRegistryRecord( final SecurityKeyRegistryRecord newRecord = new SecurityKeyRegistryRecord(
null, null,
@ -146,8 +153,6 @@ public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO {
public Result<SecurityKey> save(final SecurityKey data) { public Result<SecurityKey> save(final SecurityKey data) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
checkUniqueTag(data);
final SecurityKeyRegistryRecord newRecord = new SecurityKeyRegistryRecord( final SecurityKeyRegistryRecord newRecord = new SecurityKeyRegistryRecord(
null, null,
data.institutionId, data.institutionId,
@ -312,6 +317,32 @@ public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO {
}); });
} }
@Override
@Transactional(readOnly = true)
public Result<SecurityKey> 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 @Override
public void notifyExamDeletion(final ExamDeletionEvent event) { public void notifyExamDeletion(final ExamDeletionEvent event) {
try { try {
@ -367,18 +398,11 @@ public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO {
rec.getExamTemplateId()); rec.getExamTemplateId());
} }
private void checkUniqueTag(final SecurityKey securityKeyRegistry) { private void checkUniqueKey(final SecurityKey key) {
final Long count = this.securityKeyRegistryRecordMapper if (getGrantOr(key).getOr(key) != key) {
.countByExample()
.where(SecurityKeyRegistryRecordDynamicSqlSupport.tag, isEqualTo(securityKeyRegistry.tag))
.and(SecurityKeyRegistryRecordDynamicSqlSupport.id, isNotEqualToWhenPresent(securityKeyRegistry.id))
.build()
.execute();
if (count != null && count > 0) {
throw new FieldValidationException( throw new FieldValidationException(
Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG, Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG,
"institution:name:tag.notunique"); "securityKey:keyValue:alreadyGranted");
} }
} }

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.institution;
import java.util.Collection; import java.util.Collection;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; 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.SecurityCheckResult;
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey; import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; 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 */ /** 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"; public static final String ADDITIONAL_ATTR_APP_SIGNATURE_KEY = "APP_SIGNATURE_KEY";
Result<Collection<SecurityKey>> getPlainGrants(Long institutionId, Long examId); Result<SecurityKey> getSecurityKeyOfConnection(Long institutionId, Long connectionId);
Result<AppSignatureKeyInfo> getAppSignaturesInfo(Long institutionId, Long examId);
Result<Collection<SecurityKey>> getPlainAppSignatureKeyGrants(Long institutionId, Long examId); Result<Collection<SecurityKey>> getPlainAppSignatureKeyGrants(Long institutionId, Long examId);

View file

@ -10,8 +10,12 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.institution.impl;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils; 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.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; 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.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.SecurityCheckResult;
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey; 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.model.institution.SecurityKey.KeyType;
@ -69,10 +74,35 @@ public class SecurityKeyServiceImpl implements SecurityKeyService {
} }
@Override @Override
public Result<Collection<SecurityKey>> getPlainGrants(final Long institutionId, final Long examId) { public Result<SecurityKey> getSecurityKeyOfConnection(final Long institutionId, final Long connectionId) {
return this.securityKeyRegistryDAO return this.clientConnectionDAO.byPK(connectionId)
.getAll(institutionId, examId, null) .map(connection -> new SecurityKey(
.map(this::decryptAll); null,
institutionId,
KeyType.APP_SIGNATURE_KEY,
decryptStoredSignatureForConnection(connection),
connection.sebVersion,
null, null))
.flatMap(this.securityKeyRegistryDAO::getGrantOr);
}
@Override
public Result<AppSignatureKeyInfo> getAppSignaturesInfo(final Long institutionId, final Long examId) {
return Result.tryCatch(() -> {
final Map<String, Set<Long>> 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 @Override
@ -386,6 +416,15 @@ public class SecurityKeyServiceImpl implements SecurityKeyService {
return decryptSignatureWithConnectionToken(cc.connectionToken, signatureKey); 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) { private void saveSignatureKeyForConnection(final ClientConnection clientConnection, final String appSignatureKey) {
this.additionalAttributesDAO this.additionalAttributesDAO
.saveAdditionalAttribute( .saveAdditionalAttribute(

View file

@ -25,7 +25,7 @@ public class MockSEBRestrictionAPI implements SEBRestrictionAPI {
@Override @Override
public LmsSetupTestResult testCourseRestrictionAPI() { public LmsSetupTestResult testCourseRestrictionAPI() {
return LmsSetupTestResult.ofQuizRestrictionAPIError(LmsType.MOCKUP, "unsupported"); return LmsSetupTestResult.ofOkay(LmsType.MOCKUP);
} }
@Override @Override

View file

@ -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.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; 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.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;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features;
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey; import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
@ -190,7 +191,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
} }
// **************************************************************************** // ****************************************************************************
// **** SEB Security Key // **** SEB Security Key and App Signature Key
@RequestMapping( @RequestMapping(
path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT
@ -258,6 +259,24 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
.getOrThrow(); .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 // **** SEB Security Key
// **************************************************************************** // ****************************************************************************

View file

@ -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.Domain;
import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; 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.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction; import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
@ -463,29 +464,50 @@ public class ExamMonitoringController {
@RequestMapping( @RequestMapping(
path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + 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, API.MODEL_ID_VAR_PATH_SEGMENT,
method = RequestMethod.POST, method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
public void grantAppSignatureKey( produces = MediaType.APPLICATION_JSON_VALUE)
public SecurityKey grantAppSignatureKey(
@RequestParam( @RequestParam(
name = API.PARAM_INSTITUTION_ID, name = API.PARAM_INSTITUTION_ID,
required = true, required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, 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_PARENT_MODEL_ID, required = true) final Long examId,
@PathVariable(name = API.MODEL_ID_VAR_PATH_SEGMENT, required = true) final Long connectionId, @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long connectionId,
@RequestParam( @RequestParam(name = Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG, required = true) final String tagName) {
name = Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN,
required = false) final String connectionToken) {
checkPrivileges(institutionId, examId); checkPrivileges(institutionId, examId);
this.securityKeyService return this.securityKeyService
.registerExamAppSignatureKey(institutionId, examId, connectionId, null) .registerExamAppSignatureKey(institutionId, examId, connectionId, tagName)
.onSuccess(key -> this.securityKeyService.updateAppSignatureKeyGrants(examId)) .onSuccess(key -> this.securityKeyService.updateAppSignatureKeyGrants(examId))
.getOrThrow(); .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) { private Exam checkPrivileges(final Long institutionId, final Long examId) {
// check overall privilege // check overall privilege
this.authorization.checkRole( this.authorization.checkRole(

View file

@ -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.open.error=Failed to open the collecting room.
sebserver.exam.proctoring.collecting.close.error=Failed to close the collecting room properly. sebserver.exam.proctoring.collecting.close.error=Failed to close the collecting room properly.
sebserver.exam.signaturekey.action.edit=App Signature Key
################################ ################################
# Connection Configuration # Connection Configuration
@ -1984,6 +1986,7 @@ sebserver.monitoring.exam.connection.actions.group3=&nbsp;
sebserver.monitoring.exam.connection.notificationlist.actions= sebserver.monitoring.exam.connection.notificationlist.actions=
sebserver.monitoring.exam.connection.action.confirm.notification=Confirm Notification 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?<br/><br/>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.confirm.notification.text=Are you sure you want to confirm this pending notification?<br/><br/>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.pleaseSelect=At first please select a notification form the Pending Notification list
sebserver.monitoring.exam.connection.notificationlist.title=Pending Notification 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.list.info=SEB Connection Info
sebserver.monitoring.lock.noselection=Please select at least one active SEB client connection. 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 # Finished Exams
################################ ################################

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B