From 9d80a94bbf93f4e236f5223e756a63582fbaa8ae Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 24 Nov 2022 16:55:53 +0100 Subject: [PATCH] SEBSERV-335 implementation --- .../ch/ethz/seb/sebserver/gbl/api/API.java | 5 +- .../seb/sebserver/gbl/model/exam/Exam.java | 5 + .../institution/AppSignatureKeyInfo.java | 53 ++- .../gbl/model/session/ClientConnection.java | 8 + .../model/session/ClientMonitoringData.java | 9 + .../session/ClientMonitoringDataView.java | 4 + .../ch/ethz/seb/sebserver/gbl/util/Utils.java | 14 + .../gui/content/action/ActionCategory.java | 2 + .../gui/content/action/ActionDefinition.java | 26 ++ .../exam/AddSecurityKeyGrantPopup.java | 198 +++++++++++ .../content/exam/ExamSignatureKeyForm.java | 256 +++++++++++++- .../content/exam/SecurityKeyGrantPopup.java | 100 ++++++ .../MonitoringClientConnection.java | 15 +- .../monitoring/SignatureKeyGrantPopup.java | 21 +- .../seb/sebserver/gui/form/FieldBuilder.java | 2 +- .../gui/service/page/PageService.java | 3 +- .../api/exam/GetClientConnections.java | 43 +++ .../api/exam/seckey/AddSecurityKeyGrant.java | 42 +++ .../exam/seckey/DeleteSecurityKeyGrant.java | 43 +++ .../exam/seckey/GetAppSignatureKeyInfo.java | 44 +++ .../api/exam/seckey/GetAppSignatureKeys.java | 44 +++ .../seckey/SaveAppSignatureKeySettings.java | 42 +++ .../session/ClientConnectionTable.java | 3 +- .../seb/sebserver/gui/table/EntityTable.java | 5 +- .../sebserver/gui/widget/WidgetFactory.java | 5 +- .../servicelayer/dao/ClientConnectionDAO.java | 5 +- .../dao/SecurityKeyRegistryDAO.java | 29 ++ .../dao/impl/AdditionalAttributesDAOImpl.java | 5 +- .../dao/impl/ClientConnectionDAOImpl.java | 5 +- .../dao/impl/SecurityKeyRegistryDAOImpl.java | 14 +- .../servicelayer/exam/ExamAdminService.java | 15 + .../exam/impl/ExamAdminServiceImpl.java | 54 +++ .../institution/SecurityKeyService.java | 71 +++- .../impl/SecurityKeyServiceImpl.java | 315 +++++++++++------- .../impl/ClientConnectionDataInternal.java | 5 + .../weblayer/api/ExamAPI_V1_Controller.java | 29 +- .../api/ExamAdministrationController.java | 80 +++-- .../api/ExamMonitoringController.java | 2 +- src/main/resources/messages.properties | 51 ++- 39 files changed, 1460 insertions(+), 212 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/content/exam/AddSecurityKeyGrantPopup.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/content/exam/SecurityKeyGrantPopup.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetClientConnections.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/AddSecurityKeyGrant.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/DeleteSecurityKeyGrant.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/GetAppSignatureKeyInfo.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/GetAppSignatureKeys.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/SaveAppSignatureKeySettings.java diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index 757c434d..8f2df61c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -106,6 +106,8 @@ public final class API { public static final String EXAM_API_SEB_CONNECTION_TOKEN = "SEBConnectionToken"; + public static final String EXAM_API_EXAM_SIGNATURE_SALT_HEADER = "SEBExamSalt"; + public static final String EXAM_API_USER_SESSION_ID = "seb_user_session_id"; public static final String EXAM_API_HANDSHAKE_ENDPOINT = "/handshake"; @@ -146,7 +148,6 @@ public final class API { public static final String QUIZ_DISCOVERY_ENDPOINT = "/quiz"; public static final String EXAM_ADMINISTRATION_ENDPOINT = "/exam"; - //public static final String EXAM_ADMINISTRATION_DOWNLOAD_CONFIG_PATH_SEGMENT = "/download-config"; public static final String EXAM_ADMINISTRATION_ARCHIVE_PATH_SEGMENT = "/archive"; public static final String EXAM_ADMINISTRATION_CONSISTENCY_CHECK_PATH_SEGMENT = "/check-consistency"; public static final String EXAM_ADMINISTRATION_CONSISTENCY_CHECK_INCLUDE_RESTRICTION = "include-restriction"; @@ -156,7 +157,7 @@ public final class API { 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 = "/grant"; - public static final String EXAM_ADMINISTRATION_SEB_SECURITY_AS_KEYS_PATH_SEGMENT = "/signature-key"; + public static final String EXAM_ADMINISTRATION_SEB_SECURITY_KEY_INFO_PATH_SEGMENT = "/sebkeyinfo"; public static final String EXAM_INDICATOR_ENDPOINT = "/indicator"; public static final String EXAM_CLIENT_GROUP_ENDPOINT = "/client-group"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java index f192d497..8157715c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java @@ -31,6 +31,8 @@ import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.model.Domain.EXAM; import ch.ethz.seb.sebserver.gbl.model.GrantEntity; +import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus; +import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType; import ch.ethz.seb.sebserver.gbl.util.Utils; @JsonIgnoreProperties(ignoreUnknown = true) @@ -62,6 +64,7 @@ public final class Exam implements GrantEntity { public static final String FILTER_CACHED_QUIZZES = "cached-quizzes"; public static final String ATTR_ADDITIONAL_ATTRIBUTES = "additionalAttributes"; + /** This attribute name is used on exams to store the flag for indicating the signature key check */ public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED = "SIGNATURE_KEY_CHECK_ENABLED"; /** This attribute name is used to store the signature check grant threshold for statistical checks */ @@ -69,6 +72,8 @@ public final class Exam implements GrantEntity { /** This attribute name is used to store the signature check encryption certificate is one is used */ public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_CERT_ALIAS = "SIGNATURE_KEY_CERT_ALIAS"; + public static final String ADDITIONAL_ATTR_SIGNATURE_KEY_SALT = "SIGNATURE_KEY_SALT"; + public enum ExamStatus { UP_COMING, RUNNING, diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/AppSignatureKeyInfo.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/AppSignatureKeyInfo.java index 82a487f7..b188b360 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/AppSignatureKeyInfo.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/AppSignatureKeyInfo.java @@ -10,21 +10,26 @@ 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 org.apache.tomcat.util.buf.StringUtils; + import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.model.Domain.SEB_SECURITY_KEY_REGISTRY; +import ch.ethz.seb.sebserver.gbl.model.ModelIdAware; import ch.ethz.seb.sebserver.gbl.util.Utils; @JsonIgnoreProperties(ignoreUnknown = true) -public class AppSignatureKeyInfo { +public class AppSignatureKeyInfo implements ModelIdAware { - public static final String ATTR_KEY_CONNECTION_MAPPING = "kcMapping"; + public static final String ATTR_NUMBER_OF_CONNECTIONS = "numConnections"; + public static final String ATTR_CONNECTION_IDS = "connectionIds"; @NotNull @JsonProperty(SEB_SECURITY_KEY_REGISTRY.ATTR_INSTITUTION_ID) @@ -34,18 +39,23 @@ public class AppSignatureKeyInfo { @JsonProperty(SEB_SECURITY_KEY_REGISTRY.ATTR_EXAM_ID) public final Long examId; - @JsonProperty(ATTR_KEY_CONNECTION_MAPPING) - public final Map> keyConnectionMapping; + @JsonProperty(SEB_SECURITY_KEY_REGISTRY.ATTR_KEY_VALUE) + public final String key; + + @JsonProperty(ATTR_CONNECTION_IDS) + public final Map connectionIds; @JsonCreator public AppSignatureKeyInfo( @JsonProperty(SEB_SECURITY_KEY_REGISTRY.ATTR_INSTITUTION_ID) final Long institutionId, @JsonProperty(SEB_SECURITY_KEY_REGISTRY.ATTR_EXAM_ID) final Long examId, - @JsonProperty(ATTR_KEY_CONNECTION_MAPPING) final Map> keyConnectionMapping) { + @JsonProperty(SEB_SECURITY_KEY_REGISTRY.ATTR_KEY_VALUE) final String key, + @JsonProperty(ATTR_CONNECTION_IDS) final Map connectionIds) { this.institutionId = institutionId; this.examId = examId; - this.keyConnectionMapping = Utils.immutableMapOf(keyConnectionMapping); + this.key = key; + this.connectionIds = Utils.immutableMapOf(connectionIds); } public Long getInstitutionId() { @@ -56,8 +66,27 @@ public class AppSignatureKeyInfo { return this.examId; } - public Map> getKeyConnectionMapping() { - return this.keyConnectionMapping; + @Override + public String getModelId() { + return this.key; + } + + public String getKey() { + return this.key; + } + + public Map getConnectionIds() { + return this.connectionIds; + } + + @JsonIgnore + public String getConnectionNames() { + return StringUtils.join(this.connectionIds.values(), Constants.LIST_SEPARATOR_CHAR); + } + + @JsonIgnore + public int getNumberOfConnections() { + return this.connectionIds.size(); } @Override @@ -84,8 +113,10 @@ public class AppSignatureKeyInfo { builder.append(this.institutionId); builder.append(", examId="); builder.append(this.examId); - builder.append(", keyConnectionMapping="); - builder.append(this.keyConnectionMapping); + builder.append(", key="); + builder.append(this.key); + builder.append(", connectionIds="); + builder.append(this.connectionIds); builder.append("]"); return builder.toString(); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnection.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnection.java index 8af5e34b..9711c45a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnection.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientConnection.java @@ -29,6 +29,9 @@ import ch.ethz.seb.sebserver.gbl.util.Utils; @JsonIgnoreProperties(ignoreUnknown = true) public final class ClientConnection implements GrantEntity { + /** This attribute name is used to store the App-Signature-Key given by a SEB Client */ + public static final String ADDITIONAL_ATTR_APP_SIGNATURE_KEY = "APP_SIGNATURE_KEY"; + public enum ConnectionStatus { UNDEFINED(0, false, false), CONNECTION_REQUESTED(1, true, false), @@ -55,6 +58,11 @@ public final class ClientConnection implements GrantEntity { ConnectionStatus.AUTHENTICATED.name(), ConnectionStatus.CONNECTION_REQUESTED.name()); + public final static List SECURE_STATES = Utils.immutableListOf( + ConnectionStatus.ACTIVE.name(), + ConnectionStatus.AUTHENTICATED.name(), + ConnectionStatus.CLOSED.name()); + public static final ClientConnection EMPTY_CLIENT_CONNECTION = new ClientConnection( -1L, -1L, -1L, ConnectionStatus.UNDEFINED, diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientMonitoringData.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientMonitoringData.java index fd2f52cc..679d81e3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientMonitoringData.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientMonitoringData.java @@ -25,6 +25,7 @@ public class ClientMonitoringData implements ClientMonitoringDataView { public final ConnectionStatus status; public final Map indicatorVals; public final boolean missingPing; + public final boolean missingGrant; public final boolean pendingNotification; @JsonCreator @@ -33,12 +34,14 @@ public class ClientMonitoringData implements ClientMonitoringDataView { @JsonProperty(ATTR_STATUS) final ConnectionStatus status, @JsonProperty(ATTR_INDICATOR_VALUES) final Map indicatorVals, @JsonProperty(ATTR_MISSING_PING) final boolean missingPing, + @JsonProperty(ATTR_MISSING_GRANT) final boolean missingGrant, @JsonProperty(ATTR_PENDING_NOTIFICATION) final boolean pendingNotification) { this.id = id; this.status = status; this.indicatorVals = indicatorVals; this.missingPing = missingPing; + this.missingGrant = missingGrant; this.pendingNotification = pendingNotification; } @@ -62,6 +65,12 @@ public class ClientMonitoringData implements ClientMonitoringDataView { return this.missingPing; } + @Override + public boolean isMissingGrant() { + // TODO Auto-generated method stub + return false; + } + @Override public boolean isPendingNotification() { return this.pendingNotification; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientMonitoringDataView.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientMonitoringDataView.java index e361744c..01284c32 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientMonitoringDataView.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientMonitoringDataView.java @@ -27,6 +27,7 @@ public interface ClientMonitoringDataView { public static final String ATTR_INDICATOR_VALUES = "iv"; public static final String ATTR_CLIENT_GROUPS = "cg"; public static final String ATTR_MISSING_PING = "mp"; + public static final String ATTR_MISSING_GRANT = "mg"; public static final String ATTR_PENDING_NOTIFICATION = "pn"; @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_ID) @@ -41,6 +42,9 @@ public interface ClientMonitoringDataView { @JsonProperty(ATTR_MISSING_PING) boolean isMissingPing(); + @JsonProperty(ATTR_MISSING_GRANT) + boolean isMissingGrant(); + @JsonProperty(ATTR_PENDING_NOTIFICATION) boolean isPendingNotification(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java index fbff8459..3aa79f44 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; @@ -69,6 +70,19 @@ public final class Utils { private static final Logger log = LoggerFactory.getLogger(Utils.class); + /** Use this it merge two maps into a new one. Let the given maps unmodified. + * + * @param Key type + * @param Value type + * @param m1 First Map + * @param m2 Second Map + * @return new Map with merged entries from m1 and m2 */ + public static Map mergeMap(final Map m1, final Map m2) { + final HashMap hashMap = new HashMap<>(m1); + hashMap.putAll(m2); + return hashMap; + } + /** This Collector can be used within stream collect to get one expected singleton element from * the given Stream. * This first collects the given Stream to a list and then check if there is one expected element. diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionCategory.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionCategory.java index 2662b34a..7985f8ce 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionCategory.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionCategory.java @@ -21,6 +21,8 @@ public enum ActionCategory { EXAM_TEMPLATE_LIST(new LocTextKey("sebserver.examtemplate.list.actions"), 1), INDICATOR_TEMPLATE_LIST(new LocTextKey("sebserver.examtemplate.indicator.list.actions"), 1), CLIENT_GROUP_TEMPLATE_LIST(new LocTextKey("sebserver.examtemplate.clientgroup.list.actions"), 2), + APP_SIGNATURE_KEY_LIST(new LocTextKey("sebserver.exam.signaturekey.keylist.actions"), 1), + SECURITY_KEY_GRANT_LIST(new LocTextKey("sebserver.exam.signaturekey.grantlist.actions"), 2), EXAM_CONFIG_MAPPING_LIST(new LocTextKey("sebserver.exam.configuration.list.actions"), 1), INDICATOR_LIST(new LocTextKey("sebserver.exam.indicator.list.actions"), 2), CLIENT_GROUP_LIST(new LocTextKey("sebserver.exam.clientgroup.list.actions"), 3), diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java index b29a6d42..7bdbc575 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java @@ -426,6 +426,32 @@ public enum ActionDefinition { PageStateDefinitionImpl.SECURITY_KEY_EDIT, ActionCategory.FORM), + EXAM_SECURITY_KEY_SAVE_SETTINGS( + new LocTextKey("sebserver.exam.signaturekey.action.save"), + ImageIcon.SAVE, + PageStateDefinitionImpl.EXAM_VIEW, + ActionCategory.FORM), + EXAM_SECURITY_KEY_CANCEL_MODIFY( + new LocTextKey("sebserver.exam.signaturekey.action.cancel"), + ImageIcon.CANCEL, + PageStateDefinitionImpl.EXAM_VIEW, + ActionCategory.FORM), + EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP( + new LocTextKey("sebserver.exam.signaturekey.action.addGrant"), + ImageIcon.ADD, + PageStateDefinitionImpl.SECURITY_KEY_EDIT, + ActionCategory.APP_SIGNATURE_KEY_LIST), + EXAM_SECURITY_KEY_SHOW_GRANT_POPUP( + new LocTextKey("sebserver.exam.signaturekey.action.showGrant"), + ImageIcon.SHOW, + PageStateDefinitionImpl.SECURITY_KEY_EDIT, + ActionCategory.SECURITY_KEY_GRANT_LIST), + EXAM_SECURITY_KEY_DELETE_GRANT( + new LocTextKey("sebserver.exam.signaturekey.action.deleteGrant"), + ImageIcon.DELETE, + PageStateDefinitionImpl.SECURITY_KEY_EDIT, + ActionCategory.SECURITY_KEY_GRANT_LIST), + EXAM_SEB_CLIENT_CONFIG_EXPORT( new LocTextKey("sebserver.exam.action.createClientToStartExam"), ImageIcon.EXPORT, diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/AddSecurityKeyGrantPopup.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/AddSecurityKeyGrantPopup.java new file mode 100644 index 00000000..6b40b5dc --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/AddSecurityKeyGrantPopup.java @@ -0,0 +1,198 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.swt.widgets.Composite; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.institution.AppSignatureKeyInfo; +import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; +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.exam.GetClientConnections; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GrantClientConnectionSecurityKey; +import ch.ethz.seb.sebserver.gui.table.ColumnDefinition; +import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; + +@Lazy +@Component +@GuiProfile +public class AddSecurityKeyGrantPopup { + + private static final LocTextKey TITLE_TEXT_KEY = + new LocTextKey("sebserver.exam.signaturekey.seb.add.title"); + private static final LocTextKey TITLE_TEXT_INFO = + new LocTextKey("sebserver.exam.signaturekey.seb.add.info"); + + private static final LocTextKey TITLE_TEXT_FORM_SIGNATURE = + new LocTextKey("sebserver.exam.signaturekey.seb.add.signature"); + private static final LocTextKey TITLE_TEXT_FORM_TAG = + new LocTextKey("sebserver.exam.signaturekey.seb.add.tag"); + + private static final LocTextKey TABLE_COLUMN_NAME = + new LocTextKey("sebserver.exam.signaturekey.list.name"); + private static final LocTextKey TABLE_COLUMN_INFO = + new LocTextKey("sebserver.exam.signaturekey.list.info"); + private static final LocTextKey TABLE_COLUMN_STATUS = + new LocTextKey("sebserver.exam.signaturekey.list.status"); + + private final PageService pageService; + + protected AddSecurityKeyGrantPopup(final PageService pageService) { + this.pageService = pageService; + } + + public PageAction showGrantPopup(final PageAction action, final AppSignatureKeyInfo appSignatureKeyInfo) { + final PageContext pageContext = action.pageContext(); + final PopupComposer popupComposer = new PopupComposer(this.pageService, pageContext, appSignatureKeyInfo); + try { + final ModalInputDialog> dialog = + new ModalInputDialog<>( + action.pageContext().getParent().getShell(), + this.pageService.getWidgetFactory()); + dialog.setDialogWidth(800); + + final Predicate> applyGrant = formHandle -> applyGrant( + pageContext, + formHandle, + appSignatureKeyInfo); + + dialog.open( + TITLE_TEXT_KEY, + applyGrant, + Utils.EMPTY_EXECUTION, + popupComposer); + + } catch (final Exception e) { + action.pageContext().notifyUnexpectedError(e); + } + return action; + } + + private final class PopupComposer implements ModalInputDialogComposer> { + + private final PageService pageService; + private final PageContext pageContext; + private final AppSignatureKeyInfo appSignatureKeyInfo; + + protected PopupComposer( + final PageService pageService, + final PageContext pageContext, + final AppSignatureKeyInfo appSignatureKeyInfo) { + + this.pageService = pageService; + this.pageContext = pageContext; + this.appSignatureKeyInfo = appSignatureKeyInfo; + } + + @Override + public Supplier> compose(final Composite parent) { + final WidgetFactory widgetFactory = this.pageService.getWidgetFactory(); + widgetFactory.addFormSubContextHeader(parent, TITLE_TEXT_INFO, null); + + final PageContext formContext = this.pageContext.copyOf(parent); + final FormHandle form = this.pageService.formBuilder(formContext) + + .addField(FormBuilder.text( + Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_KEY_VALUE, + TITLE_TEXT_FORM_SIGNATURE, + String.valueOf(this.appSignatureKeyInfo.key)) + .readonly(true)) + + .addField(FormBuilder.text( + Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG, + TITLE_TEXT_FORM_TAG) + .mandatory()) + + .build(); + + final String clientConnectionIds = StringUtils.join( + this.appSignatureKeyInfo.connectionIds + .keySet() + .stream() + .map(String::valueOf) + .collect(Collectors.toList()), + Constants.LIST_SEPARATOR_CHAR); + + this.pageService.getRestService().getBuilder(GetClientConnections.class) + .withQueryParam(API.PARAM_MODEL_ID_LIST, clientConnectionIds) + .call() + .onSuccess(connections -> { + final List list = new ArrayList<>(); + this.pageService.staticListTableBuilder(list, EntityType.CLIENT_CONNECTION) + .withPaging(10) + + .withColumn(new ColumnDefinition<>( + Domain.CLIENT_CONNECTION.ATTR_EXAM_USER_SESSION_ID, + TABLE_COLUMN_NAME, + ClientConnection::getUserSessionId) + .widthProportion(2)) + + .withColumn(new ColumnDefinition<>( + ClientConnection.ATTR_INFO, + TABLE_COLUMN_INFO, + ClientConnection::getInfo) + .widthProportion(3)) + + .withColumn(new ColumnDefinition( + Domain.CLIENT_CONNECTION.ATTR_STATUS, + TABLE_COLUMN_STATUS, + row -> this.pageService.getResourceService() + .localizedClientConnectionStatusName(row.getStatus())) + .widthProportion(1)); + + }); + + return () -> form; + } + } + + private boolean applyGrant( + final PageContext pageContext, + final FormHandle formHandle, + final AppSignatureKeyInfo appSignatureKeyInfo) { + + if (appSignatureKeyInfo.connectionIds.isEmpty()) { + return true; + } + + final Long connectioId = appSignatureKeyInfo.connectionIds.keySet().iterator().next(); + + return this.pageService + .getRestService() + .getBuilder(GrantClientConnectionSecurityKey.class) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, String.valueOf(appSignatureKeyInfo.examId)) + .withURIVariable(API.PARAM_MODEL_ID, String.valueOf(connectioId)) + .withFormBinding(formHandle.getFormBinding()) + .call() + .onError(formHandle::handleError) + .hasValue(); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamSignatureKeyForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamSignatureKeyForm.java index 438ae2ff..ad7d9e09 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamSignatureKeyForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamSignatureKeyForm.java @@ -8,18 +8,41 @@ package ch.ethz.seb.sebserver.gui.content.exam; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.swt.widgets.Composite; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.Entity; 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.SecurityKey; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; +import ch.ethz.seb.sebserver.gui.form.Form; +import ch.ethz.seb.sebserver.gui.form.FormBuilder; +import ch.ethz.seb.sebserver.gui.form.FormHandle; import ch.ethz.seb.sebserver.gui.service.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.PageService.PageActionBuilder; import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; +import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.seckey.DeleteSecurityKeyGrant; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.seckey.GetAppSignatureKeyInfo; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.seckey.GetAppSignatureKeys; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.seckey.SaveAppSignatureKeySettings; +import ch.ethz.seb.sebserver.gui.table.ColumnDefinition; +import ch.ethz.seb.sebserver.gui.table.EntityTable; import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; @Lazy @@ -35,32 +58,51 @@ public class ExamSignatureKeyForm implements TemplateComposer { 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_EMPTY_LIST_TEXT_KEY = + new LocTextKey("sebserver.exam.signaturekey.keylist.empty"); private static final LocTextKey APP_SIG_KEY_LIST_TITLE = new LocTextKey("sebserver.exam.signaturekey.keylist.title"); + private static final LocTextKey APP_SIG_KEY_LIST_TITLE_TOOLTIP = + new LocTextKey("sebserver.exam.signaturekey.keylist.title" + Constants.TOOLTIP_TEXT_KEY_SUFFIX); 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 static final LocTextKey APP_SIG_KEY_LIST_CLIENT_IDS = + new LocTextKey("sebserver.exam.signaturekey.keylist.clientids"); + private static final LocTextKey APP_SIG_KEY_LIST_EMPTY_SELECTION_TEXT_KEY = + new LocTextKey("sebserver.exam.signaturekey.keylist.pleaseSelect"); + + private static final LocTextKey GRANT_LIST_TITLE = + new LocTextKey("sebserver.exam.signaturekey.grantlist.title"); + private static final LocTextKey GRANT_LIST_TITLE_TOOLTIP = + new LocTextKey("sebserver.exam.signaturekey.grantlist.title" + Constants.TOOLTIP_TEXT_KEY_SUFFIX); + private static final LocTextKey GRANT_LIST_EMPTY_LIST_TEXT_KEY = + new LocTextKey("sebserver.exam.signaturekey.grantlist..empty"); + 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 GRANT_LIST_EMPTY_SELECTION_TEXT_KEY = + new LocTextKey("sebserver.exam.signaturekey.grantlist.pleaseSelect"); + private static final LocTextKey GRANT_LIST_DELETE_CONFORM = + new LocTextKey("sebserver.exam.signaturekey.grantlist.delete.confirm"); private final PageService pageService; private final ResourceService resourceService; - private final I18nSupport i18nSupport; + private final AddSecurityKeyGrantPopup addSecurityKeyGrantPopup; + private final SecurityKeyGrantPopup securityKeyGrantPopup; public ExamSignatureKeyForm( final PageService pageService, final ResourceService resourceService, - final I18nSupport i18nSupport) { + final AddSecurityKeyGrantPopup addSecurityKeyGrantPopup, + final SecurityKeyGrantPopup securityKeyGrantPopup) { this.pageService = pageService; this.resourceService = resourceService; - this.i18nSupport = i18nSupport; + this.addSecurityKeyGrantPopup = addSecurityKeyGrantPopup; + this.securityKeyGrantPopup = securityKeyGrantPopup; } @Override @@ -68,9 +110,199 @@ public class ExamSignatureKeyForm implements TemplateComposer { final RestService restService = this.resourceService.getRestService(); final WidgetFactory widgetFactory = this.pageService.getWidgetFactory(); final EntityKey entityKey = pageContext.getEntityKey(); + final Exam exam = restService + .getBuilder(GetExam.class) + .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) + .call() + .getOrThrow(); + final boolean signatureKeyCheckEnabled = BooleanUtils.toBoolean( + exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED)); + final String ct = exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_STATISTICAL_GRANT_COUNT_THRESHOLD); - // TODO Auto-generated method stub + final Composite content = widgetFactory + .defaultPageLayout(pageContext.getParent(), TILE); + final PageActionBuilder actionBuilder = this.pageService + .pageActionBuilder(pageContext.clearEntityKeys()); + + final FormHandle form = this.pageService + .formBuilder(pageContext.copyOf(content)) + + .addField(FormBuilder.checkbox( + Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED, + FORM_ENABLED, + String.valueOf(signatureKeyCheckEnabled))) + + .addField(FormBuilder.text( + Exam.ADDITIONAL_ATTR_STATISTICAL_GRANT_COUNT_THRESHOLD, + FORM_STAT_GRANT_THRESHOLD, + (ct != null) ? ct : "2") + .asNumber(number -> { + if (StringUtils.isNotBlank(number)) { + Integer.parseInt(number); + } + }) + .mandatory() + .withInputSpan(1)) + + .build(); + + widgetFactory.addFormSubContextHeader( + content, + APP_SIG_KEY_LIST_TITLE, + APP_SIG_KEY_LIST_TITLE_TOOLTIP); + + final EntityTable connectionInfoTable = this.pageService + .remoteListTableBuilder( + restService.getRestCall(GetAppSignatureKeyInfo.class), + EntityType.SEB_SECURITY_KEY_REGISTRY) + .withRestCallAdapter(builder -> builder.withURIVariable(API.PARAM_PARENT_MODEL_ID, entityKey.modelId)) + .withEmptyMessage(APP_SIG_KEY_EMPTY_LIST_TEXT_KEY) + .withPaging(-1) + .hideNavigation() + + .withColumn(new ColumnDefinition<>( + Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_KEY_VALUE, + APP_SIG_KEY_LIST_KEY, + AppSignatureKeyInfo::getKey) + .widthProportion(2)) + + .withColumn(new ColumnDefinition<>( + AppSignatureKeyInfo.ATTR_NUMBER_OF_CONNECTIONS, + APP_SIG_KEY_LIST_NUM_CLIENTS, + AppSignatureKeyInfo::getNumberOfConnections) + .widthProportion(1)) + + .withDefaultAction(table -> actionBuilder + .newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP) + .withParentEntityKey(entityKey) + .withExec(action -> this.addSecurityKeyGrantPopup.showGrantPopup( + action, + table.getSingleSelectedROWData())) + .noEventPropagation() + .ignoreMoveAwayFromEdit() + .create()) + + .withSelectionListener(this.pageService.getSelectionPublisher( + pageContext, + ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP)) + + .compose(pageContext.copyOf(content)); + + widgetFactory.addFormSubContextHeader( + content, + GRANT_LIST_TITLE, + GRANT_LIST_TITLE_TOOLTIP); + + final EntityTable grantsList = this.pageService + .remoteListTableBuilder( + restService.getRestCall(GetAppSignatureKeys.class), + EntityType.SEB_SECURITY_KEY_REGISTRY) + .withRestCallAdapter(builder -> builder.withURIVariable(API.PARAM_PARENT_MODEL_ID, entityKey.modelId)) + .withEmptyMessage(GRANT_LIST_EMPTY_LIST_TEXT_KEY) + .withPaging(-1) + .hideNavigation() + + .withColumn(new ColumnDefinition<>( + Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_KEY_VALUE, + GRANT_LIST_KEY, + SecurityKey::getKey).widthProportion(2)) + + .withColumn(new ColumnDefinition<>( + Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG, + GRANT_LIST_TAG, + SecurityKey::getTag).widthProportion(1)) + + .withDefaultAction(table -> actionBuilder + .newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_GRANT_POPUP) + .withParentEntityKey(entityKey) + .withExec(action -> this.securityKeyGrantPopup.showGrantPopup( + action, + table.getSingleSelectedROWData())) + .noEventPropagation() + .ignoreMoveAwayFromEdit() + .create()) + + .withSelectionListener(this.pageService.getSelectionPublisher( + pageContext, + ActionDefinition.EXAM_SECURITY_KEY_SHOW_GRANT_POPUP, + ActionDefinition.EXAM_SECURITY_KEY_DELETE_GRANT)) + + .compose(pageContext.copyOf(content)); + + actionBuilder.newAction(ActionDefinition.EXAM_SECURITY_KEY_SAVE_SETTINGS) + .withEntityKey(entityKey) + .withExec(action -> this.saveSettings(action, form.getForm())) + .ignoreMoveAwayFromEdit() + .publish() + + .newAction(ActionDefinition.EXAM_SECURITY_KEY_CANCEL_MODIFY) + .withExec(this.pageService.backToCurrentFunction()) + .publish() + + .newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP) + .withParentEntityKey(entityKey) + .withSelect( + connectionInfoTable::getMultiSelection, + action -> this.addSecurityKeyGrantPopup.showGrantPopup( + action, + connectionInfoTable.getSingleSelectedROWData()), + APP_SIG_KEY_LIST_EMPTY_SELECTION_TEXT_KEY) + .ignoreMoveAwayFromEdit() + .noEventPropagation() + .publish(false) + + .newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_GRANT_POPUP) + .withEntityKey(entityKey) + .withSelect( + grantsList::getMultiSelection, + action -> this.securityKeyGrantPopup.showGrantPopup(action, + grantsList.getSingleSelectedROWData()), + GRANT_LIST_EMPTY_SELECTION_TEXT_KEY) + .ignoreMoveAwayFromEdit() + .noEventPropagation() + .publish(false) + + .newAction(ActionDefinition.EXAM_SECURITY_KEY_DELETE_GRANT) + .withConfirm(action -> GRANT_LIST_DELETE_CONFORM) + .withParentEntityKey(entityKey) + .withSelect( + grantsList::getMultiSelection, + this::deleteGrant, + GRANT_LIST_EMPTY_SELECTION_TEXT_KEY) + .ignoreMoveAwayFromEdit() + .publish(false) + + ; + } + + private PageAction saveSettings(final PageAction action, final Form form) { + final String enable = form.getFieldValue(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED); + final String threshold = form.getFieldValue(Exam.ADDITIONAL_ATTR_STATISTICAL_GRANT_COUNT_THRESHOLD); + final EntityKey entityKey = action.getEntityKey(); + + this.pageService + .getRestService() + .getBuilder(SaveAppSignatureKeySettings.class) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, entityKey.modelId) + .withFormParam(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED, enable) + .withFormParam(Exam.ADDITIONAL_ATTR_STATISTICAL_GRANT_COUNT_THRESHOLD, threshold) + .call() + .onError(error -> action.pageContext().notifySaveError(EntityType.EXAM, error)); + return action; + } + + private PageAction deleteGrant(final PageAction action) { + final EntityKey parentEntityKey = action.getParentEntityKey(); + final EntityKey singleSelection = action.getSingleSelection(); + this.pageService.getRestService() + .getBuilder(DeleteSecurityKeyGrant.class) + .withURIVariable(API.PARAM_PARENT_MODEL_ID, parentEntityKey.modelId) + .withURIVariable(API.PARAM_MODEL_ID, singleSelection.modelId) + .call() + .onError(error -> action.pageContext().notifyUnexpectedError(error)); + + return action.withEntityKey(parentEntityKey); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/SecurityKeyGrantPopup.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/SecurityKeyGrantPopup.java new file mode 100644 index 00000000..a8976989 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/SecurityKeyGrantPopup.java @@ -0,0 +1,100 @@ +/* + * 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.Domain; +import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +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.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; + +@Lazy +@Component +@GuiProfile +public class SecurityKeyGrantPopup { + + private static final LocTextKey TITLE_TEXT_KEY = + new LocTextKey("sebserver.exam.signaturekey.grant.title"); + private static final LocTextKey TITLE_TEXT_FORM_SIGNATURE = + new LocTextKey("sebserver.exam.signaturekey.grant.key"); + private static final LocTextKey TITLE_TEXT_FORM_TAG = + new LocTextKey("sebserver.exam.signaturekey.grant.tag"); + private static final LocTextKey TITLE_TEXT_FORM_TYPE = + new LocTextKey("sebserver.exam.signaturekey.grant.type"); + + private final PageService pageService; + + public SecurityKeyGrantPopup(final PageService pageService) { + this.pageService = pageService; + } + + public PageAction showGrantPopup(final PageAction action, final SecurityKey securityKey) { + + final PopupComposer popupComposer = new PopupComposer(this.pageService, securityKey); + try { + final ModalInputDialog> dialog = + new ModalInputDialog<>( + action.pageContext().getParent().getShell(), + this.pageService.getWidgetFactory()); + dialog.setDialogWidth(800); + + dialog.open( + TITLE_TEXT_KEY, + action.pageContext(), + popupComposer::compose); + + } catch (final Exception e) { + action.pageContext().notifyUnexpectedError(e); + } + return action; + } + + private final class PopupComposer { + + private final PageService pageService; + private final SecurityKey securityKey; + + protected PopupComposer(final PageService pageService, final SecurityKey securityKey) { + this.pageService = pageService; + this.securityKey = securityKey; + } + + public void compose(final PageContext pageContext) { + + this.pageService.formBuilder(pageContext) + .readonly(true) + .addField(FormBuilder.text( + Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_KEY_VALUE, + TITLE_TEXT_FORM_SIGNATURE, + String.valueOf(this.securityKey.key)) + .readonly(true)) + + .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_TYPE, + TITLE_TEXT_FORM_TYPE, + this.securityKey.keyType.name())) + + .build(); + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringClientConnection.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringClientConnection.java index 0e4a8f92..1aff6b44 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringClientConnection.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringClientConnection.java @@ -234,12 +234,13 @@ public class MonitoringClientConnection implements TemplateComposer { NOTIFICATION_LIST_TITLE_KEY, NOTIFICATION_LIST_TITLE_TOOLTIP_KEY); - final EntityTable notificationTable = this.pageService.remoteListTableBuilder( - restService.getRestCall(GetPendingClientNotifications.class), - EntityType.CLIENT_EVENT) - .withRestCallAdapter(builder -> builder.withURIVariable( - API.PARAM_PARENT_MODEL_ID, - parentEntityKey.modelId) + final EntityTable notificationTable = this.pageService + .remoteListTableBuilder( + restService.getRestCall(GetPendingClientNotifications.class), EntityType.CLIENT_EVENT) + .withRestCallAdapter(builder -> builder + .withURIVariable( + API.PARAM_PARENT_MODEL_ID, + parentEntityKey.modelId) .withURIVariable( API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)) @@ -409,7 +410,7 @@ public class MonitoringClientConnection implements TemplateComposer { .call() .getOrThrow(); - if (securityKey.id < 0) { + if (securityKey.id == null || securityKey.id < 0) { actionBuilder .newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_GRANT_SIGNATURE_KEY) .withParentEntityKey(parentEntityKey) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/SignatureKeyGrantPopup.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/SignatureKeyGrantPopup.java index 36cc5e2e..f1924593 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/SignatureKeyGrantPopup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/SignatureKeyGrantPopup.java @@ -61,7 +61,7 @@ public class SignatureKeyGrantPopup { new ModalInputDialog<>( action.pageContext().getParent().getShell(), this.pageService.getWidgetFactory()); - dialog.setDialogWidth(700); + dialog.setDialogWidth(800); final Predicate> applyGrant = formHandle -> applyGrant( pageContext, @@ -104,15 +104,18 @@ public class SignatureKeyGrantPopup { 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)) + + .addField(FormBuilder.text( + Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG, + TITLE_TEXT_FORM_TAG, + this.securityKey.tag)) + .build(); return () -> form; @@ -133,7 +136,13 @@ public class SignatureKeyGrantPopup { .withURIVariable(API.PARAM_MODEL_ID, connectionKey.modelId) .withFormBinding(formHandle.getFormBinding()) .call() - .onError(formHandle::handleError) + .onError(error -> { + if (error.getMessage().contains("\"messageCode\":\"1010\"")) { + pageContext.publishInfo(new LocTextKey("sebserver.monitoring.signaturegrant.message.granted")); + } else { + formHandle.handleError(error); + } + }) .hasValue(); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/form/FieldBuilder.java b/src/main/java/ch/ethz/seb/sebserver/gui/form/FieldBuilder.java index bde1b3a1..b52546d7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/form/FieldBuilder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/form/FieldBuilder.java @@ -154,7 +154,7 @@ public abstract class FieldBuilder { WidgetFactory.ImageIcon.MANDATORY, infoGrid, MANDATORY_TEXT_KEY); - mandatory.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false)); + mandatory.setLayoutData(new GridData(SWT.CENTER, SWT.TOP, false, false)); } return infoGrid; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageService.java index bac021ac..42d5333c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageService.java @@ -341,7 +341,8 @@ public interface PageService { TableBuilder staticListTableBuilder(final List staticList, EntityType entityType); - TableBuilder remoteListTableBuilder(RestCall> apiCall, + TableBuilder remoteListTableBuilder( + RestCall> apiCall, EntityType entityType); /** Get a new PageActionBuilder for a given PageContext. diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetClientConnections.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetClientConnections.java new file mode 100644 index 00000000..da88e7a6 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/GetClientConnections.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam; + +import java.util.Collection; + +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.session.ClientConnection; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class GetClientConnections extends RestCall> { + + public GetClientConnections() { + super(new TypeKey<>( + CallType.GET_LIST, + EntityType.CLIENT_CONNECTION, + new TypeReference>() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.SEB_CLIENT_CONNECTION_ENDPOINT + + API.LIST_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/AddSecurityKeyGrant.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/AddSecurityKeyGrant.java new file mode 100644 index 00000000..a745effd --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/AddSecurityKeyGrant.java @@ -0,0 +1,42 @@ +/* + * 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.exam.seckey; + +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 AddSecurityKeyGrant extends RestCall { + + public AddSecurityKeyGrant() { + super(new TypeKey<>( + CallType.NEW, + EntityType.SEB_SECURITY_KEY_REGISTRY, + new TypeReference() { + }), + HttpMethod.POST, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_ADMINISTRATION_ENDPOINT + + API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_ADMINISTRATION_SEB_SECURITY_KEY_GRANTS_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/DeleteSecurityKeyGrant.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/DeleteSecurityKeyGrant.java new file mode 100644 index 00000000..53e5573e --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/DeleteSecurityKeyGrant.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.seckey; + +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.EntityKey; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class DeleteSecurityKeyGrant extends RestCall { + + public DeleteSecurityKeyGrant() { + super(new TypeKey<>( + CallType.DELETE, + EntityType.SEB_SECURITY_KEY_REGISTRY, + new TypeReference() { + }), + HttpMethod.DELETE, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_ADMINISTRATION_ENDPOINT + + API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_ADMINISTRATION_SEB_SECURITY_KEY_GRANTS_PATH_SEGMENT + + API.MODEL_ID_VAR_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/GetAppSignatureKeyInfo.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/GetAppSignatureKeyInfo.java new file mode 100644 index 00000000..a49cdfd0 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/GetAppSignatureKeyInfo.java @@ -0,0 +1,44 @@ +/* + * 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.exam.seckey; + +import java.util.Collection; + +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.AppSignatureKeyInfo; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class GetAppSignatureKeyInfo extends RestCall> { + + public GetAppSignatureKeyInfo() { + super(new TypeKey<>( + CallType.GET_SINGLE, + EntityType.SEB_SECURITY_KEY_REGISTRY, + new TypeReference>() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_ADMINISTRATION_ENDPOINT + + API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_ADMINISTRATION_SEB_SECURITY_KEY_INFO_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/GetAppSignatureKeys.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/GetAppSignatureKeys.java new file mode 100644 index 00000000..171a970a --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/GetAppSignatureKeys.java @@ -0,0 +1,44 @@ +/* + * 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.exam.seckey; + +import java.util.Collection; + +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 GetAppSignatureKeys extends RestCall> { + + public GetAppSignatureKeys() { + super(new TypeKey<>( + CallType.GET_LIST, + EntityType.SEB_SECURITY_KEY_REGISTRY, + new TypeReference>() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_ADMINISTRATION_ENDPOINT + + API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_ADMINISTRATION_SEB_SECURITY_KEY_GRANTS_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/SaveAppSignatureKeySettings.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/SaveAppSignatureKeySettings.java new file mode 100644 index 00000000..73e0ee98 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/exam/seckey/SaveAppSignatureKeySettings.java @@ -0,0 +1,42 @@ +/* + * 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.exam.seckey; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class SaveAppSignatureKeySettings extends RestCall { + + public SaveAppSignatureKeySettings() { + super(new TypeKey<>( + CallType.SAVE, + EntityType.SEB_SECURITY_KEY_REGISTRY, + new TypeReference() { + }), + HttpMethod.POST, + MediaType.APPLICATION_JSON, + API.EXAM_ADMINISTRATION_ENDPOINT + + API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_ADMINISTRATION_SEB_SECURITY_KEY_INFO_PATH_SEGMENT); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionTable.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionTable.java index 0a3cf91d..f2111873 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionTable.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/ClientConnectionTable.java @@ -672,7 +672,8 @@ public final class ClientConnectionTable implements FullPageMonitoringGUIUpdate boolean push(final ClientMonitoringData monitoringData) { this.dataChanged = this.monitoringData == null || this.monitoringData.status != monitoringData.status || - this.monitoringData.missingPing != monitoringData.missingPing; + this.monitoringData.missingPing != monitoringData.missingPing || + this.monitoringData.missingGrant != monitoringData.missingGrant; this.indicatorValueChanged = this.monitoringData == null || (this.monitoringData.status.clientActiveStatus && !this.monitoringData.indicatorValuesEquals(monitoringData)); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java b/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java index 152a8706..7d785095 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/table/EntityTable.java @@ -205,6 +205,8 @@ public class EntityTable { if (selection != null) { this.pageService.executePageAction( defaultAction.withEntityKey(selection)); + } else { + this.pageService.executePageAction(defaultAction); } }); } @@ -217,8 +219,7 @@ public class EntityTable { }); this.table.addListener(SWT.Selection, event -> this.notifySelectionChange()); - - this.navigator = (pageSize > 0) ? new TableNavigator(this) : new TableNavigator(); + this.navigator = new TableNavigator(this); createTableColumns(); this.pageNumber = initCurrentPageFromUserAttr(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java index 73eb50a2..cf5ad244 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java @@ -302,8 +302,11 @@ public class WidgetFactory { return defaultPageLayout; } - public void addFormSubContextHeader(final Composite parent, final LocTextKey titleTextKey, + public void addFormSubContextHeader( + final Composite parent, + final LocTextKey titleTextKey, final LocTextKey tooltipTextKey) { + final GridData gridData = new GridData(SWT.FILL, SWT.BOTTOM, true, false); gridData.horizontalIndent = 8; gridData.verticalIndent = 10; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java index 88f74416..b1591f68 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java @@ -163,11 +163,12 @@ public interface ClientConnectionDAO extends * @return Result refer to the given Exam or to an error when happened. */ Result deleteClientIndicatorValues(Exam exam); - /** Get all client connection records for an exam. + /** Get all client connection records for exam security key check. + * Equals to all in state ACTIVE or CLOSED * * @param examId the exam identifier * @return Result refer to a collection of client connection records or to an error when happened */ - Result> getAllConnectionRecordsForExam(Long examId); + Result> getsecurityKeyConnectionRecords(Long examId); /** Get all client connection identifiers for an exam. * diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/SecurityKeyRegistryDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/SecurityKeyRegistryDAO.java index f29963a0..82dd7b84 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/SecurityKeyRegistryDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/SecurityKeyRegistryDAO.java @@ -22,17 +22,46 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamTemplateDeleti /** Concrete EntityDAO interface of SecurityKeyRegistry entities */ public interface SecurityKeyRegistryDAO extends EntityDAO { + /** Use this to make a copy of an existing security key registry entry for the given exam. + * The existing registry entry must be a global or one or one of an exam template. + * + * @param keyId The security key registry id. + * @param examId The exam identifier for the security key copy + * @return Result refer to the newly created SecurityKey or to an error when happened. */ Result registerCopyForExam(Long keyId, Long examId); + /** Use this to make a copy of r an existing security key registry entry for a given exam template. + * The existing registry entry must be a global one or one of an exam. + * + * @param keyId The security key registry id. + * @param examTemplateId The exam template identifier for the new security key copy + * @return Result refer to the newly created SecurityKey or the an error when happened */ Result registerCopyForExamTemplate(Long keyId, Long examTemplateId); + /** Used to get all security key registry entries of given institution, exam and type. + * + * @param institutionId The institution identifier + * @param examId The exam identifier + * @param type The type of the security key + * @return Result refer to collection of all matching security key registry entries or to an error when happened */ Result> getAll(Long institutionId, Long examId, KeyType type); + /** Used to delete a given security key registry entry. + * + * @param keyId The security key registry entry identifier + * @return Result refer to the EntityKey of the deleted registry entry or to an error when happened */ Result delete(Long keyId); + /** Internally used to notify exam deletion to delete all registry entries regarded to the deleted exam. + * + * @param event The ExamDeletionEvent fired on exam deletion */ @EventListener(ExamDeletionEvent.class) void notifyExamDeletion(ExamDeletionEvent event); + /** Internally used to notify exam template deletion to delete all registry entries regarded to the deleted exam + * template + * + * @param event ExamTemplateDeletionEvent fired on exam template deletion */ @EventListener(ExamTemplateDeletionEvent.class) void notifyExamTemplateDeletion(ExamTemplateDeletionEvent event); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/AdditionalAttributesDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/AdditionalAttributesDAOImpl.java index a26a55a8..3d0c9228 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/AdditionalAttributesDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/AdditionalAttributesDAOImpl.java @@ -26,6 +26,7 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.AdditionalAttribu import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.AdditionalAttributeRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.NoResourceFoundException; @Lazy @Component @@ -80,7 +81,9 @@ public class AdditionalAttributesDAOImpl implements AdditionalAttributesDAO { .execute() .stream() .findAny() - .orElse(null)); + .orElseThrow(() -> new NoResourceFoundException( + EntityType.ADDITIONAL_ATTRIBUTES, + attributeName))); } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java index f4cb96e5..53df79ff 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java @@ -764,12 +764,15 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { @Override @Transactional(readOnly = true) - public Result> getAllConnectionRecordsForExam(final Long examId) { + public Result> getsecurityKeyConnectionRecords(final Long examId) { return Result.tryCatch(() -> this.clientConnectionRecordMapper .selectByExample() .where( ClientConnectionRecordDynamicSqlSupport.examId, SqlBuilder.isEqualTo(examId)) + .and( + ClientConnectionRecordDynamicSqlSupport.status, + SqlBuilder.isIn(ClientConnection.SECURE_STATES)) .build() .execute()); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/SecurityKeyRegistryDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/SecurityKeyRegistryDAOImpl.java index 58d86b0a..e7ac3677 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/SecurityKeyRegistryDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/SecurityKeyRegistryDAOImpl.java @@ -24,9 +24,9 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException; +import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException; import ch.ethz.seb.sebserver.gbl.api.EntityType; -import ch.ethz.seb.sebserver.gbl.model.Domain; 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; @@ -273,6 +273,9 @@ public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO { .and( SecurityKeyRegistryRecordDynamicSqlSupport.keyType, isEqualToWhenPresent((type == null) ? null : type.name())) + .and( + SecurityKeyRegistryRecordDynamicSqlSupport.examId, + isNull()) .build() .execute() .stream() @@ -400,9 +403,10 @@ public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO { private void checkUniqueKey(final SecurityKey key) { if (getGrantOr(key).getOr(key) != key) { - throw new FieldValidationException( - Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG, - "securityKey:keyValue:alreadyGranted"); + throw new APIMessageException(APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT.of("Already granted")); +// throw new FieldValidationException( +// Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG, +// "securityKey:keyValue:alreadyGranted"); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java index d88459cd..af4efdc4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java @@ -44,6 +44,21 @@ public interface ExamAdminService { * @return Result refer to the created exam or to an error when happened */ Result saveLMSAttributes(Exam exam); + /** Saves the security key settings for an specific exam. + * + * @param institutionId The institution identifier + * @param examId The exam identifier + * @param enabled The enabled setting that indicates if the security key check is enabled or not + * @param statThreshold the statistical SEB client connection number grant threshold + * @return Result refer to the exam with the new settings (additional attributes) or to an error when happened */ + Result saveSecurityKeySettings( + Long institutionId, + Long examId, + Boolean enabled, + Integer statThreshold); + + Result getAppSignatureKeySalt(Long institutionId, Long examId); + /** Applies all additional SEB restriction attributes that are defined by the * type of the LMS of a given Exam to this given Exam. * diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java index b424bbe2..e9c3a246 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java @@ -16,6 +16,7 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; +import org.springframework.security.crypto.keygen.KeyGenerators; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,10 +36,12 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode.ConfigurationStatus; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationNodeDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.NoResourceFoundException; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ProctoringAdminService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; @@ -81,6 +84,57 @@ public class ExamAdminServiceImpl implements ExamAdminService { return this.examDAO.byPK(examId); } + @Override + public Result saveSecurityKeySettings( + final Long institutionId, + final Long examId, + final Boolean enabled, + final Integer statThreshold) { + + return Result.tryCatch(() -> { + if (enabled != null) { + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.EXAM, + examId, + Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED, + String.valueOf(enabled)) + .onError(error -> log.error("Failed to store ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED: ", + error)); + } + if (statThreshold != null) { + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.EXAM, + examId, + Exam.ADDITIONAL_ATTR_STATISTICAL_GRANT_COUNT_THRESHOLD, + String.valueOf(statThreshold)) + .onError(error -> log + .error("Failed to store ADDITIONAL_ATTR_STATISTICAL_GRANT_COUNT_THRESHOLD: ", error)); + } + + }).flatMap(v -> this.examDAO.byPK(examId)); + } + + @Override + public Result getAppSignatureKeySalt(final Long institutionId, final Long examId) { + return this.additionalAttributesDAO.getAdditionalAttribute( + EntityType.EXAM, + examId, + Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_SALT) + .onErrorDo(error -> { + if (error instanceof NoResourceFoundException) { + final CharSequence salt = KeyGenerators.string().generateKey(); + return this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.EXAM, + examId, + Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_SALT, salt.toString()).getOrThrow(); + } else { + throw new RuntimeException( + "Unexpected error while trying to get AppSigKey Salt for Exam: " + examId, error); + } + }) + .map(AdditionalAttributeRecord::getValue); + } + @Override public Result applyAdditionalSEBRestrictions(final Exam exam) { return Result.tryCatch(() -> { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/SecurityKeyService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/SecurityKeyService.java index 6f32f9af..8f0757d1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/SecurityKeyService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/SecurityKeyService.java @@ -12,40 +12,79 @@ 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.institution.SecurityKey.KeyType; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.util.Result; 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"; + /** Get the stored App-Signature-Key of a SEB connection within a SecurityKey container. + * + * @param institutionId The institution identifier + * @param connectionId The SEB connection identifier + * @return Result refer to the App-Signature-Key of the SEB client connection or to an error when happened */ + Result getAppSignatureKey(Long institutionId, Long connectionId); - Result getSecurityKeyOfConnection(Long institutionId, Long connectionId); + /** Get a list of all different SEB App-Signature-Key for a given exam with also the number of SEB + * clients that has propagated the respective App-Signature-Key + * + * @param institutionId The institution identifier + * @param examId The exam identifier + * @return Result refer to the list of AppSignatureKeyInfo for the given exam or to an error when happened */ + Result> getAppSignatureKeyInfo(Long institutionId, Long examId); - Result getAppSignaturesInfo(Long institutionId, Long examId); - - Result> getPlainAppSignatureKeyGrants(Long institutionId, Long examId); + /** Get a list of all security key registry entries of for given institution and exam. + * + * @param institutionId The institution identifier + * @param examId The exam identifier + * @param type The key type filter criteria + * @return Result refer to the list of security key registry entries or to an error when happened */ + Result> getSecurityKeyEntries(Long institutionId, Long examId, KeyType type); + /** Register a new security key entry in the registry. + * + * @param key The security key data + * @return Result refer to the newly created and stored security key entry or to an error when happened */ Result registerSecurityKey(SecurityKey key); + /** Register SEB client connection App-Signature-Key as a new global security key registry entry + * This is equivalent to make a global grant for specified App-Signature-Key of given SEB client connection. + * + * @param institutionId The institution identifier + * @param connectionId The client connection identifier + * @param tag A Tag for user identification of the grant within the registry + * @return Result refer to the newly created security key entry or to an error when happened */ Result registerGlobalAppSignatureKey(Long institutionId, Long connectionId, String tag); + /** Register SEB client connection App-Signature-Key as a new exam based security key registry entry + * This is equivalent to make a exam specific grant for specified App-Signature-Key of given SEB client connection. + * + * @param institutionId The institution identifier + * @param examId The exam identifier for the exam based grant + * @param connectionId The client connection identifier + * @param tag A Tag for user identification of the grant within the registry + * @return Result refer to the newly created security key entry or to an error when happened */ Result registerExamAppSignatureKey(Long institutionId, Long examId, Long connectionId, String tag); - Result applyAppSignatureCheck( - Long institutionId, - Long examId, - String connectionToken, - String appSignatureKey); - + /** Used to apply a SEB client App-signature-Key check for a given App-Signature-Key sent by the SEB. + * Note: This also stores the given App-Signature-Key sent by SEB if not already stored for the SEB connection. + * + * @param clientConnection The SEB client connection token + * @param appSignatureKey The App-Signature-Key sent by the SEB client + * @return true if the check was successful and the SEB has a grant, false otherwise */ boolean checkAppSignatureKey(ClientConnection clientConnection, String appSignatureKey); + /** Used to process an update of the App-Signature-Key grant for all SEB connection within given + * exam that has not been already granted. + * + * @param examId The exam identifier */ void updateAppSignatureKeyGrants(Long examId); - Result getDecrypted(SecurityKey key); - - Result deleteSecurityKeyGrant(String keyModelId); + /** Delete a given security key form the registry. + * + * @param keyId The security key registry entry identifier + * @return Result refer to the EntityKey of the delete registry entry or to an error when happened. */ + Result deleteSecurityKeyGrant(Long keyId); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/impl/SecurityKeyServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/impl/SecurityKeyServiceImpl.java index 00dd20cc..a03a3374 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/impl/SecurityKeyServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/institution/impl/SecurityKeyServiceImpl.java @@ -8,18 +8,19 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.institution.impl; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; 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; import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.util.encoders.Hex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; @@ -33,17 +34,18 @@ 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; 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.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Cryptor; import ch.ethz.seb.sebserver.gbl.util.Pair; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SecurityKeyRegistryDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.institution.SecurityKeyService; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ClientConnectionDataInternal; import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamSessionCacheService; @Lazy @@ -74,42 +76,43 @@ public class SecurityKeyServiceImpl implements SecurityKeyService { } @Override - public Result getSecurityKeyOfConnection(final Long institutionId, final Long connectionId) { + public Result getAppSignatureKey(final Long institutionId, final Long connectionId) { return this.clientConnectionDAO.byPK(connectionId) .map(connection -> new SecurityKey( null, institutionId, KeyType.APP_SIGNATURE_KEY, - decryptStoredSignatureForConnection(connection), + getHashedSignature(connection), connection.sebVersion, null, null)) .flatMap(this.securityKeyRegistryDAO::getGrantOr); } @Override - public Result getAppSignaturesInfo(final Long institutionId, final Long examId) { + public Result> getAppSignatureKeyInfo(final Long institutionId, final Long examId) { return Result.tryCatch(() -> { - final Map> keyMapping = new HashMap<>(); - this.clientConnectionDAO - .getAllConnectionRecordsForExam(examId) + return this.clientConnectionDAO + .getsecurityKeyConnectionRecords(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); + .reduce( + new HashMap>(), + this::reduceAppSecKey, + Utils::> mergeMap) + .entrySet() + .stream() + .map(m -> new AppSignatureKeyInfo(institutionId, examId, m.getKey(), m.getValue())) + .collect(Collectors.toList()); }); } @Override - public Result> getPlainAppSignatureKeyGrants(final Long institutionId, final Long examId) { + public Result> getSecurityKeyEntries(final Long institutionId, final Long examId, + final KeyType type) { return this.securityKeyRegistryDAO - .getAll(institutionId, examId, KeyType.APP_SIGNATURE_KEY) - .map(this::decryptAll); + .getAll(institutionId, examId, type) + .map(this::getKeysForRead); } @Override @@ -158,44 +161,6 @@ public class SecurityKeyServiceImpl implements SecurityKeyService { tag, examId, null)).getOrThrow()); } - @Override - public Result applyAppSignatureCheck( - final Long institutionId, - final Long examId, - final String connectionToken, - final String appSignatureKey) { - - if (log.isDebugEnabled()) { - log.debug("Apply app-signature-key check for connection: {}", connectionToken); - } - - return this.securityKeyRegistryDAO - .getAll(institutionId, examId, KeyType.APP_SIGNATURE_KEY) - .map(all -> { - final String decryptedSignature = decryptSignature(examId, connectionToken, appSignatureKey); - final List matches = all.stream() - .map(this::decryptGrantedKey) - .filter(pair -> pair != null && Objects.equals(decryptedSignature, pair.a)) - .map(Pair::getB) - .collect(Collectors.toList()); - - if (matches == null || matches.isEmpty()) { - return statisticalCheck(examId, decryptedSignature); - } else { - return new SecurityCheckResult( - matches.stream() - .filter(key -> key.examId != null) - .findFirst() - .isPresent(), - matches.stream() - .filter(key -> key.examId == null) - .findFirst() - .isPresent(), - false); - } - }); - } - @Override public boolean checkAppSignatureKey( final ClientConnection clientConnection, @@ -237,7 +202,12 @@ public class SecurityKeyServiceImpl implements SecurityKeyService { clientConnection.examId, clientConnection.connectionToken, signature) - .map(SecurityCheckResult::hasAnyGrant) + .map(result -> { + if (result.statisticallyGranted) { + this.updateAppSignatureKeyGrants(clientConnection.examId); + } + return result.hasAnyGrant(); + }) .onError(error -> log.error("Failed to applyAppSignatureCheck: ", error)) .getOr(false); @@ -249,19 +219,6 @@ public class SecurityKeyServiceImpl implements SecurityKeyService { } } - @Override - public Result getDecrypted(final SecurityKey key) { - return this.cryptor.decrypt(key.key) - .map(dKey -> new SecurityKey( - key.id, - key.institutionId, - key.keyType, - dKey, - key.tag, - key.examId, - key.examTemplateId)); - } - @Override public void updateAppSignatureKeyGrants(final Long examId) { if (examId == null) { @@ -271,30 +228,11 @@ public class SecurityKeyServiceImpl implements SecurityKeyService { try { this.clientConnectionDAO - .getConnectionTokens(examId) + .getsecurityKeyConnectionRecords(examId) .getOrThrow() .stream() - .forEach(token -> { - final ClientConnectionDataInternal clientConnection = - this.examSessionCacheService.getClientConnection(token); - if (!clientConnection.clientConnection.isSecurityCheckGranted()) { - if (this.checkAppSignatureKey(clientConnection.clientConnection, null)) { - // now granted, update ClientConnection on DB level - - if (log.isDebugEnabled()) { - log.debug("Update app-signature-key grant for client connection: {}", token); - } - - this.clientConnectionDAO - .save(new ClientConnection( - clientConnection.clientConnection.id, null, - null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, true)) - .onError(error -> log.error("Failed to save ClientConnection grant: ", error)) - .onSuccess(c -> this.examSessionCacheService.evictClientConnection(token)); - } - } - }); + .filter(rec -> ConnectionStatus.ACTIVE.name().equals(rec.getStatus())) + .forEach(this::updateUngrantedConnections); } catch (final Exception e) { log.error("Unexpected error while trying to update app-signature-key grants: ", e); @@ -308,9 +246,84 @@ public class SecurityKeyServiceImpl implements SecurityKeyService { } @Override - public Result deleteSecurityKeyGrant(final String keyModelId) { - return Result.tryCatch(() -> Long.parseLong(keyModelId)) - .flatMap(this.securityKeyRegistryDAO::delete); + public Result deleteSecurityKeyGrant(final Long keyId) { + return Result.tryCatch(() -> { + final SecurityKey key = this.securityKeyRegistryDAO.byPK(keyId).getOrThrow(); + + final String grantedKey = decryptGrantedKey(key).a; + this.securityKeyRegistryDAO.delete(keyId).getOrThrow(); + this.clientConnectionDAO.getsecurityKeyConnectionRecords(key.examId) + .getOrThrow() + .stream() + .filter(rec -> ConnectionStatus.ACTIVE.name().equals(rec.getStatus())) + .forEach(rec -> { + try { + final String connectionkey = this.decryptStoredSignatureForConnection( + rec.getId(), + rec.getConnectionToken()); + if (grantedKey.equals(connectionkey)) { + // we have to re-check here + final boolean granted = this.applyAppSignatureCheck( + rec.getInstitutionId(), + rec.getExamId(), + rec.getConnectionToken(), + connectionkey).getOrThrow().hasAnyGrant(); + final Boolean grantedBefore = Utils.fromByte(rec.getSecurityCheckGranted()); + if (granted != grantedBefore) { + // update grant + this.clientConnectionDAO + .save(new ClientConnection( + rec.getId(), null, + null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, granted)); + this.examSessionCacheService.evictClientConnection(rec.getConnectionToken()); + } + } + } catch (final Exception e) { + log.error("Failed to update security key grant for connection on deletion -> {}", rec, e); + } + }); + + return key.getEntityKey(); + }); + + } + + private Result applyAppSignatureCheck( + final Long institutionId, + final Long examId, + final String connectionToken, + final String appSignatureKey) { + + if (log.isDebugEnabled()) { + log.debug("Apply app-signature-key check for connection: {}", connectionToken); + } + + return this.securityKeyRegistryDAO + .getAll(institutionId, examId, KeyType.APP_SIGNATURE_KEY) + .map(all -> { + final String decryptedSignature = decryptSignature(examId, connectionToken, appSignatureKey); + final List matches = all.stream() + .map(this::decryptGrantedKey) + .filter(pair -> pair != null && Objects.equals(decryptedSignature, pair.a)) + .map(Pair::getB) + .collect(Collectors.toList()); + + if (matches == null || matches.isEmpty()) { + return statisticalCheck(examId, decryptedSignature); + } else { + return new SecurityCheckResult( + matches.stream() + .filter(key -> key.examId != null) + .findFirst() + .isPresent(), + matches.stream() + .filter(key -> key.examId == null) + .findFirst() + .isPresent(), + false); + } + }); } private Pair decryptGrantedKey(final SecurityKey key) { @@ -408,12 +421,7 @@ public class SecurityKeyServiceImpl implements SecurityKeyService { } private String decryptStoredSignatureForConnection(final ClientConnection cc) { - final String signatureKey = getSignatureKeyForConnection(cc); - if (StringUtils.isBlank(signatureKey)) { - return null; - } - - return decryptSignatureWithConnectionToken(cc.connectionToken, signatureKey); + return decryptStoredSignatureForConnection(cc.id, cc.connectionToken); } private String decryptStoredSignatureForConnection(final Long cId, final String cToken) { @@ -425,12 +433,31 @@ public class SecurityKeyServiceImpl implements SecurityKeyService { return decryptSignatureWithConnectionToken(cToken, signatureKey); } + private String getHashedSignature(final ClientConnection connection) { + return getHashedSignature(connection.id, connection.connectionToken); + } + + private String getHashedSignature(final Long cId, final String cToken) { + + final String signatureKey = getSignatureKeyForConnection(cId); + if (StringUtils.isBlank(signatureKey)) { + return null; + } + + try { + return getSignatureHash(decryptSignatureWithConnectionToken(cToken, signatureKey)); + } catch (final Exception e) { + log.error("Failed to get hashed signature key for read: ", e); + return signatureKey; + } + } + private void saveSignatureKeyForConnection(final ClientConnection clientConnection, final String appSignatureKey) { this.additionalAttributesDAO .saveAdditionalAttribute( EntityType.CLIENT_CONNECTION, clientConnection.id, - ADDITIONAL_ATTR_APP_SIGNATURE_KEY, + ClientConnection.ADDITIONAL_ATTR_APP_SIGNATURE_KEY, appSignatureKey) .onError(error -> log.error( "Failed to store App-Signature-Key for clientConnection: {}", @@ -442,7 +469,7 @@ public class SecurityKeyServiceImpl implements SecurityKeyService { .getAdditionalAttribute( EntityType.CLIENT_CONNECTION, connectionId, - ADDITIONAL_ATTR_APP_SIGNATURE_KEY) + ClientConnection.ADDITIONAL_ATTR_APP_SIGNATURE_KEY) .map(AdditionalAttributeRecord::getValue) .getOr(null); } @@ -474,14 +501,6 @@ public class SecurityKeyServiceImpl implements SecurityKeyService { .getOr(1); } - private Collection decryptAll(final Collection all) { - return all.stream() - .map(this::getDecrypted) - .filter(Result::hasValue) - .map(Result::get) - .collect(Collectors.toList()); - } - private Result encryptInternal(final SecurityKey key) { return Result.tryCatch(() -> new SecurityKey( key.id, @@ -493,4 +512,76 @@ public class SecurityKeyServiceImpl implements SecurityKeyService { key.examTemplateId)); } + private Collection getKeysForRead(final Collection keys) { + return keys.stream() + .map(this::getInternalKeyForRead) + .collect(Collectors.toList()); + } + + private SecurityKey getInternalKeyForRead(final SecurityKey key) { + try { + return new SecurityKey( + key.id, + key.institutionId, + key.keyType, + getSignatureHash(this.cryptor.decrypt(key.key).getOrThrow()), + key.tag, + key.examId, + key.examTemplateId); + } catch (final Exception e) { + log.error("Failed to internally decrypt security key value: ", e); + return key; + } + } + + private String getSignatureHash(final CharSequence signature) throws NoSuchAlgorithmException { + final MessageDigest hasher = MessageDigest.getInstance("SHA-256"); + hasher.update(Utils.toByteArray(signature)); + final String signatureHash = Hex.toHexString(hasher.digest()); + return signatureHash; + } + + private Map> reduceAppSecKey( + final Map> m, + final ClientConnectionRecord rec) { + + final Map mapping = m.computeIfAbsent( + this.getHashedSignature(rec.getId(), rec.getConnectionToken()), + s -> new HashMap<>()); + + mapping.put(rec.getId(), rec.getExamUserSessionId()); + return m; + } + + private void updateUngrantedConnections(final ClientConnectionRecord rec) { + try { + if (!Utils.fromByte(rec.getSecurityCheckGranted())) { + final String token = rec.getConnectionToken(); + if (applyAppSignatureCheck( + rec.getInstitutionId(), + rec.getExamId(), + token, + getSignatureKeyForConnection(rec.getId())) + .getOrThrow() + .hasAnyGrant()) { + // now granted, update ClientConnection on DB level + if (log.isDebugEnabled()) { + log.debug("Update app-signature-key grant for client connection: {}", token); + } + + this.clientConnectionDAO + .save(new ClientConnection( + rec.getId(), null, + null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, true)) + .onError(error -> log.error("Failed to save ClientConnection grant: ", + error)) + .onSuccess(c -> this.examSessionCacheService.evictClientConnection(token)); + } + } + } catch (final Exception e) { + log.error("Failed to updateAppSignatureKeyGrants for connection: {}", rec, e); + } + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientConnectionDataInternal.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientConnectionDataInternal.java index 985077ae..709442b3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientConnectionDataInternal.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ClientConnectionDataInternal.java @@ -141,6 +141,11 @@ public class ClientConnectionDataInternal extends ClientConnectionData { public boolean isPendingNotification() { return BooleanUtils.isTrue(pendingNotification()); } + + @Override + public boolean isMissingGrant() { + return BooleanUtils.isFalse(ClientConnectionDataInternal.this.clientConnection.securityCheckGranted); + } }; /** This is a static monitoring connection data wrapper/holder */ diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java index 9c5381f2..04bf70ee 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java @@ -51,6 +51,7 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientSessionService; @@ -63,6 +64,7 @@ public class ExamAPI_V1_Controller { private static final Logger log = LoggerFactory.getLogger(ExamAPI_V1_Controller.class); private final LmsSetupDAO lmsSetupDAO; + private final ExamAdminService examAdminService; private final ExamSessionService examSessionService; private final SEBClientConnectionService sebClientConnectionService; private final SEBClientSessionService sebClientSessionService; @@ -72,6 +74,7 @@ public class ExamAPI_V1_Controller { protected ExamAPI_V1_Controller( final LmsSetupDAO lmsSetupDAO, + final ExamAdminService examAdminService, final ExamSessionService examSessionService, final SEBClientConnectionService sebClientConnectionService, final SEBClientSessionService sebClientSessionService, @@ -80,6 +83,7 @@ public class ExamAPI_V1_Controller { @Qualifier(AsyncServiceSpringConfig.EXAM_API_EXECUTOR_BEAN_NAME) final Executor executor) { this.lmsSetupDAO = lmsSetupDAO; + this.examAdminService = examAdminService; this.examSessionService = examSessionService; this.sebClientConnectionService = sebClientConnectionService; this.sebClientSessionService = sebClientSessionService; @@ -135,6 +139,16 @@ public class ExamAPI_V1_Controller { API.EXAM_API_SEB_CONNECTION_TOKEN, clientConnection.connectionToken); + if (clientConnection.examId != null) { + this.examAdminService + .getAppSignatureKeySalt(institutionId, clientConnection.examId) + .onSuccess(salt -> response.setHeader(API.EXAM_API_EXAM_SIGNATURE_SALT_HEADER, salt)) + .onError(error -> log.error( + "Failed to get security key salt for connection: {}", + clientConnection, + error)); + } + // Crate list of running exams List result; if (examId == null) { @@ -192,7 +206,8 @@ public class ExamAPI_V1_Controller { required = false) final String browserSignatureKey, @RequestParam(name = API.EXAM_API_PARAM_CLIENT_ID, required = false) final String clientId, final Principal principal, - final HttpServletRequest request) { + final HttpServletRequest request, + final HttpServletResponse response) { return CompletableFuture.runAsync( () -> { @@ -200,7 +215,7 @@ public class ExamAPI_V1_Controller { final String remoteAddr = this.getClientAddress(request); final Long institutionId = getInstitutionId(principal); - this.sebClientConnectionService.updateClientConnection( + final ClientConnection clientConnection = this.sebClientConnectionService.updateClientConnection( connectionToken, institutionId, examId, @@ -212,6 +227,16 @@ public class ExamAPI_V1_Controller { clientId, browserSignatureKey) .getOrThrow(); + + if (clientConnection.examId != null) { + this.examAdminService + .getAppSignatureKeySalt(institutionId, clientConnection.examId) + .onSuccess(salt -> response.setHeader(API.EXAM_API_EXAM_SIGNATURE_SALT_HEADER, salt)) + .onError(error -> log.error( + "Failed to get security key salt for connection: {}", + clientConnection, + error)); + } }, this.executor); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index 808eff7f..6636b87b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -54,6 +54,7 @@ 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; +import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey.KeyType; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -195,11 +196,11 @@ public class ExamAdministrationController extends EntityController { @RequestMapping( path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT - + API.EXAM_ADMINISTRATION_SEB_SECURITY_KEY_GRANTS_PATH_SEGMENT, + + API.EXAM_ADMINISTRATION_SEB_SECURITY_KEY_INFO_PATH_SEGMENT, method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) - public Collection getSecurityGrants( - @PathVariable(name = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT, required = true) final Long examId, + public Collection getAppSignatureKeyInfo( + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId, @RequestParam( name = API.PARAM_INSTITUTION_ID, required = true, @@ -207,7 +208,52 @@ public class ExamAdministrationController extends EntityController { return this.examDAO.byPK(examId) .flatMap(this::checkReadAccess) - .flatMap(exam -> this.securityKeyService.getPlainAppSignatureKeyGrants(institutionId, examId)) + .flatMap(exam -> this.securityKeyService.getAppSignatureKeyInfo(institutionId, examId)) + .getOrThrow(); + } + + @RequestMapping( + path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_ADMINISTRATION_SEB_SECURITY_KEY_INFO_PATH_SEGMENT, + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_VALUE) + public void saveAppSignatureKeySettings( + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId, + @RequestParam( + name = API.PARAM_INSTITUTION_ID, + required = true, + defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, + @RequestParam(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED) final Boolean enableKeyCheck, + @RequestParam(Exam.ADDITIONAL_ATTR_STATISTICAL_GRANT_COUNT_THRESHOLD) final Integer threshold) { + + this.examDAO.byPK(examId) + .flatMap(this::checkReadAccess) + .flatMap(exam -> this.examAdminService.saveSecurityKeySettings( + institutionId, + examId, + enableKeyCheck, + threshold)) + .getOrThrow(); + } + + @RequestMapping( + path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_ADMINISTRATION_SEB_SECURITY_KEY_GRANTS_PATH_SEGMENT, + method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getSecurityKeyEntries( + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, 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.getSecurityKeyEntries( + institutionId, + examId, + KeyType.APP_SIGNATURE_KEY)) .getOrThrow(); } @@ -217,7 +263,7 @@ public class ExamAdministrationController extends EntityController { method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public SecurityKey newSecurityGrant( - @PathVariable(name = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT, required = true) final Long examId, + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId, @RequestParam( name = API.PARAM_INSTITUTION_ID, required = true, @@ -238,14 +284,14 @@ public class ExamAdministrationController extends EntityController { } @RequestMapping( - path = API.MODEL_ID_VAR_PATH_SEGMENT + path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT + API.EXAM_ADMINISTRATION_SEB_SECURITY_KEY_GRANTS_PATH_SEGMENT + API.MODEL_ID_VAR_PATH_SEGMENT, method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE) public EntityKey deleteSecurityGrant( - @PathVariable(name = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT, required = true) final Long examId, - @PathVariable(name = API.MODEL_ID_VAR_PATH_SEGMENT, required = true) final String keyId, + @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId, + @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long keyId, @RequestParam( name = API.PARAM_INSTITUTION_ID, required = true, @@ -259,24 +305,6 @@ public class ExamAdministrationController extends EntityController { .getOrThrow(); } - @RequestMapping( - path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT - + API.EXAM_ADMINISTRATION_SEB_SECURITY_AS_KEYS_PATH_SEGMENT, - method = RequestMethod.GET, - produces = MediaType.APPLICATION_JSON_VALUE) - public AppSignatureKeyInfo getAppSignatureKeyInfo( - @PathVariable(name = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT, required = true) final Long examId, - @RequestParam( - name = API.PARAM_INSTITUTION_ID, - required = true, - defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) { - - return this.examDAO.byPK(examId) - .flatMap(this::checkReadAccess) - .flatMap(exam -> this.securityKeyService.getAppSignaturesInfo(institutionId, examId)) - .getOrThrow(); - } - // **** SEB Security Key // **************************************************************************** diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java index 74e79d35..0cb6f692 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java @@ -503,7 +503,7 @@ public class ExamMonitoringController { checkPrivileges(institutionId, examId); return this.securityKeyService - .getSecurityKeyOfConnection(institutionId, connectionId) + .getAppSignatureKey(institutionId, connectionId) .getOrThrow(); } diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 6f041114..e94f86be 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -795,7 +795,53 @@ sebserver.exam.proctoring.collecting.open.error=Failed to open the collecting ro sebserver.exam.proctoring.collecting.close.error=Failed to close the collecting room properly. sebserver.exam.signaturekey.action.edit=App Signature Key +sebserver.exam.signaturekey.action.save=Save Settings +sebserver.exam.signaturekey.action.cancel=Cancel and back to Exam +sebserver.exam.signaturekey.action.addGrant=Add Security Grant +sebserver.exam.signaturekey.action.showGrant=Show Security Grant +sebserver.exam.signaturekey.action.deleteGrant=Delete Security Grant +sebserver.exam.signaturekey.title=App Signature Key Overview +sebserver.exam.signaturekey.form.enabled=Enable App Signature Key Check +sebserver.exam.signaturekey.form.enabled.tooltip=Enable the App Signature Key Check for this exam. If disabled no check will be applied +sebserver.exam.signaturekey.form.grant.threshold=Statistical Key Check Threshold +sebserver.exam.signaturekey.form.grant.threshold.tooltip=If there is no explicit grant registered for a given App Signature Key,
a given key will be considered valid if more then the given number of connected SEB clients has the same key. +sebserver.exam.signaturekey.keylist.actions=  +sebserver.exam.signaturekey.keylist.empty=No App Signature Key from SEB Clients available +sebserver.exam.signaturekey.keylist.title=App Signature Keys sent by SEB Clients +sebserver.exam.signaturekey.keylist.title.tooltip=List of all different App Signature Keys sent by the SEB Clients for this exam +sebserver.exam.signaturekey.keylist.key=Key Hash +sebserver.exam.signaturekey.keylist.key.tooltip=The App Signature Key sent by some SEB Client(s) +sebserver.exam.signaturekey.keylist.clients=Number of SEB Clients +sebserver.exam.signaturekey.keylist.clients.tooltip=The number of SEB Clients that sent this key within this exam. +sebserver.exam.signaturekey.keylist.clientids=User Session Identifiers +sebserver.exam.signaturekey.keylist.clientids.tooltip=List of SEB Client session identifiers of the SEB Client sessions that sent this key +sebserver.exam.signaturekey.keylist.pleaseSelect=Please select an App Signature Key from the list. + +sebserver.exam.signaturekey.seb.title=App Signature Key +sebserver.exam.signaturekey.seb.add.info=Please set a meaningful Tag Name and use OK to confirm this security key as granted. +sebserver.exam.signaturekey.seb.add.signature=Key Hash +sebserver.exam.signaturekey.seb.add.tag=Tag Name + +sebserver.exam.signaturekey.list.name=SEB Session ID +sebserver.exam.signaturekey.list.info=SEB Client Info +sebserver.exam.signaturekey.list.status=Connection Status + +sebserver.exam.signaturekey.grantlist.actions=  +sebserver.exam.signaturekey.grantlist.title=Security Key Grants +sebserver.exam.signaturekey.grantlist.title.tooltip=List of all granted security keys of this exam +sebserver.exam.signaturekey.grantlist.empty=There are currently no security key grants +sebserver.exam.signaturekey.grantlist.key=Key Hash +sebserver.exam.signaturekey.grantlist.key.tooltip=The security key that has been granted for this exam +sebserver.exam.signaturekey.grantlist.tag=Tag Name +sebserver.exam.signaturekey.grantlist.tag.tooltip=The tag name if the security key grant +sebserver.exam.signaturekey.grantlist.pleaseSelect=Please select a security key grant from the list. +sebserver.exam.signaturekey.grantlist.delete.confirm=Are you sure to delete this security key grant + +sebserver.exam.signaturekey.grant.title=Security Key Grant +sebserver.exam.signaturekey.grant.key=Granted Key Hash +sebserver.exam.signaturekey.grant.tag=Tag Name +sebserver.exam.signaturekey.grant.type=Key Type ################################ # Connection Configuration @@ -2041,9 +2087,10 @@ 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.info=Mark this App Signature Key as granted. Please also choose a meaningful tag name. +sebserver.monitoring.signaturegrant.signature=App Signature Key Hash sebserver.monitoring.signaturegrant.tag=Tag +sebserver.monitoring.signaturegrant.message.granted=This App Signature Key is already granted for this exam ################################ # Finished Exams