diff --git a/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java b/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java index f62042b7..c2b5d8f4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java @@ -40,6 +40,9 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; @Order(7) public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements ErrorController { + private static final String ERROR_PATH = "/sebserver/error"; + private static final String CHECK_PATH = "/sebserver/check"; + @Value("${sebserver.webservice.http.redirect.gui}") private String guiRedirect; @Value("${sebserver.webservice.api.exam.endpoint.discovery}") @@ -78,22 +81,27 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements E public void configure(final WebSecurity web) { web .ignoring() - .antMatchers("/error") + .antMatchers(ERROR_PATH) + .antMatchers(CHECK_PATH) .antMatchers(this.examAPIDiscoveryEndpoint) .antMatchers(this.adminAPIEndpoint + API.INFO_ENDPOINT + API.LOGO_PATH_SEGMENT + "/**") .antMatchers(this.adminAPIEndpoint + API.INFO_ENDPOINT + API.INFO_INST_PATH_SEGMENT + "/**") .antMatchers(this.adminAPIEndpoint + API.REGISTER_ENDPOINT); } - @RequestMapping("/error") + @RequestMapping(CHECK_PATH) + public void check() throws IOException { + } + + @RequestMapping(ERROR_PATH) public void handleError(final HttpServletResponse response) throws IOException { - //response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); + response.getOutputStream().print(response.getStatus()); response.setHeader(HttpHeaders.LOCATION, this.guiRedirect); response.flushBuffer(); } @Override public String getErrorPath() { - return "/error"; + return ERROR_PATH; } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index 2bdf3822..a42ebe4f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -185,6 +185,7 @@ public final class API { public static final String EXAM_PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT = "/room-connections"; public static final String EXAM_PROCTORING_ACTIVATE_TOWNHALL_ROOM = "activate-towhall-room"; public static final String EXAM_PROCTORING_TOWNHALL_ROOM_DATA = "towhall-room-data"; + public static final String EXAM_PROCTORING_TOWNHALL_ROOM_AVAILABLE = "towhall-available"; public static final String EXAM_PROCTORING_ATTR_RECEIVE_AUDIO = "receive_audio"; public static final String EXAM_PROCTORING_ATTR_RECEIVE_VIDEO = "receive_video"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/InstitutionalAuthenticationEntryPoint.java b/src/main/java/ch/ethz/seb/sebserver/gui/InstitutionalAuthenticationEntryPoint.java index 8d1460a5..49e5bf8d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/InstitutionalAuthenticationEntryPoint.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/InstitutionalAuthenticationEntryPoint.java @@ -116,60 +116,56 @@ public final class InstitutionalAuthenticationEntryPoint implements Authenticati final String institutionalEndpoint = extractInstitutionalEndpoint(request); - if (StringUtils.isNoneBlank(institutionalEndpoint) && log.isDebugEnabled()) { - log.debug("No default gui entrypoint requested: {}", institutionalEndpoint); - } else { - request.getSession().setAttribute(INST_SUFFIX_ATTRIBUTE, null); - request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE); - forwardToEntryPoint(request, response, this.guiEntryPoint, false); - return; - } - - try { - - final RestTemplate restTemplate = new RestTemplate(); - final List institutions = restTemplate - .exchange( - this.webserviceURIService.getURIBuilder() - .path(API.INFO_ENDPOINT + API.INFO_INST_ENDPOINT) - .toUriString(), - HttpMethod.GET, - HttpEntity.EMPTY, - new ParameterizedTypeReference>() { - }, - institutionalEndpoint, - API.INFO_PARAM_INST_SUFFIX, - institutionalEndpoint) - .getBody(); - - if (institutions != null && !institutions.isEmpty()) { - request.getSession().setAttribute( - INST_SUFFIX_ATTRIBUTE, - StringUtils.isNotBlank(institutionalEndpoint) - ? institutionalEndpoint - : null); - - if (log.isDebugEnabled()) { - log.debug("Known and active gui entrypoint requested: {}", institutions); - } - - final String logoImageBase64 = requestLogoImage(institutionalEndpoint); - if (StringUtils.isNotBlank(logoImageBase64)) { - request.getSession().setAttribute(API.PARAM_LOGO_IMAGE, logoImageBase64); - - } - forwardToEntryPoint(request, response, this.guiEntryPoint, false); - return; + if (StringUtils.isNotBlank(institutionalEndpoint)) { + if (log.isDebugEnabled()) { + log.debug("No default gui entrypoint requested: {}", institutionalEndpoint); } - } catch (final Exception e) { - log.error("Failed to extract and set institutional endpoint request: ", e); + try { + + final RestTemplate restTemplate = new RestTemplate(); + final List institutions = restTemplate + .exchange( + this.webserviceURIService.getURIBuilder() + .path(API.INFO_ENDPOINT + API.INFO_INST_ENDPOINT) + .toUriString(), + HttpMethod.GET, + HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, + institutionalEndpoint, + API.INFO_PARAM_INST_SUFFIX, + institutionalEndpoint) + .getBody(); + + if (institutions != null && !institutions.isEmpty()) { + request.getSession().setAttribute( + INST_SUFFIX_ATTRIBUTE, + StringUtils.isNotBlank(institutionalEndpoint) + ? institutionalEndpoint + : null); + + if (log.isDebugEnabled()) { + log.debug("Known and active gui entrypoint requested: {}", institutions); + } + + final String logoImageBase64 = requestLogoImage(institutionalEndpoint); + if (StringUtils.isNotBlank(logoImageBase64)) { + request.getSession().setAttribute(API.PARAM_LOGO_IMAGE, logoImageBase64); + + } + forwardToEntryPoint(request, response, this.guiEntryPoint, false); + return; + } + } catch (final Exception e) { + log.error("Failed to extract and set institutional endpoint request: ", e); + } } request.getSession().setAttribute(INST_SUFFIX_ATTRIBUTE, null); request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE); response.setStatus(HttpStatus.UNAUTHORIZED.value()); - forwardToEntryPoint(request, response, this.guiEntryPoint, true); + forwardToEntryPoint(request, response, this.guiEntryPoint, institutionalEndpoint == null); } @@ -203,16 +199,25 @@ public final class InstitutionalAuthenticationEntryPoint implements Authenticati public static String extractInstitutionalEndpoint(final HttpServletRequest request) { final String requestURI = request.getRequestURI(); - if (StringUtils.isBlank(requestURI) || requestURI.equals(Constants.SLASH.toString())) { + if (StringUtils.isBlank(requestURI)) { return null; } - if (log.isDebugEnabled()) { - log.debug("Trying to verify institution from requested entrypoint url: {}", requestURI); + if (requestURI.equals(Constants.SLASH.toString())) { + return StringUtils.EMPTY; } try { - return requestURI.substring(requestURI.lastIndexOf(Constants.SLASH) + 1); + if (log.isDebugEnabled()) { + log.debug("Trying to verify institution from requested entrypoint url: {}", requestURI); + } + + final String[] split = StringUtils.split(requestURI, Constants.SLASH); + if (split.length > 1) { + return null; + } + + return split[0]; } catch (final Exception e) { log.error("Failed to extract institutional URL suffix: {}", e.getMessage()); return null; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java index 1606456b..43c06fbd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java @@ -17,6 +17,7 @@ import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Function; +import org.apache.commons.lang3.BooleanUtils; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; import org.eclipse.swt.SWT; @@ -77,7 +78,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctorin import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionDataList; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetTownhallRoom; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.IsTownhallRoomAvailable; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable; import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor; @@ -123,6 +124,7 @@ public class MonitoringRunningExam implements TemplateComposer { private final ServerPushService serverPushService; private final PageService pageService; + private final RestService restService; private final ResourceService resourceService; private final AsyncRunner asyncRunner; private final InstructionProcessor instructionProcessor; @@ -147,6 +149,7 @@ public class MonitoringRunningExam implements TemplateComposer { this.serverPushService = serverPushService; this.pageService = pageService; + this.restService = pageService.getRestService(); this.resourceService = pageService.getResourceService(); this.asyncRunner = asyncRunner; this.instructionProcessor = instructionProcessor; @@ -214,13 +217,14 @@ public class MonitoringRunningExam implements TemplateComposer { this.serverPushService.runServerPush( new ServerPushContext( - content, Utils.truePredicate(), + content, + Utils.truePredicate(), createServerPushUpdateErrorHandler(this.pageService, pageContext)), this.pollInterval, context -> clientTable.updateValues(), updateTableGUI(clientTable)); - final BooleanSupplier privilege = () -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER); + final BooleanSupplier isExamSupporter = () -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER); actionBuilder @@ -242,20 +246,20 @@ public class MonitoringRunningExam implements TemplateComposer { return copyOfPageAction; }) - .publishIf(privilege, false) + .publishIf(isExamSupporter, false) .newAction(ActionDefinition.MONITOR_EXAM_QUIT_ALL) .withEntityKey(entityKey) .withConfirm(() -> CONFIRM_QUIT_ALL) .withExec(action -> this.quitSEBClients(action, clientTable, true)) .noEventPropagation() - .publishIf(privilege) + .publishIf(isExamSupporter) .newAction(ActionDefinition.MONITORING_EXAM_SEARCH_CONNECTIONS) .withEntityKey(entityKey) .withExec(this::openSearchPopup) .noEventPropagation() - .publishIf(privilege) + .publishIf(isExamSupporter) .newAction(ActionDefinition.MONITOR_EXAM_QUIT_SELECTED) .withEntityKey(entityKey) @@ -265,7 +269,7 @@ public class MonitoringRunningExam implements TemplateComposer { action -> this.quitSEBClients(action, clientTable, false), EMPTY_ACTIVE_SELECTION_TEXT_KEY) .noEventPropagation() - .publishIf(privilege, false) + .publishIf(isExamSupporter, false) .newAction(ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION) .withEntityKey(entityKey) @@ -275,81 +279,26 @@ public class MonitoringRunningExam implements TemplateComposer { action -> this.disableSEBClients(action, clientTable, false), EMPTY_SELECTION_TEXT_KEY) .noEventPropagation() - .publishIf(privilege, false); - - if (privilege.getAsBoolean()) { - - if (clientTable.isStatusHidden(ConnectionStatus.CLOSED)) { - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION) - .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED)) - .noEventPropagation() - .withSwitchAction( - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION) - .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED)) - .noEventPropagation() - .create()) - .publish(); - } else { - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION) - .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED)) - .noEventPropagation() - .withSwitchAction( - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION) - .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED)) - .noEventPropagation() - .create()) - .publish(); - } - - if (clientTable.isStatusHidden(ConnectionStatus.CONNECTION_REQUESTED)) { - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION) - .withExec(showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) - .noEventPropagation() - .withSwitchAction( - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION) - .withExec( - hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) - .noEventPropagation() - .create()) - .publish(); - } else { - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION) - .withExec(hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) - .noEventPropagation() - .withSwitchAction( - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION) - .withExec( - showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) - .noEventPropagation() - .create()) - .publish(); - } - - if (clientTable.isStatusHidden(ConnectionStatus.DISABLED)) { - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION) - .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED)) - .noEventPropagation() - .withSwitchAction( - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION) - .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED)) - .noEventPropagation() - .create()) - .publish(); - } else { - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION) - .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED)) - .noEventPropagation() - .withSwitchAction( - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION) - .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED)) - .noEventPropagation() - .create()) - .publish(); - } + .publishIf(isExamSupporter, false); + if (isExamSupporter.getAsBoolean()) { + addFilterActions(actionBuilder, clientTable, isExamSupporter); + addProctoringActions( + currentUser.getProctoringGUIService(), + pageContext, + content, + actionBuilder); } + } - final ProctoringServiceSettings proctoringSettings = restService + private void addProctoringActions( + final ProctoringGUIService proctoringGUIService, + final PageContext pageContext, + final Composite parent, + final PageActionBuilder actionBuilder) { + + final EntityKey entityKey = pageContext.getEntityKey(); + final ProctoringServiceSettings proctoringSettings = this.restService .getBuilder(GetProctoringSettings.class) .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) .call() @@ -359,7 +308,7 @@ public class MonitoringRunningExam implements TemplateComposer { actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM) .withEntityKey(entityKey) - .withExec(this::toggleTownhallRoom) + .withExec(action -> this.toggleTownhallRoom(proctoringGUIService, action)) .noEventPropagation() .publish(); @@ -375,34 +324,126 @@ public class MonitoringRunningExam implements TemplateComposer { final Map> availableRooms = new HashMap<>(); updateRoomActions( - entityKey, pageContext, availableRooms, actionBuilder, - proctoringSettings); + proctoringSettings, + proctoringGUIService); this.serverPushService.runServerPush( new ServerPushContext( - content, + parent, Utils.truePredicate(), createServerPushUpdateErrorHandler(this.pageService, pageContext)), this.proctoringRoomUpdateInterval, context -> updateRoomActions( - entityKey, pageContext, availableRooms, actionBuilder, - proctoringSettings)); + proctoringSettings, + proctoringGUIService)); + } + } + + private void addFilterActions( + final PageActionBuilder actionBuilder, + final ClientConnectionTable clientTable, + final BooleanSupplier isExamSupporter) { + + addClosedFilterAction(actionBuilder, clientTable); + addRequestedFilterAction(actionBuilder, clientTable); + addDisabledFilterAction(actionBuilder, clientTable); + } + + private void addDisabledFilterAction( + final PageActionBuilder actionBuilder, + final ClientConnectionTable clientTable) { + + if (clientTable.isStatusHidden(ConnectionStatus.DISABLED)) { + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION) + .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED)) + .noEventPropagation() + .withSwitchAction( + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION) + .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED)) + .noEventPropagation() + .create()) + .publish(); + } else { + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_DISABLED_CONNECTION) + .withExec(hideStateViewAction(clientTable, ConnectionStatus.DISABLED)) + .noEventPropagation() + .withSwitchAction( + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_DISABLED_CONNECTION) + .withExec(showStateViewAction(clientTable, ConnectionStatus.DISABLED)) + .noEventPropagation() + .create()) + .publish(); + } + } + + private void addRequestedFilterAction( + final PageActionBuilder actionBuilder, + final ClientConnectionTable clientTable) { + + if (clientTable.isStatusHidden(ConnectionStatus.CONNECTION_REQUESTED)) { + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION) + .withExec(showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) + .noEventPropagation() + .withSwitchAction( + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION) + .withExec( + hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) + .noEventPropagation() + .create()) + .publish(); + } else { + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_REQUESTED_CONNECTION) + .withExec(hideStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) + .noEventPropagation() + .withSwitchAction( + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_REQUESTED_CONNECTION) + .withExec( + showStateViewAction(clientTable, ConnectionStatus.CONNECTION_REQUESTED)) + .noEventPropagation() + .create()) + .publish(); + } + } + + private void addClosedFilterAction( + final PageActionBuilder actionBuilder, + final ClientConnectionTable clientTable) { + + if (clientTable.isStatusHidden(ConnectionStatus.CLOSED)) { + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION) + .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED)) + .noEventPropagation() + .withSwitchAction( + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION) + .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED)) + .noEventPropagation() + .create()) + .publish(); + } else { + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_HIDE_CLOSED_CONNECTION) + .withExec(hideStateViewAction(clientTable, ConnectionStatus.CLOSED)) + .noEventPropagation() + .withSwitchAction( + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_SHOW_CLOSED_CONNECTION) + .withExec(showStateViewAction(clientTable, ConnectionStatus.CLOSED)) + .noEventPropagation() + .create()) + .publish(); } } private boolean isTownhallRoomActive(final String examModelId) { - final RemoteProctoringRoom townhall = this.pageService.getRestService() - .getBuilder(GetTownhallRoom.class) + return !BooleanUtils.toBoolean(this.pageService + .getRestService() + .getBuilder(IsTownhallRoomAvailable.class) .withURIVariable(API.PARAM_MODEL_ID, examModelId) .call() - .getOr(null); - - return townhall != null && townhall.id != null; + .getOr(Constants.FALSE_STRING)); } private PageAction openSearchPopup(final PageAction action) { @@ -410,9 +451,12 @@ public class MonitoringRunningExam implements TemplateComposer { return action; } - private PageAction toggleTownhallRoom(final PageAction action) { + private PageAction toggleTownhallRoom( + final ProctoringGUIService proctoringGUIService, + final PageAction action) { + if (isTownhallRoomActive(action.getEntityKey().modelId)) { - closeTownhallRoom(action); + closeTownhallRoom(proctoringGUIService, action); this.pageService.firePageEvent( new ActionActivationEvent( true, @@ -422,7 +466,7 @@ public class MonitoringRunningExam implements TemplateComposer { action.pageContext()); return action; } else { - openTownhallRoom(action); + openTownhallRoom(proctoringGUIService, action); this.pageService.firePageEvent( new ActionActivationEvent( true, @@ -434,14 +478,13 @@ public class MonitoringRunningExam implements TemplateComposer { } } - private PageAction openTownhallRoom(final PageAction action) { + private PageAction openTownhallRoom( + final ProctoringGUIService proctoringGUIService, + final PageAction action) { + try { final EntityKey examId = action.getEntityKey(); - final ProctoringGUIService proctoringGUIService = this.pageService - .getCurrentUser() - .getProctoringGUIService(); - final String windowName = getTownhallWindowName(examId.modelId); if (!proctoringGUIService.hasWindow(windowName)) { final ProctoringRoomConnection proctoringConnectionData = proctoringGUIService @@ -475,11 +518,10 @@ public class MonitoringRunningExam implements TemplateComposer { return action; } - private String getTownhallWindowName(final String examId) { - return examId + "_townhall"; - } + private PageAction closeTownhallRoom( + final ProctoringGUIService proctoringGUIService, + final PageAction action) { - private PageAction closeTownhallRoom(final PageAction action) { final String examId = action.getEntityKey().modelId; try { @@ -494,34 +536,52 @@ public class MonitoringRunningExam implements TemplateComposer { return action; } - private void updateTownhallButton(final EntityKey entityKey, final PageContext pageContext) { + private void updateTownhallButton( + final ProctoringGUIService proctoringGUIService, + final PageContext pageContext) { + final EntityKey entityKey = pageContext.getEntityKey(); + if (isTownhallRoomActive(entityKey.modelId)) { - this.pageService.firePageEvent( - new ActionActivationEvent( - true, - new Tuple<>( - ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, - ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), - pageContext); + final boolean townhallRoomFromThisUser = proctoringGUIService + .isTownhallOpenForUser(entityKey.modelId); + if (townhallRoomFromThisUser) { + this.pageService.firePageEvent( + new ActionActivationEvent( + true, + new Tuple<>( + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, + ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), + pageContext); + } else { + this.pageService.firePageEvent( + new ActionActivationEvent( + false, + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, + ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM), + pageContext); + } } else { this.pageService.firePageEvent( new ActionActivationEvent( true, new Tuple<>( ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, - ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)), + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM), + ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, + ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM), pageContext); } } private void updateRoomActions( - final EntityKey entityKey, final PageContext pageContext, final Map> rooms, final PageActionBuilder actionBuilder, - final ProctoringServiceSettings proctoringSettings) { + final ProctoringServiceSettings proctoringSettings, + final ProctoringGUIService proctoringGUIService) { - updateTownhallButton(entityKey, pageContext); + final EntityKey entityKey = pageContext.getEntityKey(); + updateTownhallButton(proctoringGUIService, pageContext); final I18nSupport i18nSupport = this.pageService.getI18nSupport(); this.pageService .getRestService() @@ -564,7 +624,7 @@ public class MonitoringRunningExam implements TemplateComposer { this.pageService.publishAction( action, _treeItem -> rooms.put(room.name, new Pair<>(room, _treeItem))); - addRoomConnectionsPopupListener(entityKey, pageContext, rooms); + addRoomConnectionsPopupListener(pageContext, rooms); processProctorRoomActionActivation(rooms.get(room.name).b, room, pageContext); } }); @@ -589,11 +649,11 @@ public class MonitoringRunningExam implements TemplateComposer { } private void addRoomConnectionsPopupListener( - final EntityKey entityKey, final PageContext pageContext, final Map> rooms) { if (!rooms.isEmpty()) { + final EntityKey entityKey = pageContext.getEntityKey(); final TreeItem treeItem = rooms.values().iterator().next().b; final Tree tree = treeItem.getParent(); if (tree.getData(SHOW_CONNECTION_ACTION_APPLIED) == null) { @@ -775,4 +835,8 @@ public class MonitoringRunningExam implements TemplateComposer { }; } + private String getTownhallWindowName(final String examId) { + return examId + "_townhall"; + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java index 4680f122..629687c9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java @@ -84,17 +84,18 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { public void compose(final PageContext pageContext) { final ProctoringWindowData proctoringWindowData = ProctoringGUIService.getCurrentProctoringWindowData(); - final Composite parent = pageContext.getParent(); - final Composite content = new Composite(parent, SWT.NONE | SWT.NO_SCROLL); final GridLayout gridLayout = new GridLayout(); + final ProctoringGUIService proctoringGUIService = this.pageService + .getCurrentUser() + .getProctoringGUIService(); content.setLayout(gridLayout); final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true); content.setLayoutData(headerCell); - parent.addListener(SWT.Dispose, event -> closeRoom(proctoringWindowData)); + parent.addListener(SWT.Dispose, event -> closeRoom(proctoringGUIService, proctoringWindowData)); final String url = this.guiServiceInfo .getExternalServerURIBuilder() @@ -124,7 +125,7 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { final Button closeAction = widgetFactory.buttonLocalized(footer, CLOSE_WINDOW_TEXT_KEY); closeAction.setLayoutData(new RowData(150, 30)); - closeAction.addListener(SWT.Selection, event -> closeRoom(proctoringWindowData)); + closeAction.addListener(SWT.Selection, event -> closeRoom(proctoringGUIService, proctoringWindowData)); final BroadcastActionState broadcastActionState = new BroadcastActionState(); @@ -232,11 +233,15 @@ public class JitsiMeetProctoringView implements RemoteProctoringView { sendReconfigurationAttributes(examId, roomName, state); } - private void closeRoom(final ProctoringWindowData proctoringWindowData) { - this.pageService - .getCurrentUser() - .getProctoringGUIService() - .closeRoomWindow(proctoringWindowData.windowName); + private void closeRoom( + final ProctoringGUIService proctoringGUIService, + final ProctoringWindowData proctoringWindowData) { + + try { + proctoringGUIService.closeRoomWindow(proctoringWindowData.connectionData.roomName); + } catch (final Exception e) { + log.error("Failed to close proctoring window properly: ", e); + } } static final class BroadcastActionState { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/IsTownhallRoomAvailable.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/IsTownhallRoomAvailable.java new file mode 100644 index 00000000..8aff4063 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/IsTownhallRoomAvailable.java @@ -0,0 +1,41 @@ +/* + * 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.remote.webservice.api.session; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class IsTownhallRoomAvailable extends RestCall { + + public IsTownhallRoomAvailable() { + super(new TypeKey<>( + CallType.GET_SINGLE, + EntityType.REMOTE_PROCTORING_ROOM, + new TypeReference() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_PROCTORING_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_PROCTORING_TOWNHALL_ROOM_AVAILABLE); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java index 30ecce8c..db5c0696 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java @@ -50,6 +50,13 @@ public class ProctoringGUIService { this.openWindows.put(windowName, new RoomData(roomName, examId)); } + public boolean isTownhallOpenForUser(final String examId) { + return this.openWindows.values().stream() + .filter(room -> room.isTownhall && room.examId.equals(examId)) + .findFirst() + .isPresent(); + } + public static ProctoringWindowData getCurrentProctoringWindowData() { return (ProctoringWindowData) RWT.getUISession() .getHttpSession() @@ -105,7 +112,7 @@ public class ProctoringGUIService { .withFormParam(ProctoringRoomConnection.ATTR_SUBJECT, subject) .call() .map(connection -> { - this.openWindows.put(windowName, new RoomData(connection.roomName, examId)); + this.openWindows.put(windowName, new RoomData(connection.roomName, examId, true)); return connection; }); } @@ -152,10 +159,16 @@ public class ProctoringGUIService { private static final class RoomData { final String roomName; final String examId; + final boolean isTownhall; public RoomData(final String roomName, final String examId) { + this(roomName, examId, false); + } + + public RoomData(final String roomName, final String examId, final boolean townhall) { this.roomName = roomName; this.examId = examId; + this.isTownhall = townhall; } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/RemoteProctoringRoomDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/RemoteProctoringRoomDAO.java index d1d936f0..2b8f78a7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/RemoteProctoringRoomDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/RemoteProctoringRoomDAO.java @@ -59,6 +59,8 @@ public interface RemoteProctoringRoomDAO { * @return Result refer to the created room record or to an error when happened */ Result createTownhallRoom(Long examId, NewRoom room); + boolean isTownhallRoomActive(Long examId); + /** Get the town hall room record for a given exam if existing. * * @param examId the exam identifier diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java index 41840c6a..fe0c239f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java @@ -20,6 +20,8 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -42,6 +44,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.New @WebServiceProfile public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO { + private static final Logger log = LoggerFactory.getLogger(RemoteProctoringRoomDAOImpl.class); + private static final Object RESERVE_ROOM_LOCK = new Object(); private final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper; @@ -129,15 +133,10 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO { final NewRoom room) { return Result.tryCatch(() -> { - // check first if town-hall room is not already active - final long active = this.remoteProctoringRoomRecordMapper.countByExample() - .where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId)) - .and(RemoteProctoringRoomRecordDynamicSqlSupport.townhallRoom, isNotEqualTo(0)) - .build() - .execute(); - if (active > 0) { - throw new IllegalStateException("Townhall, for exam: " + examId + " already existis"); + // Check first if town-hall room is not already active + if (isTownhallRoomActive(examId)) { + throw new IllegalStateException("Townhall, for exam: " + examId + " already exists"); } final RemoteProctoringRoomRecord townhallRoomRecord = new RemoteProctoringRoomRecord( @@ -159,6 +158,24 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO { .onError(TransactionHandler::rollback); } + @Override + @Transactional(readOnly = true) + public boolean isTownhallRoomActive(final Long examId) { + try { + final long active = this.remoteProctoringRoomRecordMapper.countByExample() + .where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId)) + .and(RemoteProctoringRoomRecordDynamicSqlSupport.townhallRoom, isNotEqualTo(0)) + .build() + .execute(); + return (active > 0); + } catch (final Exception e) { + log.error( + "Failed to verify town-hall room activity for exam: {}. Mark it as active to avoid double openings", + examId, e); + return true; + } + } + @Override @Transactional public Result deleteTownhallRoom(final Long examId) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java index e58e2206..8b07bc83 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java @@ -58,6 +58,8 @@ public interface ExamProctoringRoomService { * @return Result refer to the given exam or to an error when happened */ Result disposeRoomsForExam(Exam exam); + boolean isTownhallRoomActive(final Long examId); + /** This creates a town-hall room for a specific exam. The exam must be active and running * and there must be no other town-hall room already be active. An unique room name will be * created and returned. diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java index 795aa657..18468e20 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ExamProctoringRoomServiceImpl.java @@ -318,6 +318,11 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService } } + @Override + public boolean isTownhallRoomActive(final Long examId) { + return this.remoteProctoringRoomDAO.isTownhallRoomActive(examId); + } + private void closeTownhall( final Long examId, final ProctoringServiceSettings proctoringSettings, @@ -593,4 +598,5 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService true) .onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error)); } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java index 552e88b9..93f1e935 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java @@ -21,6 +21,7 @@ import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; @@ -35,6 +36,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.JSONMapper; @@ -101,7 +103,7 @@ public class ExamAPI_V1_Controller { final POSTMapper mapper = new POSTMapper(formParams, request.getQueryString()); - final String remoteAddr = request.getRemoteAddr(); + final String remoteAddr = this.getClientAddress(request); final Long institutionId = (instIdRequestParam != null) ? instIdRequestParam : mapper.getLong(API.PARAM_INSTITUTION_ID); @@ -163,7 +165,7 @@ public class ExamAPI_V1_Controller { return CompletableFuture.runAsync( () -> { - final String remoteAddr = request.getRemoteAddr(); + final String remoteAddr = this.getClientAddress(request); final Long institutionId = getInstitutionId(principal); this.sebClientConnectionService.updateClientConnection( @@ -193,7 +195,7 @@ public class ExamAPI_V1_Controller { return CompletableFuture.runAsync( () -> { - final String remoteAddr = request.getRemoteAddr(); + final String remoteAddr = this.getClientAddress(request); final Long institutionId = getInstitutionId(principal); this.sebClientConnectionService.establishClientConnection( @@ -220,7 +222,7 @@ public class ExamAPI_V1_Controller { return CompletableFuture.runAsync( () -> { - final String remoteAddr = request.getRemoteAddr(); + final String remoteAddr = this.getClientAddress(request); final Long institutionId = getInstitutionId(principal); if (log.isDebugEnabled()) { @@ -406,4 +408,23 @@ public class ExamAPI_V1_Controller { } } + private String getClientAddress(final HttpServletRequest request) { + try { + final String ipAddress = request.getHeader("X-FORWARDED-FOR"); + + if (ipAddress == null) { + return request.getRemoteAddr(); + } + + if (ipAddress.contains(",")) { + return StringUtils.split(ipAddress, Constants.COMMA)[0]; + } + + return ipAddress; + } catch (final Exception e) { + log.warn("Failed to verify client IP address: {}", e.getMessage()); + return request.getHeader("X-FORWARDED-FOR"); + } + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java index 80d61ae0..95744458 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java @@ -234,6 +234,22 @@ public class ExamProctoringController { .onError(error -> log.error("Failed to close remote proctoring break out room {}", roomName, error)); } + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_PROCTORING_TOWNHALL_ROOM_AVAILABLE, + method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_VALUE) + public String isTownhallRoomAvialbale( + @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) { + + checkExamReadAccess(institutionId); + return String.valueOf(!this.examProcotringRoomService.isTownhallRoomActive(examId)); + } + @RequestMapping( path = API.MODEL_ID_VAR_PATH_SEGMENT + API.EXAM_PROCTORING_TOWNHALL_ROOM_DATA, @@ -246,7 +262,7 @@ public class ExamProctoringController { defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, @PathVariable(name = API.PARAM_MODEL_ID) final Long examId) { - checkAccess(institutionId, examId); + checkExamReadAccess(institutionId); return this.examProcotringRoomService .getTownhallRoomData(examId) .getOrElse(() -> RemoteProctoringRoom.NULL_ROOM); @@ -273,6 +289,13 @@ public class ExamProctoringController { .getOrThrow(); } + private void checkExamReadAccess(final Long institutionId) { + this.authorization.check( + PrivilegeType.READ, + EntityType.EXAM, + institutionId); + } + private void checkAccess(final Long institutionId, final Long examId) { this.authorization.check( PrivilegeType.READ,