SEBSERV-335, SEBSERV-363 GUI improvements

This commit is contained in:
anhefti 2023-01-19 10:18:24 +01:00
parent bf32a713ea
commit edce7275ca
27 changed files with 1038 additions and 394 deletions

View file

@ -158,6 +158,7 @@ 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_PROCTORING_RESET_PATH_SEGMENT = "reset";
public static final String EXAM_ADMINISTRATION_SEB_SECURITY_KEY_GRANTS_PATH_SEGMENT = "/grant"; public static final String EXAM_ADMINISTRATION_SEB_SECURITY_KEY_GRANTS_PATH_SEGMENT = "/grant";
public static final String EXAM_ADMINISTRATION_SEB_SECURITY_KEY_INFO_PATH_SEGMENT = "/sebkeyinfo"; public static final String EXAM_ADMINISTRATION_SEB_SECURITY_KEY_INFO_PATH_SEGMENT = "/sebkeyinfo";

View file

@ -42,10 +42,18 @@ public class ProctoringServiceSettings implements Entity {
public static final String ATTR_ENABLE_PROCTORING = "enableProctoring"; public static final String ATTR_ENABLE_PROCTORING = "enableProctoring";
public static final String ATTR_SERVER_TYPE = "serverType"; public static final String ATTR_SERVER_TYPE = "serverType";
public static final String ATTR_SERVER_URL = "serverURL"; public static final String ATTR_SERVER_URL = "serverURL";
// Jitsi access (former also Zoom)
public static final String ATTR_APP_KEY = "appKey"; public static final String ATTR_APP_KEY = "appKey";
public static final String ATTR_APP_SECRET = "appSecret"; public static final String ATTR_APP_SECRET = "appSecret";
// Zoom Access
public static final String ATTR_ACCOUNT_ID = "accountId";
public static final String ATTR_ACCOUNT_CLIENT_ID = "clientId";
public static final String ATTR_ACCOUNT_CLIENT_SECRET = "clientSecret";
public static final String ATTR_SDK_KEY = "sdkKey"; public static final String ATTR_SDK_KEY = "sdkKey";
public static final String ATTR_SDK_SECRET = "sdkSecret"; public static final String ATTR_SDK_SECRET = "sdkSecret";
public static final String ATTR_COLLECTING_ROOM_SIZE = "collectingRoomSize"; public static final String ATTR_COLLECTING_ROOM_SIZE = "collectingRoomSize";
public static final String ATTR_ENABLED_FEATURES = "enabledFeatures"; public static final String ATTR_ENABLED_FEATURES = "enabledFeatures";
public static final String ATTR_COLLECT_ALL_ROOM_NAME = "collectAllRoomName"; public static final String ATTR_COLLECT_ALL_ROOM_NAME = "collectAllRoomName";
@ -71,6 +79,15 @@ public class ProctoringServiceSettings implements Entity {
@JsonProperty(ATTR_APP_SECRET) @JsonProperty(ATTR_APP_SECRET)
public final CharSequence appSecret; public final CharSequence appSecret;
@JsonProperty(ATTR_ACCOUNT_ID)
public final String accountId;
@JsonProperty(ATTR_ACCOUNT_CLIENT_ID)
public final String clientId;
@JsonProperty(ATTR_ACCOUNT_CLIENT_SECRET)
public final CharSequence clientSecret;
@JsonProperty(ATTR_SDK_KEY) @JsonProperty(ATTR_SDK_KEY)
public final String sdkKey; public final String sdkKey;
@ -100,6 +117,9 @@ public class ProctoringServiceSettings implements Entity {
@JsonProperty(ATTR_SERVICE_IN_USE) final Boolean serviceInUse, @JsonProperty(ATTR_SERVICE_IN_USE) final Boolean serviceInUse,
@JsonProperty(ATTR_APP_KEY) final String appKey, @JsonProperty(ATTR_APP_KEY) final String appKey,
@JsonProperty(ATTR_APP_SECRET) final CharSequence appSecret, @JsonProperty(ATTR_APP_SECRET) final CharSequence appSecret,
@JsonProperty(ATTR_ACCOUNT_ID) final String accountId,
@JsonProperty(ATTR_ACCOUNT_CLIENT_ID) final String clientId,
@JsonProperty(ATTR_ACCOUNT_CLIENT_SECRET) final CharSequence clientSecret,
@JsonProperty(ATTR_SDK_KEY) final String sdkKey, @JsonProperty(ATTR_SDK_KEY) final String sdkKey,
@JsonProperty(ATTR_SDK_SECRET) final CharSequence sdkSecret, @JsonProperty(ATTR_SDK_SECRET) final CharSequence sdkSecret,
@JsonProperty(ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM) final Boolean useZoomAppClientForCollectingRoom) { @JsonProperty(ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM) final Boolean useZoomAppClientForCollectingRoom) {
@ -113,11 +133,50 @@ public class ProctoringServiceSettings implements Entity {
this.serviceInUse = serviceInUse; this.serviceInUse = serviceInUse;
this.appKey = StringUtils.trim(appKey); this.appKey = StringUtils.trim(appKey);
this.appSecret = appSecret; this.appSecret = appSecret;
this.accountId = StringUtils.trim(accountId);
this.clientId = clientId;
this.clientSecret = clientSecret;
this.sdkKey = StringUtils.trim(sdkKey); this.sdkKey = StringUtils.trim(sdkKey);
this.sdkSecret = sdkSecret; this.sdkSecret = sdkSecret;
this.useZoomAppClientForCollectingRoom = BooleanUtils.toBoolean(useZoomAppClientForCollectingRoom); this.useZoomAppClientForCollectingRoom = BooleanUtils.toBoolean(useZoomAppClientForCollectingRoom);
} }
public ProctoringServiceSettings(final Long examId) {
this.examId = examId;
this.enableProctoring = false;
this.serverType = null;
this.serverURL = null;
this.collectingRoomSize = 20;
this.enabledFeatures = EnumSet.allOf(ProctoringFeature.class);
this.serviceInUse = false;
this.appKey = null;
this.appSecret = null;
this.accountId = null;
this.clientId = null;
this.clientSecret = null;
this.sdkKey = null;
this.sdkSecret = null;
this.useZoomAppClientForCollectingRoom = false;
}
public ProctoringServiceSettings(final Long examId, final ProctoringServiceSettings copyOf) {
this.examId = examId;
this.enableProctoring = copyOf.enableProctoring;
this.serverType = copyOf.serverType;
this.serverURL = copyOf.serverURL;
this.collectingRoomSize = copyOf.collectingRoomSize;
this.enabledFeatures = copyOf.enabledFeatures;
this.serviceInUse = false;
this.appKey = copyOf.appKey;
this.appSecret = copyOf.appSecret;
this.accountId = copyOf.accountId;
this.clientId = copyOf.clientId;
this.clientSecret = copyOf.clientSecret;
this.sdkKey = copyOf.sdkKey;
this.sdkSecret = copyOf.sdkSecret;
this.useZoomAppClientForCollectingRoom = copyOf.useZoomAppClientForCollectingRoom;
}
@Override @Override
public String getModelId() { public String getModelId() {
return (this.examId != null) ? String.valueOf(this.examId) : null; return (this.examId != null) ? String.valueOf(this.examId) : null;
@ -165,6 +224,18 @@ public class ProctoringServiceSettings implements Entity {
return this.appSecret; return this.appSecret;
} }
public String getAccountId() {
return this.accountId;
}
public String getClientId() {
return this.clientId;
}
public CharSequence getClientSecret() {
return this.clientSecret;
}
public String getSdkKey() { public String getSdkKey() {
return this.sdkKey; return this.sdkKey;
} }
@ -222,18 +293,20 @@ public class ProctoringServiceSettings implements Entity {
builder.append(this.serverURL); builder.append(this.serverURL);
builder.append(", appKey="); builder.append(", appKey=");
builder.append(this.appKey); builder.append(this.appKey);
builder.append(", appSecret="); builder.append(", accountId=");
builder.append("--"); builder.append(this.accountId);
builder.append(", clientId=");
builder.append(this.clientId);
builder.append(", sdkKey="); builder.append(", sdkKey=");
builder.append(this.sdkKey); builder.append(this.sdkKey);
builder.append(", sdkSecret=");
builder.append("--");
builder.append(", collectingRoomSize="); builder.append(", collectingRoomSize=");
builder.append(this.collectingRoomSize); builder.append(this.collectingRoomSize);
builder.append(", enabledFeatures="); builder.append(", enabledFeatures=");
builder.append(this.enabledFeatures); builder.append(this.enabledFeatures);
builder.append(", serviceInUse="); builder.append(", serviceInUse=");
builder.append(this.serviceInUse); builder.append(this.serviceInUse);
builder.append(", useZoomAppClientForCollectingRoom=");
builder.append(this.useZoomAppClientForCollectingRoom);
builder.append("]"); builder.append("]");
return builder.toString(); return builder.toString();
} }

View file

@ -13,7 +13,7 @@ import java.util.Objects;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import org.apache.tomcat.util.buf.StringUtils; import org.apache.commons.lang3.StringUtils;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
@ -68,7 +68,7 @@ public class AppSignatureKeyInfo implements ModelIdAware {
@Override @Override
public String getModelId() { public String getModelId() {
return this.key; return StringUtils.isNoneBlank(this.key) ? this.key : "-1";
} }
public String getKey() { public String getKey() {

View file

@ -18,6 +18,7 @@ public enum ActionCategory {
LMS_SETUP_LIST(new LocTextKey("sebserver.lmssetup.list.actions"), 1), LMS_SETUP_LIST(new LocTextKey("sebserver.lmssetup.list.actions"), 1),
QUIZ_LIST(new LocTextKey("sebserver.quizdiscovery.list.actions"), 1), QUIZ_LIST(new LocTextKey("sebserver.quizdiscovery.list.actions"), 1),
EXAM_LIST(new LocTextKey("sebserver.exam.list.actions"), 1), EXAM_LIST(new LocTextKey("sebserver.exam.list.actions"), 1),
EXAM_SECURITY(new LocTextKey("sebserver.exam.security.actions"), 1),
EXAM_TEMPLATE_LIST(new LocTextKey("sebserver.examtemplate.list.actions"), 1), EXAM_TEMPLATE_LIST(new LocTextKey("sebserver.examtemplate.list.actions"), 1),
INDICATOR_TEMPLATE_LIST(new LocTextKey("sebserver.examtemplate.indicator.list.actions"), 1), INDICATOR_TEMPLATE_LIST(new LocTextKey("sebserver.examtemplate.indicator.list.actions"), 1),
CLIENT_GROUP_TEMPLATE_LIST(new LocTextKey("sebserver.examtemplate.clientgroup.list.actions"), 2), CLIENT_GROUP_TEMPLATE_LIST(new LocTextKey("sebserver.examtemplate.clientgroup.list.actions"), 2),

View file

@ -301,27 +301,27 @@ public enum ActionDefinition {
new LocTextKey("sebserver.exam.action.sebrestriction.details"), new LocTextKey("sebserver.exam.action.sebrestriction.details"),
ImageIcon.RESTRICTION, ImageIcon.RESTRICTION,
PageStateDefinitionImpl.EXAM_VIEW, PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM), ActionCategory.EXAM_SECURITY),
EXAM_ENABLE_SEB_RESTRICTION( EXAM_ENABLE_SEB_RESTRICTION(
new LocTextKey("sebserver.exam.action.sebrestriction.enable"), new LocTextKey("sebserver.exam.action.sebrestriction.enable"),
ImageIcon.UNLOCK, ImageIcon.UNLOCK,
PageStateDefinitionImpl.EXAM_VIEW, PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM), ActionCategory.EXAM_SECURITY),
EXAM_DISABLE_SEB_RESTRICTION( EXAM_DISABLE_SEB_RESTRICTION(
new LocTextKey("sebserver.exam.action.sebrestriction.disable"), new LocTextKey("sebserver.exam.action.sebrestriction.disable"),
ImageIcon.LOCK, ImageIcon.LOCK,
PageStateDefinitionImpl.EXAM_VIEW, PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM), ActionCategory.EXAM_SECURITY),
EXAM_PROCTORING_ON( EXAM_PROCTORING_ON(
new LocTextKey("sebserver.exam.proctoring.actions.open"), new LocTextKey("sebserver.exam.proctoring.actions.open"),
ImageIcon.VISIBILITY, ImageIcon.VISIBILITY,
PageStateDefinitionImpl.EXAM_VIEW, PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM), ActionCategory.EXAM_SECURITY),
EXAM_PROCTORING_OFF( EXAM_PROCTORING_OFF(
new LocTextKey("sebserver.exam.proctoring.actions.open"), new LocTextKey("sebserver.exam.proctoring.actions.open"),
ImageIcon.VISIBILITY_OFF, ImageIcon.VISIBILITY_OFF,
PageStateDefinitionImpl.EXAM_VIEW, PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM), ActionCategory.EXAM_SECURITY),
EXAM_CONFIGURATION_NEW( EXAM_CONFIGURATION_NEW(
new LocTextKey("sebserver.exam.configuration.action.list.new"), new LocTextKey("sebserver.exam.configuration.action.list.new"),
@ -419,12 +419,12 @@ public enum ActionDefinition {
new LocTextKey("sebserver.exam.signaturekey.action.edit"), new LocTextKey("sebserver.exam.signaturekey.action.edit"),
ImageIcon.SHIELD, ImageIcon.SHIELD,
PageStateDefinitionImpl.SECURITY_KEY_EDIT, PageStateDefinitionImpl.SECURITY_KEY_EDIT,
ActionCategory.FORM), ActionCategory.EXAM_SECURITY),
EXAM_SECURITY_KEY_DISABLED( EXAM_SECURITY_KEY_DISABLED(
new LocTextKey("sebserver.exam.signaturekey.action.edit"), new LocTextKey("sebserver.exam.signaturekey.action.edit"),
ImageIcon.NO_SHIELD, ImageIcon.NO_SHIELD,
PageStateDefinitionImpl.SECURITY_KEY_EDIT, PageStateDefinitionImpl.SECURITY_KEY_EDIT,
ActionCategory.FORM), ActionCategory.EXAM_SECURITY),
EXAM_RELOAD_SECURITY_KEY_VIEW( EXAM_RELOAD_SECURITY_KEY_VIEW(
new LocTextKey("sebserver.exam.signaturekey.action.edit"), new LocTextKey("sebserver.exam.signaturekey.action.edit"),
ImageIcon.SHIELD, ImageIcon.SHIELD,
@ -441,11 +441,21 @@ public enum ActionDefinition {
ImageIcon.CANCEL, ImageIcon.CANCEL,
PageStateDefinitionImpl.EXAM_VIEW, PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM), ActionCategory.FORM),
EXAM_SECURITY_KEY_BACK_MODIFY(
new LocTextKey("sebserver.exam.signaturekey.action.back"),
ImageIcon.BACK,
PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM),
EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP( EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP(
new LocTextKey("sebserver.exam.signaturekey.action.addGrant"), new LocTextKey("sebserver.exam.signaturekey.action.addGrant"),
ImageIcon.ADD, ImageIcon.ADD,
PageStateDefinitionImpl.SECURITY_KEY_EDIT, PageStateDefinitionImpl.SECURITY_KEY_EDIT,
ActionCategory.APP_SIGNATURE_KEY_LIST), ActionCategory.APP_SIGNATURE_KEY_LIST),
EXAM_SECURITY_KEY_SHOW_ASK_POPUP(
new LocTextKey("sebserver.exam.signaturekey.action.showASK"),
ImageIcon.ADD,
PageStateDefinitionImpl.SECURITY_KEY_EDIT,
ActionCategory.APP_SIGNATURE_KEY_LIST),
EXAM_SECURITY_KEY_SHOW_GRANT_POPUP( EXAM_SECURITY_KEY_SHOW_GRANT_POPUP(
new LocTextKey("sebserver.exam.signaturekey.action.showGrant"), new LocTextKey("sebserver.exam.signaturekey.action.showGrant"),
ImageIcon.SHOW, ImageIcon.SHOW,
@ -915,7 +925,7 @@ public enum ActionDefinition {
MONITOR_EXAM_BACK_TO_OVERVIEW( MONITOR_EXAM_BACK_TO_OVERVIEW(
new LocTextKey("sebserver.monitoring.exam.action.detail.view"), new LocTextKey("sebserver.monitoring.exam.action.detail.view"),
ImageIcon.SHOW, ImageIcon.BACK,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.FORM), ActionCategory.FORM),
@ -1025,7 +1035,7 @@ public enum ActionDefinition {
ActionCategory.CLIENT_EVENT_LIST), ActionCategory.CLIENT_EVENT_LIST),
FINISHED_EXAM_BACK_TO_OVERVIEW( FINISHED_EXAM_BACK_TO_OVERVIEW(
new LocTextKey("sebserver.finished.exam.action.detail.view"), new LocTextKey("sebserver.finished.exam.action.detail.view"),
ImageIcon.SHOW, ImageIcon.BACK,
PageStateDefinitionImpl.FINISHED_EXAM, PageStateDefinitionImpl.FINISHED_EXAM,
ActionCategory.FORM), ActionCategory.FORM),
FINISHED_EXAM_EXPORT_CSV( FINISHED_EXAM_EXPORT_CSV(

View file

@ -78,6 +78,7 @@ public class AddSecurityKeyGrantPopup {
public PageAction showGrantPopup(final PageAction action, final AppSignatureKeyInfo appSignatureKeyInfo) { public PageAction showGrantPopup(final PageAction action, final AppSignatureKeyInfo appSignatureKeyInfo) {
final PageContext pageContext = action.pageContext(); final PageContext pageContext = action.pageContext();
final PopupComposer popupComposer = new PopupComposer(this.pageService, pageContext, appSignatureKeyInfo); final PopupComposer popupComposer = new PopupComposer(this.pageService, pageContext, appSignatureKeyInfo);
final boolean readonly = action.pageContext().isReadonly();
try { try {
final ModalInputDialog<FormHandle<?>> dialog = final ModalInputDialog<FormHandle<?>> dialog =
new ModalInputDialog<>( new ModalInputDialog<>(
@ -90,7 +91,7 @@ public class AddSecurityKeyGrantPopup {
formHandle, formHandle,
appSignatureKeyInfo); appSignatureKeyInfo);
if (appSignatureKeyInfo.key == null) { if (appSignatureKeyInfo.key == null || readonly) {
dialog.open( dialog.open(
TITLE_TEXT_KEY, TITLE_TEXT_KEY,
popupComposer); popupComposer);
@ -130,6 +131,7 @@ public class AddSecurityKeyGrantPopup {
widgetFactory.addFormSubContextHeader(parent, TITLE_TEXT_INFO, null); widgetFactory.addFormSubContextHeader(parent, TITLE_TEXT_INFO, null);
final boolean hasASK = this.appSignatureKeyInfo.key != null; final boolean hasASK = this.appSignatureKeyInfo.key != null;
final PageContext formContext = this.pageContext.copyOf(parent); final PageContext formContext = this.pageContext.copyOf(parent);
final boolean readonly = this.pageContext.isReadonly();
final FormHandle<?> form = this.pageService.formBuilder(formContext) final FormHandle<?> form = this.pageService.formBuilder(formContext)
.addField(FormBuilder.text( .addField(FormBuilder.text(
@ -140,7 +142,7 @@ public class AddSecurityKeyGrantPopup {
: Constants.EMPTY_NOTE) : Constants.EMPTY_NOTE)
.readonly(true)) .readonly(true))
.addFieldIf(() -> hasASK, .addFieldIf(() -> hasASK && !readonly,
() -> FormBuilder.text( () -> FormBuilder.text(
Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG, Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG,
TITLE_TEXT_FORM_TAG) TITLE_TEXT_FORM_TAG)

View file

@ -452,6 +452,14 @@ public class ExamForm implements TemplateComposer {
exam.getName())) exam.getName()))
.publishIf(() -> editable && readonly) .publishIf(() -> editable && readonly)
.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_MODIFY_SEB_RESTRICTION_DETAILS) .newAction(ActionDefinition.EXAM_MODIFY_SEB_RESTRICTION_DETAILS)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withExec(this.examSEBRestrictionSettings.settingsFunction(this.pageService)) .withExec(this.examSEBRestrictionSettings.settingsFunction(this.pageService))
@ -473,14 +481,6 @@ 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))

View file

@ -8,6 +8,8 @@
package ch.ethz.seb.sebserver.gui.content.exam; package ch.ethz.seb.sebserver.gui.content.exam;
import java.util.function.Function;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
@ -21,6 +23,7 @@ import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.model.Entity;
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.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.institution.AppSignatureKeyInfo; import ch.ethz.seb.sebserver.gbl.model.institution.AppSignatureKeyInfo;
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey; import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
@ -31,6 +34,7 @@ import ch.ethz.seb.sebserver.gui.form.FormHandle;
import ch.ethz.seb.sebserver.gui.service.ResourceService; import ch.ethz.seb.sebserver.gui.service.ResourceService;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; 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.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageContext.AttributeKeys;
import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder; import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
@ -116,6 +120,7 @@ public class ExamSignatureKeyForm implements TemplateComposer {
final boolean signatureKeyCheckEnabled = BooleanUtils.toBoolean( final boolean signatureKeyCheckEnabled = BooleanUtils.toBoolean(
exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED)); exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED));
final String ct = exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_NUMERICAL_TRUST_THRESHOLD); final String ct = exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_NUMERICAL_TRUST_THRESHOLD);
final boolean readonly = exam.status == ExamStatus.FINISHED || exam.status == ExamStatus.ARCHIVED;
final Composite content = widgetFactory final Composite content = widgetFactory
.defaultPageLayout(pageContext.getParent(), TILE); .defaultPageLayout(pageContext.getParent(), TILE);
@ -132,7 +137,8 @@ public class ExamSignatureKeyForm implements TemplateComposer {
Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED, Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED,
FORM_ENABLED, FORM_ENABLED,
String.valueOf(signatureKeyCheckEnabled)) String.valueOf(signatureKeyCheckEnabled))
.withInputSpan(1)) .withInputSpan(1)
.readonly(readonly))
.addField(FormBuilder.text( .addField(FormBuilder.text(
Exam.ADDITIONAL_ATTR_NUMERICAL_TRUST_THRESHOLD, Exam.ADDITIONAL_ATTR_NUMERICAL_TRUST_THRESHOLD,
@ -144,7 +150,8 @@ public class ExamSignatureKeyForm implements TemplateComposer {
} }
}) })
.mandatory() .mandatory()
.withInputSpan(1)) .withInputSpan(1)
.readonly(readonly))
.build(); .build();
@ -174,7 +181,9 @@ public class ExamSignatureKeyForm implements TemplateComposer {
AppSignatureKeyInfo::getNumberOfConnections) AppSignatureKeyInfo::getNumberOfConnections)
.widthProportion(1)) .widthProportion(1))
.withDefaultAction(table -> actionBuilder .withDefaultActionIf(
() -> !readonly,
table -> actionBuilder
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP) .newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP)
.withParentEntityKey(entityKey) .withParentEntityKey(entityKey)
.withExec(action -> this.addSecurityKeyGrantPopup.showGrantPopup( .withExec(action -> this.addSecurityKeyGrantPopup.showGrantPopup(
@ -184,7 +193,9 @@ public class ExamSignatureKeyForm implements TemplateComposer {
.ignoreMoveAwayFromEdit() .ignoreMoveAwayFromEdit()
.create()) .create())
.withSelectionListener(this.pageService.getSelectionPublisher( .withSelectionListenerIf(
() -> !readonly,
this.pageService.getSelectionPublisher(
pageContext, pageContext,
ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP)) ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP))
@ -214,7 +225,9 @@ public class ExamSignatureKeyForm implements TemplateComposer {
GRANT_LIST_TAG, GRANT_LIST_TAG,
SecurityKey::getTag).widthProportion(1)) SecurityKey::getTag).widthProportion(1))
.withDefaultAction(table -> actionBuilder .withDefaultActionIf(
() -> !readonly,
table -> actionBuilder
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_GRANT_POPUP) .newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_GRANT_POPUP)
.withParentEntityKey(entityKey) .withParentEntityKey(entityKey)
.withExec(action -> this.securityKeyGrantPopup.showGrantPopup( .withExec(action -> this.securityKeyGrantPopup.showGrantPopup(
@ -235,30 +248,43 @@ public class ExamSignatureKeyForm implements TemplateComposer {
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withExec(action -> this.saveSettings(action, form.getForm())) .withExec(action -> this.saveSettings(action, form.getForm()))
.ignoreMoveAwayFromEdit() .ignoreMoveAwayFromEdit()
.publish() .publishIf(() -> !readonly)
.newAction(ActionDefinition.EXAM_SECURITY_KEY_CANCEL_MODIFY) .newAction(ActionDefinition.EXAM_SECURITY_KEY_CANCEL_MODIFY)
.withExec(this.pageService.backToCurrentFunction()) .withExec(this.pageService.backToCurrentFunction())
.publish() .publishIf(() -> !readonly)
.newAction(ActionDefinition.EXAM_SECURITY_KEY_BACK_MODIFY)
.withEntityKey(entityKey)
//.withExec(this.pageService.backToCurrentFunction())
.publishIf(() -> readonly)
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP) .newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP)
.withParentEntityKey(entityKey) .withParentEntityKey(entityKey)
.withSelect( .withSelect(
connectionInfoTable::getMultiSelection, connectionInfoTable::getMultiSelection,
action -> this.addSecurityKeyGrantPopup.showGrantPopup( this.addGrant(connectionInfoTable),
action,
connectionInfoTable.getSingleSelectedROWData()),
APP_SIG_KEY_LIST_EMPTY_SELECTION_TEXT_KEY) APP_SIG_KEY_LIST_EMPTY_SELECTION_TEXT_KEY)
.ignoreMoveAwayFromEdit() .ignoreMoveAwayFromEdit()
.noEventPropagation() .noEventPropagation()
.publish(false) .publishIf(() -> !readonly, false)
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ASK_POPUP)
.withParentEntityKey(entityKey)
.withAttribute(AttributeKeys.READ_ONLY, String.valueOf(readonly))
.withSelect(
connectionInfoTable::getMultiSelection,
this.showASK(connectionInfoTable),
APP_SIG_KEY_LIST_EMPTY_SELECTION_TEXT_KEY)
.ignoreMoveAwayFromEdit()
.noEventPropagation()
.publishIf(() -> readonly, false)
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_GRANT_POPUP) .newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_GRANT_POPUP)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withSelect( .withSelect(
grantsList::getMultiSelection, grantsList::getMultiSelection,
action -> this.securityKeyGrantPopup.showGrantPopup(action, this.showGrant(grantsList),
grantsList.getSingleSelectedROWData()),
GRANT_LIST_EMPTY_SELECTION_TEXT_KEY) GRANT_LIST_EMPTY_SELECTION_TEXT_KEY)
.ignoreMoveAwayFromEdit() .ignoreMoveAwayFromEdit()
.noEventPropagation() .noEventPropagation()
@ -272,9 +298,7 @@ public class ExamSignatureKeyForm implements TemplateComposer {
this::deleteGrant, this::deleteGrant,
GRANT_LIST_EMPTY_SELECTION_TEXT_KEY) GRANT_LIST_EMPTY_SELECTION_TEXT_KEY)
.ignoreMoveAwayFromEdit() .ignoreMoveAwayFromEdit()
.publish(false) .publishIf(() -> !readonly, false);
;
} }
private PageAction saveSettings(final PageAction action, final Form form) { private PageAction saveSettings(final PageAction action, final Form form) {
@ -293,6 +317,36 @@ public class ExamSignatureKeyForm implements TemplateComposer {
return action; return action;
} }
private Function<PageAction, PageAction> addGrant(final EntityTable<AppSignatureKeyInfo> connectionInfoTable) {
return action -> {
final EntityKey singleSelection = action.getSingleSelection();
if (singleSelection != null) {
this.addSecurityKeyGrantPopup.showGrantPopup(action, connectionInfoTable.getSingleSelectedROWData());
}
return action;
};
}
private Function<PageAction, PageAction> showASK(final EntityTable<AppSignatureKeyInfo> connectionInfoTable) {
return action -> {
final EntityKey singleSelection = action.getSingleSelection();
if (singleSelection != null) {
this.addSecurityKeyGrantPopup.showGrantPopup(action, connectionInfoTable.getSingleSelectedROWData());
}
return action;
};
}
private Function<PageAction, PageAction> showGrant(final EntityTable<SecurityKey> grantsList) {
return action -> {
final EntityKey singleSelection = action.getSingleSelection();
if (singleSelection != null) {
this.securityKeyGrantPopup.showGrantPopup(action, grantsList.getSingleSelectedROWData());
}
return action;
};
}
private PageAction deleteGrant(final PageAction action) { private PageAction deleteGrant(final PageAction action) {
final EntityKey parentEntityKey = action.getParentEntityKey(); final EntityKey parentEntityKey = action.getParentEntityKey();
final EntityKey singleSelection = action.getSingleSelection(); final EntityKey singleSelection = action.getSingleSelection();

View file

@ -10,13 +10,16 @@ package ch.ethz.seb.sebserver.gui.content.exam;
import java.util.Arrays; import java.util.Arrays;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.function.BiConsumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -47,9 +50,11 @@ 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.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamProctoringSettings; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.ResetProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExamProctoringSettings; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExamProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetExamTemplateProctoringSettings; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetExamTemplateProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.SaveExamTemplateProctoringSettings; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.SaveExamTemplateProctoringSettings;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@Lazy @Lazy
@Component @Component
@ -72,10 +77,24 @@ public class ProctoringSettingsPopup {
new LocTextKey("sebserver.exam.proctoring.form.url"); new LocTextKey("sebserver.exam.proctoring.form.url");
private final static LocTextKey SEB_PROCTORING_FORM_ROOM_SIZE = private final static LocTextKey SEB_PROCTORING_FORM_ROOM_SIZE =
new LocTextKey("sebserver.exam.proctoring.form.collectingRoomSize"); new LocTextKey("sebserver.exam.proctoring.form.collectingRoomSize");
private final static LocTextKey SEB_PROCTORING_FORM_APPKEY =
new LocTextKey("sebserver.exam.proctoring.form.appkey"); private final static LocTextKey SEB_PROCTORING_FORM_APPKEY_JITSI =
private final static LocTextKey SEB_PROCTORING_FORM_SECRET = new LocTextKey("sebserver.exam.proctoring.form.appkey.jitsi");
new LocTextKey("sebserver.exam.proctoring.form.secret"); private final static LocTextKey SEB_PROCTORING_FORM_SECRET_JITSI =
new LocTextKey("sebserver.exam.proctoring.form.secret.jitsi");
private final static LocTextKey SEB_PROCTORING_FORM_APPKEY_ZOOM =
new LocTextKey("sebserver.exam.proctoring.form.appkey.zoom");
private final static LocTextKey SEB_PROCTORING_FORM_SECRET_ZOOM =
new LocTextKey("sebserver.exam.proctoring.form.secret.zoom");
private final static LocTextKey SEB_PROCTORING_FORM_ACCOUNT_ID =
new LocTextKey("sebserver.exam.proctoring.form.accountId");
private final static LocTextKey SEB_PROCTORING_FORM_CLIENT_ID =
new LocTextKey("sebserver.exam.proctoring.form.clientId");
private final static LocTextKey SEB_PROCTORING_FORM_CLIENT_SECRET =
new LocTextKey("sebserver.exam.proctoring.form.clientSecret");
private final static LocTextKey SEB_PROCTORING_FORM_SDKKEY = private final static LocTextKey SEB_PROCTORING_FORM_SDKKEY =
new LocTextKey("sebserver.exam.proctoring.form.sdkkey"); new LocTextKey("sebserver.exam.proctoring.form.sdkkey");
private final static LocTextKey SEB_PROCTORING_FORM_SDKSECRET = private final static LocTextKey SEB_PROCTORING_FORM_SDKSECRET =
@ -86,6 +105,13 @@ public class ProctoringSettingsPopup {
private final static LocTextKey SEB_PROCTORING_FORM_FEATURES = private final static LocTextKey SEB_PROCTORING_FORM_FEATURES =
new LocTextKey("sebserver.exam.proctoring.form.features"); new LocTextKey("sebserver.exam.proctoring.form.features");
private final static LocTextKey SAVE_TEXT_KEY =
new LocTextKey("sebserver.exam.proctoring.form.saveSettings");
private final static LocTextKey RESET_TEXT_KEY =
new LocTextKey("sebserver.exam.proctoring.form.resetSettings");
private final static LocTextKey RESET_CONFIRM_KEY =
new LocTextKey("sebserver.exam.proctoring.form.resetConfirm");
Function<PageAction, PageAction> settingsFunction(final PageService pageService, final boolean modifyGrant) { Function<PageAction, PageAction> settingsFunction(final PageService pageService, final boolean modifyGrant) {
return action -> { return action -> {
@ -102,32 +128,90 @@ public class ProctoringSettingsPopup {
.setDialogWidth(860) .setDialogWidth(860)
.setDialogHeight(600); .setDialogHeight(600);
final ResetButtonHandler resetButtonHandler = new ResetButtonHandler();
if (modifyGrant) {
final BiConsumer<Composite, Supplier<FormHandle<?>>> actionComposer = (composite, handle) -> {
final WidgetFactory widgetFactory = pageService.getWidgetFactory();
final Button save = widgetFactory.buttonLocalized(composite, SAVE_TEXT_KEY);
save.setLayoutData(new RowData());
save.addListener(SWT.Selection, event -> {
if (doSaveSettings(pageService, pageContext, handle.get())) {
dialog.close();
}
});
final Button reset = widgetFactory.buttonLocalized(composite, RESET_TEXT_KEY);
reset.setLayoutData(new RowData());
reset.addListener(SWT.Selection, event -> {
pageContext.applyConfirmDialog(RESET_CONFIRM_KEY, apply -> {
if (apply && doResetSettings(pageService, pageContext)) {
dialog.close();
}
});
});
resetButtonHandler.set(reset);
};
final SEBProctoringPropertiesForm bindFormContext = new SEBProctoringPropertiesForm( final SEBProctoringPropertiesForm bindFormContext = new SEBProctoringPropertiesForm(
pageService,
pageContext);
final Predicate<FormHandle<?>> doBind = formHandle -> doSaveSettings(
pageService, pageService,
pageContext, pageContext,
formHandle); resetButtonHandler);
if (modifyGrant) { dialog.openWithActions(
dialog.open(
SEB_PROCTORING_FORM_TITLE, SEB_PROCTORING_FORM_TITLE,
doBind, actionComposer,
Utils.EMPTY_EXECUTION, Utils.EMPTY_EXECUTION,
bindFormContext); bindFormContext);
} else { } else {
dialog.open( dialog.open(
SEB_PROCTORING_FORM_TITLE, SEB_PROCTORING_FORM_TITLE,
pageContext, pageContext,
pc -> bindFormContext.compose(pc.getParent())); pc -> new SEBProctoringPropertiesForm(
pageService,
pageContext,
resetButtonHandler));
} }
return action; return action;
}; };
} }
private static final class ResetButtonHandler {
Button resetBotton = null;
boolean enabled = false;
void set(final Button resetBotton) {
this.resetBotton = resetBotton;
resetBotton.setEnabled(this.enabled);
}
void enable(final boolean enable) {
this.enabled = enable;
if (this.resetBotton != null) {
this.resetBotton.setEnabled(enable);
}
}
}
private boolean doResetSettings(
final PageService pageService,
final PageContext pageContext) {
final EntityKey entityKey = pageContext.getEntityKey();
return pageService.getRestService()
.getBuilder(ResetProctoringSettings.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.onError(error -> {
log.error("Failed to rest proctoring settings for exam: {}", entityKey, error);
pageContext.notifyUnexpectedError(error);
}).map(settings -> true).getOr(false);
}
private boolean doSaveSettings( private boolean doSaveSettings(
final PageService pageService, final PageService pageService,
final PageContext pageContext, final PageContext pageContext,
@ -168,8 +252,14 @@ public class ProctoringSettingsPopup {
false, false,
form.getFieldValue(ProctoringServiceSettings.ATTR_APP_KEY), form.getFieldValue(ProctoringServiceSettings.ATTR_APP_KEY),
form.getFieldValue(ProctoringServiceSettings.ATTR_APP_SECRET), form.getFieldValue(ProctoringServiceSettings.ATTR_APP_SECRET),
form.getFieldValue(ProctoringServiceSettings.ATTR_ACCOUNT_ID),
form.getFieldValue(ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_ID),
form.getFieldValue(ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_SECRET),
form.getFieldValue(ProctoringServiceSettings.ATTR_SDK_KEY), form.getFieldValue(ProctoringServiceSettings.ATTR_SDK_KEY),
form.getFieldValue(ProctoringServiceSettings.ATTR_SDK_SECRET), form.getFieldValue(ProctoringServiceSettings.ATTR_SDK_SECRET),
BooleanUtils.toBoolean(form.getFieldValue( BooleanUtils.toBoolean(form.getFieldValue(
ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM))); ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM)));
@ -217,23 +307,22 @@ public class ProctoringSettingsPopup {
private final PageService pageService; private final PageService pageService;
private final PageContext pageContext; private final PageContext pageContext;
private final ResetButtonHandler resetButtonHandler;
protected SEBProctoringPropertiesForm( protected SEBProctoringPropertiesForm(
final PageService pageService, final PageService pageService,
final PageContext pageContext) { final PageContext pageContext,
final ResetButtonHandler resetButtonHandler) {
this.pageService = pageService; this.pageService = pageService;
this.pageContext = pageContext; this.pageContext = pageContext;
this.resetButtonHandler = resetButtonHandler;
} }
@Override @Override
public Supplier<FormHandle<?>> compose(final Composite parent) { public Supplier<FormHandle<?>> compose(final Composite parent) {
final RestService restService = this.pageService.getRestService(); final RestService restService = this.pageService.getRestService();
final ResourceService resourceService = this.pageService.getResourceService();
final EntityKey entityKey = this.pageContext.getEntityKey(); final EntityKey entityKey = this.pageContext.getEntityKey();
final boolean isReadonly = BooleanUtils.toBoolean(
this.pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
final Composite content = this.pageService final Composite content = this.pageService
.getWidgetFactory() .getWidgetFactory()
@ -248,12 +337,170 @@ public class ProctoringSettingsPopup {
.call() .call()
.getOrThrow(); .getOrThrow();
final PageContext formContext = this.pageContext this.resetButtonHandler.enable(proctoringSettings.serviceInUse);
final FormHandleAnchor formHandleAnchor = new FormHandleAnchor();
formHandleAnchor.formContext = this.pageContext
.copyOf(content) .copyOf(content)
.clearEntityKeys(); .clearEntityKeys();
final FormHandle<ProctoringServiceSettings> formHandle = this.pageService.formBuilder( buildFormAccordingToService(
formContext) proctoringSettings,
proctoringSettings.serverType.name(),
formHandleAnchor);
return () -> formHandleAnchor.formHandle;
}
private void buildFormAccordingToService(
final ProctoringServiceSettings proctoringServiceSettings,
final String serviceType,
final FormHandleAnchor formHandleAnchor) {
if (ProctoringServerType.JITSI_MEET.name().equals(serviceType)) {
PageService.clearComposite(formHandleAnchor.formContext.getParent());
formHandleAnchor.formHandle = buildFormForJitsi(proctoringServiceSettings, formHandleAnchor);
} else if (ProctoringServerType.ZOOM.name().equals(serviceType)) {
PageService.clearComposite(formHandleAnchor.formContext.getParent());
formHandleAnchor.formHandle = buildFormForZoom(proctoringServiceSettings, formHandleAnchor);
}
if (proctoringServiceSettings.serviceInUse) {
final Form form = formHandleAnchor.formHandle.getForm();
form.getFieldInput(ProctoringServiceSettings.ATTR_SERVER_TYPE).setEnabled(false);
form.getFieldInput(ProctoringServiceSettings.ATTR_SERVER_URL).setEnabled(false);
}
formHandleAnchor.formContext.getParent().getParent().getParent().layout(true, true);
}
private FormHandle<ProctoringServiceSettings> buildFormForJitsi(
final ProctoringServiceSettings proctoringSettings,
final FormHandleAnchor formHandleAnchor) {
final ResourceService resourceService = this.pageService.getResourceService();
final boolean isReadonly = BooleanUtils.toBoolean(
this.pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
final FormBuilder formBuilder = buildHeader(proctoringSettings, formHandleAnchor, isReadonly);
formBuilder.addField(FormBuilder.singleSelection(
ProctoringServiceSettings.ATTR_SERVER_TYPE,
SEB_PROCTORING_FORM_TYPE,
ProctoringServerType.JITSI_MEET.name(),
resourceService::examProctoringTypeResources)
.withSelectionListener(form -> buildFormAccordingToService(
proctoringSettings,
form.getFieldValue(ProctoringServiceSettings.ATTR_SERVER_TYPE),
formHandleAnchor)))
.addField(FormBuilder.text(
ProctoringServiceSettings.ATTR_SERVER_URL,
SEB_PROCTORING_FORM_URL,
proctoringSettings.serverURL)
.mandatory())
.addField(FormBuilder.text(
ProctoringServiceSettings.ATTR_APP_KEY,
SEB_PROCTORING_FORM_APPKEY_JITSI,
proctoringSettings.appKey))
.withEmptyCellSeparation(false)
.addField(FormBuilder.password(
ProctoringServiceSettings.ATTR_APP_SECRET,
SEB_PROCTORING_FORM_SECRET_JITSI,
(proctoringSettings.appSecret != null)
? String.valueOf(proctoringSettings.appSecret)
: null));
return buildFooter(proctoringSettings, resourceService, formBuilder);
}
private FormHandle<ProctoringServiceSettings> buildFormForZoom(
final ProctoringServiceSettings proctoringSettings,
final FormHandleAnchor formHandleAnchor) {
final ResourceService resourceService = this.pageService.getResourceService();
final boolean isReadonly = BooleanUtils.toBoolean(
this.pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
final FormBuilder formBuilder = buildHeader(proctoringSettings, formHandleAnchor, isReadonly);
formBuilder
.addField(FormBuilder.singleSelection(
ProctoringServiceSettings.ATTR_SERVER_TYPE,
SEB_PROCTORING_FORM_TYPE,
ProctoringServerType.ZOOM.name(),
resourceService::examProctoringTypeResources)
.withSelectionListener(form -> buildFormAccordingToService(
proctoringSettings,
form.getFieldValue(ProctoringServiceSettings.ATTR_SERVER_TYPE),
formHandleAnchor)))
.addField(FormBuilder.text(
ProctoringServiceSettings.ATTR_SERVER_URL,
SEB_PROCTORING_FORM_URL,
proctoringSettings.serverURL)
.mandatory())
.addField(FormBuilder.text(
ProctoringServiceSettings.ATTR_APP_KEY,
SEB_PROCTORING_FORM_APPKEY_ZOOM,
proctoringSettings.appKey))
.withEmptyCellSeparation(false)
.addField(FormBuilder.password(
ProctoringServiceSettings.ATTR_APP_SECRET,
SEB_PROCTORING_FORM_SECRET_ZOOM,
(proctoringSettings.appSecret != null)
? String.valueOf(proctoringSettings.appSecret)
: null))
.addField(FormBuilder.text(
ProctoringServiceSettings.ATTR_ACCOUNT_ID,
SEB_PROCTORING_FORM_ACCOUNT_ID,
proctoringSettings.accountId)
.mandatory())
.withEmptyCellSeparation(false)
.addField(FormBuilder.text(
ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_ID,
SEB_PROCTORING_FORM_CLIENT_ID,
proctoringSettings.clientId)
.mandatory())
.withEmptyCellSeparation(false)
.addField(FormBuilder.password(
ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_SECRET,
SEB_PROCTORING_FORM_CLIENT_SECRET,
(proctoringSettings.clientSecret != null)
? String.valueOf(proctoringSettings.clientSecret)
: null)
.mandatory())
.addField(FormBuilder.text(
ProctoringServiceSettings.ATTR_SDK_KEY,
SEB_PROCTORING_FORM_SDKKEY,
proctoringSettings.sdkKey)
.mandatory())
.withEmptyCellSeparation(false)
.addField(FormBuilder.password(
ProctoringServiceSettings.ATTR_SDK_SECRET,
SEB_PROCTORING_FORM_SDKSECRET,
(proctoringSettings.sdkSecret != null)
? String.valueOf(proctoringSettings.sdkSecret)
: null)
.mandatory());
return buildFooter(proctoringSettings, resourceService, formBuilder);
}
private FormBuilder buildHeader(final ProctoringServiceSettings proctoringSettings,
final FormHandleAnchor formHandleAnchor, final boolean isReadonly) {
final FormBuilder formBuilder = this.pageService.formBuilder(
formHandleAnchor.formContext)
.withDefaultSpanInput(5) .withDefaultSpanInput(5)
.withEmptyCellSeparation(true) .withEmptyCellSeparation(true)
.withDefaultSpanEmptyCell(1) .withDefaultSpanEmptyCell(1)
@ -270,49 +517,13 @@ public class ProctoringSettingsPopup {
.addField(FormBuilder.checkbox( .addField(FormBuilder.checkbox(
ProctoringServiceSettings.ATTR_ENABLE_PROCTORING, ProctoringServiceSettings.ATTR_ENABLE_PROCTORING,
SEB_PROCTORING_FORM_ENABLE, SEB_PROCTORING_FORM_ENABLE,
String.valueOf(proctoringSettings.enableProctoring))) String.valueOf(proctoringSettings.enableProctoring)));
return formBuilder;
}
.addField(FormBuilder.singleSelection( private FormHandle<ProctoringServiceSettings> buildFooter(final ProctoringServiceSettings proctoringSettings,
ProctoringServiceSettings.ATTR_SERVER_TYPE, final ResourceService resourceService, final FormBuilder formBuilder) {
SEB_PROCTORING_FORM_TYPE, return formBuilder.withDefaultSpanInput(1)
proctoringSettings.serverType.name(),
resourceService::examProctoringTypeResources))
.addField(FormBuilder.text(
ProctoringServiceSettings.ATTR_SERVER_URL,
SEB_PROCTORING_FORM_URL,
proctoringSettings.serverURL)
.mandatory())
.addField(FormBuilder.text(
ProctoringServiceSettings.ATTR_APP_KEY,
SEB_PROCTORING_FORM_APPKEY,
proctoringSettings.appKey)
.mandatory())
.withEmptyCellSeparation(false)
.addField(FormBuilder.password(
ProctoringServiceSettings.ATTR_APP_SECRET,
SEB_PROCTORING_FORM_SECRET,
(proctoringSettings.appSecret != null)
? String.valueOf(proctoringSettings.appSecret)
: null)
.mandatory())
.addField(FormBuilder.text(
ProctoringServiceSettings.ATTR_SDK_KEY,
SEB_PROCTORING_FORM_SDKKEY,
proctoringSettings.sdkKey))
.withEmptyCellSeparation(false)
.addField(FormBuilder.password(
ProctoringServiceSettings.ATTR_SDK_SECRET,
SEB_PROCTORING_FORM_SDKSECRET,
(proctoringSettings.sdkSecret != null)
? String.valueOf(proctoringSettings.sdkSecret)
: null))
.withDefaultSpanInput(1)
.addField(FormBuilder.text( .addField(FormBuilder.text(
ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE, ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE,
SEB_PROCTORING_FORM_ROOM_SIZE, SEB_PROCTORING_FORM_ROOM_SIZE,
@ -337,14 +548,14 @@ public class ProctoringSettingsPopup {
resourceService::examProctoringFeaturesResources)) resourceService::examProctoringFeaturesResources))
.build(); .build();
}
if (proctoringSettings.serviceInUse) {
formHandle.getForm().getFieldInput(ProctoringServiceSettings.ATTR_SERVER_TYPE).setEnabled(false);
formHandle.getForm().getFieldInput(ProctoringServiceSettings.ATTR_SERVER_URL).setEnabled(false);
} }
return () -> formHandle; private static final class FormHandleAnchor {
}
FormHandle<ProctoringServiceSettings> formHandle;
PageContext formContext;
} }
} }

View file

@ -8,6 +8,7 @@
package ch.ethz.seb.sebserver.gui.service.page.impl; package ch.ethz.seb.sebserver.gui.service.page.impl;
import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -17,6 +18,8 @@ import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Dialog; import org.eclipse.swt.widgets.Dialog;
@ -110,9 +113,53 @@ public class ModalInputDialog<T> extends Dialog {
open(title, predicate, cancelCallback, contentComposer); open(title, predicate, cancelCallback, contentComposer);
} }
public void openWithActions(
final LocTextKey title,
final BiConsumer<Composite, Supplier<T>> actionComposer,
final Runnable cancelCallback,
final ModalInputDialogComposer<T> contentComposer) {
// Create the selection dialog window
this.shell = new Shell(getParent(), getStyle());
this.shell.setText(getText());
this.shell.setData(RWT.CUSTOM_VARIANT, CustomVariant.MESSAGE.key);
this.shell.setText(this.widgetFactory.getI18nSupport().getText(title));
this.shell.setLayout(new GridLayout(1, true));
final GridData gridData2 = new GridData(SWT.FILL, SWT.BOTTOM, false, false);
this.shell.setLayoutData(gridData2);
final Composite main = new Composite(this.shell, SWT.NONE);
main.setLayout(new GridLayout());
final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
gridData.horizontalSpan = 2;
gridData.widthHint = this.dialogWidth;
main.setLayoutData(gridData);
final Supplier<T> valueSupplier = contentComposer.compose(main);
gridData.heightHint = calcDialogHeight(main);
final Composite actions = new Composite(this.shell, SWT.NONE);
final RowLayout rowLayout = new RowLayout(SWT.HORIZONTAL);
actions.setLayout(rowLayout);
if (actionComposer != null) {
actionComposer.accept(actions, valueSupplier);
}
actions.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true));
final Button cancel = this.widgetFactory.buttonLocalized(actions, CANCEL_TEXT_KEY);
cancel.setLayoutData(new RowData());
cancel.addListener(SWT.Selection, event -> {
if (cancelCallback != null) {
cancelCallback.run();
}
this.shell.close();
});
finishUp(this.shell);
}
public void open( public void open(
final LocTextKey title, final LocTextKey title,
final Predicate<T> callback, final Predicate<T> okCallback,
final Runnable cancelCallback, final Runnable cancelCallback,
final ModalInputDialogComposer<T> contentComposer) { final ModalInputDialogComposer<T> contentComposer) {
@ -142,7 +189,7 @@ public class ModalInputDialog<T> extends Dialog {
ok.addListener(SWT.Selection, event -> { ok.addListener(SWT.Selection, event -> {
if (valueSupplier != null) { if (valueSupplier != null) {
final T result = valueSupplier.get(); final T result = valueSupplier.get();
if (callback.test(result)) { if (okCallback.test(result)) {
this.shell.close(); this.shell.close();
} }
} else { } else {

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class ResetProctoringSettings extends RestCall<ProctoringServiceSettings> {
public ResetProctoringSettings() {
super(new TypeKey<>(
CallType.SAVE,
EntityType.EXAM_PROCTOR_DATA,
new TypeReference<ProctoringServiceSettings>() {
}),
HttpMethod.POST,
MediaType.APPLICATION_JSON,
API.EXAM_ADMINISTRATION_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_PROCTORING_RESET_PATH_SEGMENT);
}
}

View file

@ -157,6 +157,16 @@ public class TableBuilder<ROW extends ModelIdAware> {
return this; return this;
} }
public TableBuilder<ROW> withSelectionListenerIf(
final BooleanSupplier check,
final Consumer<EntityTable<ROW>> selectionListener) {
if (check.getAsBoolean()) {
this.selectionListener = selectionListener;
}
return this;
}
public TableBuilder<ROW> withContentChangeListener(final Consumer<Integer> contentChangeListener) { public TableBuilder<ROW> withContentChangeListener(final Consumer<Integer> contentChangeListener) {
this.contentChangeListener = contentChangeListener; this.contentChangeListener = contentChangeListener;
return this; return this;

View file

@ -147,7 +147,8 @@ public class WidgetFactory {
NOTIFICATION("notification.png"), NOTIFICATION("notification.png"),
VERIFY("verify.png"), VERIFY("verify.png"),
SHIELD("shield.png"), SHIELD("shield.png"),
NO_SHIELD("no_shield.png"); NO_SHIELD("no_shield.png"),
BACK("back.png");
public String fileName; public String fileName;
private ImageData image = null; private ImageData image = null;

View file

@ -116,6 +116,8 @@ public interface ExamAdminService {
* @return ExamProctoringService instance */ * @return ExamProctoringService instance */
Result<ExamProctoringService> getExamProctoringService(final Long examId); Result<ExamProctoringService> getExamProctoringService(final Long examId);
Result<Exam> resetProctoringSettings(Exam exam);
/** This archives a finished exam and set it to archived state as well as the assigned /** This archives a finished exam and set it to archived state as well as the assigned
* exam configurations that are also set to archived state. * exam configurations that are also set to archived state.
* *

View file

@ -60,7 +60,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
private static final Logger log = LoggerFactory.getLogger(ExamAdminServiceImpl.class); private static final Logger log = LoggerFactory.getLogger(ExamAdminServiceImpl.class);
private final ExamDAO examDAO; private final ExamDAO examDAO;
private final ProctoringAdminService proctoringServiceSettingsService; private final ProctoringAdminService proctoringAdminService;
private final AdditionalAttributesDAO additionalAttributesDAO; private final AdditionalAttributesDAO additionalAttributesDAO;
private final ConfigurationNodeDAO configurationNodeDAO; private final ConfigurationNodeDAO configurationNodeDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO; private final ExamConfigurationMapDAO examConfigurationMapDAO;
@ -70,7 +70,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
protected ExamAdminServiceImpl( protected ExamAdminServiceImpl(
final ExamDAO examDAO, final ExamDAO examDAO,
final ProctoringAdminService proctoringServiceSettingsService, final ProctoringAdminService proctoringAdminService,
final AdditionalAttributesDAO additionalAttributesDAO, final AdditionalAttributesDAO additionalAttributesDAO,
final ConfigurationNodeDAO configurationNodeDAO, final ConfigurationNodeDAO configurationNodeDAO,
final ExamConfigurationMapDAO examConfigurationMapDAO, final ExamConfigurationMapDAO examConfigurationMapDAO,
@ -79,7 +79,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
final @Value("${sebserver.webservice.api.admin.exam.app.signature.key.numerical.threshold:2}") int defaultNumericalTrustThreshold) { final @Value("${sebserver.webservice.api.admin.exam.app.signature.key.numerical.threshold:2}") int defaultNumericalTrustThreshold) {
this.examDAO = examDAO; this.examDAO = examDAO;
this.proctoringServiceSettingsService = proctoringServiceSettingsService; this.proctoringAdminService = proctoringAdminService;
this.additionalAttributesDAO = additionalAttributesDAO; this.additionalAttributesDAO = additionalAttributesDAO;
this.configurationNodeDAO = configurationNodeDAO; this.configurationNodeDAO = configurationNodeDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO; this.examConfigurationMapDAO = examConfigurationMapDAO;
@ -200,7 +200,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
@Override @Override
public Result<ProctoringServiceSettings> getProctoringServiceSettings(final Long examId) { public Result<ProctoringServiceSettings> getProctoringServiceSettings(final Long examId) {
return this.proctoringServiceSettingsService.getProctoringSettings(new EntityKey(examId, EntityType.EXAM)); return this.proctoringAdminService.getProctoringSettings(new EntityKey(examId, EntityType.EXAM));
} }
@Override @Override
@ -209,7 +209,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
final Long examId, final Long examId,
final ProctoringServiceSettings proctoringServiceSettings) { final ProctoringServiceSettings proctoringServiceSettings) {
return this.proctoringServiceSettingsService return this.proctoringAdminService
.saveProctoringServiceSettings( .saveProctoringServiceSettings(
new EntityKey(examId, EntityType.EXAM), new EntityKey(examId, EntityType.EXAM),
proctoringServiceSettings) proctoringServiceSettings)
@ -239,10 +239,30 @@ public class ExamAdminServiceImpl implements ExamAdminService {
@Override @Override
public Result<ExamProctoringService> getExamProctoringService(final Long examId) { public Result<ExamProctoringService> getExamProctoringService(final Long examId) {
return getProctoringServiceSettings(examId) return getProctoringServiceSettings(examId)
.flatMap(settings -> this.proctoringServiceSettingsService .flatMap(settings -> this.proctoringAdminService
.getExamProctoringService(settings.serverType)); .getExamProctoringService(settings.serverType));
} }
@Override
public Result<Exam> resetProctoringSettings(final Exam exam) {
return getProctoringServiceSettings(exam.id)
.map(settings -> {
ProctoringServiceSettings resetSettings;
if (exam.examTemplateId != null) {
// get settings from origin template
resetSettings = this.proctoringAdminService
.getProctoringSettings(new EntityKey(exam.examTemplateId, EntityType.EXAM_TEMPLATE))
.map(template -> new ProctoringServiceSettings(exam.id, template))
.getOr(new ProctoringServiceSettings(exam.id));
} else {
// create new reseted settings
resetSettings = new ProctoringServiceSettings(exam.id);
}
return resetSettings;
}).flatMap(settings -> saveProctoringServiceSettings(exam.id, settings))
.map(settings -> exam);
}
private Result<Exam> initAdditionalAttributesForMoodleExams(final Exam exam) { private Result<Exam> initAdditionalAttributesForMoodleExams(final Exam exam) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final LmsAPITemplate lmsTemplate = this.lmsAPIService final LmsAPITemplate lmsTemplate = this.lmsAPIService

View file

@ -91,6 +91,9 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
: false, : false,
getString(mapping, ProctoringServiceSettings.ATTR_APP_KEY), getString(mapping, ProctoringServiceSettings.ATTR_APP_KEY),
getString(mapping, ProctoringServiceSettings.ATTR_APP_SECRET), getString(mapping, ProctoringServiceSettings.ATTR_APP_SECRET),
getString(mapping, ProctoringServiceSettings.ATTR_ACCOUNT_ID),
getString(mapping, ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_ID),
getString(mapping, ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_SECRET),
getString(mapping, ProctoringServiceSettings.ATTR_SDK_KEY), getString(mapping, ProctoringServiceSettings.ATTR_SDK_KEY),
getString(mapping, ProctoringServiceSettings.ATTR_SDK_SECRET), getString(mapping, ProctoringServiceSettings.ATTR_SDK_SECRET),
getBoolean(mapping, getBoolean(mapping,
@ -139,6 +142,7 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE, ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE,
String.valueOf(proctoringServiceSettings.collectingRoomSize)); String.valueOf(proctoringServiceSettings.collectingRoomSize));
if (StringUtils.isNotBlank(proctoringServiceSettings.appKey)) {
this.additionalAttributesDAO.saveAdditionalAttribute( this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType, parentEntityKey.entityType,
entityId, entityId,
@ -152,6 +156,28 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.appSecret)) this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.appSecret))
.getOrThrow() .getOrThrow()
.toString()); .toString());
}
if (StringUtils.isNotBlank(proctoringServiceSettings.accountId)) {
this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType,
entityId,
ProctoringServiceSettings.ATTR_ACCOUNT_ID,
StringUtils.trim(proctoringServiceSettings.accountId));
this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType,
entityId,
ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_ID,
StringUtils.trim(proctoringServiceSettings.clientId));
this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType,
entityId,
ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_SECRET,
this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.clientSecret))
.getOrThrow()
.toString());
}
if (StringUtils.isNotBlank(proctoringServiceSettings.sdkKey)) { if (StringUtils.isNotBlank(proctoringServiceSettings.sdkKey)) {
this.additionalAttributesDAO.saveAdditionalAttribute( this.additionalAttributesDAO.saveAdditionalAttribute(

View file

@ -17,10 +17,13 @@ import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
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.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ProctoringAdminService;
/** Defines functionality to deal with proctoring rooms in a generic way (independent from meeting service) */ /** Defines functionality to deal with proctoring rooms in a generic way (independent from meeting service) */
public interface ExamProctoringRoomService { public interface ExamProctoringRoomService {
ProctoringAdminService getProctoringAdminService();
/** Get all existing default proctoring rooms of an exam. /** Get all existing default proctoring rooms of an exam.
* *
* @param examId The exam identifier * @param examId The exam identifier
@ -136,4 +139,6 @@ public interface ExamProctoringRoomService {
* @return Result refer to void or to an error when happened */ * @return Result refer to void or to an error when happened */
Result<Void> notifyRoomOpened(Long examId, String roomName); Result<Void> notifyRoomOpened(Long examId, String roomName);
Result<Exam> cleanupAllRooms(Exam exam);
} }

View file

@ -84,6 +84,11 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
this.sendBroadcastReset = sendBroadcastReset; this.sendBroadcastReset = sendBroadcastReset;
} }
@Override
public ProctoringAdminService getProctoringAdminService() {
return this.proctoringAdminService;
}
@Override @Override
public Result<Collection<RemoteProctoringRoom>> getProctoringCollectingRooms(final Long examId) { public Result<Collection<RemoteProctoringRoom>> getProctoringCollectingRooms(final Long examId) {
return this.remoteProctoringRoomDAO.getCollectingRooms(examId); return this.remoteProctoringRoomDAO.getCollectingRooms(examId);
@ -298,6 +303,19 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
}); });
} }
@Override
public Result<Exam> cleanupAllRooms(final Exam exam) {
return this.clientConnectionDAO
.getAllActiveConnectionTokens(exam.id)
.map(activeConnections -> {
if (activeConnections != null && !activeConnections.isEmpty()) {
throw new IllegalStateException("There are still active connections for this exam");
}
return exam;
})
.flatMap(this::disposeRoomsForExam);
}
private void assignToCollectingRoom(final ClientConnectionRecord cc) { private void assignToCollectingRoom(final ClientConnectionRecord cc) {
synchronized (RESERVE_ROOM_LOCK) { synchronized (RESERVE_ROOM_LOCK) {
try { try {

View file

@ -15,7 +15,9 @@ import java.util.Base64;
import java.util.Base64.Encoder; import java.util.Base64.Encoder;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -36,6 +38,10 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClientResponseException; import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
@ -86,15 +92,12 @@ public class ZoomProctoringService implements ExamProctoringService {
private static final String TOKEN_ENCODE_ALG = "HmacSHA256"; private static final String TOKEN_ENCODE_ALG = "HmacSHA256";
@Deprecated
private static final String ZOOM_ACCESS_TOKEN_HEADER = private static final String ZOOM_ACCESS_TOKEN_HEADER =
"{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
@Deprecated @Deprecated
private static final String ZOOM_API_ACCESS_TOKEN_PAYLOAD = private static final String ZOOM_API_ACCESS_TOKEN_PAYLOAD =
"{\"iss\":\"%s\",\"exp\":%s}"; "{\"iss\":\"%s\",\"exp\":%s}";
@Deprecated
private static final String ZOOM_SDK_ACCESS_TOKEN_PAYLOAD =
"{\"appKey\":\"%s\",\"iat\":%s,\"exp\":%s,\"tokenExp\":%s}";
private static final Map<String, String> SEB_API_NAME_INSTRUCTION_NAME_MAPPING = Utils.immutableMapOf(Arrays.asList( private static final Map<String, String> SEB_API_NAME_INSTRUCTION_NAME_MAPPING = Utils.immutableMapOf(Arrays.asList(
new Tuple<>( new Tuple<>(
@ -125,7 +128,6 @@ public class ZoomProctoringService implements ExamProctoringService {
private final Cryptor cryptor; private final Cryptor cryptor;
private final AsyncService asyncService; private final AsyncService asyncService;
private final JSONMapper jsonMapper; private final JSONMapper jsonMapper;
private final ZoomRestTemplate zoomRestTemplate;
private final RemoteProctoringRoomDAO remoteProctoringRoomDAO; private final RemoteProctoringRoomDAO remoteProctoringRoomDAO;
private final AuthorizationService authorizationService; private final AuthorizationService authorizationService;
private final SEBClientInstructionService sebInstructionService; private final SEBClientInstructionService sebInstructionService;
@ -149,7 +151,6 @@ public class ZoomProctoringService implements ExamProctoringService {
this.cryptor = cryptor; this.cryptor = cryptor;
this.asyncService = asyncService; this.asyncService = asyncService;
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.zoomRestTemplate = new ZoomRestTemplate(this);
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO; this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
this.sebInstructionService = sebInstructionService; this.sebInstructionService = sebInstructionService;
@ -173,16 +174,26 @@ public class ZoomProctoringService implements ExamProctoringService {
try { try {
final ClientCredentials credentials = new ClientCredentials( final ProctoringServiceSettings proctoringServiceSettings = new ProctoringServiceSettings(
proctoringSettings.appKey, proctoringSettings.examId,
this.cryptor proctoringSettings.enableProctoring,
.encrypt(proctoringSettings.appSecret) proctoringSettings.serverType,
.getOrThrow());
final ResponseEntity<String> result = this.zoomRestTemplate
.testServiceConnection(
proctoringSettings.serverURL, proctoringSettings.serverURL,
credentials); proctoringSettings.collectingRoomSize,
proctoringSettings.enabledFeatures,
proctoringSettings.serviceInUse,
proctoringSettings.appKey,
this.cryptor.encrypt(proctoringSettings.appSecret).getOrThrow(),
proctoringSettings.accountId,
proctoringSettings.clientId,
this.cryptor.encrypt(proctoringSettings.clientSecret).getOrThrow(),
proctoringSettings.sdkKey,
this.cryptor.encrypt(proctoringSettings.sdkSecret).getOrThrow(),
proctoringSettings.useZoomAppClientForCollectingRoom);
final ZoomRestTemplate newRestTemplate = createNewRestTemplate(proctoringServiceSettings);
final ResponseEntity<String> result = newRestTemplate.testServiceConnection();
if (result.getStatusCode() != HttpStatus.OK) { if (result.getStatusCode() != HttpStatus.OK) {
throw new APIMessageException(Arrays.asList( throw new APIMessageException(Arrays.asList(
@ -415,13 +426,8 @@ public class ZoomProctoringService implements ExamProctoringService {
roomData.getAdditionalRoomData(), roomData.getAdditionalRoomData(),
AdditionalZoomRoomData.class); AdditionalZoomRoomData.class);
final ClientCredentials credentials = new ClientCredentials(
proctoringSettings.appKey,
proctoringSettings.appSecret);
this.deleteAdHocMeeting( this.deleteAdHocMeeting(
proctoringSettings, proctoringSettings,
credentials,
additionalZoomRoomData.meeting_id, additionalZoomRoomData.meeting_id,
additionalZoomRoomData.user_id) additionalZoomRoomData.user_id)
.getOrThrow(); .getOrThrow();
@ -521,15 +527,10 @@ public class ZoomProctoringService implements ExamProctoringService {
final ProctoringServiceSettings proctoringSettings) { final ProctoringServiceSettings proctoringSettings) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final ClientCredentials credentials = new ClientCredentials(
proctoringSettings.appKey,
proctoringSettings.appSecret);
final ZoomRestTemplate zoomRestTemplate = getZoomRestTemplate(proctoringSettings);
// First create a new user/host for the new room // First create a new user/host for the new room
final ResponseEntity<String> createUser = this.zoomRestTemplate.createUser( final ResponseEntity<String> createUser = zoomRestTemplate.createUser(roomName);
proctoringSettings.serverURL,
credentials,
roomName);
final int statusCodeValue = createUser.getStatusCodeValue(); final int statusCodeValue = createUser.getStatusCodeValue();
if (statusCodeValue >= 400) { if (statusCodeValue >= 400) {
@ -540,16 +541,11 @@ public class ZoomProctoringService implements ExamProctoringService {
createUser.getBody(), createUser.getBody(),
UserResponse.class); UserResponse.class);
this.zoomRestTemplate.applyUserSettings( zoomRestTemplate.applyUserSettings(userResponse.id);
proctoringSettings.serverURL,
credentials,
userResponse.id);
// Then create new meeting with the ad-hoc user/host // Then create new meeting with the ad-hoc user/host
final CharSequence meetingPwd = UUID.randomUUID().toString().subSequence(0, 9); final CharSequence meetingPwd = UUID.randomUUID().toString().subSequence(0, 9);
final ResponseEntity<String> createMeeting = this.zoomRestTemplate.createMeeting( final ResponseEntity<String> createMeeting = zoomRestTemplate.createMeeting(
proctoringSettings.serverURL,
credentials,
userResponse.id, userResponse.id,
subject, subject,
duration, duration,
@ -580,69 +576,18 @@ public class ZoomProctoringService implements ExamProctoringService {
private Result<Void> deleteAdHocMeeting( private Result<Void> deleteAdHocMeeting(
final ProctoringServiceSettings proctoringSettings, final ProctoringServiceSettings proctoringSettings,
final ClientCredentials credentials,
final Long meetingId, final Long meetingId,
final String userId) { final String userId) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
this.zoomRestTemplate.deleteMeeting(proctoringSettings.serverURL, credentials, meetingId); final ZoomRestTemplate zoomRestTemplate = getZoomRestTemplate(proctoringSettings);
this.zoomRestTemplate.deleteUser(proctoringSettings.serverURL, credentials, userId); zoomRestTemplate.deleteMeeting(meetingId);
zoomRestTemplate.deleteUser(userId);
}); });
} }
private String createJWTForAPIAccess(
final ClientCredentials credentials,
final Long expTime) {
try {
final CharSequence decryptedSecret = this.cryptor
.decrypt(credentials.secret)
.getOrThrow();
final StringBuilder builder = new StringBuilder();
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String jwtHeaderPart = urlEncoder
.encodeToString(ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8));
final String jwtPayload = String.format(
ZOOM_API_ACCESS_TOKEN_PAYLOAD
.replaceAll(" ", "")
.replaceAll("\n", ""),
credentials.clientIdAsString(),
expTime);
if (log.isTraceEnabled()) {
log.trace("Zoom API Token payload: {}", jwtPayload);
}
final String jwtPayloadPart = urlEncoder
.encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8));
final String message = jwtHeaderPart + "." + jwtPayloadPart;
final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG);
final SecretKeySpec secret_key = new SecretKeySpec(
Utils.toByteArray(decryptedSecret),
TOKEN_ENCODE_ALG);
sha256_HMAC.init(secret_key);
final String hash = urlEncoder
.encodeToString(sha256_HMAC.doFinal(Utils.toByteArray(message)));
builder.append(message)
.append(".")
.append(hash);
return builder.toString();
} catch (final Exception e) {
throw new RuntimeException("Failed to create JWT for Zoom API access: ", e);
}
}
private String createSDKJWT( private String createSDKJWT(
final ClientCredentials sdkCredentials, final ClientCredentials sdkCredentials,
final Long expTime, final Long expTime,
@ -706,62 +651,6 @@ public class ZoomProctoringService implements ExamProctoringService {
} }
} }
// private String createJWTForSDKAccess(
// final ClientCredentials sdkCredentials,
// final Long expTime) {
//
// try {
//
// final CharSequence decryptedSecret = this.cryptor
// .decrypt(sdkCredentials.secret)
// .getOrThrow();
//
// final StringBuilder builder = new StringBuilder();
// final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
//
// final String jwtHeaderPart = urlEncoder
// .encodeToString(ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8));
//
// // epoch time in seconds
// final long secondsNow = Utils.getSecondsNow();
//
// final String jwtPayload = String.format(
// ZOOM_SDK_ACCESS_TOKEN_PAYLOAD
// .replaceAll(" ", "")
// .replaceAll("\n", ""),
// sdkCredentials.clientIdAsString(),
// secondsNow,
// expTime,
// expTime);
//
// if (log.isTraceEnabled()) {
// log.trace("Zoom SDK Token payload: {}", jwtPayload);
// }
//
// final String jwtPayloadPart = urlEncoder
// .encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8));
//
// final String message = jwtHeaderPart + "." + jwtPayloadPart;
//
// final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG);
// final SecretKeySpec secret_key = new SecretKeySpec(
// Utils.toByteArray(decryptedSecret),
// TOKEN_ENCODE_ALG);
//
// sha256_HMAC.init(secret_key);
// final String hash = urlEncoder
// .encodeToString(sha256_HMAC.doFinal(Utils.toByteArray(message)));
//
// builder.append(message)
// .append(".")
// .append(hash);
//
// return builder.toString();
// } catch (final Exception e) {
// throw new RuntimeException("Failed to create JWT for Zoom API access: ", e);
// }
// }
private long expiryTimeforExam(final ProctoringServiceSettings examProctoring) { private long expiryTimeforExam(final ProctoringServiceSettings examProctoring) {
// NOTE: following is the original code that includes the exam end time but seems to make trouble for OLAT // NOTE: following is the original code that includes the exam end time but seems to make trouble for OLAT
@ -795,44 +684,93 @@ public class ZoomProctoringService implements ExamProctoringService {
return nowPlusOneDayInSeconds - 10; return nowPlusOneDayInSeconds - 10;
} }
private final static class ZoomRestTemplate { private final LinkedHashMap<Long, ZoomRestTemplate> restTemplatesCache = new LinkedHashMap<>();
private static final int LIZENSED_USER = 2; private synchronized ZoomRestTemplate getZoomRestTemplate(final ProctoringServiceSettings proctoringSettings) {
private static final String API_TEST_ENDPOINT = "v2/users"; if (!this.restTemplatesCache.containsKey(proctoringSettings.examId)) {
private static final String API_CREATE_USER_ENDPOINT = "v2/users"; this.restTemplatesCache.put(
private static final String API_APPLY_USER_SETTINGS_ENDPOINT = "v2/users/{userId}/settings"; proctoringSettings.examId,
private static final String API_DELETE_USER_ENDPOINT = "v2/users/{userid}?action=delete"; createNewRestTemplate(proctoringSettings));
private static final String API_USER_CUST_CREATE = "custCreate"; } else {
private static final String API_ZOOM_ROOM_USER = "SEBProctoringRoomUser"; final ZoomRestTemplate zoomRestTemplate = this.restTemplatesCache.get(proctoringSettings.examId);
private static final String API_CREATE_MEETING_ENDPOINT = "v2/users/{userid}/meetings"; if (!zoomRestTemplate.isValid(proctoringSettings)) {
private static final String API_DELETE_MEETING_ENDPOINT = "v2/meetings/{meetingid}"; this.restTemplatesCache.remove(proctoringSettings.examId);
private static final String API_END_MEETING_ENDPOINT = "v2/meetings/{meetingid}/status"; this.restTemplatesCache.put(
proctoringSettings.examId,
createNewRestTemplate(proctoringSettings));
}
}
private final ZoomProctoringService zoomProctoringService; if (this.restTemplatesCache.size() > 5) {
private final RestTemplate restTemplate; final Long toRemove = this.restTemplatesCache.keySet().iterator().next();
private final CircuitBreaker<ResponseEntity<String>> circuitBreaker; if (!Objects.equals(proctoringSettings.examId, toRemove)) {
this.restTemplatesCache.remove(toRemove);
}
}
public ZoomRestTemplate(final ZoomProctoringService zoomProctoringService) { return this.restTemplatesCache.get(proctoringSettings.examId);
}
private ZoomRestTemplate createNewRestTemplate(final ProctoringServiceSettings proctoringSettings) {
if (StringUtils.isNoneBlank(proctoringSettings.accountId)) {
log.info("Create new OAuthZoomRestTemplate for settings: {}", proctoringSettings);
return new OAuthZoomRestTemplate(this, proctoringSettings);
} else {
log.info("Create new JWTZoomRestTemplate for settings: {}", proctoringSettings);
return new JWTZoomRestTemplate(this, proctoringSettings);
}
}
private static abstract class ZoomRestTemplate {
protected static final int LIZENSED_USER = 2;
protected static final String API_TEST_ENDPOINT = "v2/users";
protected static final String API_CREATE_USER_ENDPOINT = "v2/users";
protected static final String API_APPLY_USER_SETTINGS_ENDPOINT = "v2/users/{userId}/settings";
protected static final String API_DELETE_USER_ENDPOINT = "v2/users/{userid}?action=delete";
protected static final String API_USER_CUST_CREATE = "custCreate";
protected static final String API_ZOOM_ROOM_USER = "SEBProctoringRoomUser";
protected static final String API_CREATE_MEETING_ENDPOINT = "v2/users/{userid}/meetings";
protected static final String API_DELETE_MEETING_ENDPOINT = "v2/meetings/{meetingid}";
protected static final String API_END_MEETING_ENDPOINT = "v2/meetings/{meetingid}/status";
protected final ZoomProctoringService zoomProctoringService;
protected final CircuitBreaker<ResponseEntity<String>> circuitBreaker;
protected final ProctoringServiceSettings proctoringSettings;
protected ClientCredentials credentials;
protected RestTemplate restTemplate;
public ZoomRestTemplate(
final ZoomProctoringService zoomProctoringService,
final ProctoringServiceSettings proctoringSettings) {
this.zoomProctoringService = zoomProctoringService; this.zoomProctoringService = zoomProctoringService;
this.restTemplate = new RestTemplate(zoomProctoringService.clientHttpRequestFactoryService
.getClientHttpRequestFactory()
.getOrThrow());
this.circuitBreaker = zoomProctoringService.asyncService.createCircuitBreaker( this.circuitBreaker = zoomProctoringService.asyncService.createCircuitBreaker(
2, 2,
10 * Constants.SECOND_IN_MILLIS, 10 * Constants.SECOND_IN_MILLIS,
10 * Constants.SECOND_IN_MILLIS); 10 * Constants.SECOND_IN_MILLIS);
this.proctoringSettings = proctoringSettings;
initConnection();
} }
public ResponseEntity<String> testServiceConnection( protected abstract void initConnection();
final String zoomServerUrl,
final ClientCredentials credentials) { protected abstract HttpHeaders getHeaders();
boolean isValid(final ProctoringServiceSettings proctoringSettings) {
return Objects.equals(proctoringSettings.serverURL, this.proctoringSettings.serverURL) &&
Objects.equals(proctoringSettings.appKey, this.proctoringSettings.appKey) &&
Objects.equals(proctoringSettings.accountId, this.proctoringSettings.accountId) &&
Objects.equals(proctoringSettings.clientId, this.proctoringSettings.clientId);
}
public ResponseEntity<String> testServiceConnection() {
try { try {
final String url = UriComponentsBuilder final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl) .fromUriString(this.proctoringSettings.serverURL)
.path(API_TEST_ENDPOINT) .path(API_TEST_ENDPOINT)
.queryParam("status", "active") .queryParam("status", "active")
.queryParam("page_size", "10") .queryParam("page_size", "10")
@ -840,7 +778,7 @@ public class ZoomProctoringService implements ExamProctoringService {
.queryParam("data_type", "Json") .queryParam("data_type", "Json")
.build() .build()
.toUriString(); .toUriString();
return exchange(url, HttpMethod.GET, credentials); return exchange(url, HttpMethod.GET);
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to test zoom service connection: ", e); log.error("Failed to test zoom service connection: ", e);
@ -848,17 +786,15 @@ public class ZoomProctoringService implements ExamProctoringService {
} }
} }
public ResponseEntity<String> createUser( public ResponseEntity<String> createUser(final String roomName) {
final String zoomServerUrl,
final ClientCredentials credentials,
final String roomName) {
try { try {
final String url = UriComponentsBuilder final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl) .fromUriString(this.proctoringSettings.serverURL)
.path(API_CREATE_USER_ENDPOINT) .path(API_CREATE_USER_ENDPOINT)
.toUriString(); .toUriString();
final String host = new URL(zoomServerUrl).getHost(); final String host = new URL(this.proctoringSettings.serverURL).getHost();
final CreateUserRequest createUserRequest = new CreateUserRequest( final CreateUserRequest createUserRequest = new CreateUserRequest(
API_USER_CUST_CREATE, API_USER_CUST_CREATE,
new CreateUserRequest.UserInfo( new CreateUserRequest.UserInfo(
@ -868,7 +804,7 @@ public class ZoomProctoringService implements ExamProctoringService {
API_ZOOM_ROOM_USER)); API_ZOOM_ROOM_USER));
final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createUserRequest); final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createUserRequest);
final HttpHeaders headers = getHeaders(credentials); final HttpHeaders headers = getHeaders();
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
return exchange(url, HttpMethod.POST, body, headers); return exchange(url, HttpMethod.POST, body, headers);
@ -878,13 +814,10 @@ public class ZoomProctoringService implements ExamProctoringService {
} }
} }
public ResponseEntity<String> applyUserSettings( public ResponseEntity<String> applyUserSettings(final String userId) {
final String zoomServerUrl,
final ClientCredentials credentials,
final String userId) {
try { try {
final String url = UriComponentsBuilder final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl) .fromUriString(this.proctoringSettings.serverURL)
.path(API_APPLY_USER_SETTINGS_ENDPOINT) .path(API_APPLY_USER_SETTINGS_ENDPOINT)
.buildAndExpand(userId) .buildAndExpand(userId)
.normalize() .normalize()
@ -892,7 +825,7 @@ public class ZoomProctoringService implements ExamProctoringService {
final ApplyUserSettingsRequest applySettingsRequest = new ApplyUserSettingsRequest(); final ApplyUserSettingsRequest applySettingsRequest = new ApplyUserSettingsRequest();
final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(applySettingsRequest); final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(applySettingsRequest);
final HttpHeaders headers = getHeaders(credentials); final HttpHeaders headers = getHeaders();
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
final ResponseEntity<String> exchange = exchange(url, HttpMethod.PATCH, body, headers); final ResponseEntity<String> exchange = exchange(url, HttpMethod.PATCH, body, headers);
@ -904,8 +837,6 @@ public class ZoomProctoringService implements ExamProctoringService {
} }
public ResponseEntity<String> createMeeting( public ResponseEntity<String> createMeeting(
final String zoomServerUrl,
final ClientCredentials credentials,
final String userId, final String userId,
final String topic, final String topic,
final int duration, final int duration,
@ -915,7 +846,7 @@ public class ZoomProctoringService implements ExamProctoringService {
try { try {
final String url = UriComponentsBuilder final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl) .fromUriString(this.proctoringSettings.serverURL)
.path(API_CREATE_MEETING_ENDPOINT) .path(API_CREATE_MEETING_ENDPOINT)
.buildAndExpand(userId) .buildAndExpand(userId)
.toUriString(); .toUriString();
@ -927,7 +858,7 @@ public class ZoomProctoringService implements ExamProctoringService {
waitingRoom); waitingRoom);
final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createRoomRequest); final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createRoomRequest);
final HttpHeaders headers = getHeaders(credentials); final HttpHeaders headers = getHeaders();
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
return exchange(url, HttpMethod.POST, body, headers); return exchange(url, HttpMethod.POST, body, headers);
@ -937,21 +868,18 @@ public class ZoomProctoringService implements ExamProctoringService {
} }
} }
public ResponseEntity<String> deleteMeeting( public ResponseEntity<String> deleteMeeting(final Long meetingId) {
final String zoomServerUrl,
final ClientCredentials credentials,
final Long meetingId) {
// try to set set meeting status to ended first // try to set set meeting status to ended first
try { try {
final String url = UriComponentsBuilder final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl) .fromUriString(this.proctoringSettings.serverURL)
.path(API_END_MEETING_ENDPOINT) .path(API_END_MEETING_ENDPOINT)
.buildAndExpand(meetingId) .buildAndExpand(meetingId)
.toUriString(); .toUriString();
final HttpHeaders headers = getHeaders(credentials); final HttpHeaders headers = getHeaders();
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
final ResponseEntity<String> exchange = exchange( final ResponseEntity<String> exchange = exchange(
@ -976,12 +904,12 @@ public class ZoomProctoringService implements ExamProctoringService {
try { try {
final String url = UriComponentsBuilder final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl) .fromUriString(this.proctoringSettings.serverURL)
.path(API_DELETE_MEETING_ENDPOINT) .path(API_DELETE_MEETING_ENDPOINT)
.buildAndExpand(meetingId) .buildAndExpand(meetingId)
.toUriString(); .toUriString();
return exchange(url, HttpMethod.DELETE, credentials); return exchange(url, HttpMethod.DELETE);
} catch (final Exception e) { } catch (final Exception e) {
log.warn("Failed to delete Zoom ad-hoc meeting: {} cause: {} / {}", log.warn("Failed to delete Zoom ad-hoc meeting: {} cause: {} / {}",
@ -992,20 +920,17 @@ public class ZoomProctoringService implements ExamProctoringService {
} }
} }
public ResponseEntity<String> deleteUser( public ResponseEntity<String> deleteUser(final String userId) {
final String zoomServerUrl,
final ClientCredentials credentials,
final String userId) {
try { try {
final String url = UriComponentsBuilder final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl) .fromUriString(this.proctoringSettings.serverURL)
.path(API_DELETE_USER_ENDPOINT) .path(API_DELETE_USER_ENDPOINT)
.buildAndExpand(userId) .buildAndExpand(userId)
.normalize() .normalize()
.toUriString(); .toUriString();
return exchange(url, HttpMethod.DELETE, credentials); return exchange(url, HttpMethod.DELETE);
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to delete Zoom ad-hoc user with id: {} cause: {} / {}", log.error("Failed to delete Zoom ad-hoc user with id: {} cause: {} / {}",
@ -1016,24 +941,11 @@ public class ZoomProctoringService implements ExamProctoringService {
} }
} }
private HttpHeaders getHeaders(final ClientCredentials credentials) {
final String jwt = this.zoomProctoringService
.createJWTForAPIAccess(
credentials,
System.currentTimeMillis() + Constants.MINUTE_IN_MILLIS);
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwt);
httpHeaders.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
return httpHeaders;
}
private ResponseEntity<String> exchange( private ResponseEntity<String> exchange(
final String url, final String url,
final HttpMethod method, final HttpMethod method) {
final ClientCredentials credentials) {
return exchange(url, method, null, getHeaders(credentials)); return exchange(url, method, null, getHeaders());
} }
private ResponseEntity<String> exchange( private ResponseEntity<String> exchange(
@ -1070,4 +982,138 @@ public class ZoomProctoringService implements ExamProctoringService {
} }
} }
private final static class OAuthZoomRestTemplate extends ZoomRestTemplate {
private BaseOAuth2ProtectedResourceDetails resource;
public OAuthZoomRestTemplate(
final ZoomProctoringService zoomProctoringService,
final ProctoringServiceSettings proctoringSettings) {
super(zoomProctoringService, proctoringSettings);
}
@Override
protected void initConnection() {
if (this.resource == null) {
this.credentials = new ClientCredentials(
this.proctoringSettings.clientId,
this.proctoringSettings.clientSecret);
final CharSequence decryptedSecret = this.zoomProctoringService.cryptor
.decrypt(this.credentials.secret)
.getOrThrow();
this.resource = new BaseOAuth2ProtectedResourceDetails();
this.resource.setAccessTokenUri(this.proctoringSettings.serverURL + "/oauth/token");
this.resource.setClientId(this.credentials.clientIdAsString());
this.resource.setClientSecret(decryptedSecret.toString());
this.resource.setGrantType("account_credentials");
final DefaultAccessTokenRequest defaultAccessTokenRequest = new DefaultAccessTokenRequest();
defaultAccessTokenRequest.set("account_id", this.proctoringSettings.accountId);
this.restTemplate = new OAuth2RestTemplate(
this.resource,
new DefaultOAuth2ClientContext(defaultAccessTokenRequest));
}
}
@Override
public HttpHeaders getHeaders() {
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
return httpHeaders;
}
}
@Deprecated
private final static class JWTZoomRestTemplate extends ZoomRestTemplate {
public JWTZoomRestTemplate(
final ZoomProctoringService zoomProctoringService,
final ProctoringServiceSettings proctoringSettings) {
super(zoomProctoringService, proctoringSettings);
}
@Override
public void initConnection() {
if (this.restTemplate == null) {
this.credentials = new ClientCredentials(
this.proctoringSettings.appKey,
this.proctoringSettings.appSecret);
this.restTemplate = new RestTemplate(this.zoomProctoringService.clientHttpRequestFactoryService
.getClientHttpRequestFactory()
.getOrThrow());
}
}
@Override
public HttpHeaders getHeaders() {
final String jwt = this.createJWTForAPIAccess(
this.credentials,
System.currentTimeMillis() + Constants.MINUTE_IN_MILLIS);
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwt);
httpHeaders.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
return httpHeaders;
}
private String createJWTForAPIAccess(
final ClientCredentials credentials,
final Long expTime) {
try {
final CharSequence decryptedSecret = this.zoomProctoringService.cryptor
.decrypt(credentials.secret)
.getOrThrow();
final StringBuilder builder = new StringBuilder();
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String jwtHeaderPart = urlEncoder
.encodeToString(ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8));
final String jwtPayload = String.format(
ZOOM_API_ACCESS_TOKEN_PAYLOAD
.replaceAll(" ", "")
.replaceAll("\n", ""),
credentials.clientIdAsString(),
expTime);
if (log.isTraceEnabled()) {
log.trace("Zoom API Token payload: {}", jwtPayload);
}
final String jwtPayloadPart = urlEncoder
.encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8));
final String message = jwtHeaderPart + "." + jwtPayloadPart;
final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG);
final SecretKeySpec secret_key = new SecretKeySpec(
Utils.toByteArray(decryptedSecret),
TOKEN_ENCODE_ALG);
sha256_HMAC.init(secret_key);
final String hash = urlEncoder
.encodeToString(sha256_HMAC.doFinal(Utils.toByteArray(message)));
builder.append(message)
.append(".")
.append(hash);
return builder.toString();
} catch (final Exception e) {
throw new RuntimeException("Failed to create JWT for Zoom API access: ", e);
}
}
}
} }

View file

@ -236,7 +236,7 @@ public interface ZoomRoomRequestResponse {
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
static class Settings { static class Settings {
@JsonProperty final boolean host_video = false; @JsonProperty final boolean host_video = true;
@JsonProperty final boolean mute_upon_entry = false; @JsonProperty final boolean mute_upon_entry = false;
@JsonProperty final boolean join_before_host; @JsonProperty final boolean join_before_host;
@JsonProperty final int jbh_time = 0; @JsonProperty final int jbh_time = 0;

View file

@ -25,7 +25,6 @@ public class ProctoringSettingsValidator
return false; return false;
} }
//if (value.enableProctoring) {
if (value.serverType == ProctoringServerType.JITSI_MEET || value.serverType == ProctoringServerType.ZOOM) { if (value.serverType == ProctoringServerType.JITSI_MEET || value.serverType == ProctoringServerType.ZOOM) {
boolean passed = true; boolean passed = true;
@ -37,6 +36,7 @@ public class ProctoringSettingsValidator
passed = false; passed = false;
} }
if (value.serverType == ProctoringServerType.JITSI_MEET) {
if (StringUtils.isBlank(value.appKey)) { if (StringUtils.isBlank(value.appKey)) {
context.disableDefaultConstraintViolation(); context.disableDefaultConstraintViolation();
context context
@ -52,10 +52,28 @@ public class ProctoringSettingsValidator
.addPropertyNode("appSecret").addConstraintViolation(); .addPropertyNode("appSecret").addConstraintViolation();
passed = false; passed = false;
} }
}
if (value.serverType == ProctoringServerType.ZOOM) {
if (StringUtils.isBlank(value.sdkKey)) {
context.disableDefaultConstraintViolation();
context
.buildConstraintViolationWithTemplate("proctoringSettings:sdkKey:notNull")
.addPropertyNode("sdkKey").addConstraintViolation();
passed = false;
}
if (StringUtils.isBlank(value.sdkSecret)) {
context.disableDefaultConstraintViolation();
context
.buildConstraintViolationWithTemplate("proctoringSettings:sdkSecret:notNull")
.addPropertyNode("sdkSecret").addConstraintViolation();
passed = false;
}
}
return passed; return passed;
} }
//}
return true; return true;
} }

View file

@ -71,6 +71,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateService;
import ch.ethz.seb.sebserver.webservice.servicelayer.institution.SecurityKeyService; import ch.ethz.seb.sebserver.webservice.servicelayer.institution.SecurityKeyService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringRoomService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
@ -87,6 +88,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
private final ExamSessionService examSessionService; private final ExamSessionService examSessionService;
private final SEBRestrictionService sebRestrictionService; private final SEBRestrictionService sebRestrictionService;
private final SecurityKeyService securityKeyService; private final SecurityKeyService securityKeyService;
private final ExamProctoringRoomService examProctoringRoomService;
public ExamAdministrationController( public ExamAdministrationController(
final AuthorizationService authorization, final AuthorizationService authorization,
@ -101,7 +103,8 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
final ExamTemplateService examTemplateService, final ExamTemplateService examTemplateService,
final ExamSessionService examSessionService, final ExamSessionService examSessionService,
final SEBRestrictionService sebRestrictionService, final SEBRestrictionService sebRestrictionService,
final SecurityKeyService securityKeyService) { final SecurityKeyService securityKeyService,
final ExamProctoringRoomService examProctoringRoomService) {
super(authorization, super(authorization,
bulkActionService, bulkActionService,
@ -118,6 +121,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
this.examSessionService = examSessionService; this.examSessionService = examSessionService;
this.sebRestrictionService = sebRestrictionService; this.sebRestrictionService = sebRestrictionService;
this.securityKeyService = securityKeyService; this.securityKeyService = securityKeyService;
this.examProctoringRoomService = examProctoringRoomService;
} }
@Override @Override
@ -486,6 +490,29 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
.getOrThrow(); .getOrThrow();
} }
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_PROCTORING_RESET_PATH_SEGMENT,
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE)
public Exam resetProctoringServiceSettings(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@PathVariable(API.PARAM_MODEL_ID) final Long examId) {
checkModifyPrivilege(institutionId);
return this.entityDAO
.byPK(examId)
.flatMap(this.examProctoringRoomService::cleanupAllRooms)
.flatMap(this.examAdminService::resetProctoringSettings)
.getOrThrow();
}
// **** Proctoring // **** Proctoring
// **************************************************************************** // ****************************************************************************

View file

@ -483,6 +483,7 @@ sebserver.quizdiscovery.quiz.details.additional.time_limit.toolitp=The time limi
################################ ################################
sebserver.exam.list.actions= sebserver.exam.list.actions=
sebserver.exam.security.actions=&nbsp;
sebserver.exam.list.title=Exam sebserver.exam.list.title=Exam
sebserver.exam.list.title.subtitle= sebserver.exam.list.title.subtitle=
sebserver.exam.list.column.institution=Institution sebserver.exam.list.column.institution=Institution
@ -795,14 +796,28 @@ sebserver.exam.proctoring.form.url=Server URL
sebserver.exam.proctoring.form.url.tooltip=The proctoring server URL sebserver.exam.proctoring.form.url.tooltip=The proctoring server URL
sebserver.exam.proctoring.form.collectingRoomSize=Collecting Room Size sebserver.exam.proctoring.form.collectingRoomSize=Collecting Room Size
sebserver.exam.proctoring.form.collectingRoomSize.tooltip=The size of proctor rooms to collect connecting SEB clients into. sebserver.exam.proctoring.form.collectingRoomSize.tooltip=The size of proctor rooms to collect connecting SEB clients into.
sebserver.exam.proctoring.form.appkey=App Key
sebserver.exam.proctoring.form.appkey.tooltip=The application key of the proctoring service server sebserver.exam.proctoring.form.appkey.jitsi=App Key
sebserver.exam.proctoring.form.secret=App Secret sebserver.exam.proctoring.form.appkey.jitsi.tooltip=The application key of the proctoring service server
sebserver.exam.proctoring.form.secret.tooltip=The secret used to access the proctoring service sebserver.exam.proctoring.form.secret.jitsi=App Secret
sebserver.exam.proctoring.form.sdkkey=SDK Key (Zoom - MacOS/iOS) sebserver.exam.proctoring.form.secret.jitsi.tooltip=The secret used to access the proctoring service
sebserver.exam.proctoring.form.sdkkey.tooltip=The SDK key and secret are used for live proctoring with SEB clients for iOS and/or MacOS<br/>This is only relevant for proctoring with Zoom service.
sebserver.exam.proctoring.form.sdksecret=SDK Secret (Zoom - MacOS/iOS) sebserver.exam.proctoring.form.appkey.zoom=App Key (Deprecated)
sebserver.exam.proctoring.form.sdksecret.tooltip=The SDK key and secret are used for live proctoring with SEB clients for iOS and/or MacOS<br/>This is only relevant for proctoring with Zoom service. sebserver.exam.proctoring.form.appkey.zoom.tooltip=Since Zoom no longer supports JWT App, this is deprecated and the below Account ID, Client ID and Client Secret must be used
sebserver.exam.proctoring.form.secret.zoom=App Secret (Deprecated)
sebserver.exam.proctoring.form.secret.zoom.tooltip=Since Zoom no longer supports JWT App, this is deprecated and the below Account ID, Client ID and Client Secret must be used
sebserver.exam.proctoring.form.accountId=Account ID
sebserver.exam.proctoring.form.accountId.tooltip=This is the Account ID from the Zoom Server-to-Server OAuth application and needed by SEB server to automatically create meetings
sebserver.exam.proctoring.form.clientId=Client ID
sebserver.exam.proctoring.form.clientId.tooltip=This is the Client ID from the Zoom Server-to-Server OAuth application and needed by SEB server to automatically create meetings
sebserver.exam.proctoring.form.clientSecret=Client Secret
sebserver.exam.proctoring.form.clientSecret.tooltip=This is the Client Secret from the Zoom Server-to-Server OAuth application and needed by SEB server to automatically create meetings
sebserver.exam.proctoring.form.sdkkey=SDK Key
sebserver.exam.proctoring.form.sdkkey.tooltip=The SDK key and secret are used for live proctoring with SEB clients<br/>This is only relevant for proctoring with Zoom service.
sebserver.exam.proctoring.form.sdksecret=SDK Secret
sebserver.exam.proctoring.form.sdksecret.tooltip=The SDK key and secret are used for live proctoring with SEB clients<br/>This is only relevant for proctoring with Zoom service.
sebserver.exam.proctoring.form.features=Enabled Features sebserver.exam.proctoring.form.features=Enabled Features
sebserver.exam.proctoring.form.features.TOWN_HALL=Town-Hall Room sebserver.exam.proctoring.form.features.TOWN_HALL=Town-Hall Room
sebserver.exam.proctoring.form.features.ONE_TO_ONE=One to One Room sebserver.exam.proctoring.form.features.ONE_TO_ONE=One to One Room
@ -813,6 +828,10 @@ sebserver.exam.proctoring.form.features.SEND_REJOIN_COLLECTING_ROOM=Force rejoin
sebserver.exam.proctoring.form.features.RESET_BROADCAST_ON_LAVE=Reset broadcast on leave sebserver.exam.proctoring.form.features.RESET_BROADCAST_ON_LAVE=Reset broadcast on leave
sebserver.exam.proctoring.form.useZoomAppClient=Use Zoom App-Client sebserver.exam.proctoring.form.useZoomAppClient=Use Zoom App-Client
sebserver.exam.proctoring.form.useZoomAppClient.tooltip=If this is set SEB Server opens a start link for the meeting instead of a new popup-window with the Zoom Web Client.<br/>A Zoom App Client must already be installed on the proctor's device or can be installed by following the instructions shown in the browser window. sebserver.exam.proctoring.form.useZoomAppClient.tooltip=If this is set SEB Server opens a start link for the meeting instead of a new popup-window with the Zoom Web Client.<br/>A Zoom App Client must already be installed on the proctor's device or can be installed by following the instructions shown in the browser window.
sebserver.exam.proctoring.form.saveSettings=Save Settings
sebserver.exam.proctoring.form.resetSettings=Reset Settings
sebserver.exam.proctoring.form.resetConfirm=Reset will first cleanup and remove all existing proctoring rooms for this exam and then reset to default or template settings.<br/>Please make sure there are no active SEB client connections for this exam, otherwise this reset will be denied.<br/><br/>Do you want to reset proctoring for this exam now?
sebserver.exam.proctoring.type.servertype.JITSI_MEET=Jitsi Meet Server sebserver.exam.proctoring.type.servertype.JITSI_MEET=Jitsi Meet Server
sebserver.exam.proctoring.type.servertype.JITSI_MEET.tooltip=Use a Jitsi Meet server for proctoring sebserver.exam.proctoring.type.servertype.JITSI_MEET.tooltip=Use a Jitsi Meet server for proctoring
@ -829,7 +848,9 @@ sebserver.exam.proctoring.collecting.close.error=Failed to close the collecting
sebserver.exam.signaturekey.action.edit=App Signature Key sebserver.exam.signaturekey.action.edit=App Signature Key
sebserver.exam.signaturekey.action.save=Save Settings sebserver.exam.signaturekey.action.save=Save Settings
sebserver.exam.signaturekey.action.cancel=Cancel and back to Exam sebserver.exam.signaturekey.action.cancel=Cancel and back to Exam
sebserver.exam.signaturekey.action.back=Back to Exam
sebserver.exam.signaturekey.action.addGrant=Add Security Grant sebserver.exam.signaturekey.action.addGrant=Add Security Grant
sebserver.exam.signaturekey.action.showASK=Show App Signature Key
sebserver.exam.signaturekey.action.showGrant=Show Security Grant sebserver.exam.signaturekey.action.showGrant=Show Security Grant
sebserver.exam.signaturekey.action.deleteGrant=Delete Security Grant sebserver.exam.signaturekey.action.deleteGrant=Delete Security Grant
sebserver.exam.signaturekey.title=App Signature Key Overview sebserver.exam.signaturekey.title=App Signature Key Overview

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

View file

@ -3597,6 +3597,9 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
false, false,
"apiKey", "apiKey",
"apiSecret", "apiSecret",
"accountId",
"clientId",
"clientSecret",
"sdkKey", "sdkKey",
"sdkSecret", "sdkSecret",
false); false);
@ -3721,6 +3724,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
EnumSet.allOf(ProctoringFeature.class), EnumSet.allOf(ProctoringFeature.class),
true, true,
"appKey", "appSecret", "appKey", "appSecret",
"accountId", "clientId", "clientSecret",
"sdkKey", "sdkSecret", "sdkKey", "sdkSecret",
false); false);

View file

@ -78,7 +78,9 @@ public class ExamProctoringRoomServiceTest extends AdministrationAPIIntegrationT
2L, 2L,
new ProctoringServiceSettings( new ProctoringServiceSettings(
2L, true, ProctoringServerType.JITSI_MEET, "", 1, null, false, 2L, true, ProctoringServerType.JITSI_MEET, "", 1, null, false,
"app-key", "app.secret", "sdk-key", "sdk.secret", false)); "app-key", "app.secret", "accountId",
"clientId",
"clientSecret", "sdk-key", "sdk.secret", false));
assertTrue(this.examAdminService.isProctoringEnabled(2L).get()); assertTrue(this.examAdminService.isProctoringEnabled(2L).get());
} }

View file

@ -43,7 +43,7 @@ public class ExamJITSIProctoringServiceTest {
final ProctoringServiceSettings proctoringServiceSettings = new ProctoringServiceSettings( final ProctoringServiceSettings proctoringServiceSettings = new ProctoringServiceSettings(
1L, true, ProctoringServerType.JITSI_MEET, "URL?", 1L, true, ProctoringServerType.JITSI_MEET, "URL?",
null, null, null, null, null, null, null, null); null, null, null, null, null, null, null, null, null, null, null);
final Result<Boolean> testExamProctoring = jitsiProctoringService final Result<Boolean> testExamProctoring = jitsiProctoringService
.testExamProctoring(proctoringServiceSettings); .testExamProctoring(proctoringServiceSettings);
@ -59,7 +59,9 @@ public class ExamJITSIProctoringServiceTest {
final ProctoringServiceSettings proctoringServiceSettings = new ProctoringServiceSettings( final ProctoringServiceSettings proctoringServiceSettings = new ProctoringServiceSettings(
1L, true, ProctoringServerType.JITSI_MEET, "http://jitsi.ch", 1L, true, ProctoringServerType.JITSI_MEET, "http://jitsi.ch",
2, null, true, "key", "secret", null, null, false); 2, null, true, "key", "secret", "accountId",
"clientId",
"clientSecret", null, null, false);
final Result<ProctoringRoomConnection> proctorRoomConnection = jitsiProctoringService final Result<ProctoringRoomConnection> proctorRoomConnection = jitsiProctoringService
.getProctorRoomConnection(proctoringServiceSettings, "TestRoom", "Test-User"); .getProctorRoomConnection(proctoringServiceSettings, "TestRoom", "Test-User");