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_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";

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. */
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 */

View file

@ -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"),

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.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),

View file

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

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.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)

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.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() {

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.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);

View file

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

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.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) {

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"),
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;

View file

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

View file

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

View file

@ -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(() -> {

View file

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

View file

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

View file

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

View file

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

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.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
// ****************************************************************************

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.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(

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.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=&nbsp;
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
################################

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