SEBSERV-335 implementation Exam
This commit is contained in:
parent
949552bbf7
commit
ea13bb6ca4
30 changed files with 712 additions and 86 deletions
|
@ -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";
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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 */
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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<ClientConnectionData, String> 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<ConnectionStatus, String> 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<ClientConnectionData, String> 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<ConnectionStatus, String> 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<MonitoringEntry, String> localizedClientMonitoringStatusNameFunction() {
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<ClientConnectionData>.RestCallBuilder restCallBuilder;
|
||||
private final FormHandle<?> formHandle;
|
||||
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 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<ClientConnectionData> 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);
|
||||
|
|
|
@ -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<UpdatableTableItem>, MonitoringEntry {
|
||||
|
||||
final Long connectionId;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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<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);
|
||||
|
||||
}
|
||||
|
|
|
@ -36,4 +36,11 @@ public interface SecurityKeyRegistryDAO extends EntityDAO<SecurityKey, SecurityK
|
|||
@EventListener(ExamTemplateDeletionEvent.class)
|
||||
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);
|
||||
|
||||
}
|
||||
|
|
|
@ -764,7 +764,7 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
|
|||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Result<Collection<ClientConnectionRecord>> getAllConnectionIdsForExam(final Long examId) {
|
||||
public Result<Collection<ClientConnectionRecord>> 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<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) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
|
|
|
@ -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<SecurityKey> 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<SecurityKey> 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<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
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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);
|
||||
|
||||
|
|
|
@ -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<Collection<SecurityKey>> getPlainGrants(final Long institutionId, final Long examId) {
|
||||
return this.securityKeyRegistryDAO
|
||||
.getAll(institutionId, examId, null)
|
||||
.map(this::decryptAll);
|
||||
public Result<SecurityKey> 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<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
|
||||
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Exam, Exam> {
|
|||
}
|
||||
|
||||
// ****************************************************************************
|
||||
// **** 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<Exam, Exam> {
|
|||
.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
|
||||
// ****************************************************************************
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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?<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.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
|
||||
################################
|
||||
|
|
BIN
src/main/resources/static/images/no_shield.png
Normal file
BIN
src/main/resources/static/images/no_shield.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 317 B |
BIN
src/main/resources/static/images/shield.png
Normal file
BIN
src/main/resources/static/images/shield.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 285 B |
BIN
src/main/resources/static/images/verify.png
Normal file
BIN
src/main/resources/static/images/verify.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 333 B |
Loading…
Reference in a new issue