Merge remote-tracking branch 'origin/dev-1.1.0' into development

Conflicts:
	src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java
This commit is contained in:
anhefti 2021-02-16 12:55:11 +01:00
commit ad7f06e521
24 changed files with 590 additions and 373 deletions

View file

@ -18,7 +18,7 @@
<packaging>jar</packaging>
<properties>
<sebserver-version>1.1.0-rc1</sebserver-version>
<sebserver-version>1.2.0-SNAPSHOT</sebserver-version>
<build-version>${sebserver-version}</build-version>
<revision>${sebserver-version}</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View file

@ -15,7 +15,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType;
@JsonIgnoreProperties(ignoreUnknown = true)
public class SEBProctoringConnectionData {
public class SEBProctoringConnection {
public static final String ATTR_CONNECTION_TOKEN = "connectionToken";
public static final String ATTR_SERVER_HOST = "serverHost";
@ -47,7 +47,7 @@ public class SEBProctoringConnectionData {
public final String accessToken;
@JsonCreator
public SEBProctoringConnectionData(
public SEBProctoringConnection(
@JsonProperty(ProctoringSettings.ATTR_SERVER_TYPE) final ProctoringServerType proctoringServerType,
@JsonProperty(ATTR_CONNECTION_TOKEN) final String connectionToken,
@JsonProperty(ATTR_SERVER_HOST) final String serverHost,

View file

@ -32,7 +32,7 @@ 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.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
@ -455,12 +455,12 @@ public class MonitoringClientConnection implements TemplateComposer {
if (roomOptional.isPresent()) {
final RemoteProctoringRoom room = roomOptional.get();
final SEBProctoringConnectionData proctoringConnectionData = this.pageService
final SEBProctoringConnection proctoringConnectionData = this.pageService
.getRestService()
.getBuilder(GetProctorRoomConnectionData.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId))
.withQueryParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, room.name)
.withQueryParam(SEBProctoringConnectionData.ATTR_SUBJECT, Utils.encodeFormURL_UTF_8(room.subject))
.withQueryParam(SEBProctoringConnection.ATTR_ROOM_NAME, room.name)
.withQueryParam(SEBProctoringConnection.ATTR_SUBJECT, Utils.encodeFormURL_UTF_8(room.subject))
.call()
.getOrThrow();
@ -499,7 +499,7 @@ public class MonitoringClientConnection implements TemplateComposer {
.getProctoringGUIService();
if (!proctoringGUIService.hasRoom(roomName)) {
final SEBProctoringConnectionData proctoringConnectionData = proctoringGUIService
final SEBProctoringConnection proctoringConnectionData = proctoringGUIService
.registerNewSingleProcotringRoom(
examId,
roomName,

View file

@ -44,7 +44,7 @@ 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.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
@ -445,7 +445,7 @@ public class MonitoringRunningExam implements TemplateComposer {
String activeAllRoomName = proctoringGUIService.getTownhallRoom(examId.modelId);
if (activeAllRoomName == null) {
final SEBProctoringConnectionData proctoringConnectionData = proctoringGUIService
final SEBProctoringConnection proctoringConnectionData = proctoringGUIService
.registerTownhallRoom(
examId.modelId,
this.pageService.getI18nSupport().getText(EXAM_ROOM_NAME))
@ -642,12 +642,12 @@ public class MonitoringRunningExam implements TemplateComposer {
final RemoteProctoringRoom room,
final PageAction action) {
final SEBProctoringConnectionData proctoringConnectionData = this.pageService
final SEBProctoringConnection proctoringConnectionData = this.pageService
.getRestService()
.getBuilder(GetProctorRoomConnectionData.class)
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId))
.withQueryParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, room.name)
.withQueryParam(SEBProctoringConnectionData.ATTR_SUBJECT, Utils.encodeFormURL_UTF_8(room.subject))
.withQueryParam(SEBProctoringConnection.ATTR_ROOM_NAME, room.name)
.withQueryParam(SEBProctoringConnection.ATTR_SUBJECT, Utils.encodeFormURL_UTF_8(room.subject))
.call()
.getOrThrow();

View file

@ -115,9 +115,11 @@ public class QuizLookupList implements TemplateComposer {
private final ResourceService resourceService;
private final PageService pageService;
private final int pageSize;
private final DateTime filterStartDate;
protected QuizLookupList(
final PageService pageService,
@Value("${sebserver.gui.filter.date.from.years:2}") final Integer startYearFromNow,
@Value("${sebserver.gui.list.page.size:20}") final Integer pageSize) {
this.pageService = pageService;
@ -125,6 +127,10 @@ public class QuizLookupList implements TemplateComposer {
this.resourceService = pageService.getResourceService();
this.pageSize = pageSize;
this.filterStartDate = Utils
.toDateTimeUTC(Utils.getMillisecondsNow())
.minusYears(startYearFromNow);
this.institutionFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION,
Entity.FILTER_ATTR_INSTITUTION,
@ -194,9 +200,7 @@ public class QuizLookupList implements TemplateComposer {
.withFilter(new TableFilterAttribute(
CriteriaType.DATE,
QuizData.FILTER_ATTR_START_TIME,
Utils.toDateTimeUTC(Utils.getMillisecondsNow())
.minusYears(1)
.toString()))
this.filterStartDate.toString()))
.sortable())
.withColumn(new ColumnDefinition<>(

View file

@ -245,13 +245,6 @@ public class SEBExamConfigForm implements TemplateComposer {
.noEventPropagation()
.publishIf(() -> modifyGrant && isReadonly)
// // TODO shall this got to settings form?
// .newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_EXISTING_CONFIG)
// .withEntityKey(entityKey)
// .withExec(this.sebExamConfigImportPopup.importFunction(false))
// .noEventPropagation()
// .publishIf(() -> modifyGrant && isReadonly && !isAttachedToExam)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_PROP_SAVE)
.withEntityKey(entityKey)
.withExec(formHandle::processFormSave)

View file

@ -104,133 +104,92 @@ public class SEBExamConfigImportPopup {
final Control fieldControl = form.getFieldInput(API.IMPORT_FILE_ATTR_NAME);
final PageContext context = formHandle.getContext();
// Ad-hoc field validation
if (newConfig) {
formHandle.process(name -> true, Form.FormFieldAccessor::resetError);
final String fieldValue = form.getFieldValue(Domain.CONFIGURATION_NODE.ATTR_NAME);
if (StringUtils.isBlank(fieldValue)) {
form.setFieldError(
Domain.CONFIGURATION_NODE.ATTR_NAME,
this.pageService
.getI18nSupport()
.getText(new LocTextKey("sebserver.form.validation.fieldError.notNull")));
return false;
} else if (fieldValue.length() < 3 || fieldValue.length() > 255) {
form.setFieldError(
Domain.CONFIGURATION_NODE.ATTR_NAME,
this.pageService
.getI18nSupport()
.getText(new LocTextKey("sebserver.form.validation.fieldError.size",
null,
null,
null,
3,
255)));
return false;
} else {
// check if name already exists
try {
if (this.pageService.getRestService()
.getBuilder(GetExamConfigNodeNames.class)
.call()
.getOrThrow()
.stream()
.filter(n -> n.name.equals(fieldValue))
.findFirst()
.isPresent()) {
form.setFieldError(
Domain.CONFIGURATION_NODE.ATTR_NAME,
this.pageService
.getI18nSupport()
.getText(new LocTextKey(
"sebserver.form.validation.fieldError.name.notunique")));
return false;
}
} catch (final Exception e) {
log.error("Failed to verify unique name: {}", e.getMessage());
}
}
if (!(fieldControl instanceof FileUploadSelection)) {
return false;
}
if (fieldControl instanceof FileUploadSelection) {
final FileUploadSelection fileUpload = (FileUploadSelection) fieldControl;
final InputStream inputStream = fileUpload.getInputStream();
if (inputStream != null) {
final RestCall<Configuration>.RestCallBuilder restCall = (newConfig)
? this.pageService.getRestService()
.getBuilder(ImportNewExamConfig.class)
: this.pageService.getRestService()
.getBuilder(ImportExamConfigOnExistingConfig.class);
if (!checkInput(formHandle, newConfig, form)) {
return false;
}
final FileUploadSelection fileUpload = (FileUploadSelection) fieldControl;
final InputStream inputStream = fileUpload.getInputStream();
if (inputStream != null) {
final RestCall<Configuration>.RestCallBuilder restCall = (newConfig)
? this.pageService.getRestService()
.getBuilder(ImportNewExamConfig.class)
: this.pageService.getRestService()
.getBuilder(ImportExamConfigOnExistingConfig.class);
restCall
.withHeader(
API.IMPORT_PASSWORD_ATTR_NAME,
form.getFieldValue(API.IMPORT_PASSWORD_ATTR_NAME))
.withBody(inputStream);
if (newConfig) {
restCall
.withHeader(
API.IMPORT_PASSWORD_ATTR_NAME,
form.getFieldValue(API.IMPORT_PASSWORD_ATTR_NAME))
.withBody(inputStream);
Domain.CONFIGURATION_NODE.ATTR_NAME,
form.getFieldValue(Domain.CONFIGURATION_NODE.ATTR_NAME))
.withHeader(
Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION,
form.getFieldValue(Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION))
.withHeader(
Domain.CONFIGURATION_NODE.ATTR_TEMPLATE_ID,
form.getFieldValue(Domain.CONFIGURATION_NODE.ATTR_TEMPLATE_ID));
} else {
restCall.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId);
}
if (newConfig) {
restCall
.withHeader(
Domain.CONFIGURATION_NODE.ATTR_NAME,
form.getFieldValue(Domain.CONFIGURATION_NODE.ATTR_NAME))
.withHeader(
Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION,
form.getFieldValue(Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION))
.withHeader(
Domain.CONFIGURATION_NODE.ATTR_TEMPLATE_ID,
form.getFieldValue(Domain.CONFIGURATION_NODE.ATTR_TEMPLATE_ID));
} else {
restCall.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId);
}
final Result<Configuration> configuration = restCall
.call();
final Result<Configuration> configuration = restCall
.call();
if (!configuration.hasError()) {
context.publishInfo(SEBExamConfigForm.FORM_IMPORT_CONFIRM_TEXT_KEY);
if (newConfig) {
final PageAction action = this.pageService.pageActionBuilder(context)
if (!configuration.hasError()) {
context.publishInfo(SEBExamConfigForm.FORM_IMPORT_CONFIRM_TEXT_KEY);
final PageAction action = (newConfig)
? this.pageService.pageActionBuilder(context)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_NEW_CONFIG)
.create()
: this.pageService.pageActionBuilder(context)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_MODIFY)
.create();
this.pageService.firePageEvent(
new ActionEvent(action),
action.pageContext());
}
} else {
final Exception error = configuration.getError();
if (error instanceof RestCallError) {
((RestCallError) error)
.getErrorMessages()
.stream()
.findFirst()
.ifPresent(message -> {
if (APIMessage.ErrorMessage.MISSING_PASSWORD.isOf(message)) {
formHandle
.getContext()
.publishPageMessage(MISSING_PASSWORD);
} else {
formHandle
.getContext()
.notifyImportError(EntityType.CONFIGURATION_NODE, error);
}
});
return true;
}
this.pageService.firePageEvent(
new ActionEvent(action),
action.pageContext());
formHandle.getContext().notifyError(
SEBExamConfigForm.FORM_TITLE,
configuration.getError());
}
return true;
} else {
formHandle.getContext().publishPageMessage(
new LocTextKey("sebserver.error.unexpected"),
new LocTextKey("Please select a valid SEB Exam Configuration File"));
final Exception error = configuration.getError();
if (error instanceof RestCallError) {
((RestCallError) error)
.getErrorMessages()
.stream()
.findFirst()
.ifPresent(message -> {
if (APIMessage.ErrorMessage.MISSING_PASSWORD.isOf(message)) {
formHandle
.getContext()
.publishPageMessage(MISSING_PASSWORD);
} else {
formHandle
.getContext()
.notifyImportError(EntityType.CONFIGURATION_NODE, error);
}
});
return true;
}
formHandle.getContext().notifyError(
SEBExamConfigForm.FORM_TITLE,
configuration.getError());
}
return true;
} else {
formHandle.getContext().publishPageMessage(
new LocTextKey("sebserver.error.unexpected"),
new LocTextKey("Please select a valid SEB Exam Configuration File"));
}
return false;
@ -240,6 +199,58 @@ public class SEBExamConfigImportPopup {
}
}
private boolean checkInput(final FormHandle<ConfigurationNode> formHandle, final boolean newConfig,
final Form form) {
if (newConfig) {
formHandle.process(name -> true, Form.FormFieldAccessor::resetError);
final String fieldValue = form.getFieldValue(Domain.CONFIGURATION_NODE.ATTR_NAME);
if (StringUtils.isBlank(fieldValue)) {
form.setFieldError(
Domain.CONFIGURATION_NODE.ATTR_NAME,
this.pageService
.getI18nSupport()
.getText(new LocTextKey("sebserver.form.validation.fieldError.notNull")));
return false;
} else if (fieldValue.length() < 3 || fieldValue.length() > 255) {
form.setFieldError(
Domain.CONFIGURATION_NODE.ATTR_NAME,
this.pageService
.getI18nSupport()
.getText(new LocTextKey("sebserver.form.validation.fieldError.size",
null,
null,
null,
3,
255)));
return false;
} else {
// check if name already exists
try {
if (this.pageService.getRestService()
.getBuilder(GetExamConfigNodeNames.class)
.call()
.getOrThrow()
.stream()
.filter(n -> n.name.equals(fieldValue))
.findFirst()
.isPresent()) {
form.setFieldError(
Domain.CONFIGURATION_NODE.ATTR_NAME,
this.pageService
.getI18nSupport()
.getText(new LocTextKey(
"sebserver.form.validation.fieldError.name.notunique")));
return false;
}
} catch (final Exception e) {
log.error("Failed to verify unique name: {}", e.getMessage());
}
}
}
return true;
}
private final class ImportFormContext implements ModalInputDialogComposer<FormHandle<ConfigurationNode>> {
private final PageService pageService;

View file

@ -255,7 +255,6 @@ public class SEBSettingsForm implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_EXISTING_CONFIG)
.withEntityKey(entityKey)
.withExec(this.sebExamConfigImportPopup.importFunction(false))
.noEventPropagation()
.publishIf(() -> examConfigGrant.iw() && !readonly && !isAttachedToExam)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP)

View file

@ -56,7 +56,9 @@ public class BrowserViewModeRule implements ValueChangeRule {
if (KEY_TOUCH_OPTIMIZED.equals(attribute.name)) {
if (BooleanUtils.toBoolean(value.value)) {
context.disableGroup(KEY_MAIN_WINDOW_GROUP);
context.setValue(KEY_BROWSER_VIEW_MODE, "2");
context.setValue(
KEY_BROWSER_VIEW_MODE,
context.getAttributeByName(KEY_BROWSER_VIEW_MODE).defaultValue);
} else {
context.setValue(KEY_TOUCH_EXIT, Constants.FALSE_STRING);
context.disable(KEY_TOUCH_EXIT);
@ -70,7 +72,9 @@ public class BrowserViewModeRule implements ValueChangeRule {
case 1: {
context.disable(KEY_TOUCH_EXIT);
context.disableGroup(KEY_MAIN_WINDOW_GROUP);
context.setValue(KEY_TOUCH_OPTIMIZED, Constants.FALSE_STRING);
context.setValue(
KEY_TOUCH_OPTIMIZED,
context.getAttributeByName(KEY_TOUCH_OPTIMIZED).defaultValue);
break;
}
case 2: {
@ -79,7 +83,9 @@ public class BrowserViewModeRule implements ValueChangeRule {
}
default: {
context.disable(KEY_TOUCH_EXIT);
context.setValue(KEY_TOUCH_OPTIMIZED, Constants.FALSE_STRING);
context.setValue(
KEY_TOUCH_OPTIMIZED,
context.getAttributeByName(KEY_TOUCH_OPTIMIZED).defaultValue);
break;
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright (c) 2021 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.examconfig.impl.rules;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.examconfig.ValueChangeRule;
import ch.ethz.seb.sebserver.gui.service.examconfig.impl.ViewContext;
@Lazy
@Service
@GuiProfile
public class IgnoreSEBService implements ValueChangeRule {
public static final String KEY_IGNORE_SEB_SERVICE = "sebServiceIgnore";
public static final String KEY_SEB_SERVICE_POLICY = "sebServicePolicy";
public static final String KEY_ATTR_1 = "enableWindowsUpdate";
public static final String KEY_ATTR_2 = "enableChromeNotifications";
public static final String KEY_ATTR_3 = "allowScreenSharing";
@Override
public boolean observesAttribute(final ConfigurationAttribute attribute) {
return KEY_IGNORE_SEB_SERVICE.equals(attribute.name);
}
@Override
public void applyRule(
final ViewContext context,
final ConfigurationAttribute attribute,
final ConfigurationValue value) {
if (KEY_IGNORE_SEB_SERVICE.equals(attribute.name)) {
if (BooleanUtils.toBoolean(value.value)) {
context.disable(KEY_SEB_SERVICE_POLICY);
context.disable(KEY_ATTR_1);
context.disable(KEY_ATTR_2);
context.disable(KEY_ATTR_3);
context.setValue(
KEY_SEB_SERVICE_POLICY,
context.getAttributeByName(KEY_SEB_SERVICE_POLICY).defaultValue);
context.setValue(
KEY_ATTR_1,
context.getAttributeByName(KEY_ATTR_1).defaultValue);
context.setValue(
KEY_ATTR_2,
context.getAttributeByName(KEY_ATTR_2).defaultValue);
context.setValue(
KEY_ATTR_3,
context.getAttributeByName(KEY_ATTR_3).defaultValue);
} else {
context.enable(KEY_SEB_SERVICE_POLICY);
context.enable(KEY_ATTR_1);
context.enable(KEY_ATTR_2);
context.enable(KEY_ATTR_3);
}
}
}
}

View file

@ -17,20 +17,20 @@ 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.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class ActivateTownhallRoom extends RestCall<SEBProctoringConnectionData> {
public class ActivateTownhallRoom extends RestCall<SEBProctoringConnection> {
public ActivateTownhallRoom() {
super(new TypeKey<>(
CallType.UNDEFINED,
EntityType.EXAM_PROCTOR_DATA,
new TypeReference<SEBProctoringConnectionData>() {
new TypeReference<SEBProctoringConnection>() {
}),
HttpMethod.POST,
MediaType.APPLICATION_FORM_URLENCODED,

View file

@ -17,20 +17,20 @@ 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.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class GetProctorRoomConnectionData extends RestCall<SEBProctoringConnectionData> {
public class GetProctorRoomConnectionData extends RestCall<SEBProctoringConnection> {
public GetProctorRoomConnectionData() {
super(new TypeKey<>(
CallType.GET_SINGLE,
EntityType.EXAM_PROCTOR_DATA,
new TypeReference<SEBProctoringConnectionData>() {
new TypeReference<SEBProctoringConnection>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,

View file

@ -17,20 +17,20 @@ 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.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class SendJoinRemoteProctoringRoom extends RestCall<SEBProctoringConnectionData> {
public class SendJoinRemoteProctoringRoom extends RestCall<SEBProctoringConnection> {
public SendJoinRemoteProctoringRoom() {
super(new TypeKey<>(
CallType.UNDEFINED,
EntityType.EXAM_PROCTOR_DATA,
new TypeReference<SEBProctoringConnectionData>() {
new TypeReference<SEBProctoringConnection>() {
}),
HttpMethod.POST,
MediaType.APPLICATION_FORM_URLENCODED,

View file

@ -27,7 +27,7 @@ import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.ActivateTownhallRoom;
@ -82,7 +82,7 @@ public class ProctoringGUIService {
public static void setCurrentProctoringWindowData(
final String examId,
final SEBProctoringConnectionData data) {
final SEBProctoringConnection data) {
RWT.getUISession().getHttpSession().setAttribute(
SESSION_ATTR_PROCTORING_DATA,
@ -103,7 +103,7 @@ public class ProctoringGUIService {
.orElseGet(() -> null);
}
public Result<SEBProctoringConnectionData> registerNewSingleProcotringRoom(
public Result<SEBProctoringConnection> registerNewSingleProcotringRoom(
final String examId,
final String roomName,
final String subject,
@ -111,8 +111,8 @@ public class ProctoringGUIService {
return this.restService.getBuilder(SendJoinRemoteProctoringRoom.class)
.withURIVariable(API.PARAM_MODEL_ID, examId)
.withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, roomName)
.withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject)
.withFormParam(SEBProctoringConnection.ATTR_ROOM_NAME, roomName)
.withFormParam(SEBProctoringConnection.ATTR_SUBJECT, subject)
.withFormParam(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)
.call()
.map(connection -> {
@ -122,13 +122,13 @@ public class ProctoringGUIService {
});
}
public Result<SEBProctoringConnectionData> registerTownhallRoom(
public Result<SEBProctoringConnection> registerTownhallRoom(
final String examId,
final String subject) {
return this.restService.getBuilder(ActivateTownhallRoom.class)
.withURIVariable(API.PARAM_MODEL_ID, examId)
.withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject)
.withFormParam(SEBProctoringConnection.ATTR_SUBJECT, subject)
.call()
.map(connection -> {
this.rooms.put(
@ -139,7 +139,7 @@ public class ProctoringGUIService {
});
}
public Result<SEBProctoringConnectionData> registerNewProcotringRoom(
public Result<SEBProctoringConnection> registerNewProcotringRoom(
final String examId,
final String roomName,
final String subject,
@ -147,8 +147,8 @@ public class ProctoringGUIService {
return this.restService.getBuilder(SendJoinRemoteProctoringRoom.class)
.withURIVariable(API.PARAM_MODEL_ID, examId)
.withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, roomName)
.withFormParam(SEBProctoringConnectionData.ATTR_SUBJECT, subject)
.withFormParam(SEBProctoringConnection.ATTR_ROOM_NAME, roomName)
.withFormParam(SEBProctoringConnection.ATTR_SUBJECT, subject)
.withFormParam(
API.EXAM_API_SEB_CONNECTION_TOKEN,
StringUtils.join(connectionTokens, Constants.LIST_SEPARATOR_CHAR))
@ -172,7 +172,7 @@ public class ProctoringGUIService {
}
this.restService.getBuilder(SendJoinRemoteProctoringRoom.class)
.withURIVariable(API.PARAM_MODEL_ID, examId)
.withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, room)
.withFormParam(SEBProctoringConnection.ATTR_ROOM_NAME, room)
.withFormParam(
API.EXAM_API_SEB_CONNECTION_TOKEN,
StringUtils.join(connectionTokens, Constants.LIST_SEPARATOR_CHAR))
@ -288,11 +288,11 @@ public class ProctoringGUIService {
public static class ProctoringWindowData {
public final String examId;
public final SEBProctoringConnectionData connectionData;
public final SEBProctoringConnection connectionData;
protected ProctoringWindowData(
final String examId,
final SEBProctoringConnectionData connectionData) {
final SEBProctoringConnection connectionData) {
this.examId = examId;
this.connectionData = connectionData;
}

View file

@ -28,6 +28,7 @@ import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
@ -65,7 +66,11 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
@Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return this.openEdxCourseAccess.getQuizzes(filterMap);
return this.openEdxCourseAccess
.getQuizzes(filterMap)
.map(quizzes -> quizzes.stream()
.filter(LmsAPIService.quizFilterPredicate(filterMap))
.collect(Collectors.toList()));
}
@Override

View file

@ -28,6 +28,7 @@ import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.NoSEBRestrictionException;
@ -67,7 +68,11 @@ public class MoodleLmsAPITemplate implements LmsAPITemplate {
@Override
public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
return this.moodleCourseAccess.getQuizzes(filterMap);
return this.moodleCourseAccess
.getQuizzes(filterMap)
.map(quizzes -> quizzes.stream()
.filter(LmsAPIService.quizFilterPredicate(filterMap))
.collect(Collectors.toList()));
}
@Override

View file

@ -10,46 +10,51 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.util.Result;
public interface ExamProctoringService {
/** Get the proctoring server type of the specific implementation
*
* @return the proctoring service type of the specific implementation */
ProctoringServerType getType();
/** Use this to test the proctoring service settings against the remote proctoring server.
*
* @param examProctoring the settings to test
* @return Result refer to true if the settings are correct and the proctoring server can be accessed. */
Result<Boolean> testExamProctoring(final ProctoringSettings examProctoring);
Result<SEBProctoringConnectionData> createProctorPublicRoomConnection(
/** Used to get the proctor's room connection data.
*
* @param proctoringSettings the proctoring settings
* @param roomName the name of the room
* @param subject name of the room
* @return SEBProctoringConnectionData that contains all connection data */
Result<SEBProctoringConnection> createProctorPublicRoomConnection(
final ProctoringSettings proctoringSettings,
final String roomName,
final String subject);
Result<SEBProctoringConnectionData> getClientExamCollectingRoomConnectionData(
final ProctoringSettings proctoringSettings,
final String connectionToken);
Result<SEBProctoringConnectionData> getClientExamCollectingRoomConnectionData(
Result<SEBProctoringConnection> getClientExamCollectingRoomConnection(
final ProctoringSettings proctoringSettings,
final ClientConnection connection);
Result<SEBProctoringConnectionData> getClientExamCollectingRoomConnectionData(
Result<SEBProctoringConnection> getClientExamCollectingRoomConnection(
final ProctoringSettings proctoringSettings,
final String connectionToken,
final String roomName,
final String subject);
Result<SEBProctoringConnectionData> getClientRoomConnectionData(
final ProctoringSettings proctoringSettings,
final String connectionToken);
Result<SEBProctoringConnectionData> getClientRoomConnectionData(
Result<SEBProctoringConnection> getClientRoomConnection(
final ProctoringSettings examProctoring,
final String connectionToken,
final String roomName,
final String subject);
Result<SEBProctoringConnectionData> createProctoringConnectionData(
Result<SEBProctoringConnection> createProctoringConnection(
final ProctoringServerType proctoringServerType,
final String connectionToken,
final String url,
@ -59,7 +64,8 @@ public interface ExamProctoringService {
final String clientKey,
final String roomName,
final String subject,
final Long expTime);
final Long expTime,
final boolean moderator);
Result<String> createClientAccessToken(
final ProctoringSettings proctoringSettings,

View file

@ -25,7 +25,7 @@ import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@ -46,7 +46,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
"{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
private static final String JITSI_ACCESS_TOKEN_PAYLOAD =
"{\"context\":{\"user\":{\"name\":\"%s\"}},\"iss\":\"%s\",\"aud\":\"%s\",\"sub\":\"%s\",\"room\":\"%s\"%s}";
"{\"context\":{\"user\":{\"name\":\"%s\"}},\"iss\":\"%s\",\"aud\":\"%s\",\"sub\":\"%s\",\"room\":\"%s\"%s%s}";
private final RemoteProctoringRoomDAO remoteProctoringRoomDAO;
private final AuthorizationService authorizationService;
@ -77,13 +77,13 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
}
@Override
public Result<SEBProctoringConnectionData> createProctorPublicRoomConnection(
public Result<SEBProctoringConnection> createProctorPublicRoomConnection(
final ProctoringSettings proctoringSettings,
final String roomName,
final String subject) {
return Result.tryCatch(() -> {
return createProctoringConnectionData(
return createProctoringConnection(
proctoringSettings.serverType,
null,
proctoringSettings.serverURL,
@ -93,25 +93,14 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
"seb-server",
roomName,
subject,
forExam(proctoringSettings))
forExam(proctoringSettings),
true)
.getOrThrow();
});
}
@Override
public Result<SEBProctoringConnectionData> getClientExamCollectingRoomConnectionData(
final ProctoringSettings proctoringSettings,
final String connectionToken) {
return this.examSessionService
.getConnectionData(connectionToken)
.flatMap(connection -> getClientExamCollectingRoomConnectionData(
proctoringSettings,
connection.clientConnection));
}
@Override
public Result<SEBProctoringConnectionData> getClientExamCollectingRoomConnectionData(
public Result<SEBProctoringConnection> getClientExamCollectingRoomConnection(
final ProctoringSettings proctoringSettings,
final ClientConnection connection) {
@ -121,7 +110,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
.getRoomName(connection.getRemoteProctoringRoomId())
.getOrThrow();
return createProctoringConnectionData(
return createProctoringConnection(
proctoringSettings.serverType,
null,
proctoringSettings.serverURL,
@ -131,13 +120,14 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
"seb-client",
roomName,
connection.userSessionId,
forExam(proctoringSettings))
forExam(proctoringSettings),
false)
.getOrThrow();
});
}
@Override
public Result<SEBProctoringConnectionData> getClientExamCollectingRoomConnectionData(
public Result<SEBProctoringConnection> getClientExamCollectingRoomConnection(
final ProctoringSettings proctoringSettings,
final String connectionToken,
final String roomName,
@ -148,7 +138,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
.getConnectionData(connectionToken)
.getOrThrow();
return createProctoringConnectionData(
return createProctoringConnection(
proctoringSettings.serverType,
null,
proctoringSettings.serverURL,
@ -158,35 +148,14 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
"seb-client",
roomName,
subject,
forExam(proctoringSettings))
forExam(proctoringSettings),
false)
.getOrThrow();
});
}
@Override
public Result<SEBProctoringConnectionData> getClientRoomConnectionData(
final ProctoringSettings proctoringSettings,
final String connectionToken) {
return Result.tryCatch(() -> this.examSessionService
.getConnectionData(connectionToken)
.getOrThrow()
).flatMap(clientConnection -> {
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String roomName = urlEncoder.encodeToString(
Utils.toByteArray(clientConnection.clientConnection.connectionToken));
return getClientRoomConnectionData(
proctoringSettings,
connectionToken,
roomName,
clientConnection.clientConnection.userSessionId);
});
}
@Override
public Result<SEBProctoringConnectionData> getClientRoomConnectionData(
public Result<SEBProctoringConnection> getClientRoomConnection(
final ProctoringSettings proctoringSettings,
final String connectionToken,
final String roomName,
@ -199,7 +168,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
.getConnectionData(connectionToken)
.getOrThrow();
return createProctoringConnectionData(
return createProctoringConnection(
proctoringSettings.serverType,
connectionToken,
proctoringSettings.serverURL,
@ -209,14 +178,15 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
"seb-client",
roomName,
subject,
expTime)
expTime,
false)
.getOrThrow();
});
}
@Override
public Result<SEBProctoringConnectionData> createProctoringConnectionData(
public Result<SEBProctoringConnection> createProctoringConnection(
final ProctoringServerType proctoringServerType,
final String connectionToken,
final String url,
@ -226,7 +196,8 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
final String clientKey,
final String roomName,
final String subject,
final Long expTime) {
final Long expTime,
final boolean moderator) {
return Result.tryCatch(() -> {
@ -242,9 +213,10 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
clientKey,
roomName,
expTime,
host);
host,
moderator);
return new SEBProctoringConnectionData(
return new SEBProctoringConnection(
proctoringServerType,
connectionToken,
host,
@ -279,34 +251,27 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
"seb-client",
roomName,
forExam(proctoringSettings),
host);
host,
false);
});
}
private String internalCreateAccessToken(
protected String internalCreateAccessToken(
final String appKey,
final CharSequence appSecret,
final String clientName,
final String clientKey,
final String roomName,
final Long expTime,
final String host) throws NoSuchAlgorithmException, InvalidKeyException {
final String host,
final boolean moderator) throws NoSuchAlgorithmException, InvalidKeyException {
final StringBuilder builder = new StringBuilder();
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String jwtHeaderPart = urlEncoder
.encodeToString(JITSI_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8));
final String jwtPayload = String.format(
JITSI_ACCESS_TOKEN_PAYLOAD.replaceAll(" ", "").replaceAll("\n", ""),
clientName,
appKey,
clientKey,
host,
roomName,
(expTime != null)
? String.format(",\"exp\":%s", String.valueOf(expTime))
: "");
final String jwtPayload = createPayload(appKey, clientName, clientKey, roomName, expTime, host, moderator);
final String jwtPayloadPart = urlEncoder
.encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8));
final String message = jwtHeaderPart + "." + jwtPayloadPart;
@ -324,6 +289,31 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
return builder.toString();
}
protected String createPayload(
final String appKey,
final String clientName,
final String clientKey,
final String roomName,
final Long expTime,
final String host,
final boolean moderator) {
final String jwtPayload = String.format(
JITSI_ACCESS_TOKEN_PAYLOAD.replaceAll(" ", "").replaceAll("\n", ""),
clientName,
appKey,
clientKey,
host,
roomName,
(moderator)
? ",\"moderator\":true"
: ",\"moderator\":false",
(expTime != null)
? String.format(",\"exp\":%s", String.valueOf(expTime))
: "");
return jwtPayload;
}
private long forExam(final ProctoringSettings examProctoring) {
if (examProctoring.examId == null) {
throw new IllegalStateException("Missing exam identifier from ExamProctoring data");

View file

@ -20,7 +20,7 @@ import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType;
@ -196,9 +196,9 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
.getExamProctoring(examId)
.getOrThrow();
final SEBProctoringConnectionData proctoringData =
final SEBProctoringConnection proctoringData =
this.examAdminService.getExamProctoringService(proctoringSettings.serverType)
.flatMap(s -> s.getClientExamCollectingRoomConnectionData(
.flatMap(s -> s.getClientExamCollectingRoomConnection(
proctoringSettings,
connectionToken,
roomName,

View file

@ -9,6 +9,8 @@
package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.util.Arrays;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@ -31,13 +33,14 @@ import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
@ -107,15 +110,16 @@ public class ExamProctoringController {
path = API.MODEL_ID_VAR_PATH_SEGMENT,
method = RequestMethod.GET,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public SEBProctoringConnectionData getProctorRoomData(
public SEBProctoringConnection getProctorRoomData(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@PathVariable(name = API.PARAM_MODEL_ID) final Long examId,
@RequestParam(name = SEBProctoringConnectionData.ATTR_ROOM_NAME, required = true) final String roomName,
@RequestParam(name = SEBProctoringConnectionData.ATTR_SUBJECT, required = false) final String subject) {
@RequestParam(name = SEBProctoringConnection.ATTR_ROOM_NAME, required = true) final String roomName,
@RequestParam(name = SEBProctoringConnection.ATTR_SUBJECT, required = false) final String subject) {
checkAccess(institutionId, examId);
@ -217,27 +221,12 @@ public class ExamProctoringController {
checkAccess(institutionId, examId);
final ProctoringSettings settings = this.examSessionService
final ProctoringSettings proctoringSettings = this.examSessionService
.getRunningExam(examId)
.flatMap(this.examAdminService::getExamProctoring)
.getOrThrow();
final ExamProctoringService examProctoringService = this.examAdminService
.getExamProctoringService(settings.serverType)
.getOrThrow();
Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
.stream()
.forEach(connectionToken -> {
examProctoringService
.getClientExamCollectingRoomConnectionData(
settings,
connectionToken)
.flatMap(data -> this.sendJoinInstruction(examId, connectionToken, data))
.onError(error -> log.error("Failed to send rejoin for: {} cause: {}",
connectionToken,
error.getMessage()));
});
sendJoinInstructions(connectionTokens, proctoringSettings);
}
@RequestMapping(
@ -245,17 +234,17 @@ public class ExamProctoringController {
+ API.EXAM_PROCTORING_JOIN_ROOM_PATH_SEGMENT,
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE)
public SEBProctoringConnectionData sendJoinProctoringRoomToClients(
public SEBProctoringConnection sendJoinProctoringRoomToClients(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@PathVariable(name = API.PARAM_MODEL_ID) final Long examId,
@RequestParam(
name = SEBProctoringConnectionData.ATTR_ROOM_NAME,
name = SEBProctoringConnection.ATTR_ROOM_NAME,
required = true) final String roomName,
@RequestParam(
name = SEBProctoringConnectionData.ATTR_SUBJECT,
name = SEBProctoringConnection.ATTR_SUBJECT,
required = false) final String subject,
@RequestParam(
name = API.EXAM_API_SEB_CONNECTION_TOKEN,
@ -273,39 +262,30 @@ public class ExamProctoringController {
.getOrThrow();
if (StringUtils.isNotBlank(connectionTokens)) {
final boolean single = connectionTokens.contains(Constants.LIST_SEPARATOR);
(single
? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
: Arrays.asList(connectionTokens))
.stream()
.forEach(connectionToken -> {
final SEBProctoringConnectionData data = (single)
? examProctoringService
.getClientRoomConnectionData(settings, connectionToken)
.onError(error -> log.error(
"Failed to get client room connection data for {} cause: {}",
connectionToken,
error.getMessage()))
.get()
: examProctoringService
.getClientRoomConnectionData(
settings,
connectionToken,
roomName,
(StringUtils.isNotBlank(subject)) ? subject : roomName)
.onError(error -> log.error(
"Failed to get client room connection data for {} cause: {}",
connectionToken,
error.getMessage()))
.get();
if (data != null) {
sendJoinInstruction(examId, connectionToken, data)
.onError(error -> log.error(
"Failed to send proctoring leave instruction to client: {} cause: {}",
connectionToken,
error.getMessage()));
}
});
Arrays.asList(connectionTokens.split(Constants.LIST_SEPARATOR))
.stream()
.forEach(connectionToken -> {
final SEBProctoringConnection proctoringConnection =
examProctoringService
.getClientRoomConnection(
settings,
connectionToken,
verifyRoomName(roomName, connectionToken),
(StringUtils.isNotBlank(subject)) ? subject : roomName)
.onError(error -> log.error(
"Failed to get client room connection data for {} cause: {}",
connectionToken,
error.getMessage()))
.get();
if (proctoringConnection != null) {
sendJoinInstruction(settings.examId, connectionToken, proctoringConnection)
.onError(error -> log.error(
"Failed to send proctoring leave instruction to client: {} cause: {}",
connectionToken,
error.getMessage()));
}
});
}
return examProctoringService.createProctorPublicRoomConnection(
@ -315,6 +295,16 @@ public class ExamProctoringController {
.getOrThrow();
}
private String verifyRoomName(final String requestedRoomName, final String connectionToken) {
if (StringUtils.isNotBlank(requestedRoomName)) {
return requestedRoomName;
}
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
return urlEncoder.encodeToString(
Utils.toByteArray(connectionToken));
}
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_PROCTORING_TOWNHALL_ROOM_DATA,
@ -338,14 +328,14 @@ public class ExamProctoringController {
+ API.EXAM_PROCTORING_ACTIVATE_TOWNHALL_ROOM,
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE)
public SEBProctoringConnectionData activateTownhall(
public SEBProctoringConnection activateTownhall(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@PathVariable(name = API.PARAM_MODEL_ID) final Long examId,
@RequestParam(
name = SEBProctoringConnectionData.ATTR_SUBJECT,
name = SEBProctoringConnection.ATTR_SUBJECT,
required = false) final String subject) {
checkAccess(institutionId, examId);
@ -371,8 +361,8 @@ public class ExamProctoringController {
.getOrThrow()
.stream()
.forEach(cc -> {
final SEBProctoringConnectionData data = examProctoringService
.getClientRoomConnectionData(
final SEBProctoringConnection data = examProctoringService
.getClientRoomConnection(
settings,
cc.clientConnection.connectionToken,
townhallRoom.name,
@ -431,7 +421,7 @@ public class ExamProctoringController {
.stream()
.forEach(cc -> {
examProctoringService
.getClientExamCollectingRoomConnectionData(
.getClientExamCollectingRoomConnection(
settings,
cc.clientConnection)
.flatMap(data -> this.sendJoinInstruction(
@ -455,64 +445,16 @@ public class ExamProctoringController {
return;
}
if (StringUtils.isNotBlank(connectionTokens)) {
// we have defined connection tokens to send instructions to
final boolean definedClients = StringUtils.isNotBlank(connectionTokens);
final boolean inTownhall = this.examProcotringRoomService.getTownhallRoomData(examId).hasValue();
final boolean roomSpecified = StringUtils.isNotBlank(roomName);
final boolean single = connectionTokens.contains(Constants.LIST_SEPARATOR);
(single
? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
: Arrays.asList(connectionTokens))
.stream()
.forEach(connectionToken -> {
this.sebInstructionService.registerInstruction(
examId,
InstructionType.SEB_RECONFIGURE_SETTINGS,
attributes,
connectionToken,
true)
.onError(error -> log.error(
"Failed to register reconfiguring instruction for connection: {}",
connectionToken,
error));
});
} else if (this.examProcotringRoomService.getTownhallRoomData(examId).hasValue()) {
// we are in the town hall so all active connections are involved
this.examSessionService.getAllActiveConnectionData(examId)
.getOrThrow()
.stream()
.forEach(connection -> {
this.sebInstructionService.registerInstruction(
examId,
InstructionType.SEB_RECONFIGURE_SETTINGS,
attributes,
connection.clientConnection.connectionToken,
true)
.onError(error -> log.error(
"Failed to register reconfiguring instruction for connection: {}",
connection.clientConnection.connectionToken,
error));
});
} else if (StringUtils.isNotBlank(roomName)) {
// we have a room name so all connection of this room are involved
this.examProcotringRoomService.getRoomConnections(examId, roomName)
.getOrThrow()
.stream()
.filter(ExamSessionService.ACTIVE_CONNECTION_FILTER)
.forEach(connection -> {
this.sebInstructionService.registerInstruction(
examId,
InstructionType.SEB_RECONFIGURE_SETTINGS,
attributes,
connection.connectionToken,
true)
.onError(error -> log.error(
"Failed to register reconfiguring instruction for connection: {}",
connection.connectionToken,
error));
});
if (definedClients) {
sendBroadcastInstructionsToClients(examId, connectionTokens, attributes);
} else if (inTownhall) {
sendBroadcastInstructionToClientsInExam(examId, attributes);
} else if (roomSpecified) {
sendBroadcastInstructionToClientsInRoom(examId, roomName, attributes);
} else {
throw new RuntimeException("API attribute validation error: missing "
+ Domain.REMOTE_PROCTORING_ROOM.ATTR_ID + " and/or" +
@ -520,12 +462,111 @@ public class ExamProctoringController {
}
}
private void sendBroadcastInstructionsToClients(final Long examId, final String connectionTokens,
final Map<String, String> attributes) {
final boolean single = connectionTokens.contains(Constants.LIST_SEPARATOR);
(single
? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
: Arrays.asList(connectionTokens))
.stream()
.forEach(connectionToken -> {
this.sebInstructionService.registerInstruction(
examId,
InstructionType.SEB_RECONFIGURE_SETTINGS,
attributes,
connectionToken,
true)
.onError(error -> log.error(
"Failed to register reconfiguring instruction for connection: {}",
connectionToken,
error));
});
}
private void sendBroadcastInstructionToClientsInExam(final Long examId, final Map<String, String> attributes) {
this.examSessionService
.getAllActiveConnectionData(examId)
.getOrThrow()
.stream()
.forEach(connection -> {
this.sebInstructionService.registerInstruction(
examId,
InstructionType.SEB_RECONFIGURE_SETTINGS,
attributes,
connection.clientConnection.connectionToken,
true)
.onError(error -> log.error(
"Failed to register reconfiguring instruction for connection: {}",
connection.clientConnection.connectionToken,
error));
});
}
private void sendBroadcastInstructionToClientsInRoom(
final Long examId,
final String roomName,
final Map<String, String> attributes) {
this.examProcotringRoomService
.getRoomConnections(examId, roomName)
.getOrThrow()
.stream()
.filter(ExamSessionService.ACTIVE_CONNECTION_FILTER)
.forEach(connection -> {
this.sebInstructionService.registerInstruction(
examId,
InstructionType.SEB_RECONFIGURE_SETTINGS,
attributes,
connection.connectionToken,
true)
.onError(error -> log.error(
"Failed to register reconfiguring instruction for connection: {}",
connection.connectionToken,
error));
});
}
private void sendJoinInstructions(
final String connectionTokens,
final ProctoringSettings proctoringSettings) {
final ExamProctoringService examProctoringService = this.examAdminService
.getExamProctoringService(proctoringSettings.serverType)
.getOrThrow();
Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
.stream()
.forEach(connectionToken -> {
sendJoinInstructionToClient(proctoringSettings, examProctoringService, connectionToken);
});
}
private void sendJoinInstructionToClient(
final ProctoringSettings proctoringSettings,
final ExamProctoringService examProctoringService,
final String connectionToken) {
this.examSessionService
.getConnectionData(connectionToken)
.flatMap(connection -> examProctoringService.getClientExamCollectingRoomConnection(
proctoringSettings,
connection.clientConnection))
.flatMap(data -> this.sendJoinInstruction(
proctoringSettings.examId,
connectionToken, data))
.onError(error -> log.error("Failed to send rejoin for: {} cause: {}",
connectionToken,
error.getMessage()));
}
private Result<Void> sendJoinInstruction(
final Long examId,
final String connectionToken,
final SEBProctoringConnectionData data) {
final SEBProctoringConnection data) {
final Map<String, String> attributes = new HashMap<>();
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.SERVICE_TYPE,
ProctoringSettings.ProctoringServerType.JITSI_MEET.name());
@ -541,6 +582,7 @@ public class ExamProctoringController {
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_TOKEN,
data.accessToken);
return this.sebInstructionService.registerInstruction(
examId,
InstructionType.SEB_PROCTORING,

View file

@ -31,6 +31,8 @@ sebserver.gui.webservice.moodle-lms-enabled=true
sebserver.gui.seb.client.config.download.filename=SEBServerSettings.seb
sebserver.gui.seb.exam.config.download.filename=SEBExamSettings.seb
sebserver.gui.filter.date.from.years=2
# remote proctoring
sebserver.gui.remote.proctoring.entrypoint=/remote-proctoring
sebserver.gui.remote.proctoring.api-servler.endpoint=/remote-view-servlet

View file

@ -0,0 +1,36 @@
INSERT IGNORE INTO configuration_attribute VALUES
(318, 'sebServiceIgnore', 'CHECKBOX', null, null, null, null, 'true'),
(319, 'allowApplicationLog', 'CHECKBOX', null, null, null, null, 'false'),
(320, 'showApplicationLogButton', 'CHECKBOX', null, null, null, null, 'false'),
(321, 'enableWindowsUpdate', 'CHECKBOX', null, null, null, null, 'false'),
(322, 'enableChromeNotifications', 'CHECKBOX', null, null, null, null, 'false')
;
UPDATE orientation SET y_position='13', width='12' WHERE id='305';
UPDATE orientation SET y_position='16', width='10' WHERE id='306';
UPDATE orientation SET y_position='17', width='10' WHERE id='307';
UPDATE orientation SET y_position='18' WHERE id='317';
UPDATE orientation SET x_position='0', y_position='9' WHERE id='301';
UPDATE orientation SET x_position='3', y_position='10', width='4' WHERE id='501';
UPDATE orientation SET x_position='3', y_position='11', width='4' WHERE id='304';
UPDATE orientation SET x_position='3', y_position='12', width='4' WHERE id='302';
UPDATE orientation SET group_id='sebService', x_position='4', y_position='6', width='3' WHERE id='303';
UPDATE orientation SET group_id='sebService', y_position='2', width='7', title='TOP' WHERE id='300';
UPDATE orientation SET y_position='3' WHERE id='309';
UPDATE orientation SET y_position='4' WHERE id='310';
UPDATE orientation SET y_position='5' WHERE id='311';
UPDATE orientation SET y_position='6' WHERE id='312';
UPDATE orientation SET y_position='7' WHERE id='313';
UPDATE orientation SET y_position='8' WHERE id='314';
UPDATE orientation SET y_position='11' WHERE id='315';
UPDATE orientation SET y_position='12' WHERE id='316';
INSERT IGNORE INTO orientation VALUES
(318, 318, 0, 9, 'sebService', 0, 0, 7, 1, 'NONE'),
(319, 319, 0, 9, 'logging', 0, 14, 5, 1, 'NONE'),
(320, 320, 0, 9, 'logging', 0, 15, 5, 1, 'NONE'),
(321, 321, 0, 9, 'sebService', 0, 6, 4, 1, 'NONE'),
(322, 322, 0, 9, 'sebService', 0, 7, 4, 1, 'NONE')
;

View file

@ -1166,11 +1166,15 @@ sebserver.examconfig.props.label.RTSPUsername=Username
sebserver.examconfig.props.label.RTSPPassword=Password
sebserver.examconfig.props.group.servicePolicy=SEB Service policy
sebserver.examconfig.props.group.sebService=SEB Service (Win)
sebserver.examconfig.props.label.sebServicePolicy=<span style=\"color:#aa0000\">IMPORTANT: The SEB Service changes aspects of the system configuration.<br/>Some anti-virus software providers might falsely identify its operation as malicious,<br/>thus it is not recommended to use the SEB Service in uncontrolled environments (e.g. BYOD).</span><br/><br/>SEB Service policy
sebserver.examconfig.props.label.sebServicePolicy.0=allow to run SEB without service
sebserver.examconfig.props.label.sebServicePolicy.1=display warning when service is not running
sebserver.examconfig.props.label.sebServicePolicy.2=allow to use SEB only with service
sebserver.examconfig.props.label.sebServicePolicy.tooltip=Policy that applies when an exam client doesn't have the SEB client running
sebserver.examconfig.props.label.sebServiceIgnore=Ignore SEB Service
sebserver.examconfig.props.label.enableWindowsUpdate=Allow Windows Update to run while SEB is running
sebserver.examconfig.props.label.enableChromeNotifications=Allow notifications from Chrome browsers
sebserver.examconfig.props.group.kioskMode=Kiosk Mode
sebserver.examconfig.props.label.kioskMode.tooltip=The kiosk mode setting reflects how the computer is locked down in SEB
@ -1232,6 +1236,8 @@ sebserver.examconfig.props.label.logLevel.3=Debug
sebserver.examconfig.props.label.logLevel.3.tooltip=Debug is reserved for information which is only necessary for in-deep program code debugging
sebserver.examconfig.props.label.logLevel.4=Verbose
sebserver.examconfig.props.label.logLevel.4.tooltip=Verbose level contains events of all levels
sebserver.examconfig.props.label.allowApplicationLog=Allow access to application log (Win)
sebserver.examconfig.props.label.showApplicationLogButton=Show log button on taskbar (Win)
sebserver.examconfig.props.group.registry=While running SEB
sebserver.examconfig.props.group.registry.tooltip=Options in the Windows Security Screen invoked by Ctrl-Alt-Del

View file

@ -11,22 +11,59 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import org.junit.Test;
import org.mockito.Mockito;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
public class ExamJITSIProctoringServiceTest {
@Test
public void testTokenPayload() throws InvalidKeyException, NoSuchAlgorithmException {
final Cryptor cryptorMock = Mockito.mock(Cryptor.class);
Mockito.when(cryptorMock.decrypt(Mockito.any())).thenReturn("fbvgeghergrgrthrehreg123");
final ExamJITSIProctoringService examJITSIProctoringService =
new ExamJITSIProctoringService(null, null, null, cryptorMock);
String accessToken = examJITSIProctoringService.createPayload(
"test-app",
"Test Name",
"test-client",
"SomeRoom",
1609459200L,
"https://test.ch",
false);
assertEquals(
"{\"context\":{\"user\":{\"name\":\"Test Name\"}},\"iss\":\"test-app\",\"aud\":\"test-client\",\"sub\":\"https://test.ch\",\"room\":\"SomeRoom\",\"moderator\":false,\"exp\":1609459200}",
accessToken);
accessToken = examJITSIProctoringService.createPayload(
"test-app",
"Test Name",
"test-client",
"SomeRoom",
1609459200L,
"https://test.ch",
true);
assertEquals(
"{\"context\":{\"user\":{\"name\":\"Test Name\"}},\"iss\":\"test-app\",\"aud\":\"test-client\",\"sub\":\"https://test.ch\",\"room\":\"SomeRoom\",\"moderator\":true,\"exp\":1609459200}",
accessToken);
}
@Test
public void testCreateProctoringURL() {
final Cryptor cryptorMock = Mockito.mock(Cryptor.class);
Mockito.when(cryptorMock.decrypt(Mockito.any())).thenReturn("fbvgeghergrgrthrehreg123");
final ExamJITSIProctoringService examJITSIProctoringService =
new ExamJITSIProctoringService(null, null, null, cryptorMock);
final SEBProctoringConnectionData data = examJITSIProctoringService.createProctoringConnectionData(
final SEBProctoringConnection data = examJITSIProctoringService.createProctoringConnection(
ProctoringServerType.JITSI_MEET,
"connectionToken",
"https://seb-jitsi.example.ch",
@ -36,15 +73,17 @@ public class ExamJITSIProctoringServiceTest {
"test-client",
"SomeRoom",
"Subject",
1609459200L)
1609459200L,
true)
.getOrThrow();
assertNotNull(data);
assertEquals(
"https://seb-jitsi.example.ch",
data.serverURL);
assertEquals(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb250ZXh0Ijp7InVzZXIiOnsibmFtZSI6IlRlc3QgTmFtZSJ9fSwiaXNzIjoidGVzdC1hcHAiLCJhdWQiOiJ0ZXN0LWNsaWVudCIsInN1YiI6InNlYi1qaXRzaS5leGFtcGxlLmNoIiwicm9vbSI6IlNvbWVSb29tIiwiZXhwIjoxNjA5NDU5MjAwfQ.4ovqUkG6jrLvkDEZNdhbtFI_DFLDFsM2eBJHhcYq7a4",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb250ZXh0Ijp7InVzZXIiOnsibmFtZSI6IlRlc3QgTmFtZSJ9fSwiaXNzIjoidGVzdC1hcHAiLCJhdWQiOiJ0ZXN0LWNsaWVudCIsInN1YiI6InNlYi1qaXRzaS5leGFtcGxlLmNoIiwicm9vbSI6IlNvbWVSb29tIiwibW9kZXJhdG9yIjp0cnVlLCJleHAiOjE2MDk0NTkyMDB9.poOwfCsRjNqCizEQM3qFFWbjuX0bZLer3cqlbaPK9wc",
data.accessToken);
}