SEBSERV-335 implementation

This commit is contained in:
anhefti 2022-11-24 16:55:53 +01:00
parent 4bcc6cc9cb
commit 9d80a94bbf
39 changed files with 1460 additions and 212 deletions

View file

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

View file

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

View file

@ -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<String, Set<Long>> keyConnectionMapping;
@JsonProperty(SEB_SECURITY_KEY_REGISTRY.ATTR_KEY_VALUE)
public final String key;
@JsonProperty(ATTR_CONNECTION_IDS)
public final Map<Long, String> 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<String, Set<Long>> keyConnectionMapping) {
@JsonProperty(SEB_SECURITY_KEY_REGISTRY.ATTR_KEY_VALUE) final String key,
@JsonProperty(ATTR_CONNECTION_IDS) final Map<Long, String> 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<String, Set<Long>> getKeyConnectionMapping() {
return this.keyConnectionMapping;
@Override
public String getModelId() {
return this.key;
}
public String getKey() {
return this.key;
}
public Map<Long, String> 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();
}

View file

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

View file

@ -25,6 +25,7 @@ public class ClientMonitoringData implements ClientMonitoringDataView {
public final ConnectionStatus status;
public final Map<Long, String> 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<Long, String> 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;

View file

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

View file

@ -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 <K> Key type
* @param <T> Value type
* @param m1 First Map
* @param m2 Second Map
* @return new Map with merged entries from m1 and m2 */
public static <K, T> Map<K, T> mergeMap(final Map<K, T> m1, final Map<K, T> m2) {
final HashMap<K, T> 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.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -234,12 +234,13 @@ public class MonitoringClientConnection implements TemplateComposer {
NOTIFICATION_LIST_TITLE_KEY,
NOTIFICATION_LIST_TITLE_TOOLTIP_KEY);
final EntityTable<ClientNotification> notificationTable = this.pageService.remoteListTableBuilder(
restService.getRestCall(GetPendingClientNotifications.class),
EntityType.CLIENT_EVENT)
.withRestCallAdapter(builder -> builder.withURIVariable(
API.PARAM_PARENT_MODEL_ID,
parentEntityKey.modelId)
final EntityTable<ClientNotification> 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)

View file

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

View file

@ -154,7 +154,7 @@ public abstract class FieldBuilder<T> {
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;

View file

@ -341,7 +341,8 @@ public interface PageService {
<T extends ModelIdAware> TableBuilder<T> staticListTableBuilder(final List<T> staticList, EntityType entityType);
<T extends ModelIdAware> TableBuilder<T> remoteListTableBuilder(RestCall<Collection<T>> apiCall,
<T extends ModelIdAware> TableBuilder<T> remoteListTableBuilder(
RestCall<Collection<T>> apiCall,
EntityType entityType);
/** Get a new PageActionBuilder for a given PageContext.

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.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<Collection<ClientConnection>> {
public GetClientConnections() {
super(new TypeKey<>(
CallType.GET_LIST,
EntityType.CLIENT_CONNECTION,
new TypeReference<Collection<ClientConnection>>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.SEB_CLIENT_CONNECTION_ENDPOINT
+ API.LIST_PATH_SEGMENT);
}
}

View file

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

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.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<EntityKey> {
public DeleteSecurityKeyGrant() {
super(new TypeKey<>(
CallType.DELETE,
EntityType.SEB_SECURITY_KEY_REGISTRY,
new TypeReference<EntityKey>() {
}),
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);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -205,6 +205,8 @@ public class EntityTable<ROW extends ModelIdAware> {
if (selection != null) {
this.pageService.executePageAction(
defaultAction.withEntityKey(selection));
} else {
this.pageService.executePageAction(defaultAction);
}
});
}
@ -217,8 +219,7 @@ public class EntityTable<ROW extends ModelIdAware> {
});
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();

View file

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

View file

@ -163,11 +163,12 @@ public interface ClientConnectionDAO extends
* @return Result refer to the given Exam or to an error when happened. */
Result<Exam> 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<Collection<ClientConnectionRecord>> getAllConnectionRecordsForExam(Long examId);
Result<Collection<ClientConnectionRecord>> getsecurityKeyConnectionRecords(Long examId);
/** Get all client connection identifiers for an exam.
*

View file

@ -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<SecurityKey, SecurityKey> {
/** 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<SecurityKey> 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<SecurityKey> 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<Collection<SecurityKey>> 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<EntityKey> 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);

View file

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

View file

@ -764,12 +764,15 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
@Override
@Transactional(readOnly = true)
public Result<Collection<ClientConnectionRecord>> getAllConnectionRecordsForExam(final Long examId) {
public Result<Collection<ClientConnectionRecord>> 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());
}

View file

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

View file

@ -44,6 +44,21 @@ public interface ExamAdminService {
* @return Result refer to the created exam or to an error when happened */
Result<Exam> 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<Exam> saveSecurityKeySettings(
Long institutionId,
Long examId,
Boolean enabled,
Integer statThreshold);
Result<String> 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.
*

View file

@ -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<Exam> 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<String> 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<Exam> applyAdditionalSEBRestrictions(final Exam exam) {
return Result.tryCatch(() -> {

View file

@ -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<SecurityKey> getAppSignatureKey(Long institutionId, Long connectionId);
Result<SecurityKey> 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<Collection<AppSignatureKeyInfo>> getAppSignatureKeyInfo(Long institutionId, Long examId);
Result<AppSignatureKeyInfo> getAppSignaturesInfo(Long institutionId, Long examId);
Result<Collection<SecurityKey>> 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<Collection<SecurityKey>> 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<SecurityKey> 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<SecurityKey> 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<SecurityKey> registerExamAppSignatureKey(Long institutionId, Long examId, Long connectionId, String tag);
Result<SecurityCheckResult> 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<SecurityKey> getDecrypted(SecurityKey key);
Result<EntityKey> 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<EntityKey> deleteSecurityKeyGrant(Long keyId);
}

View file

@ -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<SecurityKey> getSecurityKeyOfConnection(final Long institutionId, final Long connectionId) {
public Result<SecurityKey> 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<AppSignatureKeyInfo> getAppSignaturesInfo(final Long institutionId, final Long examId) {
public Result<Collection<AppSignatureKeyInfo>> getAppSignatureKeyInfo(final Long institutionId, final Long examId) {
return Result.tryCatch(() -> {
final Map<String, Set<Long>> 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<String, Map<Long, String>>(),
this::reduceAppSecKey,
Utils::<String, Map<Long, String>> mergeMap)
.entrySet()
.stream()
.map(m -> new AppSignatureKeyInfo(institutionId, examId, m.getKey(), m.getValue()))
.collect(Collectors.toList());
});
}
@Override
public Result<Collection<SecurityKey>> getPlainAppSignatureKeyGrants(final Long institutionId, final Long examId) {
public Result<Collection<SecurityKey>> 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<SecurityCheckResult> 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<SecurityKey> 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<SecurityKey> 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<EntityKey> deleteSecurityKeyGrant(final String keyModelId) {
return Result.tryCatch(() -> Long.parseLong(keyModelId))
.flatMap(this.securityKeyRegistryDAO::delete);
public Result<EntityKey> 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<SecurityCheckResult> 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<SecurityKey> 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<String, SecurityKey> 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<SecurityKey> decryptAll(final Collection<SecurityKey> all) {
return all.stream()
.map(this::getDecrypted)
.filter(Result::hasValue)
.map(Result::get)
.collect(Collectors.toList());
}
private Result<SecurityKey> encryptInternal(final SecurityKey key) {
return Result.tryCatch(() -> new SecurityKey(
key.id,
@ -493,4 +512,76 @@ public class SecurityKeyServiceImpl implements SecurityKeyService {
key.examTemplateId));
}
private Collection<SecurityKey> getKeysForRead(final Collection<SecurityKey> 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<String, Map<Long, String>> reduceAppSecKey(
final Map<String, Map<Long, String>> m,
final ClientConnectionRecord rec) {
final Map<Long, String> 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);
}
}
}

View file

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

View file

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

View file

@ -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<Exam, Exam> {
@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<SecurityKey> getSecurityGrants(
@PathVariable(name = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT, required = true) final Long examId,
public Collection<AppSignatureKeyInfo> 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<Exam, Exam> {
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<SecurityKey> 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<Exam, Exam> {
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<Exam, Exam> {
}
@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<Exam, Exam> {
.getOrThrow();
}
@RequestMapping(
path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_SEB_SECURITY_AS_KEYS_PATH_SEGMENT,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public AppSignatureKeyInfo getAppSignatureKeyInfo(
@PathVariable(name = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT, required = true) final Long examId,
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) {
return this.examDAO.byPK(examId)
.flatMap(this::checkReadAccess)
.flatMap(exam -> this.securityKeyService.getAppSignaturesInfo(institutionId, examId))
.getOrThrow();
}
// **** SEB Security Key
// ****************************************************************************

View file

@ -503,7 +503,7 @@ public class ExamMonitoringController {
checkPrivileges(institutionId, examId);
return this.securityKeyService
.getSecurityKeyOfConnection(institutionId, connectionId)
.getAppSignatureKey(institutionId, connectionId)
.getOrThrow();
}

View file

@ -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,<br/>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=&nbsp;
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=&nbsp;
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