SEBSERV-335 implementation Exam
This commit is contained in:
		
							parent
							
								
									949552bbf7
								
							
						
					
					
						commit
						ea13bb6ca4
					
				
					 30 changed files with 712 additions and 86 deletions
				
			
		| 
						 | 
					@ -155,7 +155,8 @@ public final class API {
 | 
				
			||||||
    public static final String EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported";
 | 
					    public static final String EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported";
 | 
				
			||||||
    public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_CHAPTERS_PATH_SEGMENT = "/chapters";
 | 
					    public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_CHAPTERS_PATH_SEGMENT = "/chapters";
 | 
				
			||||||
    public static final String EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT = "/proctoring";
 | 
					    public static final String EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT = "/proctoring";
 | 
				
			||||||
    public static final String EXAM_ADMINISTRATION_SEB_SECURITY_KEY_GRANTS_PATH_SEGMENT = "/seb-grants";
 | 
					    public static final String EXAM_ADMINISTRATION_SEB_SECURITY_KEY_GRANTS_PATH_SEGMENT = "/grant";
 | 
				
			||||||
 | 
					    public static final String EXAM_ADMINISTRATION_SEB_SECURITY_AS_KEYS_PATH_SEGMENT = "/signature-key";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static final String EXAM_INDICATOR_ENDPOINT = "/indicator";
 | 
					    public static final String EXAM_INDICATOR_ENDPOINT = "/indicator";
 | 
				
			||||||
    public static final String EXAM_CLIENT_GROUP_ENDPOINT = "/client-group";
 | 
					    public static final String EXAM_CLIENT_GROUP_ENDPOINT = "/client-group";
 | 
				
			||||||
| 
						 | 
					@ -200,7 +201,7 @@ public final class API {
 | 
				
			||||||
    public static final String EXAM_MONITORING_INSTRUCTION_ENDPOINT = "/instruction";
 | 
					    public static final String EXAM_MONITORING_INSTRUCTION_ENDPOINT = "/instruction";
 | 
				
			||||||
    public static final String EXAM_MONITORING_NOTIFICATION_ENDPOINT = "/notification";
 | 
					    public static final String EXAM_MONITORING_NOTIFICATION_ENDPOINT = "/notification";
 | 
				
			||||||
    public static final String EXAM_MONITORING_DISABLE_CONNECTION_ENDPOINT = "/disable-connection";
 | 
					    public static final String EXAM_MONITORING_DISABLE_CONNECTION_ENDPOINT = "/disable-connection";
 | 
				
			||||||
    public static final String EXAM_MONITORING_GRANT_APP_SIGNATURE_KEY_ENDPOINT = "/apply-grant";
 | 
					    public static final String EXAM_MONITORING_SIGNATURE_KEY_ENDPOINT = "/signature";
 | 
				
			||||||
    public static final String EXAM_MONITORING_STATE_FILTER = "hidden-states";
 | 
					    public static final String EXAM_MONITORING_STATE_FILTER = "hidden-states";
 | 
				
			||||||
    public static final String EXAM_MONITORING_CLIENT_GROUP_FILTER = "hidden-client-group";
 | 
					    public static final String EXAM_MONITORING_CLIENT_GROUP_FILTER = "hidden-client-group";
 | 
				
			||||||
    public static final String EXAM_MONITORING_FINISHED_ENDPOINT = "/finishedexams";
 | 
					    public static final String EXAM_MONITORING_FINISHED_ENDPOINT = "/finishedexams";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,93 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * This Source Code Form is subject to the terms of the Mozilla Public
 | 
				
			||||||
 | 
					 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
				
			||||||
 | 
					 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ch.ethz.seb.sebserver.gbl.model.institution;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					import java.util.Objects;
 | 
				
			||||||
 | 
					import java.util.Set;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.validation.constraints.NotNull;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.annotation.JsonCreator;
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.annotation.JsonProperty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.Domain.SEB_SECURITY_KEY_REGISTRY;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.util.Utils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@JsonIgnoreProperties(ignoreUnknown = true)
 | 
				
			||||||
 | 
					public class AppSignatureKeyInfo {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static final String ATTR_KEY_CONNECTION_MAPPING = "kcMapping";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @NotNull
 | 
				
			||||||
 | 
					    @JsonProperty(SEB_SECURITY_KEY_REGISTRY.ATTR_INSTITUTION_ID)
 | 
				
			||||||
 | 
					    public final Long institutionId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @NotNull
 | 
				
			||||||
 | 
					    @JsonProperty(SEB_SECURITY_KEY_REGISTRY.ATTR_EXAM_ID)
 | 
				
			||||||
 | 
					    public final Long examId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @JsonProperty(ATTR_KEY_CONNECTION_MAPPING)
 | 
				
			||||||
 | 
					    public final Map<String, Set<Long>> keyConnectionMapping;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @JsonCreator
 | 
				
			||||||
 | 
					    public AppSignatureKeyInfo(
 | 
				
			||||||
 | 
					            @JsonProperty(SEB_SECURITY_KEY_REGISTRY.ATTR_INSTITUTION_ID) final Long institutionId,
 | 
				
			||||||
 | 
					            @JsonProperty(SEB_SECURITY_KEY_REGISTRY.ATTR_EXAM_ID) final Long examId,
 | 
				
			||||||
 | 
					            @JsonProperty(ATTR_KEY_CONNECTION_MAPPING) final Map<String, Set<Long>> keyConnectionMapping) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.institutionId = institutionId;
 | 
				
			||||||
 | 
					        this.examId = examId;
 | 
				
			||||||
 | 
					        this.keyConnectionMapping = Utils.immutableMapOf(keyConnectionMapping);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public Long getInstitutionId() {
 | 
				
			||||||
 | 
					        return this.institutionId;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public Long getExamId() {
 | 
				
			||||||
 | 
					        return this.examId;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public Map<String, Set<Long>> getKeyConnectionMapping() {
 | 
				
			||||||
 | 
					        return this.keyConnectionMapping;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public int hashCode() {
 | 
				
			||||||
 | 
					        return Objects.hash(this.examId, this.institutionId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public boolean equals(final Object obj) {
 | 
				
			||||||
 | 
					        if (this == obj)
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        if (obj == null)
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        if (getClass() != obj.getClass())
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        final AppSignatureKeyInfo other = (AppSignatureKeyInfo) obj;
 | 
				
			||||||
 | 
					        return Objects.equals(this.examId, other.examId) && Objects.equals(this.institutionId, other.institutionId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public String toString() {
 | 
				
			||||||
 | 
					        final StringBuilder builder = new StringBuilder();
 | 
				
			||||||
 | 
					        builder.append("AppSignatureKeyInfo [institutionId=");
 | 
				
			||||||
 | 
					        builder.append(this.institutionId);
 | 
				
			||||||
 | 
					        builder.append(", examId=");
 | 
				
			||||||
 | 
					        builder.append(this.examId);
 | 
				
			||||||
 | 
					        builder.append(", keyConnectionMapping=");
 | 
				
			||||||
 | 
					        builder.append(this.keyConnectionMapping);
 | 
				
			||||||
 | 
					        builder.append("]");
 | 
				
			||||||
 | 
					        return builder.toString();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -55,7 +55,7 @@ public final class LmsSetup implements GrantEntity, Activatable {
 | 
				
			||||||
     * Also defines the supports feature(s) for each type of LMS binding. */
 | 
					     * Also defines the supports feature(s) for each type of LMS binding. */
 | 
				
			||||||
    public enum LmsType {
 | 
					    public enum LmsType {
 | 
				
			||||||
        /** Mockup LMS type used to create test setups */
 | 
					        /** Mockup LMS type used to create test setups */
 | 
				
			||||||
        MOCKUP(Features.COURSE_API),
 | 
					        MOCKUP(Features.COURSE_API, Features.SEB_RESTRICTION),
 | 
				
			||||||
        /** The Open edX LMS binding features both APIs, course access as well as SEB restriction */
 | 
					        /** The Open edX LMS binding features both APIs, course access as well as SEB restriction */
 | 
				
			||||||
        OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION),
 | 
					        OPEN_EDX(Features.COURSE_API, Features.SEB_RESTRICTION),
 | 
				
			||||||
        /** The Moodle binding features only the course access API so far */
 | 
					        /** The Moodle binding features only the course access API so far */
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -415,6 +415,17 @@ public enum ActionDefinition {
 | 
				
			||||||
            PageStateDefinitionImpl.EXAM_VIEW,
 | 
					            PageStateDefinitionImpl.EXAM_VIEW,
 | 
				
			||||||
            ActionCategory.FORM),
 | 
					            ActionCategory.FORM),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    EXAM_SECURITY_KEY_ENABLED(
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.exam.signaturekey.action.edit"),
 | 
				
			||||||
 | 
					            ImageIcon.SHIELD,
 | 
				
			||||||
 | 
					            PageStateDefinitionImpl.SECURITY_KEY_EDIT,
 | 
				
			||||||
 | 
					            ActionCategory.FORM),
 | 
				
			||||||
 | 
					    EXAM_SECURITY_KEY_DISABLED(
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.exam.signaturekey.action.edit"),
 | 
				
			||||||
 | 
					            ImageIcon.NO_SHIELD,
 | 
				
			||||||
 | 
					            PageStateDefinitionImpl.SECURITY_KEY_EDIT,
 | 
				
			||||||
 | 
					            ActionCategory.FORM),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    EXAM_SEB_CLIENT_CONFIG_EXPORT(
 | 
					    EXAM_SEB_CLIENT_CONFIG_EXPORT(
 | 
				
			||||||
            new LocTextKey("sebserver.exam.action.createClientToStartExam"),
 | 
					            new LocTextKey("sebserver.exam.action.createClientToStartExam"),
 | 
				
			||||||
            ImageIcon.EXPORT,
 | 
					            ImageIcon.EXPORT,
 | 
				
			||||||
| 
						 | 
					@ -849,6 +860,11 @@ public enum ActionDefinition {
 | 
				
			||||||
            ImageIcon.YES,
 | 
					            ImageIcon.YES,
 | 
				
			||||||
            PageStateDefinitionImpl.MONITORING_CLIENT_CONNECTION,
 | 
					            PageStateDefinitionImpl.MONITORING_CLIENT_CONNECTION,
 | 
				
			||||||
            ActionCategory.EXAM_MONITORING_NOTIFICATION_LIST),
 | 
					            ActionCategory.EXAM_MONITORING_NOTIFICATION_LIST),
 | 
				
			||||||
 | 
					    MONITOR_EXAM_CLIENT_CONNECTION_GRANT_SIGNATURE_KEY(
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.monitoring.exam.connection.action.grant.signaturekey"),
 | 
				
			||||||
 | 
					            ImageIcon.VERIFY,
 | 
				
			||||||
 | 
					            PageStateDefinitionImpl.MONITORING_CLIENT_CONNECTION,
 | 
				
			||||||
 | 
					            ActionCategory.FORM),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    MONITOR_EXAM_QUIT_SELECTED(
 | 
					    MONITOR_EXAM_QUIT_SELECTED(
 | 
				
			||||||
            new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.selected"),
 | 
					            new LocTextKey("sebserver.monitoring.exam.connection.action.instruction.quit.selected"),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,6 +28,7 @@ import ch.ethz.seb.sebserver.gui.content.exam.ClientGroupForm;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.content.exam.ClientGroupTemplateForm;
 | 
					import ch.ethz.seb.sebserver.gui.content.exam.ClientGroupTemplateForm;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.content.exam.ExamForm;
 | 
					import ch.ethz.seb.sebserver.gui.content.exam.ExamForm;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.content.exam.ExamList;
 | 
					import ch.ethz.seb.sebserver.gui.content.exam.ExamList;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.content.exam.ExamSignatureKeyForm;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.content.exam.ExamTemplateForm;
 | 
					import ch.ethz.seb.sebserver.gui.content.exam.ExamTemplateForm;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.content.exam.ExamTemplateList;
 | 
					import ch.ethz.seb.sebserver.gui.content.exam.ExamTemplateList;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.content.exam.IndicatorForm;
 | 
					import ch.ethz.seb.sebserver.gui.content.exam.IndicatorForm;
 | 
				
			||||||
| 
						 | 
					@ -68,6 +69,7 @@ public enum PageStateDefinitionImpl implements PageStateDefinition {
 | 
				
			||||||
    EXAM_EDIT(Type.FORM_EDIT, ExamForm.class, ActivityDefinition.EXAM),
 | 
					    EXAM_EDIT(Type.FORM_EDIT, ExamForm.class, ActivityDefinition.EXAM),
 | 
				
			||||||
    INDICATOR_EDIT(Type.FORM_EDIT, IndicatorForm.class, ActivityDefinition.EXAM),
 | 
					    INDICATOR_EDIT(Type.FORM_EDIT, IndicatorForm.class, ActivityDefinition.EXAM),
 | 
				
			||||||
    CLIENT_GROUP_EDIT(Type.FORM_EDIT, ClientGroupForm.class, ActivityDefinition.EXAM),
 | 
					    CLIENT_GROUP_EDIT(Type.FORM_EDIT, ClientGroupForm.class, ActivityDefinition.EXAM),
 | 
				
			||||||
 | 
					    SECURITY_KEY_EDIT(Type.FORM_EDIT, ExamSignatureKeyForm.class, ActivityDefinition.EXAM),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    EXAM_TEMPLATE_LIST(Type.LIST_VIEW, ExamTemplateList.class, ActivityDefinition.EXAM_TEMPLATE),
 | 
					    EXAM_TEMPLATE_LIST(Type.LIST_VIEW, ExamTemplateList.class, ActivityDefinition.EXAM_TEMPLATE),
 | 
				
			||||||
    EXAM_TEMPLATE_VIEW(Type.LIST_VIEW, ExamTemplateForm.class, ActivityDefinition.EXAM_TEMPLATE),
 | 
					    EXAM_TEMPLATE_VIEW(Type.LIST_VIEW, ExamTemplateForm.class, ActivityDefinition.EXAM_TEMPLATE),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -252,6 +252,8 @@ public class ExamForm implements TemplateComposer {
 | 
				
			||||||
        final ExamStatus examStatus = exam.getStatus();
 | 
					        final ExamStatus examStatus = exam.getStatus();
 | 
				
			||||||
        final boolean editable = modifyGrant &&
 | 
					        final boolean editable = modifyGrant &&
 | 
				
			||||||
                (examStatus == ExamStatus.UP_COMING || examStatus == ExamStatus.RUNNING);
 | 
					                (examStatus == ExamStatus.UP_COMING || examStatus == ExamStatus.RUNNING);
 | 
				
			||||||
 | 
					        final boolean signatureKeyCheckEnabled = BooleanUtils.toBoolean(
 | 
				
			||||||
 | 
					                exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        final boolean sebRestrictionAvailable = testSEBRestrictionAPI(exam);
 | 
					        final boolean sebRestrictionAvailable = testSEBRestrictionAPI(exam);
 | 
				
			||||||
        final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService
 | 
					        final boolean isRestricted = readonly && sebRestrictionAvailable && this.restService
 | 
				
			||||||
| 
						 | 
					@ -465,6 +467,14 @@ public class ExamForm implements TemplateComposer {
 | 
				
			||||||
                .publishIf(() -> sebRestrictionAvailable && readonly && modifyGrant && !importFromQuizData
 | 
					                .publishIf(() -> sebRestrictionAvailable && readonly && modifyGrant && !importFromQuizData
 | 
				
			||||||
                        && BooleanUtils.isTrue(isRestricted))
 | 
					                        && BooleanUtils.isTrue(isRestricted))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                .newAction(ActionDefinition.EXAM_SECURITY_KEY_ENABLED)
 | 
				
			||||||
 | 
					                .withEntityKey(entityKey)
 | 
				
			||||||
 | 
					                .publishIf(() -> signatureKeyCheckEnabled && readonly)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                .newAction(ActionDefinition.EXAM_SECURITY_KEY_DISABLED)
 | 
				
			||||||
 | 
					                .withEntityKey(entityKey)
 | 
				
			||||||
 | 
					                .publishIf(() -> !signatureKeyCheckEnabled && readonly)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                .newAction(ActionDefinition.EXAM_PROCTORING_ON)
 | 
					                .newAction(ActionDefinition.EXAM_PROCTORING_ON)
 | 
				
			||||||
                .withEntityKey(entityKey)
 | 
					                .withEntityKey(entityKey)
 | 
				
			||||||
                .withExec(this.proctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable))
 | 
					                .withExec(this.proctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,76 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * This Source Code Form is subject to the terms of the Mozilla Public
 | 
				
			||||||
 | 
					 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
				
			||||||
 | 
					 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ch.ethz.seb.sebserver.gui.content.exam;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.springframework.context.annotation.Lazy;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Component;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.EntityKey;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.ResourceService;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.page.PageContext;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.page.PageService;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Lazy
 | 
				
			||||||
 | 
					@Component
 | 
				
			||||||
 | 
					@GuiProfile
 | 
				
			||||||
 | 
					public class ExamSignatureKeyForm implements TemplateComposer {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final LocTextKey TILE =
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.exam.signaturekey.title");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final LocTextKey FORM_ENABLED =
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.exam.signaturekey.form.enabled");
 | 
				
			||||||
 | 
					    private static final LocTextKey FORM_STAT_GRANT_THRESHOLD =
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.exam.signaturekey.form.grant.threshold");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final LocTextKey GRANT_LIST_TITLE =
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.exam.signaturekey.grantlist.title");
 | 
				
			||||||
 | 
					    private static final LocTextKey GRANT_LIST_KEY =
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.exam.signaturekey.grantlist.key");
 | 
				
			||||||
 | 
					    private static final LocTextKey GRANT_LIST_TAG =
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.exam.signaturekey.grantlist.tag");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final LocTextKey APP_SIG_KEY_LIST_TITLE =
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.exam.signaturekey.keylist.title");
 | 
				
			||||||
 | 
					    private static final LocTextKey APP_SIG_KEY_LIST_KEY =
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.exam.signaturekey.keylist.key");
 | 
				
			||||||
 | 
					    private static final LocTextKey APP_SIG_KEY_LIST_NUM_CLIENTS =
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.exam.signaturekey.keylist.clients");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private final PageService pageService;
 | 
				
			||||||
 | 
					    private final ResourceService resourceService;
 | 
				
			||||||
 | 
					    private final I18nSupport i18nSupport;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public ExamSignatureKeyForm(
 | 
				
			||||||
 | 
					            final PageService pageService,
 | 
				
			||||||
 | 
					            final ResourceService resourceService,
 | 
				
			||||||
 | 
					            final I18nSupport i18nSupport) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.pageService = pageService;
 | 
				
			||||||
 | 
					        this.resourceService = resourceService;
 | 
				
			||||||
 | 
					        this.i18nSupport = i18nSupport;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void compose(final PageContext pageContext) {
 | 
				
			||||||
 | 
					        final RestService restService = this.resourceService.getRestService();
 | 
				
			||||||
 | 
					        final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
 | 
				
			||||||
 | 
					        final EntityKey entityKey = pageContext.getEntityKey();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // TODO Auto-generated method stub
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -31,6 +31,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
 | 
					import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
 | 
					import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringFeature;
 | 
					import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringFeature;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
 | 
				
			||||||
| 
						 | 
					@ -61,6 +62,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.indicator.Ge
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs.GetExtendedClientEventPage;
 | 
					import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs.GetExtendedClientEventPage;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.ConfirmPendingClientNotification;
 | 
					import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.ConfirmPendingClientNotification;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionData;
 | 
					import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionData;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionSecurityKey;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetPendingClientNotifications;
 | 
					import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetPendingClientNotifications;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
 | 
					import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionDetails;
 | 
					import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionDetails;
 | 
				
			||||||
| 
						 | 
					@ -124,6 +126,7 @@ public class MonitoringClientConnection implements TemplateComposer {
 | 
				
			||||||
    private final InstructionProcessor instructionProcessor;
 | 
					    private final InstructionProcessor instructionProcessor;
 | 
				
			||||||
    private final SEBClientEventDetailsPopup sebClientLogDetailsPopup;
 | 
					    private final SEBClientEventDetailsPopup sebClientLogDetailsPopup;
 | 
				
			||||||
    private final SEBSendLockPopup sebSendLockPopup;
 | 
					    private final SEBSendLockPopup sebSendLockPopup;
 | 
				
			||||||
 | 
					    private final SignatureKeyGrantPopup signatureKeyGrantPopup;
 | 
				
			||||||
    private final MonitoringProctoringService monitoringProctoringService;
 | 
					    private final MonitoringProctoringService monitoringProctoringService;
 | 
				
			||||||
    private final long pollInterval;
 | 
					    private final long pollInterval;
 | 
				
			||||||
    private final int pageSize;
 | 
					    private final int pageSize;
 | 
				
			||||||
| 
						 | 
					@ -139,6 +142,7 @@ public class MonitoringClientConnection implements TemplateComposer {
 | 
				
			||||||
            final SEBClientEventDetailsPopup sebClientLogDetailsPopup,
 | 
					            final SEBClientEventDetailsPopup sebClientLogDetailsPopup,
 | 
				
			||||||
            final MonitoringProctoringService monitoringProctoringService,
 | 
					            final MonitoringProctoringService monitoringProctoringService,
 | 
				
			||||||
            final SEBSendLockPopup sebSendLockPopup,
 | 
					            final SEBSendLockPopup sebSendLockPopup,
 | 
				
			||||||
 | 
					            final SignatureKeyGrantPopup signatureKeyGrantPopup,
 | 
				
			||||||
            @Value("${sebserver.gui.webservice.poll-interval:500}") final long pollInterval,
 | 
					            @Value("${sebserver.gui.webservice.poll-interval:500}") final long pollInterval,
 | 
				
			||||||
            @Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
 | 
					            @Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -151,6 +155,7 @@ public class MonitoringClientConnection implements TemplateComposer {
 | 
				
			||||||
        this.pollInterval = pollInterval;
 | 
					        this.pollInterval = pollInterval;
 | 
				
			||||||
        this.sebClientLogDetailsPopup = sebClientLogDetailsPopup;
 | 
					        this.sebClientLogDetailsPopup = sebClientLogDetailsPopup;
 | 
				
			||||||
        this.sebSendLockPopup = sebSendLockPopup;
 | 
					        this.sebSendLockPopup = sebSendLockPopup;
 | 
				
			||||||
 | 
					        this.signatureKeyGrantPopup = signatureKeyGrantPopup;
 | 
				
			||||||
        this.pageSize = pageSize;
 | 
					        this.pageSize = pageSize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.typeFilter = new TableFilterAttribute(
 | 
					        this.typeFilter = new TableFilterAttribute(
 | 
				
			||||||
| 
						 | 
					@ -395,6 +400,26 @@ public class MonitoringClientConnection implements TemplateComposer {
 | 
				
			||||||
                .publishIf(() -> isExamSupporter.getAsBoolean() &&
 | 
					                .publishIf(() -> isExamSupporter.getAsBoolean() &&
 | 
				
			||||||
                        connectionData.clientConnection.status.clientActiveStatus);
 | 
					                        connectionData.clientConnection.status.clientActiveStatus);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (clientConnectionDetails.checkSecurityGrant) {
 | 
				
			||||||
 | 
					            final SecurityKey securityKey = this.pageService
 | 
				
			||||||
 | 
					                    .getRestService()
 | 
				
			||||||
 | 
					                    .getBuilder(GetClientConnectionSecurityKey.class)
 | 
				
			||||||
 | 
					                    .withURIVariable(API.PARAM_PARENT_MODEL_ID, parentEntityKey.modelId)
 | 
				
			||||||
 | 
					                    .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
 | 
				
			||||||
 | 
					                    .call()
 | 
				
			||||||
 | 
					                    .getOrThrow();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (securityKey.id < 0) {
 | 
				
			||||||
 | 
					                actionBuilder
 | 
				
			||||||
 | 
					                        .newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_GRANT_SIGNATURE_KEY)
 | 
				
			||||||
 | 
					                        .withParentEntityKey(parentEntityKey)
 | 
				
			||||||
 | 
					                        .withEntityKey(entityKey)
 | 
				
			||||||
 | 
					                        .withExec(action -> this.signatureKeyGrantPopup.showGrantPopup(action, securityKey))
 | 
				
			||||||
 | 
					                        .noEventPropagation()
 | 
				
			||||||
 | 
					                        .publishIf(isExamSupporter);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (connectionData.clientConnection.status == ConnectionStatus.ACTIVE) {
 | 
					        if (connectionData.clientConnection.status == ConnectionStatus.ACTIVE) {
 | 
				
			||||||
            final ProctoringServiceSettings proctoringSettings = restService
 | 
					            final ProctoringServiceSettings proctoringSettings = restService
 | 
				
			||||||
                    .getBuilder(GetExamProctoringSettings.class)
 | 
					                    .getBuilder(GetExamProctoringSettings.class)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,140 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * This Source Code Form is subject to the terms of the Mozilla Public
 | 
				
			||||||
 | 
					 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
				
			||||||
 | 
					 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ch.ethz.seb.sebserver.gui.content.monitoring;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.function.Predicate;
 | 
				
			||||||
 | 
					import java.util.function.Supplier;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.eclipse.swt.widgets.Composite;
 | 
				
			||||||
 | 
					import org.springframework.context.annotation.Lazy;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Component;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.api.API;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.Domain;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.EntityKey;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.util.Utils;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.form.FormBuilder;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.form.FormHandle;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.page.ModalInputDialogComposer;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.page.PageContext;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.page.PageService;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GrantClientConnectionSecurityKey;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Lazy
 | 
				
			||||||
 | 
					@Component
 | 
				
			||||||
 | 
					@GuiProfile
 | 
				
			||||||
 | 
					public class SignatureKeyGrantPopup {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final LocTextKey TITLE_TEXT_KEY =
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.monitoring.signaturegrant.title");
 | 
				
			||||||
 | 
					    private static final LocTextKey TITLE_TEXT_INFO =
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.monitoring.signaturegrant.info");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final LocTextKey TITLE_TEXT_FORM_SIGNATURE =
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.monitoring.signaturegrant.signature");
 | 
				
			||||||
 | 
					    private static final LocTextKey TITLE_TEXT_FORM_TAG =
 | 
				
			||||||
 | 
					            new LocTextKey("sebserver.monitoring.signaturegrant.tag");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private final PageService pageService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected SignatureKeyGrantPopup(final PageService pageService) {
 | 
				
			||||||
 | 
					        this.pageService = pageService;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public PageAction showGrantPopup(final PageAction action, final SecurityKey securityKey) {
 | 
				
			||||||
 | 
					        final PageContext pageContext = action.pageContext();
 | 
				
			||||||
 | 
					        final PopupComposer popupComposer = new PopupComposer(this.pageService, pageContext, securityKey);
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            final ModalInputDialog<FormHandle<?>> dialog =
 | 
				
			||||||
 | 
					                    new ModalInputDialog<>(
 | 
				
			||||||
 | 
					                            action.pageContext().getParent().getShell(),
 | 
				
			||||||
 | 
					                            this.pageService.getWidgetFactory());
 | 
				
			||||||
 | 
					            dialog.setDialogWidth(700);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            final Predicate<FormHandle<?>> applyGrant = formHandle -> applyGrant(
 | 
				
			||||||
 | 
					                    pageContext,
 | 
				
			||||||
 | 
					                    formHandle);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            dialog.open(
 | 
				
			||||||
 | 
					                    TITLE_TEXT_KEY,
 | 
				
			||||||
 | 
					                    applyGrant,
 | 
				
			||||||
 | 
					                    Utils.EMPTY_EXECUTION,
 | 
				
			||||||
 | 
					                    popupComposer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } catch (final Exception e) {
 | 
				
			||||||
 | 
					            action.pageContext().notifyUnexpectedError(e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return action;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private final class PopupComposer implements ModalInputDialogComposer<FormHandle<?>> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private final PageService pageService;
 | 
				
			||||||
 | 
					        private final PageContext pageContext;
 | 
				
			||||||
 | 
					        private final SecurityKey securityKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        protected PopupComposer(
 | 
				
			||||||
 | 
					                final PageService pageService,
 | 
				
			||||||
 | 
					                final PageContext pageContext,
 | 
				
			||||||
 | 
					                final SecurityKey securityKey) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.pageService = pageService;
 | 
				
			||||||
 | 
					            this.pageContext = pageContext;
 | 
				
			||||||
 | 
					            this.securityKey = securityKey;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        public Supplier<FormHandle<?>> compose(final Composite parent) {
 | 
				
			||||||
 | 
					            final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
 | 
				
			||||||
 | 
					            widgetFactory.addFormSubContextHeader(parent, TITLE_TEXT_INFO, null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            //final Composite defaultPageLayout = widgetFactory.defaultPageLayout(parent, TITLE_TEXT_INFO);
 | 
				
			||||||
 | 
					            final PageContext formContext = this.pageContext.copyOf(parent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            final FormHandle<?> form = this.pageService.formBuilder(formContext)
 | 
				
			||||||
 | 
					                    .addField(FormBuilder.text(
 | 
				
			||||||
 | 
					                            Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG,
 | 
				
			||||||
 | 
					                            TITLE_TEXT_FORM_TAG,
 | 
				
			||||||
 | 
					                            this.securityKey.tag))
 | 
				
			||||||
 | 
					                    .addField(FormBuilder.text(
 | 
				
			||||||
 | 
					                            Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_KEY_VALUE,
 | 
				
			||||||
 | 
					                            TITLE_TEXT_FORM_SIGNATURE,
 | 
				
			||||||
 | 
					                            String.valueOf(this.securityKey.key))
 | 
				
			||||||
 | 
					                            .readonly(true))
 | 
				
			||||||
 | 
					                    .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return () -> form;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private boolean applyGrant(
 | 
				
			||||||
 | 
					            final PageContext pageContext,
 | 
				
			||||||
 | 
					            final FormHandle<?> formHandle) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        final EntityKey examKey = pageContext.getParentEntityKey();
 | 
				
			||||||
 | 
					        final EntityKey connectionKey = pageContext.getEntityKey();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return this.pageService
 | 
				
			||||||
 | 
					                .getRestService()
 | 
				
			||||||
 | 
					                .getBuilder(GrantClientConnectionSecurityKey.class)
 | 
				
			||||||
 | 
					                .withURIVariable(API.PARAM_PARENT_MODEL_ID, examKey.modelId)
 | 
				
			||||||
 | 
					                .withURIVariable(API.PARAM_MODEL_ID, connectionKey.modelId)
 | 
				
			||||||
 | 
					                .withFormBinding(formHandle.getFormBinding())
 | 
				
			||||||
 | 
					                .call()
 | 
				
			||||||
 | 
					                .onError(formHandle::handleError)
 | 
				
			||||||
 | 
					                .hasValue();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -60,7 +60,6 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.SEBClientConfig.VDIType;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.sebconfig.TemplateAttribute;
 | 
					import ch.ethz.seb.sebserver.gbl.model.sebconfig.TemplateAttribute;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.sebconfig.View;
 | 
					import ch.ethz.seb.sebserver.gbl.model.sebconfig.View;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
 | 
					 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
 | 
				
			||||||
| 
						 | 
					@ -87,7 +86,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.Ge
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetViews;
 | 
					import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetViews;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.useraccount.GetUserAccountNames;
 | 
					import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.useraccount.GetUserAccountNames;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
 | 
					import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable.MonitoringEntry;
 | 
					import ch.ethz.seb.sebserver.gui.service.session.MonitoringEntry;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Lazy
 | 
					@Lazy
 | 
				
			||||||
@Service
 | 
					@Service
 | 
				
			||||||
| 
						 | 
					@ -626,27 +625,38 @@ public class ResourceService {
 | 
				
			||||||
                .getText(ResourceService.EXAMCONFIG_STATUS_PREFIX + config.configStatus.name());
 | 
					                .getText(ResourceService.EXAMCONFIG_STATUS_PREFIX + config.configStatus.name());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Function<ClientConnectionData, String> localizedClientConnectionStatusNameFunction() {
 | 
					//    public Function<ClientConnectionData, String> localizedClientConnectionStatusNameFunction() {
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
        // Memoizing
 | 
					//        // Memoizing
 | 
				
			||||||
        final String missing = this.i18nSupport.getText(
 | 
					//        final String missing = this.i18nSupport.getText(
 | 
				
			||||||
                SEB_CONNECTION_STATUS_KEY_PREFIX + MISSING_CLIENT_PING_NAME_KEY,
 | 
					//                SEB_CONNECTION_STATUS_KEY_PREFIX + MISSING_CLIENT_PING_NAME_KEY,
 | 
				
			||||||
                MISSING_CLIENT_PING_NAME_KEY);
 | 
					//                MISSING_CLIENT_PING_NAME_KEY);
 | 
				
			||||||
        final EnumMap<ConnectionStatus, String> localizedNames = new EnumMap<>(ConnectionStatus.class);
 | 
					//        final String missingGrant = this.i18nSupport.getText(
 | 
				
			||||||
        Arrays.asList(ConnectionStatus.values()).stream().forEach(state -> localizedNames.put(state, this.i18nSupport
 | 
					//                SEB_CONNECTION_STATUS_KEY_PREFIX + MISSING_CLIENT_SEC_GRANT_NAME_KEY,
 | 
				
			||||||
                .getText(SEB_CONNECTION_STATUS_KEY_PREFIX + state.name(), state.name())));
 | 
					//                MISSING_CLIENT_SEC_GRANT_NAME_KEY);
 | 
				
			||||||
 | 
					//        final EnumMap<ConnectionStatus, String> localizedNames = new EnumMap<>(ConnectionStatus.class);
 | 
				
			||||||
        return connectionData -> {
 | 
					//        Arrays.asList(ConnectionStatus.values()).stream().forEach(state -> localizedNames.put(state, this.i18nSupport
 | 
				
			||||||
            if (connectionData == null) {
 | 
					//                .getText(SEB_CONNECTION_STATUS_KEY_PREFIX + state.name(), state.name())));
 | 
				
			||||||
                localizedNames.get(ConnectionStatus.UNDEFINED);
 | 
					//
 | 
				
			||||||
            }
 | 
					//        return connectionData -> {
 | 
				
			||||||
            if (connectionData.missingPing && connectionData.clientConnection.status.establishedStatus) {
 | 
					//            if (connectionData == null) {
 | 
				
			||||||
                return missing;
 | 
					//                localizedNames.get(ConnectionStatus.UNDEFINED);
 | 
				
			||||||
            } else {
 | 
					//            }
 | 
				
			||||||
                return localizedNames.get(connectionData.clientConnection.status);
 | 
					//            if (connectionData.clientConnection.status.establishedStatus) {
 | 
				
			||||||
            }
 | 
					//                if (connectionData.c !connectionData.clientConnection.securityCheckGranted) {
 | 
				
			||||||
        };
 | 
					//                    return missingGrant;
 | 
				
			||||||
    }
 | 
					//                }
 | 
				
			||||||
 | 
					//                if (connectionData.missingPing) {
 | 
				
			||||||
 | 
					//                    return missing;
 | 
				
			||||||
 | 
					//                }
 | 
				
			||||||
 | 
					//            }
 | 
				
			||||||
 | 
					//            if (connectionData.missingPing && connectionData.clientConnection.status.establishedStatus) {
 | 
				
			||||||
 | 
					//                return missing;
 | 
				
			||||||
 | 
					//            } else {
 | 
				
			||||||
 | 
					//                return localizedNames.get(connectionData.clientConnection.status);
 | 
				
			||||||
 | 
					//            }
 | 
				
			||||||
 | 
					//        };
 | 
				
			||||||
 | 
					//    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Function<MonitoringEntry, String> localizedClientMonitoringStatusNameFunction() {
 | 
					    public Function<MonitoringEntry, String> localizedClientMonitoringStatusNameFunction() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * This Source Code Form is subject to the terms of the Mozilla Public
 | 
				
			||||||
 | 
					 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
				
			||||||
 | 
					 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.springframework.context.annotation.Lazy;
 | 
				
			||||||
 | 
					import org.springframework.http.HttpMethod;
 | 
				
			||||||
 | 
					import org.springframework.http.MediaType;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Component;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.core.type.TypeReference;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.api.API;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.api.EntityType;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Lazy
 | 
				
			||||||
 | 
					@Component
 | 
				
			||||||
 | 
					@GuiProfile
 | 
				
			||||||
 | 
					public class GetClientConnectionSecurityKey extends RestCall<SecurityKey> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public GetClientConnectionSecurityKey() {
 | 
				
			||||||
 | 
					        super(new TypeKey<>(
 | 
				
			||||||
 | 
					                CallType.GET_SINGLE,
 | 
				
			||||||
 | 
					                EntityType.SEB_SECURITY_KEY_REGISTRY,
 | 
				
			||||||
 | 
					                new TypeReference<SecurityKey>() {
 | 
				
			||||||
 | 
					                }),
 | 
				
			||||||
 | 
					                HttpMethod.GET,
 | 
				
			||||||
 | 
					                MediaType.APPLICATION_FORM_URLENCODED,
 | 
				
			||||||
 | 
					                API.EXAM_MONITORING_ENDPOINT +
 | 
				
			||||||
 | 
					                        API.PARENT_MODEL_ID_VAR_PATH_SEGMENT +
 | 
				
			||||||
 | 
					                        API.EXAM_MONITORING_SIGNATURE_KEY_ENDPOINT +
 | 
				
			||||||
 | 
					                        API.MODEL_ID_VAR_PATH_SEGMENT);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * This Source Code Form is subject to the terms of the Mozilla Public
 | 
				
			||||||
 | 
					 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
				
			||||||
 | 
					 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.springframework.context.annotation.Lazy;
 | 
				
			||||||
 | 
					import org.springframework.http.HttpMethod;
 | 
				
			||||||
 | 
					import org.springframework.http.MediaType;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Component;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.core.type.TypeReference;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.api.API;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.api.EntityType;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Lazy
 | 
				
			||||||
 | 
					@Component
 | 
				
			||||||
 | 
					@GuiProfile
 | 
				
			||||||
 | 
					public class GrantClientConnectionSecurityKey extends RestCall<SecurityKey> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public GrantClientConnectionSecurityKey() {
 | 
				
			||||||
 | 
					        super(new TypeKey<>(
 | 
				
			||||||
 | 
					                CallType.GET_SINGLE,
 | 
				
			||||||
 | 
					                EntityType.SEB_SECURITY_KEY_REGISTRY,
 | 
				
			||||||
 | 
					                new TypeReference<SecurityKey>() {
 | 
				
			||||||
 | 
					                }),
 | 
				
			||||||
 | 
					                HttpMethod.POST,
 | 
				
			||||||
 | 
					                MediaType.APPLICATION_FORM_URLENCODED,
 | 
				
			||||||
 | 
					                API.EXAM_MONITORING_ENDPOINT +
 | 
				
			||||||
 | 
					                        API.PARENT_MODEL_ID_VAR_PATH_SEGMENT +
 | 
				
			||||||
 | 
					                        API.EXAM_MONITORING_SIGNATURE_KEY_ENDPOINT +
 | 
				
			||||||
 | 
					                        API.MODEL_ID_VAR_PATH_SEGMENT);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -28,6 +28,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
 | 
					import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
 | 
					import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.monitoring.IndicatorValue;
 | 
					import ch.ethz.seb.sebserver.gbl.monitoring.IndicatorValue;
 | 
				
			||||||
| 
						 | 
					@ -47,7 +48,7 @@ import ch.ethz.seb.sebserver.gui.service.session.IndicatorData.ThresholdColor;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.table.EntityTable;
 | 
					import ch.ethz.seb.sebserver.gui.table.EntityTable;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
 | 
					import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class ClientConnectionDetails {
 | 
					public class ClientConnectionDetails implements MonitoringEntry {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static final Logger log = LoggerFactory.getLogger(ClientConnectionDetails.class);
 | 
					    private static final Logger log = LoggerFactory.getLogger(ClientConnectionDetails.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -72,7 +73,8 @@ public class ClientConnectionDetails {
 | 
				
			||||||
    private final RestCall<ClientConnectionData>.RestCallBuilder restCallBuilder;
 | 
					    private final RestCall<ClientConnectionData>.RestCallBuilder restCallBuilder;
 | 
				
			||||||
    private final FormHandle<?> formHandle;
 | 
					    private final FormHandle<?> formHandle;
 | 
				
			||||||
    private final ColorData colorData;
 | 
					    private final ColorData colorData;
 | 
				
			||||||
    private final Function<ClientConnectionData, String> localizedClientConnectionStatusNameFunction;
 | 
					    private final Function<MonitoringEntry, String> localizedClientConnectionStatusNameFunction;
 | 
				
			||||||
 | 
					    public final boolean checkSecurityGrant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private ClientConnectionData connectionData = null;
 | 
					    private ClientConnectionData connectionData = null;
 | 
				
			||||||
    private boolean statusChanged = true;
 | 
					    private boolean statusChanged = true;
 | 
				
			||||||
| 
						 | 
					@ -94,6 +96,8 @@ public class ClientConnectionDetails {
 | 
				
			||||||
        this.resourceService = pageService.getResourceService();
 | 
					        this.resourceService = pageService.getResourceService();
 | 
				
			||||||
        this.restCallBuilder = restCallBuilder;
 | 
					        this.restCallBuilder = restCallBuilder;
 | 
				
			||||||
        this.colorData = new ColorData(display);
 | 
					        this.colorData = new ColorData(display);
 | 
				
			||||||
 | 
					        this.checkSecurityGrant = BooleanUtils.toBoolean(
 | 
				
			||||||
 | 
					                exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED));
 | 
				
			||||||
        this.indicatorMapping = IndicatorData.createFormIndicators(
 | 
					        this.indicatorMapping = IndicatorData.createFormIndicators(
 | 
				
			||||||
                indicators,
 | 
					                indicators,
 | 
				
			||||||
                display,
 | 
					                display,
 | 
				
			||||||
| 
						 | 
					@ -146,7 +150,26 @@ public class ClientConnectionDetails {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.formHandle = formBuilder.build();
 | 
					        this.formHandle = formBuilder.build();
 | 
				
			||||||
        this.localizedClientConnectionStatusNameFunction =
 | 
					        this.localizedClientConnectionStatusNameFunction =
 | 
				
			||||||
                this.resourceService.localizedClientConnectionStatusNameFunction();
 | 
					                this.resourceService.localizedClientMonitoringStatusNameFunction();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public ConnectionStatus getStatus() {
 | 
				
			||||||
 | 
					        if (this.connectionData == null) {
 | 
				
			||||||
 | 
					            return ConnectionStatus.UNDEFINED;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return this.connectionData.clientConnection.status;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public boolean hasMissingPing() {
 | 
				
			||||||
 | 
					        return (this.connectionData != null) ? this.connectionData.missingPing : false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public boolean hasMissingGrant() {
 | 
				
			||||||
 | 
					        return (this.connectionData != null)
 | 
				
			||||||
 | 
					                ? this.checkSecurityGrant && !this.connectionData.clientConnection.securityCheckGranted : false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public void setStatusChangeListener(final Consumer<ClientConnectionData> statusChangeListener) {
 | 
					    public void setStatusChangeListener(final Consumer<ClientConnectionData> statusChangeListener) {
 | 
				
			||||||
| 
						 | 
					@ -210,8 +233,8 @@ public class ClientConnectionDetails {
 | 
				
			||||||
            // update status
 | 
					            // update status
 | 
				
			||||||
            form.setFieldValue(
 | 
					            form.setFieldValue(
 | 
				
			||||||
                    Domain.CLIENT_CONNECTION.ATTR_STATUS,
 | 
					                    Domain.CLIENT_CONNECTION.ATTR_STATUS,
 | 
				
			||||||
                    this.localizedClientConnectionStatusNameFunction.apply(this.connectionData));
 | 
					                    this.localizedClientConnectionStatusNameFunction.apply(this));
 | 
				
			||||||
            final Color statusColor = this.colorData.getStatusColor(this.connectionData);
 | 
					            final Color statusColor = this.colorData.getStatusColor(this);
 | 
				
			||||||
            final Color statusTextColor = this.colorData.getStatusTextColor(statusColor);
 | 
					            final Color statusTextColor = this.colorData.getStatusTextColor(statusColor);
 | 
				
			||||||
            form.setFieldColor(Domain.CLIENT_CONNECTION.ATTR_STATUS, statusColor);
 | 
					            form.setFieldColor(Domain.CLIENT_CONNECTION.ATTR_STATUS, statusColor);
 | 
				
			||||||
            form.setFieldTextColor(Domain.CLIENT_CONNECTION.ATTR_STATUS, statusTextColor);
 | 
					            form.setFieldTextColor(Domain.CLIENT_CONNECTION.ATTR_STATUS, statusTextColor);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -449,14 +449,6 @@ public final class ClientConnectionTable implements FullPageMonitoringGUIUpdate
 | 
				
			||||||
        // TODO if right click get selected item and show additional information (notification)
 | 
					        // TODO if right click get selected item and show additional information (notification)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public interface MonitoringEntry {
 | 
					 | 
				
			||||||
        ConnectionStatus getStatus();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        boolean hasMissingPing();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        boolean hasMissingGrant();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private final class UpdatableTableItem implements Comparable<UpdatableTableItem>, MonitoringEntry {
 | 
					    private final class UpdatableTableItem implements Comparable<UpdatableTableItem>, MonitoringEntry {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        final Long connectionId;
 | 
					        final Long connectionId;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,6 @@ import ch.ethz.seb.sebserver.gbl.Constants;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
 | 
					import ch.ethz.seb.sebserver.gbl.util.Utils;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable.MonitoringEntry;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class ColorData {
 | 
					public class ColorData {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,19 +35,6 @@ public class ColorData {
 | 
				
			||||||
        this.lightColor = new Color(display, Constants.WHITE_RGB);
 | 
					        this.lightColor = new Color(display, Constants.WHITE_RGB);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Color getStatusColor(final ClientConnectionData connectionData) {
 | 
					 | 
				
			||||||
        if (connectionData == null || connectionData.clientConnection == null) {
 | 
					 | 
				
			||||||
            return this.defaultColor;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        switch (connectionData.clientConnection.status) {
 | 
					 | 
				
			||||||
            case ACTIVE:
 | 
					 | 
				
			||||||
                return (connectionData.missingPing) ? this.color2 : this.color1;
 | 
					 | 
				
			||||||
            default:
 | 
					 | 
				
			||||||
                return this.defaultColor;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Color getStatusColor(final MonitoringEntry entry) {
 | 
					    Color getStatusColor(final MonitoringEntry entry) {
 | 
				
			||||||
        final ConnectionStatus status = entry.getStatus();
 | 
					        final ConnectionStatus status = entry.getStatus();
 | 
				
			||||||
        if (status == null) {
 | 
					        if (status == null) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * This Source Code Form is subject to the terms of the Mozilla Public
 | 
				
			||||||
 | 
					 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
				
			||||||
 | 
					 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ch.ethz.seb.sebserver.gui.service.session;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public interface MonitoringEntry {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    ConnectionStatus getStatus();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    boolean hasMissingPing();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    boolean hasMissingGrant();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -144,7 +144,10 @@ public class WidgetFactory {
 | 
				
			||||||
        RESTRICTION("restriction.png"),
 | 
					        RESTRICTION("restriction.png"),
 | 
				
			||||||
        VISIBILITY("visibility.png"),
 | 
					        VISIBILITY("visibility.png"),
 | 
				
			||||||
        VISIBILITY_OFF("visibility_off.png"),
 | 
					        VISIBILITY_OFF("visibility_off.png"),
 | 
				
			||||||
        NOTIFICATION("notification.png");
 | 
					        NOTIFICATION("notification.png"),
 | 
				
			||||||
 | 
					        VERIFY("verify.png"),
 | 
				
			||||||
 | 
					        SHIELD("shield.png"),
 | 
				
			||||||
 | 
					        NO_SHIELD("no_shield.png");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public String fileName;
 | 
					        public String fileName;
 | 
				
			||||||
        private ImageData image = null;
 | 
					        private ImageData image = null;
 | 
				
			||||||
| 
						 | 
					@ -466,6 +469,7 @@ public class WidgetFactory {
 | 
				
			||||||
            final LocTextKey locToolTextKey) {
 | 
					            final LocTextKey locToolTextKey) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        final Label label = new Label(parent, SWT.NONE);
 | 
					        final Label label = new Label(parent, SWT.NONE);
 | 
				
			||||||
 | 
					        label.setData(RWT.MARKUP_ENABLED, true);
 | 
				
			||||||
        this.polyglotPageService.injectI18n(label, locTextKey, locToolTextKey);
 | 
					        this.polyglotPageService.injectI18n(label, locTextKey, locToolTextKey);
 | 
				
			||||||
        return label;
 | 
					        return label;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -477,6 +481,7 @@ public class WidgetFactory {
 | 
				
			||||||
            final LocTextKey locToolTextKey) {
 | 
					            final LocTextKey locToolTextKey) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        final Label label = new Label(parent, SWT.NONE);
 | 
					        final Label label = new Label(parent, SWT.NONE);
 | 
				
			||||||
 | 
					        label.setData(RWT.MARKUP_ENABLED, true);
 | 
				
			||||||
        this.polyglotPageService.injectI18n(label, locTextKey, locToolTextKey);
 | 
					        this.polyglotPageService.injectI18n(label, locTextKey, locToolTextKey);
 | 
				
			||||||
        label.setData(RWT.CUSTOM_VARIANT, variant.key);
 | 
					        label.setData(RWT.CUSTOM_VARIANT, variant.key);
 | 
				
			||||||
        return label;
 | 
					        return label;
 | 
				
			||||||
| 
						 | 
					@ -484,6 +489,7 @@ public class WidgetFactory {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Label labelLocalizedTitle(final Composite content, final LocTextKey locTextKey) {
 | 
					    public Label labelLocalizedTitle(final Composite content, final LocTextKey locTextKey) {
 | 
				
			||||||
        final Label labelLocalized = labelLocalized(content, CustomVariant.TEXT_H1, locTextKey);
 | 
					        final Label labelLocalized = labelLocalized(content, CustomVariant.TEXT_H1, locTextKey);
 | 
				
			||||||
 | 
					        labelLocalized.setData(RWT.MARKUP_ENABLED, true);
 | 
				
			||||||
        final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false);
 | 
					        final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false);
 | 
				
			||||||
        labelLocalized.setLayoutData(gridData);
 | 
					        labelLocalized.setLayoutData(gridData);
 | 
				
			||||||
        return labelLocalized;
 | 
					        return labelLocalized;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -167,6 +167,12 @@ public interface ClientConnectionDAO extends
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param examId the exam identifier
 | 
					     * @param examId the exam identifier
 | 
				
			||||||
     * @return Result refer to a collection of client connection records or to an error when happened */
 | 
					     * @return Result refer to a collection of client connection records or to an error when happened */
 | 
				
			||||||
    Result<Collection<ClientConnectionRecord>> getAllConnectionIdsForExam(Long examId);
 | 
					    Result<Collection<ClientConnectionRecord>> getAllConnectionRecordsForExam(Long examId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** Get all client connection identifiers for an exam.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param examId the exam identifier
 | 
				
			||||||
 | 
					     * @return Result refer to a collection of client connection identifiers or to an error when happened */
 | 
				
			||||||
 | 
					    Result<Collection<Long>> getAllConnectionIdsForExam(Long examId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,4 +36,11 @@ public interface SecurityKeyRegistryDAO extends EntityDAO<SecurityKey, SecurityK
 | 
				
			||||||
    @EventListener(ExamTemplateDeletionEvent.class)
 | 
					    @EventListener(ExamTemplateDeletionEvent.class)
 | 
				
			||||||
    void notifyExamTemplateDeletion(ExamTemplateDeletionEvent event);
 | 
					    void notifyExamTemplateDeletion(ExamTemplateDeletionEvent event);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** This checks if there is already a grant for the given key and return it if available
 | 
				
			||||||
 | 
					     * or the given key otherwise.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param key SecurityKey data to check if there is a grant registered
 | 
				
			||||||
 | 
					     * @return Result refer to the grant if available or the the given key if not or to an error when happened */
 | 
				
			||||||
 | 
					    Result<SecurityKey> getGrantOr(final SecurityKey key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -764,7 +764,7 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    @Transactional(readOnly = true)
 | 
					    @Transactional(readOnly = true)
 | 
				
			||||||
    public Result<Collection<ClientConnectionRecord>> getAllConnectionIdsForExam(final Long examId) {
 | 
					    public Result<Collection<ClientConnectionRecord>> getAllConnectionRecordsForExam(final Long examId) {
 | 
				
			||||||
        return Result.tryCatch(() -> this.clientConnectionRecordMapper
 | 
					        return Result.tryCatch(() -> this.clientConnectionRecordMapper
 | 
				
			||||||
                .selectByExample()
 | 
					                .selectByExample()
 | 
				
			||||||
                .where(
 | 
					                .where(
 | 
				
			||||||
| 
						 | 
					@ -774,6 +774,18 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
 | 
				
			||||||
                .execute());
 | 
					                .execute());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    @Transactional(readOnly = true)
 | 
				
			||||||
 | 
					    public Result<Collection<Long>> getAllConnectionIdsForExam(final Long examId) {
 | 
				
			||||||
 | 
					        return Result.tryCatch(() -> this.clientConnectionRecordMapper
 | 
				
			||||||
 | 
					                .selectIdsByExample()
 | 
				
			||||||
 | 
					                .where(
 | 
				
			||||||
 | 
					                        ClientConnectionRecordDynamicSqlSupport.examId,
 | 
				
			||||||
 | 
					                        SqlBuilder.isEqualTo(examId))
 | 
				
			||||||
 | 
					                .build()
 | 
				
			||||||
 | 
					                .execute());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private Result<ClientConnectionRecord> recordById(final Long id) {
 | 
					    private Result<ClientConnectionRecord> recordById(final Long id) {
 | 
				
			||||||
        return Result.tryCatch(() -> {
 | 
					        return Result.tryCatch(() -> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,7 @@ import java.util.ArrayList;
 | 
				
			||||||
import java.util.Collection;
 | 
					import java.util.Collection;
 | 
				
			||||||
import java.util.Collections;
 | 
					import java.util.Collections;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Objects;
 | 
				
			||||||
import java.util.Set;
 | 
					import java.util.Set;
 | 
				
			||||||
import java.util.function.Predicate;
 | 
					import java.util.function.Predicate;
 | 
				
			||||||
import java.util.stream.Collectors;
 | 
					import java.util.stream.Collectors;
 | 
				
			||||||
| 
						 | 
					@ -30,6 +31,7 @@ import ch.ethz.seb.sebserver.gbl.model.EntityKey;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey.KeyType;
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey.KeyType;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
 | 
					import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.util.Cryptor;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
					import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
 | 
					import ch.ethz.seb.sebserver.gbl.util.Utils;
 | 
				
			||||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.SecurityKeyRegistryRecordDynamicSqlSupport;
 | 
					import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.SecurityKeyRegistryRecordDynamicSqlSupport;
 | 
				
			||||||
| 
						 | 
					@ -46,9 +48,14 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
 | 
				
			||||||
public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO {
 | 
					public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private final SecurityKeyRegistryRecordMapper securityKeyRegistryRecordMapper;
 | 
					    private final SecurityKeyRegistryRecordMapper securityKeyRegistryRecordMapper;
 | 
				
			||||||
 | 
					    private final Cryptor cryptor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public SecurityKeyRegistryDAOImpl(
 | 
				
			||||||
 | 
					            final SecurityKeyRegistryRecordMapper securityKeyRegistryRecordMapper,
 | 
				
			||||||
 | 
					            final Cryptor cryptor) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public SecurityKeyRegistryDAOImpl(final SecurityKeyRegistryRecordMapper securityKeyRegistryRecordMapper) {
 | 
					 | 
				
			||||||
        this.securityKeyRegistryRecordMapper = securityKeyRegistryRecordMapper;
 | 
					        this.securityKeyRegistryRecordMapper = securityKeyRegistryRecordMapper;
 | 
				
			||||||
 | 
					        this.cryptor = cryptor;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
| 
						 | 
					@ -123,7 +130,7 @@ public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO {
 | 
				
			||||||
    public Result<SecurityKey> createNew(final SecurityKey data) {
 | 
					    public Result<SecurityKey> createNew(final SecurityKey data) {
 | 
				
			||||||
        return Result.tryCatch(() -> {
 | 
					        return Result.tryCatch(() -> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            checkUniqueTag(data);
 | 
					            checkUniqueKey(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            final SecurityKeyRegistryRecord newRecord = new SecurityKeyRegistryRecord(
 | 
					            final SecurityKeyRegistryRecord newRecord = new SecurityKeyRegistryRecord(
 | 
				
			||||||
                    null,
 | 
					                    null,
 | 
				
			||||||
| 
						 | 
					@ -146,8 +153,6 @@ public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO {
 | 
				
			||||||
    public Result<SecurityKey> save(final SecurityKey data) {
 | 
					    public Result<SecurityKey> save(final SecurityKey data) {
 | 
				
			||||||
        return Result.tryCatch(() -> {
 | 
					        return Result.tryCatch(() -> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            checkUniqueTag(data);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            final SecurityKeyRegistryRecord newRecord = new SecurityKeyRegistryRecord(
 | 
					            final SecurityKeyRegistryRecord newRecord = new SecurityKeyRegistryRecord(
 | 
				
			||||||
                    null,
 | 
					                    null,
 | 
				
			||||||
                    data.institutionId,
 | 
					                    data.institutionId,
 | 
				
			||||||
| 
						 | 
					@ -312,6 +317,32 @@ public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO {
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    @Transactional(readOnly = true)
 | 
				
			||||||
 | 
					    public Result<SecurityKey> getGrantOr(final SecurityKey key) {
 | 
				
			||||||
 | 
					        return Result.tryCatch(() -> {
 | 
				
			||||||
 | 
					            final CharSequence signature = this.cryptor.decrypt(key.key).get();
 | 
				
			||||||
 | 
					            return this.securityKeyRegistryRecordMapper
 | 
				
			||||||
 | 
					                    .selectByExample()
 | 
				
			||||||
 | 
					                    .where(
 | 
				
			||||||
 | 
					                            SecurityKeyRegistryRecordDynamicSqlSupport.institutionId,
 | 
				
			||||||
 | 
					                            SqlBuilder.isEqualTo(key.institutionId))
 | 
				
			||||||
 | 
					                    .and(
 | 
				
			||||||
 | 
					                            SecurityKeyRegistryRecordDynamicSqlSupport.examId,
 | 
				
			||||||
 | 
					                            isEqualToWhenPresent(key.examId))
 | 
				
			||||||
 | 
					                    .and(
 | 
				
			||||||
 | 
					                            SecurityKeyRegistryRecordDynamicSqlSupport.keyType,
 | 
				
			||||||
 | 
					                            isEqualToWhenPresent((key.keyType == null) ? null : key.keyType.name()))
 | 
				
			||||||
 | 
					                    .build()
 | 
				
			||||||
 | 
					                    .execute()
 | 
				
			||||||
 | 
					                    .stream()
 | 
				
			||||||
 | 
					                    .filter(other -> Objects.equals(signature, this.cryptor.decrypt(other.getKeyValue()).getOr(null)))
 | 
				
			||||||
 | 
					                    .findFirst()
 | 
				
			||||||
 | 
					                    .map(this::toDomainModel)
 | 
				
			||||||
 | 
					                    .orElse(key);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public void notifyExamDeletion(final ExamDeletionEvent event) {
 | 
					    public void notifyExamDeletion(final ExamDeletionEvent event) {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
| 
						 | 
					@ -367,18 +398,11 @@ public class SecurityKeyRegistryDAOImpl implements SecurityKeyRegistryDAO {
 | 
				
			||||||
                rec.getExamTemplateId());
 | 
					                rec.getExamTemplateId());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private void checkUniqueTag(final SecurityKey securityKeyRegistry) {
 | 
					    private void checkUniqueKey(final SecurityKey key) {
 | 
				
			||||||
        final Long count = this.securityKeyRegistryRecordMapper
 | 
					        if (getGrantOr(key).getOr(key) != key) {
 | 
				
			||||||
                .countByExample()
 | 
					 | 
				
			||||||
                .where(SecurityKeyRegistryRecordDynamicSqlSupport.tag, isEqualTo(securityKeyRegistry.tag))
 | 
					 | 
				
			||||||
                .and(SecurityKeyRegistryRecordDynamicSqlSupport.id, isNotEqualToWhenPresent(securityKeyRegistry.id))
 | 
					 | 
				
			||||||
                .build()
 | 
					 | 
				
			||||||
                .execute();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (count != null && count > 0) {
 | 
					 | 
				
			||||||
            throw new FieldValidationException(
 | 
					            throw new FieldValidationException(
 | 
				
			||||||
                    Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG,
 | 
					                    Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG,
 | 
				
			||||||
                    "institution:name:tag.notunique");
 | 
					                    "securityKey:keyValue:alreadyGranted");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.institution;
 | 
				
			||||||
import java.util.Collection;
 | 
					import java.util.Collection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
 | 
					import ch.ethz.seb.sebserver.gbl.model.EntityKey;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.AppSignatureKeyInfo;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityCheckResult;
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.SecurityCheckResult;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
 | 
				
			||||||
| 
						 | 
					@ -21,7 +22,9 @@ public interface SecurityKeyService {
 | 
				
			||||||
    /** This attribute name is used to store the App-Signature-Key given by a SEB Client */
 | 
					    /** This attribute name is used to store the App-Signature-Key given by a SEB Client */
 | 
				
			||||||
    public static final String ADDITIONAL_ATTR_APP_SIGNATURE_KEY = "APP_SIGNATURE_KEY";
 | 
					    public static final String ADDITIONAL_ATTR_APP_SIGNATURE_KEY = "APP_SIGNATURE_KEY";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Result<Collection<SecurityKey>> getPlainGrants(Long institutionId, Long examId);
 | 
					    Result<SecurityKey> getSecurityKeyOfConnection(Long institutionId, Long connectionId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Result<AppSignatureKeyInfo> getAppSignaturesInfo(Long institutionId, Long examId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Result<Collection<SecurityKey>> getPlainAppSignatureKeyGrants(Long institutionId, Long examId);
 | 
					    Result<Collection<SecurityKey>> getPlainAppSignatureKeyGrants(Long institutionId, Long examId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,8 +10,12 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.institution.impl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.security.cert.Certificate;
 | 
					import java.security.cert.Certificate;
 | 
				
			||||||
import java.util.Collection;
 | 
					import java.util.Collection;
 | 
				
			||||||
 | 
					import java.util.HashMap;
 | 
				
			||||||
 | 
					import java.util.HashSet;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
import java.util.Objects;
 | 
					import java.util.Objects;
 | 
				
			||||||
 | 
					import java.util.Set;
 | 
				
			||||||
import java.util.stream.Collectors;
 | 
					import java.util.stream.Collectors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import org.apache.commons.lang3.BooleanUtils;
 | 
					import org.apache.commons.lang3.BooleanUtils;
 | 
				
			||||||
| 
						 | 
					@ -24,6 +28,7 @@ import org.springframework.stereotype.Service;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
 | 
					import ch.ethz.seb.sebserver.gbl.api.EntityType;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
 | 
					import ch.ethz.seb.sebserver.gbl.model.EntityKey;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
 | 
					import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.AppSignatureKeyInfo;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityCheckResult;
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.SecurityCheckResult;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey.KeyType;
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey.KeyType;
 | 
				
			||||||
| 
						 | 
					@ -69,10 +74,35 @@ public class SecurityKeyServiceImpl implements SecurityKeyService {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public Result<Collection<SecurityKey>> getPlainGrants(final Long institutionId, final Long examId) {
 | 
					    public Result<SecurityKey> getSecurityKeyOfConnection(final Long institutionId, final Long connectionId) {
 | 
				
			||||||
        return this.securityKeyRegistryDAO
 | 
					        return this.clientConnectionDAO.byPK(connectionId)
 | 
				
			||||||
                .getAll(institutionId, examId, null)
 | 
					                .map(connection -> new SecurityKey(
 | 
				
			||||||
                .map(this::decryptAll);
 | 
					                        null,
 | 
				
			||||||
 | 
					                        institutionId,
 | 
				
			||||||
 | 
					                        KeyType.APP_SIGNATURE_KEY,
 | 
				
			||||||
 | 
					                        decryptStoredSignatureForConnection(connection),
 | 
				
			||||||
 | 
					                        connection.sebVersion,
 | 
				
			||||||
 | 
					                        null, null))
 | 
				
			||||||
 | 
					                .flatMap(this.securityKeyRegistryDAO::getGrantOr);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public Result<AppSignatureKeyInfo> getAppSignaturesInfo(final Long institutionId, final Long examId) {
 | 
				
			||||||
 | 
					        return Result.tryCatch(() -> {
 | 
				
			||||||
 | 
					            final Map<String, Set<Long>> keyMapping = new HashMap<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.clientConnectionDAO
 | 
				
			||||||
 | 
					                    .getAllConnectionRecordsForExam(examId)
 | 
				
			||||||
 | 
					                    .getOrThrow()
 | 
				
			||||||
 | 
					                    .stream()
 | 
				
			||||||
 | 
					                    .forEach(rec -> keyMapping.computeIfAbsent(
 | 
				
			||||||
 | 
					                            this.decryptStoredSignatureForConnection(
 | 
				
			||||||
 | 
					                                    rec.getId(),
 | 
				
			||||||
 | 
					                                    rec.getConnectionToken()),
 | 
				
			||||||
 | 
					                            s -> new HashSet<>()).add(rec.getId()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return new AppSignatureKeyInfo(institutionId, examId, keyMapping);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
| 
						 | 
					@ -386,6 +416,15 @@ public class SecurityKeyServiceImpl implements SecurityKeyService {
 | 
				
			||||||
        return decryptSignatureWithConnectionToken(cc.connectionToken, signatureKey);
 | 
					        return decryptSignatureWithConnectionToken(cc.connectionToken, signatureKey);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String decryptStoredSignatureForConnection(final Long cId, final String cToken) {
 | 
				
			||||||
 | 
					        final String signatureKey = getSignatureKeyForConnection(cId);
 | 
				
			||||||
 | 
					        if (StringUtils.isBlank(signatureKey)) {
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return decryptSignatureWithConnectionToken(cToken, signatureKey);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private void saveSignatureKeyForConnection(final ClientConnection clientConnection, final String appSignatureKey) {
 | 
					    private void saveSignatureKeyForConnection(final ClientConnection clientConnection, final String appSignatureKey) {
 | 
				
			||||||
        this.additionalAttributesDAO
 | 
					        this.additionalAttributesDAO
 | 
				
			||||||
                .saveAdditionalAttribute(
 | 
					                .saveAdditionalAttribute(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,7 +25,7 @@ public class MockSEBRestrictionAPI implements SEBRestrictionAPI {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public LmsSetupTestResult testCourseRestrictionAPI() {
 | 
					    public LmsSetupTestResult testCourseRestrictionAPI() {
 | 
				
			||||||
        return LmsSetupTestResult.ofQuizRestrictionAPIError(LmsType.MOCKUP, "unsupported");
 | 
					        return LmsSetupTestResult.ofOkay(LmsType.MOCKUP);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -50,6 +50,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
 | 
					import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
 | 
					import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
 | 
					import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.AppSignatureKeyInfo;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features;
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
 | 
				
			||||||
| 
						 | 
					@ -190,7 +191,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // ****************************************************************************
 | 
					    // ****************************************************************************
 | 
				
			||||||
    // **** SEB Security Key
 | 
					    // **** SEB Security Key and App Signature Key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @RequestMapping(
 | 
					    @RequestMapping(
 | 
				
			||||||
            path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT
 | 
					            path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT
 | 
				
			||||||
| 
						 | 
					@ -258,6 +259,24 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
 | 
				
			||||||
                .getOrThrow();
 | 
					                .getOrThrow();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @RequestMapping(
 | 
				
			||||||
 | 
					            path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT
 | 
				
			||||||
 | 
					                    + API.EXAM_ADMINISTRATION_SEB_SECURITY_AS_KEYS_PATH_SEGMENT,
 | 
				
			||||||
 | 
					            method = RequestMethod.GET,
 | 
				
			||||||
 | 
					            produces = MediaType.APPLICATION_JSON_VALUE)
 | 
				
			||||||
 | 
					    public AppSignatureKeyInfo getAppSignatureKeyInfo(
 | 
				
			||||||
 | 
					            @PathVariable(name = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT, required = true) final Long examId,
 | 
				
			||||||
 | 
					            @RequestParam(
 | 
				
			||||||
 | 
					                    name = API.PARAM_INSTITUTION_ID,
 | 
				
			||||||
 | 
					                    required = true,
 | 
				
			||||||
 | 
					                    defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return this.examDAO.byPK(examId)
 | 
				
			||||||
 | 
					                .flatMap(this::checkReadAccess)
 | 
				
			||||||
 | 
					                .flatMap(exam -> this.securityKeyService.getAppSignaturesInfo(institutionId, examId))
 | 
				
			||||||
 | 
					                .getOrThrow();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // **** SEB Security Key
 | 
					    // **** SEB Security Key
 | 
				
			||||||
    // ****************************************************************************
 | 
					    // ****************************************************************************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,6 +46,7 @@ import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.Domain;
 | 
					import ch.ethz.seb.sebserver.gbl.model.Domain;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.Page;
 | 
					import ch.ethz.seb.sebserver.gbl.model.Page;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
 | 
					import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
 | 
					import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
 | 
				
			||||||
| 
						 | 
					@ -463,29 +464,50 @@ public class ExamMonitoringController {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @RequestMapping(
 | 
					    @RequestMapping(
 | 
				
			||||||
            path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT +
 | 
					            path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT +
 | 
				
			||||||
                    API.EXAM_MONITORING_GRANT_APP_SIGNATURE_KEY_ENDPOINT +
 | 
					                    API.EXAM_MONITORING_SIGNATURE_KEY_ENDPOINT +
 | 
				
			||||||
                    API.MODEL_ID_VAR_PATH_SEGMENT,
 | 
					                    API.MODEL_ID_VAR_PATH_SEGMENT,
 | 
				
			||||||
            method = RequestMethod.POST,
 | 
					            method = RequestMethod.POST,
 | 
				
			||||||
            consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
 | 
					            consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
 | 
				
			||||||
    public void grantAppSignatureKey(
 | 
					            produces = MediaType.APPLICATION_JSON_VALUE)
 | 
				
			||||||
 | 
					    public SecurityKey grantAppSignatureKey(
 | 
				
			||||||
            @RequestParam(
 | 
					            @RequestParam(
 | 
				
			||||||
                    name = API.PARAM_INSTITUTION_ID,
 | 
					                    name = API.PARAM_INSTITUTION_ID,
 | 
				
			||||||
                    required = true,
 | 
					                    required = true,
 | 
				
			||||||
                    defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
 | 
					                    defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
 | 
				
			||||||
            @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId,
 | 
					            @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId,
 | 
				
			||||||
            @PathVariable(name = API.MODEL_ID_VAR_PATH_SEGMENT, required = true) final Long connectionId,
 | 
					            @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long connectionId,
 | 
				
			||||||
            @RequestParam(
 | 
					            @RequestParam(name = Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG, required = true) final String tagName) {
 | 
				
			||||||
                    name = Domain.CLIENT_CONNECTION.ATTR_CONNECTION_TOKEN,
 | 
					 | 
				
			||||||
                    required = false) final String connectionToken) {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        checkPrivileges(institutionId, examId);
 | 
					        checkPrivileges(institutionId, examId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.securityKeyService
 | 
					        return this.securityKeyService
 | 
				
			||||||
                .registerExamAppSignatureKey(institutionId, examId, connectionId, null)
 | 
					                .registerExamAppSignatureKey(institutionId, examId, connectionId, tagName)
 | 
				
			||||||
                .onSuccess(key -> this.securityKeyService.updateAppSignatureKeyGrants(examId))
 | 
					                .onSuccess(key -> this.securityKeyService.updateAppSignatureKeyGrants(examId))
 | 
				
			||||||
                .getOrThrow();
 | 
					                .getOrThrow();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @RequestMapping(
 | 
				
			||||||
 | 
					            path = API.PARENT_MODEL_ID_VAR_PATH_SEGMENT +
 | 
				
			||||||
 | 
					                    API.EXAM_MONITORING_SIGNATURE_KEY_ENDPOINT +
 | 
				
			||||||
 | 
					                    API.MODEL_ID_VAR_PATH_SEGMENT,
 | 
				
			||||||
 | 
					            method = RequestMethod.GET,
 | 
				
			||||||
 | 
					            consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
 | 
				
			||||||
 | 
					            produces = MediaType.APPLICATION_JSON_VALUE)
 | 
				
			||||||
 | 
					    public SecurityKey getAppSignatureKey(
 | 
				
			||||||
 | 
					            @RequestParam(
 | 
				
			||||||
 | 
					                    name = API.PARAM_INSTITUTION_ID,
 | 
				
			||||||
 | 
					                    required = true,
 | 
				
			||||||
 | 
					                    defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
 | 
				
			||||||
 | 
					            @PathVariable(name = API.PARAM_PARENT_MODEL_ID, required = true) final Long examId,
 | 
				
			||||||
 | 
					            @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long connectionId) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        checkPrivileges(institutionId, examId);
 | 
				
			||||||
 | 
					        return this.securityKeyService
 | 
				
			||||||
 | 
					                .getSecurityKeyOfConnection(institutionId, connectionId)
 | 
				
			||||||
 | 
					                .getOrThrow();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private Exam checkPrivileges(final Long institutionId, final Long examId) {
 | 
					    private Exam checkPrivileges(final Long institutionId, final Long examId) {
 | 
				
			||||||
        // check overall privilege
 | 
					        // check overall privilege
 | 
				
			||||||
        this.authorization.checkRole(
 | 
					        this.authorization.checkRole(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -794,6 +794,8 @@ sebserver.exam.proctoring.one.close.error=Failed to close the one-to-one room pr
 | 
				
			||||||
sebserver.exam.proctoring.collecting.open.error=Failed to open the collecting room.
 | 
					sebserver.exam.proctoring.collecting.open.error=Failed to open the collecting room.
 | 
				
			||||||
sebserver.exam.proctoring.collecting.close.error=Failed to close the collecting room properly.
 | 
					sebserver.exam.proctoring.collecting.close.error=Failed to close the collecting room properly.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sebserver.exam.signaturekey.action.edit=App Signature Key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
################################
 | 
					################################
 | 
				
			||||||
# Connection Configuration
 | 
					# Connection Configuration
 | 
				
			||||||
| 
						 | 
					@ -1984,6 +1986,7 @@ sebserver.monitoring.exam.connection.actions.group3= 
 | 
				
			||||||
sebserver.monitoring.exam.connection.notificationlist.actions=
 | 
					sebserver.monitoring.exam.connection.notificationlist.actions=
 | 
				
			||||||
sebserver.monitoring.exam.connection.action.confirm.notification=Confirm Notification
 | 
					sebserver.monitoring.exam.connection.action.confirm.notification=Confirm Notification
 | 
				
			||||||
sebserver.monitoring.exam.connection.action.confirm.notification.text=Are you sure you want to confirm this pending notification?<br/><br/>Note that this will send a notification confirmation instruction to the SEB client and remove this notification from the pending list.
 | 
					sebserver.monitoring.exam.connection.action.confirm.notification.text=Are you sure you want to confirm this pending notification?<br/><br/>Note that this will send a notification confirmation instruction to the SEB client and remove this notification from the pending list.
 | 
				
			||||||
 | 
					sebserver.monitoring.exam.connection.action.grant.signaturekey=Grant App Signature Key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sebserver.monitoring.exam.connection.notificationlist.pleaseSelect=At first please select a notification form the Pending Notification list
 | 
					sebserver.monitoring.exam.connection.notificationlist.pleaseSelect=At first please select a notification form the Pending Notification list
 | 
				
			||||||
sebserver.monitoring.exam.connection.notificationlist.title=Pending Notification
 | 
					sebserver.monitoring.exam.connection.notificationlist.title=Pending Notification
 | 
				
			||||||
| 
						 | 
					@ -2037,6 +2040,11 @@ sebserver.monitoring.lock.list.name=SEB User Session Identifier
 | 
				
			||||||
sebserver.monitoring.lock.list.info=SEB Connection Info
 | 
					sebserver.monitoring.lock.list.info=SEB Connection Info
 | 
				
			||||||
sebserver.monitoring.lock.noselection=Please select at least one active SEB client connection.
 | 
					sebserver.monitoring.lock.noselection=Please select at least one active SEB client connection.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sebserver.monitoring.signaturegrant.title=Grant App Signature Key
 | 
				
			||||||
 | 
					sebserver.monitoring.signaturegrant.info=Mark this App Signature Key as granted. Please also choose a meaningful tag.
 | 
				
			||||||
 | 
					sebserver.monitoring.signaturegrant.signature=App Signature Key
 | 
				
			||||||
 | 
					sebserver.monitoring.signaturegrant.tag=Tag
 | 
				
			||||||
 | 
					
 | 
				
			||||||
################################
 | 
					################################
 | 
				
			||||||
# Finished Exams
 | 
					# Finished Exams
 | 
				
			||||||
################################
 | 
					################################
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								src/main/resources/static/images/no_shield.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/main/resources/static/images/no_shield.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 317 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/main/resources/static/images/shield.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/main/resources/static/images/shield.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 285 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/main/resources/static/images/verify.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/main/resources/static/images/verify.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 333 B  | 
		Loading…
	
	Add table
		
		Reference in a new issue