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_SEB_RESTRICTION_CHAPTERS_PATH_SEGMENT = "/chapters";
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_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_SERVER_TYPE = "serverType";
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_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_SECRET = "sdkSecret";
public static final String ATTR_COLLECTING_ROOM_SIZE = "collectingRoomSize";
public static final String ATTR_ENABLED_FEATURES = "enabledFeatures";
public static final String ATTR_COLLECT_ALL_ROOM_NAME = "collectAllRoomName";
@ -71,6 +79,15 @@ public class ProctoringServiceSettings implements Entity {
@JsonProperty(ATTR_APP_SECRET)
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)
public final String sdkKey;
@ -100,6 +117,9 @@ public class ProctoringServiceSettings implements Entity {
@JsonProperty(ATTR_SERVICE_IN_USE) final Boolean serviceInUse,
@JsonProperty(ATTR_APP_KEY) final String appKey,
@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_SECRET) final CharSequence sdkSecret,
@JsonProperty(ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM) final Boolean useZoomAppClientForCollectingRoom) {
@ -113,11 +133,50 @@ public class ProctoringServiceSettings implements Entity {
this.serviceInUse = serviceInUse;
this.appKey = StringUtils.trim(appKey);
this.appSecret = appSecret;
this.accountId = StringUtils.trim(accountId);
this.clientId = clientId;
this.clientSecret = clientSecret;
this.sdkKey = StringUtils.trim(sdkKey);
this.sdkSecret = sdkSecret;
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
public String getModelId() {
return (this.examId != null) ? String.valueOf(this.examId) : null;
@ -165,6 +224,18 @@ public class ProctoringServiceSettings implements Entity {
return this.appSecret;
}
public String getAccountId() {
return this.accountId;
}
public String getClientId() {
return this.clientId;
}
public CharSequence getClientSecret() {
return this.clientSecret;
}
public String getSdkKey() {
return this.sdkKey;
}
@ -222,18 +293,20 @@ public class ProctoringServiceSettings implements Entity {
builder.append(this.serverURL);
builder.append(", appKey=");
builder.append(this.appKey);
builder.append(", appSecret=");
builder.append("--");
builder.append(", accountId=");
builder.append(this.accountId);
builder.append(", clientId=");
builder.append(this.clientId);
builder.append(", sdkKey=");
builder.append(this.sdkKey);
builder.append(", sdkSecret=");
builder.append("--");
builder.append(", collectingRoomSize=");
builder.append(this.collectingRoomSize);
builder.append(", enabledFeatures=");
builder.append(this.enabledFeatures);
builder.append(", serviceInUse=");
builder.append(this.serviceInUse);
builder.append(", useZoomAppClientForCollectingRoom=");
builder.append(this.useZoomAppClientForCollectingRoom);
builder.append("]");
return builder.toString();
}

View file

@ -13,7 +13,7 @@ import java.util.Objects;
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.JsonIgnore;
@ -68,7 +68,7 @@ public class AppSignatureKeyInfo implements ModelIdAware {
@Override
public String getModelId() {
return this.key;
return StringUtils.isNoneBlank(this.key) ? this.key : "-1";
}
public String getKey() {

View file

@ -18,6 +18,7 @@ public enum ActionCategory {
LMS_SETUP_LIST(new LocTextKey("sebserver.lmssetup.list.actions"), 1),
QUIZ_LIST(new LocTextKey("sebserver.quizdiscovery.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),
INDICATOR_TEMPLATE_LIST(new LocTextKey("sebserver.examtemplate.indicator.list.actions"), 1),
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"),
ImageIcon.RESTRICTION,
PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM),
ActionCategory.EXAM_SECURITY),
EXAM_ENABLE_SEB_RESTRICTION(
new LocTextKey("sebserver.exam.action.sebrestriction.enable"),
ImageIcon.UNLOCK,
PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM),
ActionCategory.EXAM_SECURITY),
EXAM_DISABLE_SEB_RESTRICTION(
new LocTextKey("sebserver.exam.action.sebrestriction.disable"),
ImageIcon.LOCK,
PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM),
ActionCategory.EXAM_SECURITY),
EXAM_PROCTORING_ON(
new LocTextKey("sebserver.exam.proctoring.actions.open"),
ImageIcon.VISIBILITY,
PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM),
ActionCategory.EXAM_SECURITY),
EXAM_PROCTORING_OFF(
new LocTextKey("sebserver.exam.proctoring.actions.open"),
ImageIcon.VISIBILITY_OFF,
PageStateDefinitionImpl.EXAM_VIEW,
ActionCategory.FORM),
ActionCategory.EXAM_SECURITY),
EXAM_CONFIGURATION_NEW(
new LocTextKey("sebserver.exam.configuration.action.list.new"),
@ -419,12 +419,12 @@ public enum ActionDefinition {
new LocTextKey("sebserver.exam.signaturekey.action.edit"),
ImageIcon.SHIELD,
PageStateDefinitionImpl.SECURITY_KEY_EDIT,
ActionCategory.FORM),
ActionCategory.EXAM_SECURITY),
EXAM_SECURITY_KEY_DISABLED(
new LocTextKey("sebserver.exam.signaturekey.action.edit"),
ImageIcon.NO_SHIELD,
PageStateDefinitionImpl.SECURITY_KEY_EDIT,
ActionCategory.FORM),
ActionCategory.EXAM_SECURITY),
EXAM_RELOAD_SECURITY_KEY_VIEW(
new LocTextKey("sebserver.exam.signaturekey.action.edit"),
ImageIcon.SHIELD,
@ -441,11 +441,21 @@ public enum ActionDefinition {
ImageIcon.CANCEL,
PageStateDefinitionImpl.EXAM_VIEW,
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(
new LocTextKey("sebserver.exam.signaturekey.action.addGrant"),
ImageIcon.ADD,
PageStateDefinitionImpl.SECURITY_KEY_EDIT,
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(
new LocTextKey("sebserver.exam.signaturekey.action.showGrant"),
ImageIcon.SHOW,
@ -915,7 +925,7 @@ public enum ActionDefinition {
MONITOR_EXAM_BACK_TO_OVERVIEW(
new LocTextKey("sebserver.monitoring.exam.action.detail.view"),
ImageIcon.SHOW,
ImageIcon.BACK,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.FORM),
@ -1025,7 +1035,7 @@ public enum ActionDefinition {
ActionCategory.CLIENT_EVENT_LIST),
FINISHED_EXAM_BACK_TO_OVERVIEW(
new LocTextKey("sebserver.finished.exam.action.detail.view"),
ImageIcon.SHOW,
ImageIcon.BACK,
PageStateDefinitionImpl.FINISHED_EXAM,
ActionCategory.FORM),
FINISHED_EXAM_EXPORT_CSV(

View file

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

View file

@ -452,6 +452,14 @@ public class ExamForm implements TemplateComposer {
exam.getName()))
.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)
.withEntityKey(entityKey)
.withExec(this.examSEBRestrictionSettings.settingsFunction(this.pageService))
@ -473,14 +481,6 @@ public class ExamForm implements TemplateComposer {
.publishIf(() -> sebRestrictionAvailable && readonly && modifyGrant && !importFromQuizData
&& BooleanUtils.isTrue(isRestricted))
.newAction(ActionDefinition.EXAM_SECURITY_KEY_ENABLED)
.withEntityKey(entityKey)
.publishIf(() -> signatureKeyCheckEnabled && readonly)
.newAction(ActionDefinition.EXAM_SECURITY_KEY_DISABLED)
.withEntityKey(entityKey)
.publishIf(() -> !signatureKeyCheckEnabled && readonly)
.newAction(ActionDefinition.EXAM_PROCTORING_ON)
.withEntityKey(entityKey)
.withExec(this.proctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable))

View file

@ -8,6 +8,8 @@
package ch.ethz.seb.sebserver.gui.content.exam;
import java.util.function.Function;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
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.EntityKey;
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.SecurityKey;
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.i18n.LocTextKey;
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.PageActionBuilder;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
@ -116,6 +120,7 @@ public class ExamSignatureKeyForm implements TemplateComposer {
final boolean signatureKeyCheckEnabled = BooleanUtils.toBoolean(
exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED));
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
.defaultPageLayout(pageContext.getParent(), TILE);
@ -132,7 +137,8 @@ public class ExamSignatureKeyForm implements TemplateComposer {
Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED,
FORM_ENABLED,
String.valueOf(signatureKeyCheckEnabled))
.withInputSpan(1))
.withInputSpan(1)
.readonly(readonly))
.addField(FormBuilder.text(
Exam.ADDITIONAL_ATTR_NUMERICAL_TRUST_THRESHOLD,
@ -144,7 +150,8 @@ public class ExamSignatureKeyForm implements TemplateComposer {
}
})
.mandatory()
.withInputSpan(1))
.withInputSpan(1)
.readonly(readonly))
.build();
@ -174,19 +181,23 @@ public class ExamSignatureKeyForm implements TemplateComposer {
AppSignatureKeyInfo::getNumberOfConnections)
.widthProportion(1))
.withDefaultAction(table -> actionBuilder
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP)
.withParentEntityKey(entityKey)
.withExec(action -> this.addSecurityKeyGrantPopup.showGrantPopup(
action,
table.getSingleSelectedROWData()))
.noEventPropagation()
.ignoreMoveAwayFromEdit()
.create())
.withDefaultActionIf(
() -> !readonly,
table -> actionBuilder
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP)
.withParentEntityKey(entityKey)
.withExec(action -> this.addSecurityKeyGrantPopup.showGrantPopup(
action,
table.getSingleSelectedROWData()))
.noEventPropagation()
.ignoreMoveAwayFromEdit()
.create())
.withSelectionListener(this.pageService.getSelectionPublisher(
pageContext,
ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP))
.withSelectionListenerIf(
() -> !readonly,
this.pageService.getSelectionPublisher(
pageContext,
ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP))
.compose(pageContext.copyOf(content));
@ -214,15 +225,17 @@ public class ExamSignatureKeyForm implements TemplateComposer {
GRANT_LIST_TAG,
SecurityKey::getTag).widthProportion(1))
.withDefaultAction(table -> actionBuilder
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_GRANT_POPUP)
.withParentEntityKey(entityKey)
.withExec(action -> this.securityKeyGrantPopup.showGrantPopup(
action,
table.getSingleSelectedROWData()))
.noEventPropagation()
.ignoreMoveAwayFromEdit()
.create())
.withDefaultActionIf(
() -> !readonly,
table -> actionBuilder
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_GRANT_POPUP)
.withParentEntityKey(entityKey)
.withExec(action -> this.securityKeyGrantPopup.showGrantPopup(
action,
table.getSingleSelectedROWData()))
.noEventPropagation()
.ignoreMoveAwayFromEdit()
.create())
.withSelectionListener(this.pageService.getSelectionPublisher(
pageContext,
@ -235,30 +248,43 @@ public class ExamSignatureKeyForm implements TemplateComposer {
.withEntityKey(entityKey)
.withExec(action -> this.saveSettings(action, form.getForm()))
.ignoreMoveAwayFromEdit()
.publish()
.publishIf(() -> !readonly)
.newAction(ActionDefinition.EXAM_SECURITY_KEY_CANCEL_MODIFY)
.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)
.withParentEntityKey(entityKey)
.withSelect(
connectionInfoTable::getMultiSelection,
action -> this.addSecurityKeyGrantPopup.showGrantPopup(
action,
connectionInfoTable.getSingleSelectedROWData()),
this.addGrant(connectionInfoTable),
APP_SIG_KEY_LIST_EMPTY_SELECTION_TEXT_KEY)
.ignoreMoveAwayFromEdit()
.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)
.withEntityKey(entityKey)
.withSelect(
grantsList::getMultiSelection,
action -> this.securityKeyGrantPopup.showGrantPopup(action,
grantsList.getSingleSelectedROWData()),
this.showGrant(grantsList),
GRANT_LIST_EMPTY_SELECTION_TEXT_KEY)
.ignoreMoveAwayFromEdit()
.noEventPropagation()
@ -272,9 +298,7 @@ public class ExamSignatureKeyForm implements TemplateComposer {
this::deleteGrant,
GRANT_LIST_EMPTY_SELECTION_TEXT_KEY)
.ignoreMoveAwayFromEdit()
.publish(false)
;
.publishIf(() -> !readonly, false);
}
private PageAction saveSettings(final PageAction action, final Form form) {
@ -293,6 +317,36 @@ public class ExamSignatureKeyForm implements TemplateComposer {
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) {
final EntityKey parentEntityKey = action.getParentEntityKey();
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.EnumSet;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils;
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.slf4j.Logger;
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.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.ResetProctoringSettings;
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.SaveExamTemplateProctoringSettings;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@Lazy
@Component
@ -72,10 +77,24 @@ public class ProctoringSettingsPopup {
new LocTextKey("sebserver.exam.proctoring.form.url");
private final static LocTextKey SEB_PROCTORING_FORM_ROOM_SIZE =
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_SECRET =
new LocTextKey("sebserver.exam.proctoring.form.secret");
private final static LocTextKey SEB_PROCTORING_FORM_APPKEY_JITSI =
new LocTextKey("sebserver.exam.proctoring.form.appkey.jitsi");
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 =
new LocTextKey("sebserver.exam.proctoring.form.sdkkey");
private final static LocTextKey SEB_PROCTORING_FORM_SDKSECRET =
@ -86,6 +105,13 @@ public class ProctoringSettingsPopup {
private final static LocTextKey SEB_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) {
return action -> {
@ -102,32 +128,90 @@ public class ProctoringSettingsPopup {
.setDialogWidth(860)
.setDialogHeight(600);
final SEBProctoringPropertiesForm bindFormContext = new SEBProctoringPropertiesForm(
pageService,
pageContext);
final Predicate<FormHandle<?>> doBind = formHandle -> doSaveSettings(
pageService,
pageContext,
formHandle);
final ResetButtonHandler resetButtonHandler = new ResetButtonHandler();
if (modifyGrant) {
dialog.open(
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(
pageService,
pageContext,
resetButtonHandler);
dialog.openWithActions(
SEB_PROCTORING_FORM_TITLE,
doBind,
actionComposer,
Utils.EMPTY_EXECUTION,
bindFormContext);
} else {
dialog.open(
SEB_PROCTORING_FORM_TITLE,
pageContext,
pc -> bindFormContext.compose(pc.getParent()));
pc -> new SEBProctoringPropertiesForm(
pageService,
pageContext,
resetButtonHandler));
}
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(
final PageService pageService,
final PageContext pageContext,
@ -168,8 +252,14 @@ public class ProctoringSettingsPopup {
false,
form.getFieldValue(ProctoringServiceSettings.ATTR_APP_KEY),
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_SECRET),
BooleanUtils.toBoolean(form.getFieldValue(
ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM)));
@ -217,23 +307,22 @@ public class ProctoringSettingsPopup {
private final PageService pageService;
private final PageContext pageContext;
private final ResetButtonHandler resetButtonHandler;
protected SEBProctoringPropertiesForm(
final PageService pageService,
final PageContext pageContext) {
final PageContext pageContext,
final ResetButtonHandler resetButtonHandler) {
this.pageService = pageService;
this.pageContext = pageContext;
this.resetButtonHandler = resetButtonHandler;
}
@Override
public Supplier<FormHandle<?>> compose(final Composite parent) {
final RestService restService = this.pageService.getRestService();
final ResourceService resourceService = this.pageService.getResourceService();
final EntityKey entityKey = this.pageContext.getEntityKey();
final boolean isReadonly = BooleanUtils.toBoolean(
this.pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
final Composite content = this.pageService
.getWidgetFactory()
@ -248,12 +337,170 @@ public class ProctoringSettingsPopup {
.call()
.getOrThrow();
final PageContext formContext = this.pageContext
this.resetButtonHandler.enable(proctoringSettings.serviceInUse);
final FormHandleAnchor formHandleAnchor = new FormHandleAnchor();
formHandleAnchor.formContext = this.pageContext
.copyOf(content)
.clearEntityKeys();
final FormHandle<ProctoringServiceSettings> formHandle = this.pageService.formBuilder(
formContext)
buildFormAccordingToService(
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)
.withEmptyCellSeparation(true)
.withDefaultSpanEmptyCell(1)
@ -270,49 +517,13 @@ public class ProctoringSettingsPopup {
.addField(FormBuilder.checkbox(
ProctoringServiceSettings.ATTR_ENABLE_PROCTORING,
SEB_PROCTORING_FORM_ENABLE,
String.valueOf(proctoringSettings.enableProctoring)))
String.valueOf(proctoringSettings.enableProctoring)));
return formBuilder;
}
.addField(FormBuilder.singleSelection(
ProctoringServiceSettings.ATTR_SERVER_TYPE,
SEB_PROCTORING_FORM_TYPE,
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)
private FormHandle<ProctoringServiceSettings> buildFooter(final ProctoringServiceSettings proctoringSettings,
final ResourceService resourceService, final FormBuilder formBuilder) {
return formBuilder.withDefaultSpanInput(1)
.addField(FormBuilder.text(
ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE,
SEB_PROCTORING_FORM_ROOM_SIZE,
@ -337,14 +548,14 @@ public class ProctoringSettingsPopup {
resourceService::examProctoringFeaturesResources))
.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;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
@ -17,6 +18,8 @@ import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
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.Composite;
import org.eclipse.swt.widgets.Dialog;
@ -110,9 +113,53 @@ public class ModalInputDialog<T> extends Dialog {
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(
final LocTextKey title,
final Predicate<T> callback,
final Predicate<T> okCallback,
final Runnable cancelCallback,
final ModalInputDialogComposer<T> contentComposer) {
@ -142,7 +189,7 @@ public class ModalInputDialog<T> extends Dialog {
ok.addListener(SWT.Selection, event -> {
if (valueSupplier != null) {
final T result = valueSupplier.get();
if (callback.test(result)) {
if (okCallback.test(result)) {
this.shell.close();
}
} 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;
}
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) {
this.contentChangeListener = contentChangeListener;
return this;

View file

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

View file

@ -116,6 +116,8 @@ public interface ExamAdminService {
* @return ExamProctoringService instance */
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
* 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 final ExamDAO examDAO;
private final ProctoringAdminService proctoringServiceSettingsService;
private final ProctoringAdminService proctoringAdminService;
private final AdditionalAttributesDAO additionalAttributesDAO;
private final ConfigurationNodeDAO configurationNodeDAO;
private final ExamConfigurationMapDAO examConfigurationMapDAO;
@ -70,7 +70,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
protected ExamAdminServiceImpl(
final ExamDAO examDAO,
final ProctoringAdminService proctoringServiceSettingsService,
final ProctoringAdminService proctoringAdminService,
final AdditionalAttributesDAO additionalAttributesDAO,
final ConfigurationNodeDAO configurationNodeDAO,
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) {
this.examDAO = examDAO;
this.proctoringServiceSettingsService = proctoringServiceSettingsService;
this.proctoringAdminService = proctoringAdminService;
this.additionalAttributesDAO = additionalAttributesDAO;
this.configurationNodeDAO = configurationNodeDAO;
this.examConfigurationMapDAO = examConfigurationMapDAO;
@ -200,7 +200,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
@Override
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
@ -209,7 +209,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
final Long examId,
final ProctoringServiceSettings proctoringServiceSettings) {
return this.proctoringServiceSettingsService
return this.proctoringAdminService
.saveProctoringServiceSettings(
new EntityKey(examId, EntityType.EXAM),
proctoringServiceSettings)
@ -239,10 +239,30 @@ public class ExamAdminServiceImpl implements ExamAdminService {
@Override
public Result<ExamProctoringService> getExamProctoringService(final Long examId) {
return getProctoringServiceSettings(examId)
.flatMap(settings -> this.proctoringServiceSettingsService
.flatMap(settings -> this.proctoringAdminService
.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) {
return Result.tryCatch(() -> {
final LmsAPITemplate lmsTemplate = this.lmsAPIService

View file

@ -91,6 +91,9 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
: false,
getString(mapping, ProctoringServiceSettings.ATTR_APP_KEY),
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_SECRET),
getBoolean(mapping,
@ -139,19 +142,42 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE,
String.valueOf(proctoringServiceSettings.collectingRoomSize));
this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType,
entityId,
ProctoringServiceSettings.ATTR_APP_KEY,
StringUtils.trim(proctoringServiceSettings.appKey));
if (StringUtils.isNotBlank(proctoringServiceSettings.appKey)) {
this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType,
entityId,
ProctoringServiceSettings.ATTR_APP_KEY,
StringUtils.trim(proctoringServiceSettings.appKey));
this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType,
entityId,
ProctoringServiceSettings.ATTR_APP_SECRET,
this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.appSecret))
.getOrThrow()
.toString());
this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType,
entityId,
ProctoringServiceSettings.ATTR_APP_SECRET,
this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.appSecret))
.getOrThrow()
.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)) {
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.RemoteProctoringRoom;
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) */
public interface ExamProctoringRoomService {
ProctoringAdminService getProctoringAdminService();
/** Get all existing default proctoring rooms of an exam.
*
* @param examId The exam identifier
@ -136,4 +139,6 @@ public interface ExamProctoringRoomService {
* @return Result refer to void or to an error when happened */
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;
}
@Override
public ProctoringAdminService getProctoringAdminService() {
return this.proctoringAdminService;
}
@Override
public Result<Collection<RemoteProctoringRoom>> getProctoringCollectingRooms(final Long 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) {
synchronized (RESERVE_ROOM_LOCK) {
try {

View file

@ -15,7 +15,9 @@ import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
@ -36,6 +38,10 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
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.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;
@ -86,15 +92,12 @@ public class ZoomProctoringService implements ExamProctoringService {
private static final String TOKEN_ENCODE_ALG = "HmacSHA256";
@Deprecated
private static final String ZOOM_ACCESS_TOKEN_HEADER =
"{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
@Deprecated
private static final String ZOOM_API_ACCESS_TOKEN_PAYLOAD =
"{\"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(
new Tuple<>(
@ -125,7 +128,6 @@ public class ZoomProctoringService implements ExamProctoringService {
private final Cryptor cryptor;
private final AsyncService asyncService;
private final JSONMapper jsonMapper;
private final ZoomRestTemplate zoomRestTemplate;
private final RemoteProctoringRoomDAO remoteProctoringRoomDAO;
private final AuthorizationService authorizationService;
private final SEBClientInstructionService sebInstructionService;
@ -149,7 +151,6 @@ public class ZoomProctoringService implements ExamProctoringService {
this.cryptor = cryptor;
this.asyncService = asyncService;
this.jsonMapper = jsonMapper;
this.zoomRestTemplate = new ZoomRestTemplate(this);
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
this.authorizationService = authorizationService;
this.sebInstructionService = sebInstructionService;
@ -173,16 +174,26 @@ public class ZoomProctoringService implements ExamProctoringService {
try {
final ClientCredentials credentials = new ClientCredentials(
final ProctoringServiceSettings proctoringServiceSettings = new ProctoringServiceSettings(
proctoringSettings.examId,
proctoringSettings.enableProctoring,
proctoringSettings.serverType,
proctoringSettings.serverURL,
proctoringSettings.collectingRoomSize,
proctoringSettings.enabledFeatures,
proctoringSettings.serviceInUse,
proctoringSettings.appKey,
this.cryptor
.encrypt(proctoringSettings.appSecret)
.getOrThrow());
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 ResponseEntity<String> result = this.zoomRestTemplate
.testServiceConnection(
proctoringSettings.serverURL,
credentials);
final ZoomRestTemplate newRestTemplate = createNewRestTemplate(proctoringServiceSettings);
final ResponseEntity<String> result = newRestTemplate.testServiceConnection();
if (result.getStatusCode() != HttpStatus.OK) {
throw new APIMessageException(Arrays.asList(
@ -415,13 +426,8 @@ public class ZoomProctoringService implements ExamProctoringService {
roomData.getAdditionalRoomData(),
AdditionalZoomRoomData.class);
final ClientCredentials credentials = new ClientCredentials(
proctoringSettings.appKey,
proctoringSettings.appSecret);
this.deleteAdHocMeeting(
proctoringSettings,
credentials,
additionalZoomRoomData.meeting_id,
additionalZoomRoomData.user_id)
.getOrThrow();
@ -521,15 +527,10 @@ public class ZoomProctoringService implements ExamProctoringService {
final ProctoringServiceSettings proctoringSettings) {
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
final ResponseEntity<String> createUser = this.zoomRestTemplate.createUser(
proctoringSettings.serverURL,
credentials,
roomName);
final ResponseEntity<String> createUser = zoomRestTemplate.createUser(roomName);
final int statusCodeValue = createUser.getStatusCodeValue();
if (statusCodeValue >= 400) {
@ -540,16 +541,11 @@ public class ZoomProctoringService implements ExamProctoringService {
createUser.getBody(),
UserResponse.class);
this.zoomRestTemplate.applyUserSettings(
proctoringSettings.serverURL,
credentials,
userResponse.id);
zoomRestTemplate.applyUserSettings(userResponse.id);
// Then create new meeting with the ad-hoc user/host
final CharSequence meetingPwd = UUID.randomUUID().toString().subSequence(0, 9);
final ResponseEntity<String> createMeeting = this.zoomRestTemplate.createMeeting(
proctoringSettings.serverURL,
credentials,
final ResponseEntity<String> createMeeting = zoomRestTemplate.createMeeting(
userResponse.id,
subject,
duration,
@ -580,69 +576,18 @@ public class ZoomProctoringService implements ExamProctoringService {
private Result<Void> deleteAdHocMeeting(
final ProctoringServiceSettings proctoringSettings,
final ClientCredentials credentials,
final Long meetingId,
final String userId) {
return Result.tryCatch(() -> {
this.zoomRestTemplate.deleteMeeting(proctoringSettings.serverURL, credentials, meetingId);
this.zoomRestTemplate.deleteUser(proctoringSettings.serverURL, credentials, userId);
final ZoomRestTemplate zoomRestTemplate = getZoomRestTemplate(proctoringSettings);
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(
final ClientCredentials sdkCredentials,
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) {
// 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;
}
private final static class ZoomRestTemplate {
private final LinkedHashMap<Long, ZoomRestTemplate> restTemplatesCache = new LinkedHashMap<>();
private static final int LIZENSED_USER = 2;
private static final String API_TEST_ENDPOINT = "v2/users";
private static final String API_CREATE_USER_ENDPOINT = "v2/users";
private static final String API_APPLY_USER_SETTINGS_ENDPOINT = "v2/users/{userId}/settings";
private static final String API_DELETE_USER_ENDPOINT = "v2/users/{userid}?action=delete";
private static final String API_USER_CUST_CREATE = "custCreate";
private static final String API_ZOOM_ROOM_USER = "SEBProctoringRoomUser";
private static final String API_CREATE_MEETING_ENDPOINT = "v2/users/{userid}/meetings";
private static final String API_DELETE_MEETING_ENDPOINT = "v2/meetings/{meetingid}";
private static final String API_END_MEETING_ENDPOINT = "v2/meetings/{meetingid}/status";
private synchronized ZoomRestTemplate getZoomRestTemplate(final ProctoringServiceSettings proctoringSettings) {
if (!this.restTemplatesCache.containsKey(proctoringSettings.examId)) {
this.restTemplatesCache.put(
proctoringSettings.examId,
createNewRestTemplate(proctoringSettings));
} else {
final ZoomRestTemplate zoomRestTemplate = this.restTemplatesCache.get(proctoringSettings.examId);
if (!zoomRestTemplate.isValid(proctoringSettings)) {
this.restTemplatesCache.remove(proctoringSettings.examId);
this.restTemplatesCache.put(
proctoringSettings.examId,
createNewRestTemplate(proctoringSettings));
}
}
private final ZoomProctoringService zoomProctoringService;
private final RestTemplate restTemplate;
private final CircuitBreaker<ResponseEntity<String>> circuitBreaker;
if (this.restTemplatesCache.size() > 5) {
final Long toRemove = this.restTemplatesCache.keySet().iterator().next();
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.restTemplate = new RestTemplate(zoomProctoringService.clientHttpRequestFactoryService
.getClientHttpRequestFactory()
.getOrThrow());
this.circuitBreaker = zoomProctoringService.asyncService.createCircuitBreaker(
2,
10 * Constants.SECOND_IN_MILLIS,
10 * Constants.SECOND_IN_MILLIS);
this.proctoringSettings = proctoringSettings;
initConnection();
}
public ResponseEntity<String> testServiceConnection(
final String zoomServerUrl,
final ClientCredentials credentials) {
protected abstract void initConnection();
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 {
final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl)
.fromUriString(this.proctoringSettings.serverURL)
.path(API_TEST_ENDPOINT)
.queryParam("status", "active")
.queryParam("page_size", "10")
@ -840,7 +778,7 @@ public class ZoomProctoringService implements ExamProctoringService {
.queryParam("data_type", "Json")
.build()
.toUriString();
return exchange(url, HttpMethod.GET, credentials);
return exchange(url, HttpMethod.GET);
} catch (final Exception e) {
log.error("Failed to test zoom service connection: ", e);
@ -848,17 +786,15 @@ public class ZoomProctoringService implements ExamProctoringService {
}
}
public ResponseEntity<String> createUser(
final String zoomServerUrl,
final ClientCredentials credentials,
final String roomName) {
public ResponseEntity<String> createUser(final String roomName) {
try {
final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl)
.fromUriString(this.proctoringSettings.serverURL)
.path(API_CREATE_USER_ENDPOINT)
.toUriString();
final String host = new URL(zoomServerUrl).getHost();
final String host = new URL(this.proctoringSettings.serverURL).getHost();
final CreateUserRequest createUserRequest = new CreateUserRequest(
API_USER_CUST_CREATE,
new CreateUserRequest.UserInfo(
@ -868,7 +804,7 @@ public class ZoomProctoringService implements ExamProctoringService {
API_ZOOM_ROOM_USER));
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);
return exchange(url, HttpMethod.POST, body, headers);
@ -878,13 +814,10 @@ public class ZoomProctoringService implements ExamProctoringService {
}
}
public ResponseEntity<String> applyUserSettings(
final String zoomServerUrl,
final ClientCredentials credentials,
final String userId) {
public ResponseEntity<String> applyUserSettings(final String userId) {
try {
final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl)
.fromUriString(this.proctoringSettings.serverURL)
.path(API_APPLY_USER_SETTINGS_ENDPOINT)
.buildAndExpand(userId)
.normalize()
@ -892,7 +825,7 @@ public class ZoomProctoringService implements ExamProctoringService {
final ApplyUserSettingsRequest applySettingsRequest = new ApplyUserSettingsRequest();
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);
final ResponseEntity<String> exchange = exchange(url, HttpMethod.PATCH, body, headers);
@ -904,8 +837,6 @@ public class ZoomProctoringService implements ExamProctoringService {
}
public ResponseEntity<String> createMeeting(
final String zoomServerUrl,
final ClientCredentials credentials,
final String userId,
final String topic,
final int duration,
@ -915,7 +846,7 @@ public class ZoomProctoringService implements ExamProctoringService {
try {
final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl)
.fromUriString(this.proctoringSettings.serverURL)
.path(API_CREATE_MEETING_ENDPOINT)
.buildAndExpand(userId)
.toUriString();
@ -927,7 +858,7 @@ public class ZoomProctoringService implements ExamProctoringService {
waitingRoom);
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);
return exchange(url, HttpMethod.POST, body, headers);
@ -937,21 +868,18 @@ public class ZoomProctoringService implements ExamProctoringService {
}
}
public ResponseEntity<String> deleteMeeting(
final String zoomServerUrl,
final ClientCredentials credentials,
final Long meetingId) {
public ResponseEntity<String> deleteMeeting(final Long meetingId) {
// try to set set meeting status to ended first
try {
final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl)
.fromUriString(this.proctoringSettings.serverURL)
.path(API_END_MEETING_ENDPOINT)
.buildAndExpand(meetingId)
.toUriString();
final HttpHeaders headers = getHeaders(credentials);
final HttpHeaders headers = getHeaders();
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
final ResponseEntity<String> exchange = exchange(
@ -976,12 +904,12 @@ public class ZoomProctoringService implements ExamProctoringService {
try {
final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl)
.fromUriString(this.proctoringSettings.serverURL)
.path(API_DELETE_MEETING_ENDPOINT)
.buildAndExpand(meetingId)
.toUriString();
return exchange(url, HttpMethod.DELETE, credentials);
return exchange(url, HttpMethod.DELETE);
} catch (final Exception e) {
log.warn("Failed to delete Zoom ad-hoc meeting: {} cause: {} / {}",
@ -992,20 +920,17 @@ public class ZoomProctoringService implements ExamProctoringService {
}
}
public ResponseEntity<String> deleteUser(
final String zoomServerUrl,
final ClientCredentials credentials,
final String userId) {
public ResponseEntity<String> deleteUser(final String userId) {
try {
final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl)
.fromUriString(this.proctoringSettings.serverURL)
.path(API_DELETE_USER_ENDPOINT)
.buildAndExpand(userId)
.normalize()
.toUriString();
return exchange(url, HttpMethod.DELETE, credentials);
return exchange(url, HttpMethod.DELETE);
} catch (final Exception e) {
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(
final String url,
final HttpMethod method,
final ClientCredentials credentials) {
final HttpMethod method) {
return exchange(url, method, null, getHeaders(credentials));
return exchange(url, method, null, getHeaders());
}
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)
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 join_before_host;
@JsonProperty final int jbh_time = 0;

View file

@ -25,7 +25,6 @@ public class ProctoringSettingsValidator
return false;
}
//if (value.enableProctoring) {
if (value.serverType == ProctoringServerType.JITSI_MEET || value.serverType == ProctoringServerType.ZOOM) {
boolean passed = true;
@ -37,25 +36,44 @@ public class ProctoringSettingsValidator
passed = false;
}
if (StringUtils.isBlank(value.appKey)) {
context.disableDefaultConstraintViolation();
context
.buildConstraintViolationWithTemplate("proctoringSettings:appKey:notNull")
.addPropertyNode("appKey").addConstraintViolation();
passed = false;
if (value.serverType == ProctoringServerType.JITSI_MEET) {
if (StringUtils.isBlank(value.appKey)) {
context.disableDefaultConstraintViolation();
context
.buildConstraintViolationWithTemplate("proctoringSettings:appKey:notNull")
.addPropertyNode("appKey").addConstraintViolation();
passed = false;
}
if (StringUtils.isBlank(value.appSecret)) {
context.disableDefaultConstraintViolation();
context
.buildConstraintViolationWithTemplate("proctoringSettings:appSecret:notNull")
.addPropertyNode("appSecret").addConstraintViolation();
passed = false;
}
}
if (StringUtils.isBlank(value.appSecret)) {
context.disableDefaultConstraintViolation();
context
.buildConstraintViolationWithTemplate("proctoringSettings:appSecret:notNull")
.addPropertyNode("appSecret").addConstraintViolation();
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 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.lms.LmsAPIService;
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.validation.BeanValidationService;
@ -87,6 +88,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
private final ExamSessionService examSessionService;
private final SEBRestrictionService sebRestrictionService;
private final SecurityKeyService securityKeyService;
private final ExamProctoringRoomService examProctoringRoomService;
public ExamAdministrationController(
final AuthorizationService authorization,
@ -101,7 +103,8 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
final ExamTemplateService examTemplateService,
final ExamSessionService examSessionService,
final SEBRestrictionService sebRestrictionService,
final SecurityKeyService securityKeyService) {
final SecurityKeyService securityKeyService,
final ExamProctoringRoomService examProctoringRoomService) {
super(authorization,
bulkActionService,
@ -118,6 +121,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
this.examSessionService = examSessionService;
this.sebRestrictionService = sebRestrictionService;
this.securityKeyService = securityKeyService;
this.examProctoringRoomService = examProctoringRoomService;
}
@Override
@ -486,6 +490,29 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
.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
// ****************************************************************************

View file

@ -483,6 +483,7 @@ sebserver.quizdiscovery.quiz.details.additional.time_limit.toolitp=The time limi
################################
sebserver.exam.list.actions=
sebserver.exam.security.actions=&nbsp;
sebserver.exam.list.title=Exam
sebserver.exam.list.title.subtitle=
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.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.appkey=App Key
sebserver.exam.proctoring.form.appkey.tooltip=The application key of the proctoring service server
sebserver.exam.proctoring.form.secret=App Secret
sebserver.exam.proctoring.form.secret.tooltip=The secret used to access the proctoring service
sebserver.exam.proctoring.form.sdkkey=SDK Key (Zoom - MacOS/iOS)
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.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.jitsi=App Key
sebserver.exam.proctoring.form.appkey.jitsi.tooltip=The application key of the proctoring service server
sebserver.exam.proctoring.form.secret.jitsi=App Secret
sebserver.exam.proctoring.form.secret.jitsi.tooltip=The secret used to access the proctoring service
sebserver.exam.proctoring.form.appkey.zoom=App Key (Deprecated)
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.TOWN_HALL=Town-Hall 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.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.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.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.save=Save Settings
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.showASK=Show App Signature Key
sebserver.exam.signaturekey.action.showGrant=Show Security Grant
sebserver.exam.signaturekey.action.deleteGrant=Delete Security Grant
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,
"apiKey",
"apiSecret",
"accountId",
"clientId",
"clientSecret",
"sdkKey",
"sdkSecret",
false);
@ -3721,6 +3724,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
EnumSet.allOf(ProctoringFeature.class),
true,
"appKey", "appSecret",
"accountId", "clientId", "clientSecret",
"sdkKey", "sdkSecret",
false);

View file

@ -78,7 +78,9 @@ public class ExamProctoringRoomServiceTest extends AdministrationAPIIntegrationT
2L,
new ProctoringServiceSettings(
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());
}

View file

@ -43,7 +43,7 @@ public class ExamJITSIProctoringServiceTest {
final ProctoringServiceSettings proctoringServiceSettings = new ProctoringServiceSettings(
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
.testExamProctoring(proctoringServiceSettings);
@ -59,7 +59,9 @@ public class ExamJITSIProctoringServiceTest {
final ProctoringServiceSettings proctoringServiceSettings = new ProctoringServiceSettings(
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
.getProctorRoomConnection(proctoringServiceSettings, "TestRoom", "Test-User");