SEBSERV-139 Proctoring room join and leave API

This commit is contained in:
anhefti 2020-08-25 16:47:30 +02:00
parent 9923e06029
commit 3ea936cf81
12 changed files with 252 additions and 51 deletions

View file

@ -128,7 +128,9 @@ public final class API {
public static final String EXAM_ADMINISTRATION_CHECK_RESTRICTION_PATH_SEGMENT = "/check-seb-restriction";
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_PROCTOR_PATH_SEGMENT = "/proctoring";
public static final String PROCTOR_PATH_SEGMENT = "/proctoring";
public static final String PROCTOR_JOIN_ROOM_PATH_SEGMENT = "/join";
public static final String PROCTOR_LEAVE_ROOM_PATH_SEGMENT = "/leave";
public static final String EXAM_INDICATOR_ENDPOINT = "/indicator";
@ -166,9 +168,9 @@ public final class API {
public static final String EXAM_MONITORING_ENDPOINT = "/monitoring";
public static final String EXAM_MONITORING_INSTRUCTION_ENDPOINT = "/instruction";
public static final String EXAM_MONITORING_DISABLE_CONNECTION_ENDPOINT = "/disable-connection";
public static final String EXAM_MONITORING_STATE_FILTER = "hidden-states";
public static final String EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT =
"/{" + EXAM_API_SEB_CONNECTION_TOKEN + "}";
public static final String EXAM_MONITORING_STATE_FILTER = "hidden-states";
public static final String SEB_CLIENT_CONNECTION_ENDPOINT = "/seb-client-connection";

View file

@ -28,6 +28,11 @@ public final class ClientInstruction {
SEB_PROCTORING
}
public enum ProctoringInstructionMethod {
JOIN,
LEAVE
}
public interface SEB_INSTRUCTION_ATTRIBUTES {
public interface SEB_PROCTORING {
public static final String SERVICE_TYPE = "service-type";

View file

@ -54,7 +54,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicator
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs.GetExtendedClientEventPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionData;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorURLForClient;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorDataForSEBClient;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionDetails;
import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor;
@ -296,7 +296,7 @@ public class MonitoringClientConnection implements TemplateComposer {
private PageAction openProctorScreen(final PageAction action, final String connectionToken) {
final SEBClientProctoringConnectionData proctoringConnectionData =
this.pageService.getRestService().getBuilder(GetProctorURLForClient.class)
this.pageService.getRestService().getBuilder(GetProctorDataForSEBClient.class)
.withURIVariable(API.PARAM_MODEL_ID, action.getEntityKey().modelId)
.withURIVariable(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)
.call()

View file

@ -21,7 +21,7 @@ import ch.ethz.seb.sebserver.gui.service.examconfig.impl.ViewContext;
@Lazy
@Service
@GuiProfile
public class ProcotringViewRules implements ValueChangeRule {
public class ProctoringViewRules implements ValueChangeRule {
public static final String KEY_ENABLE_AI = "proctoringAIEnable";
public static final String KEY_ENABLE_JITSI = "jitsiMeetEnable";

View file

@ -36,7 +36,7 @@ public class GetProctoringSettings extends RestCall<ProctoringSettings> {
MediaType.APPLICATION_JSON_UTF8,
API.EXAM_ADMINISTRATION_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_PROCTOR_PATH_SEGMENT);
+ API.PROCTOR_PATH_SEGMENT);
}
}

View file

@ -36,7 +36,7 @@ public class SaveProctoringSettings extends RestCall<Exam> {
MediaType.APPLICATION_JSON_UTF8,
API.EXAM_ADMINISTRATION_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_PROCTOR_PATH_SEGMENT);
+ API.PROCTOR_PATH_SEGMENT);
}
}

View file

@ -24,9 +24,9 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class GetProctorURLForClient extends RestCall<SEBClientProctoringConnectionData> {
public class GetProctorDataForSEBClient extends RestCall<SEBClientProctoringConnectionData> {
public GetProctorURLForClient() {
public GetProctorDataForSEBClient() {
super(new TypeKey<>(
CallType.GET_SINGLE,
EntityType.EXAM_PROCTOR_DATA,
@ -34,9 +34,9 @@ public class GetProctorURLForClient extends RestCall<SEBClientProctoringConnecti
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_ADMINISTRATION_ENDPOINT
API.EXAM_MONITORING_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_PROCTOR_PATH_SEGMENT
+ API.PROCTOR_PATH_SEGMENT
+ API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT);
}

View file

@ -30,4 +30,9 @@ public interface ExamProctoringService {
ClientConnection clientConnection,
boolean server);
Result<SEBClientProctoringConnectionData> createProcotringDataForRoom(
final ProctoringSettings examProctoring,
final String roomName,
final boolean server);
}

View file

@ -96,19 +96,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
return Result.tryCatch(() -> {
if (examProctoring.examId == null) {
throw new IllegalStateException("Missing exam identifier from ExamProctoring data");
}
long expTime = System.currentTimeMillis() + Constants.DAY_IN_MILLIS;
if (this.examSessionService.isExamRunning(examProctoring.examId)) {
final Exam exam = this.examSessionService.getRunningExam(examProctoring.examId)
.getOrThrow();
if (exam.endTime != null) {
expTime = exam.endTime.getMillis();
}
}
final long expTime = forExam(examProctoring);
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String roomName = urlEncoder.encodeToString(
Utils.toByteArray(clientConnection.connectionToken));
@ -127,6 +115,29 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
}
@Override
public Result<SEBClientProctoringConnectionData> createProcotringDataForRoom(
final ProctoringSettings examProctoring,
final String roomName,
final boolean server) {
return Result.tryCatch(() -> {
final long expTime = forExam(examProctoring);
return createProctoringConnectionData(
examProctoring.serverURL,
examProctoring.appKey,
examProctoring.getAppSecret(),
this.authorizationService.getUserService().getCurrentUser().getUsername(),
(server) ? "seb-server" : "seb-client",
roomName,
roomName,
expTime)
.getOrThrow();
});
}
public Result<SEBClientProctoringConnectionData> createProctoringConnectionData(
final String url,
final String appKey,
@ -224,4 +235,20 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
return builder.toString();
}
private long forExam(final ProctoringSettings examProctoring) {
if (examProctoring.examId == null) {
throw new IllegalStateException("Missing exam identifier from ExamProctoring data");
}
long expTime = System.currentTimeMillis() + Constants.DAY_IN_MILLIS;
if (this.examSessionService.isExamRunning(examProctoring.examId)) {
final Exam exam = this.examSessionService.getRunningExam(examProctoring.examId)
.getOrThrow();
if (exam.endTime != null) {
expTime = exam.endTime.getMillis();
}
}
return expTime;
}
}

View file

@ -52,7 +52,6 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Chapters;
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.QuizData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features;
@ -384,7 +383,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_PROCTOR_PATH_SEGMENT,
+ API.PROCTOR_PATH_SEGMENT,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ProctoringSettings getExamProctoring(
@ -403,7 +402,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_PROCTOR_PATH_SEGMENT,
+ API.PROCTOR_PATH_SEGMENT,
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Exam saveExamProctoring(
@ -425,30 +424,6 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
.getOrThrow();
}
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_ADMINISTRATION_PROCTOR_PATH_SEGMENT
+ API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public SEBClientProctoringConnectionData getExamProctoringURL(
@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,
@PathVariable(name = API.EXAM_API_SEB_CONNECTION_TOKEN) final String connectionToken) {
checkReadPrivilege(institutionId);
return this.entityDAO.byPK(examId)
.flatMap(this.authorization::checkRead)
.flatMap(this.examAdminService::getExamProctoring)
.flatMap(proc -> this.examAdminService
.getExamProctoringService(proc.serverType)
.flatMap(s -> s.createProctoringConnectionData(proc, connectionToken, true)))
.getOrThrow();
}
// **** Proctoring
// ****************************************************************************

View file

@ -9,9 +9,12 @@
package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
@ -39,16 +42,22 @@ import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBClientProctoringConnectionData;
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.ClientInstruction;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PermissionDeniedException;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamProctoringService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBInstructionService;
@ -62,16 +71,19 @@ public class ExamMonitoringController {
private final SEBClientConnectionService sebClientConnectionService;
private final ExamSessionService examSessionService;
private final ExamAdminService examAdminService;
private final SEBInstructionService sebInstructionService;
private final AuthorizationService authorization;
private final PaginationService paginationService;
public ExamMonitoringController(
final ExamAdminService examAdminService,
final SEBClientConnectionService sebClientConnectionService,
final SEBInstructionService sebInstructionService,
final AuthorizationService authorization,
final PaginationService paginationService) {
this.examAdminService = examAdminService;
this.sebClientConnectionService = sebClientConnectionService;
this.examSessionService = sebClientConnectionService.getExamSessionService();
this.sebInstructionService = sebInstructionService;
@ -269,6 +281,127 @@ public class ExamMonitoringController {
}
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTOR_PATH_SEGMENT
+ API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public SEBClientProctoringConnectionData getClientSingleRoomProctoringData(
@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,
@PathVariable(name = API.EXAM_API_SEB_CONNECTION_TOKEN) final String connectionToken) {
this.authorization.check(
PrivilegeType.READ,
EntityType.EXAM,
institutionId);
return this.examSessionService.getRunningExam(examId)
.flatMap(this.authorization::checkRead)
.flatMap(this.examAdminService::getExamProctoring)
.flatMap(proc -> this.examAdminService
.getExamProctoringService(proc.serverType)
.flatMap(s -> s.createProctoringConnectionData(proc, connectionToken, true)))
.getOrThrow();
}
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTOR_PATH_SEGMENT
+ API.PROCTOR_JOIN_ROOM_PATH_SEGMENT,
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public SEBClientProctoringConnectionData joinProctoringRoom(
@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 = SEBClientProctoringConnectionData.ATTR_ROOM_NAME,
required = true) final String roomName,
@RequestParam(
name = API.EXAM_API_SEB_CONNECTION_TOKEN,
required = true) final String connectionTokens) {
this.authorization.check(
PrivilegeType.READ,
EntityType.EXAM,
institutionId);
final ProctoringSettings settings = this.examSessionService
.getRunningExam(examId)
.flatMap(this.authorization::checkRead)
.flatMap(this.examAdminService::getExamProctoring)
.getOrThrow();
final SEBClientProctoringConnectionData result = this.examAdminService
.getExamProctoringService(settings.serverType)
.flatMap(s -> s.createProcotringDataForRoom(settings, roomName, false))
.getOrThrow();
if (StringUtils.isNotBlank(connectionTokens)) {
(connectionTokens.contains(Constants.LIST_SEPARATOR)
? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
: Arrays.asList(connectionTokens)).stream()
.forEach(connectionToken -> sendJoinInstruction(examId, connectionToken, result)
.onError(error -> log.error(
"Failed to send proctoring leave instruction to client: {} ",
connectionToken, error)));
}
return result;
}
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTOR_PATH_SEGMENT
+ API.PROCTOR_LEAVE_ROOM_PATH_SEGMENT,
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public void leaveProctoringRoom(
@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 = SEBClientProctoringConnectionData.ATTR_ROOM_NAME,
required = true) final String roomName,
@RequestParam(
name = API.EXAM_API_SEB_CONNECTION_TOKEN,
required = true) final String connectionTokens) {
this.authorization.check(
PrivilegeType.READ,
EntityType.EXAM,
institutionId);
final ProctoringSettings settings = this.examSessionService
.getRunningExam(examId)
.flatMap(this.authorization::checkRead)
.flatMap(this.examAdminService::getExamProctoring)
.getOrThrow();
final ExamProctoringService examProctoringService = this.examAdminService
.getExamProctoringService(settings.serverType)
.getOrThrow();
(connectionTokens.contains(Constants.LIST_SEPARATOR)
? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
: Arrays.asList(connectionTokens)).stream()
.forEach(connectionToken -> examProctoringService
.createProctoringConnectionData(settings, connectionToken, false)
.flatMap(data -> sendLeaveInstruction(examId, connectionToken, data))
.onError(error -> log.error(
"Failed to send proctoring leave instruction to client: {} ",
connectionToken, error)));
}
private boolean hasRunningExamPrivilege(final Long examId, final Long institution) {
return hasRunningExamPrivilege(
this.examSessionService.getRunningExam(examId).getOr(null),
@ -284,4 +417,57 @@ public class ExamMonitoringController {
return exam.institutionId.equals(institution) && exam.isOwner(userId);
}
private Result<Void> sendJoinInstruction(
final Long examId,
final String connectionToken,
final SEBClientProctoringConnectionData data) {
return sendProctorInstruction(
examId,
connectionToken,
data,
ClientInstruction.ProctoringInstructionMethod.JOIN.name());
}
private Result<Void> sendLeaveInstruction(
final Long examId,
final String connectionToken,
final SEBClientProctoringConnectionData data) {
return sendProctorInstruction(
examId,
connectionToken,
data,
ClientInstruction.ProctoringInstructionMethod.LEAVE.name());
}
private Result<Void> sendProctorInstruction(
final Long examId,
final String connectionToken,
final SEBClientProctoringConnectionData data,
final String method) {
final Map<String, String> attributes = new HashMap<>();
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.SERVICE_TYPE,
ProctoringSettings.ServerType.JITSI_MEET.name());
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.METHOD,
method);
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_URL,
data.serverURL);
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_ROOM,
data.roomName);
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_TOKEN,
data.accessToken);
return this.sebInstructionService.registerInstruction(
examId,
InstructionType.SEB_PROCTORING,
attributes,
connectionToken,
true);
}
}

View file

@ -1,6 +1,7 @@
server.address=localhost
server.port=8080
sebserver.gui.http.external.scheme=http
sebserver.gui.entrypoint=/gui
sebserver.gui.webservice.protocol=http
sebserver.gui.webservice.address=localhost