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

Conflicts:
	src/main/java/ch/ethz/seb/sebserver/gui/content/MonitoringRunningExam.java
	src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/JitsiMeetProctoringView.java
	src/main/java/ch/ethz/seb/sebserver/gui/service/session/ProctoringGUIService.java
	src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/RemoteProctoringRoomDAO.java
	src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/RemoteProctoringRoomDAOImpl.java
	src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java
	src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProctoringRoomServiceImpl.java
	src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java
This commit is contained in:
anhefti 2021-03-29 14:53:49 +02:00
commit 6ff8b703c9
13 changed files with 402 additions and 194 deletions

View file

@ -40,6 +40,9 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@Order(7) @Order(7)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements ErrorController { 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}") @Value("${sebserver.webservice.http.redirect.gui}")
private String guiRedirect; private String guiRedirect;
@Value("${sebserver.webservice.api.exam.endpoint.discovery}") @Value("${sebserver.webservice.api.exam.endpoint.discovery}")
@ -78,22 +81,27 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements E
public void configure(final WebSecurity web) { public void configure(final WebSecurity web) {
web web
.ignoring() .ignoring()
.antMatchers("/error") .antMatchers(ERROR_PATH)
.antMatchers(CHECK_PATH)
.antMatchers(this.examAPIDiscoveryEndpoint) .antMatchers(this.examAPIDiscoveryEndpoint)
.antMatchers(this.adminAPIEndpoint + API.INFO_ENDPOINT + API.LOGO_PATH_SEGMENT + "/**") .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.INFO_ENDPOINT + API.INFO_INST_PATH_SEGMENT + "/**")
.antMatchers(this.adminAPIEndpoint + API.REGISTER_ENDPOINT); .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 { 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.setHeader(HttpHeaders.LOCATION, this.guiRedirect);
response.flushBuffer(); response.flushBuffer();
} }
@Override @Override
public String getErrorPath() { public String getErrorPath() {
return "/error"; return ERROR_PATH;
} }
} }

View file

@ -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_ROOM_CONNECTIONS_PATH_SEGMENT = "/room-connections";
public static final String EXAM_PROCTORING_ACTIVATE_TOWNHALL_ROOM = "activate-towhall-room"; 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_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_AUDIO = "receive_audio";
public static final String EXAM_PROCTORING_ATTR_RECEIVE_VIDEO = "receive_video"; public static final String EXAM_PROCTORING_ATTR_RECEIVE_VIDEO = "receive_video";

View file

@ -116,60 +116,56 @@ public final class InstitutionalAuthenticationEntryPoint implements Authenticati
final String institutionalEndpoint = extractInstitutionalEndpoint(request); final String institutionalEndpoint = extractInstitutionalEndpoint(request);
if (StringUtils.isNoneBlank(institutionalEndpoint) && log.isDebugEnabled()) { if (StringUtils.isNotBlank(institutionalEndpoint)) {
log.debug("No default gui entrypoint requested: {}", institutionalEndpoint); if (log.isDebugEnabled()) {
} else { log.debug("No default gui entrypoint requested: {}", institutionalEndpoint);
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<EntityName> institutions = restTemplate
.exchange(
this.webserviceURIService.getURIBuilder()
.path(API.INFO_ENDPOINT + API.INFO_INST_ENDPOINT)
.toUriString(),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<EntityName>>() {
},
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);
try {
final RestTemplate restTemplate = new RestTemplate();
final List<EntityName> institutions = restTemplate
.exchange(
this.webserviceURIService.getURIBuilder()
.path(API.INFO_ENDPOINT + API.INFO_INST_ENDPOINT)
.toUriString(),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<EntityName>>() {
},
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().setAttribute(INST_SUFFIX_ATTRIBUTE, null);
request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE); request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE);
response.setStatus(HttpStatus.UNAUTHORIZED.value()); 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) { public static String extractInstitutionalEndpoint(final HttpServletRequest request) {
final String requestURI = request.getRequestURI(); final String requestURI = request.getRequestURI();
if (StringUtils.isBlank(requestURI) || requestURI.equals(Constants.SLASH.toString())) { if (StringUtils.isBlank(requestURI)) {
return null; return null;
} }
if (log.isDebugEnabled()) { if (requestURI.equals(Constants.SLASH.toString())) {
log.debug("Trying to verify institution from requested entrypoint url: {}", requestURI); return StringUtils.EMPTY;
} }
try { 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) { } catch (final Exception e) {
log.error("Failed to extract institutional URL suffix: {}", e.getMessage()); log.error("Failed to extract institutional URL suffix: {}", e.getMessage());
return null; return null;

View file

@ -17,6 +17,7 @@ import java.util.function.BooleanSupplier;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import org.apache.commons.lang3.BooleanUtils;
import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; import org.eclipse.rap.rwt.client.service.JavaScriptExecutor;
import org.eclipse.swt.SWT; 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.GetClientConnectionDataList;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms; 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.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.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable; import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable;
import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor; import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor;
@ -123,6 +124,7 @@ public class MonitoringRunningExam implements TemplateComposer {
private final ServerPushService serverPushService; private final ServerPushService serverPushService;
private final PageService pageService; private final PageService pageService;
private final RestService restService;
private final ResourceService resourceService; private final ResourceService resourceService;
private final AsyncRunner asyncRunner; private final AsyncRunner asyncRunner;
private final InstructionProcessor instructionProcessor; private final InstructionProcessor instructionProcessor;
@ -147,6 +149,7 @@ public class MonitoringRunningExam implements TemplateComposer {
this.serverPushService = serverPushService; this.serverPushService = serverPushService;
this.pageService = pageService; this.pageService = pageService;
this.restService = pageService.getRestService();
this.resourceService = pageService.getResourceService(); this.resourceService = pageService.getResourceService();
this.asyncRunner = asyncRunner; this.asyncRunner = asyncRunner;
this.instructionProcessor = instructionProcessor; this.instructionProcessor = instructionProcessor;
@ -214,13 +217,14 @@ public class MonitoringRunningExam implements TemplateComposer {
this.serverPushService.runServerPush( this.serverPushService.runServerPush(
new ServerPushContext( new ServerPushContext(
content, Utils.truePredicate(), content,
Utils.truePredicate(),
createServerPushUpdateErrorHandler(this.pageService, pageContext)), createServerPushUpdateErrorHandler(this.pageService, pageContext)),
this.pollInterval, this.pollInterval,
context -> clientTable.updateValues(), context -> clientTable.updateValues(),
updateTableGUI(clientTable)); updateTableGUI(clientTable));
final BooleanSupplier privilege = () -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER); final BooleanSupplier isExamSupporter = () -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER);
actionBuilder actionBuilder
@ -242,20 +246,20 @@ public class MonitoringRunningExam implements TemplateComposer {
return copyOfPageAction; return copyOfPageAction;
}) })
.publishIf(privilege, false) .publishIf(isExamSupporter, false)
.newAction(ActionDefinition.MONITOR_EXAM_QUIT_ALL) .newAction(ActionDefinition.MONITOR_EXAM_QUIT_ALL)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withConfirm(() -> CONFIRM_QUIT_ALL) .withConfirm(() -> CONFIRM_QUIT_ALL)
.withExec(action -> this.quitSEBClients(action, clientTable, true)) .withExec(action -> this.quitSEBClients(action, clientTable, true))
.noEventPropagation() .noEventPropagation()
.publishIf(privilege) .publishIf(isExamSupporter)
.newAction(ActionDefinition.MONITORING_EXAM_SEARCH_CONNECTIONS) .newAction(ActionDefinition.MONITORING_EXAM_SEARCH_CONNECTIONS)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withExec(this::openSearchPopup) .withExec(this::openSearchPopup)
.noEventPropagation() .noEventPropagation()
.publishIf(privilege) .publishIf(isExamSupporter)
.newAction(ActionDefinition.MONITOR_EXAM_QUIT_SELECTED) .newAction(ActionDefinition.MONITOR_EXAM_QUIT_SELECTED)
.withEntityKey(entityKey) .withEntityKey(entityKey)
@ -265,7 +269,7 @@ public class MonitoringRunningExam implements TemplateComposer {
action -> this.quitSEBClients(action, clientTable, false), action -> this.quitSEBClients(action, clientTable, false),
EMPTY_ACTIVE_SELECTION_TEXT_KEY) EMPTY_ACTIVE_SELECTION_TEXT_KEY)
.noEventPropagation() .noEventPropagation()
.publishIf(privilege, false) .publishIf(isExamSupporter, false)
.newAction(ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION) .newAction(ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION)
.withEntityKey(entityKey) .withEntityKey(entityKey)
@ -275,81 +279,26 @@ public class MonitoringRunningExam implements TemplateComposer {
action -> this.disableSEBClients(action, clientTable, false), action -> this.disableSEBClients(action, clientTable, false),
EMPTY_SELECTION_TEXT_KEY) EMPTY_SELECTION_TEXT_KEY)
.noEventPropagation() .noEventPropagation()
.publishIf(privilege, false); .publishIf(isExamSupporter, 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();
}
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) .getBuilder(GetProctoringSettings.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call() .call()
@ -359,7 +308,7 @@ public class MonitoringRunningExam implements TemplateComposer {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM) actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withExec(this::toggleTownhallRoom) .withExec(action -> this.toggleTownhallRoom(proctoringGUIService, action))
.noEventPropagation() .noEventPropagation()
.publish(); .publish();
@ -375,34 +324,126 @@ public class MonitoringRunningExam implements TemplateComposer {
final Map<String, Pair<RemoteProctoringRoom, TreeItem>> availableRooms = new HashMap<>(); final Map<String, Pair<RemoteProctoringRoom, TreeItem>> availableRooms = new HashMap<>();
updateRoomActions( updateRoomActions(
entityKey,
pageContext, pageContext,
availableRooms, availableRooms,
actionBuilder, actionBuilder,
proctoringSettings); proctoringSettings,
proctoringGUIService);
this.serverPushService.runServerPush( this.serverPushService.runServerPush(
new ServerPushContext( new ServerPushContext(
content, parent,
Utils.truePredicate(), Utils.truePredicate(),
createServerPushUpdateErrorHandler(this.pageService, pageContext)), createServerPushUpdateErrorHandler(this.pageService, pageContext)),
this.proctoringRoomUpdateInterval, this.proctoringRoomUpdateInterval,
context -> updateRoomActions( context -> updateRoomActions(
entityKey,
pageContext, pageContext,
availableRooms, availableRooms,
actionBuilder, 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) { private boolean isTownhallRoomActive(final String examModelId) {
final RemoteProctoringRoom townhall = this.pageService.getRestService() return !BooleanUtils.toBoolean(this.pageService
.getBuilder(GetTownhallRoom.class) .getRestService()
.getBuilder(IsTownhallRoomAvailable.class)
.withURIVariable(API.PARAM_MODEL_ID, examModelId) .withURIVariable(API.PARAM_MODEL_ID, examModelId)
.call() .call()
.getOr(null); .getOr(Constants.FALSE_STRING));
return townhall != null && townhall.id != null;
} }
private PageAction openSearchPopup(final PageAction action) { private PageAction openSearchPopup(final PageAction action) {
@ -410,9 +451,12 @@ public class MonitoringRunningExam implements TemplateComposer {
return action; return action;
} }
private PageAction toggleTownhallRoom(final PageAction action) { private PageAction toggleTownhallRoom(
final ProctoringGUIService proctoringGUIService,
final PageAction action) {
if (isTownhallRoomActive(action.getEntityKey().modelId)) { if (isTownhallRoomActive(action.getEntityKey().modelId)) {
closeTownhallRoom(action); closeTownhallRoom(proctoringGUIService, action);
this.pageService.firePageEvent( this.pageService.firePageEvent(
new ActionActivationEvent( new ActionActivationEvent(
true, true,
@ -422,7 +466,7 @@ public class MonitoringRunningExam implements TemplateComposer {
action.pageContext()); action.pageContext());
return action; return action;
} else { } else {
openTownhallRoom(action); openTownhallRoom(proctoringGUIService, action);
this.pageService.firePageEvent( this.pageService.firePageEvent(
new ActionActivationEvent( new ActionActivationEvent(
true, 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 { try {
final EntityKey examId = action.getEntityKey(); final EntityKey examId = action.getEntityKey();
final ProctoringGUIService proctoringGUIService = this.pageService
.getCurrentUser()
.getProctoringGUIService();
final String windowName = getTownhallWindowName(examId.modelId); final String windowName = getTownhallWindowName(examId.modelId);
if (!proctoringGUIService.hasWindow(windowName)) { if (!proctoringGUIService.hasWindow(windowName)) {
final ProctoringRoomConnection proctoringConnectionData = proctoringGUIService final ProctoringRoomConnection proctoringConnectionData = proctoringGUIService
@ -475,11 +518,10 @@ public class MonitoringRunningExam implements TemplateComposer {
return action; return action;
} }
private String getTownhallWindowName(final String examId) { private PageAction closeTownhallRoom(
return examId + "_townhall"; final ProctoringGUIService proctoringGUIService,
} final PageAction action) {
private PageAction closeTownhallRoom(final PageAction action) {
final String examId = action.getEntityKey().modelId; final String examId = action.getEntityKey().modelId;
try { try {
@ -494,34 +536,52 @@ public class MonitoringRunningExam implements TemplateComposer {
return action; 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)) { if (isTownhallRoomActive(entityKey.modelId)) {
this.pageService.firePageEvent( final boolean townhallRoomFromThisUser = proctoringGUIService
new ActionActivationEvent( .isTownhallOpenForUser(entityKey.modelId);
true, if (townhallRoomFromThisUser) {
new Tuple<>( this.pageService.firePageEvent(
ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM, new ActionActivationEvent(
ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM)), true,
pageContext); 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 { } else {
this.pageService.firePageEvent( this.pageService.firePageEvent(
new ActionActivationEvent( new ActionActivationEvent(
true, true,
new Tuple<>( 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_OPEN_TOWNHALL_PROCTOR_ROOM,
ActionDefinition.MONITOR_EXAM_CLOSE_TOWNHALL_PROCTOR_ROOM),
pageContext); pageContext);
} }
} }
private void updateRoomActions( private void updateRoomActions(
final EntityKey entityKey,
final PageContext pageContext, final PageContext pageContext,
final Map<String, Pair<RemoteProctoringRoom, TreeItem>> rooms, final Map<String, Pair<RemoteProctoringRoom, TreeItem>> rooms,
final PageActionBuilder actionBuilder, 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(); final I18nSupport i18nSupport = this.pageService.getI18nSupport();
this.pageService this.pageService
.getRestService() .getRestService()
@ -564,7 +624,7 @@ public class MonitoringRunningExam implements TemplateComposer {
this.pageService.publishAction( this.pageService.publishAction(
action, action,
_treeItem -> rooms.put(room.name, new Pair<>(room, _treeItem))); _treeItem -> rooms.put(room.name, new Pair<>(room, _treeItem)));
addRoomConnectionsPopupListener(entityKey, pageContext, rooms); addRoomConnectionsPopupListener(pageContext, rooms);
processProctorRoomActionActivation(rooms.get(room.name).b, room, pageContext); processProctorRoomActionActivation(rooms.get(room.name).b, room, pageContext);
} }
}); });
@ -589,11 +649,11 @@ public class MonitoringRunningExam implements TemplateComposer {
} }
private void addRoomConnectionsPopupListener( private void addRoomConnectionsPopupListener(
final EntityKey entityKey,
final PageContext pageContext, final PageContext pageContext,
final Map<String, Pair<RemoteProctoringRoom, TreeItem>> rooms) { final Map<String, Pair<RemoteProctoringRoom, TreeItem>> rooms) {
if (!rooms.isEmpty()) { if (!rooms.isEmpty()) {
final EntityKey entityKey = pageContext.getEntityKey();
final TreeItem treeItem = rooms.values().iterator().next().b; final TreeItem treeItem = rooms.values().iterator().next().b;
final Tree tree = treeItem.getParent(); final Tree tree = treeItem.getParent();
if (tree.getData(SHOW_CONNECTION_ACTION_APPLIED) == null) { 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";
}
} }

View file

@ -84,17 +84,18 @@ public class JitsiMeetProctoringView implements RemoteProctoringView {
public void compose(final PageContext pageContext) { public void compose(final PageContext pageContext) {
final ProctoringWindowData proctoringWindowData = ProctoringGUIService.getCurrentProctoringWindowData(); final ProctoringWindowData proctoringWindowData = ProctoringGUIService.getCurrentProctoringWindowData();
final Composite parent = pageContext.getParent(); final Composite parent = pageContext.getParent();
final Composite content = new Composite(parent, SWT.NONE | SWT.NO_SCROLL); final Composite content = new Composite(parent, SWT.NONE | SWT.NO_SCROLL);
final GridLayout gridLayout = new GridLayout(); final GridLayout gridLayout = new GridLayout();
final ProctoringGUIService proctoringGUIService = this.pageService
.getCurrentUser()
.getProctoringGUIService();
content.setLayout(gridLayout); content.setLayout(gridLayout);
final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true); final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true);
content.setLayoutData(headerCell); content.setLayoutData(headerCell);
parent.addListener(SWT.Dispose, event -> closeRoom(proctoringWindowData)); parent.addListener(SWT.Dispose, event -> closeRoom(proctoringGUIService, proctoringWindowData));
final String url = this.guiServiceInfo final String url = this.guiServiceInfo
.getExternalServerURIBuilder() .getExternalServerURIBuilder()
@ -124,7 +125,7 @@ public class JitsiMeetProctoringView implements RemoteProctoringView {
final Button closeAction = widgetFactory.buttonLocalized(footer, CLOSE_WINDOW_TEXT_KEY); final Button closeAction = widgetFactory.buttonLocalized(footer, CLOSE_WINDOW_TEXT_KEY);
closeAction.setLayoutData(new RowData(150, 30)); 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(); final BroadcastActionState broadcastActionState = new BroadcastActionState();
@ -232,11 +233,15 @@ public class JitsiMeetProctoringView implements RemoteProctoringView {
sendReconfigurationAttributes(examId, roomName, state); sendReconfigurationAttributes(examId, roomName, state);
} }
private void closeRoom(final ProctoringWindowData proctoringWindowData) { private void closeRoom(
this.pageService final ProctoringGUIService proctoringGUIService,
.getCurrentUser() final ProctoringWindowData proctoringWindowData) {
.getProctoringGUIService()
.closeRoomWindow(proctoringWindowData.windowName); try {
proctoringGUIService.closeRoomWindow(proctoringWindowData.connectionData.roomName);
} catch (final Exception e) {
log.error("Failed to close proctoring window properly: ", e);
}
} }
static final class BroadcastActionState { static final class BroadcastActionState {

View file

@ -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<String> {
public IsTownhallRoomAvailable() {
super(new TypeKey<>(
CallType.GET_SINGLE,
EntityType.REMOTE_PROCTORING_ROOM,
new TypeReference<String>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_PROCTORING_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_PROCTORING_TOWNHALL_ROOM_AVAILABLE);
}
}

View file

@ -50,6 +50,13 @@ public class ProctoringGUIService {
this.openWindows.put(windowName, new RoomData(roomName, examId)); 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() { public static ProctoringWindowData getCurrentProctoringWindowData() {
return (ProctoringWindowData) RWT.getUISession() return (ProctoringWindowData) RWT.getUISession()
.getHttpSession() .getHttpSession()
@ -105,7 +112,7 @@ public class ProctoringGUIService {
.withFormParam(ProctoringRoomConnection.ATTR_SUBJECT, subject) .withFormParam(ProctoringRoomConnection.ATTR_SUBJECT, subject)
.call() .call()
.map(connection -> { .map(connection -> {
this.openWindows.put(windowName, new RoomData(connection.roomName, examId)); this.openWindows.put(windowName, new RoomData(connection.roomName, examId, true));
return connection; return connection;
}); });
} }
@ -152,10 +159,16 @@ public class ProctoringGUIService {
private static final class RoomData { private static final class RoomData {
final String roomName; final String roomName;
final String examId; final String examId;
final boolean isTownhall;
public RoomData(final String roomName, final String examId) { 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.roomName = roomName;
this.examId = examId; this.examId = examId;
this.isTownhall = townhall;
} }
} }

View file

@ -59,6 +59,8 @@ public interface RemoteProctoringRoomDAO {
* @return Result refer to the created room record or to an error when happened */ * @return Result refer to the created room record or to an error when happened */
Result<RemoteProctoringRoom> createTownhallRoom(Long examId, NewRoom room); Result<RemoteProctoringRoom> createTownhallRoom(Long examId, NewRoom room);
boolean isTownhallRoomActive(Long examId);
/** Get the town hall room record for a given exam if existing. /** Get the town hall room record for a given exam if existing.
* *
* @param examId the exam identifier * @param examId the exam identifier

View file

@ -20,6 +20,8 @@ import java.util.stream.Collectors;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -42,6 +44,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.New
@WebServiceProfile @WebServiceProfile
public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO { public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
private static final Logger log = LoggerFactory.getLogger(RemoteProctoringRoomDAOImpl.class);
private static final Object RESERVE_ROOM_LOCK = new Object(); private static final Object RESERVE_ROOM_LOCK = new Object();
private final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper; private final RemoteProctoringRoomRecordMapper remoteProctoringRoomRecordMapper;
@ -129,15 +133,10 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
final NewRoom room) { final NewRoom room) {
return Result.tryCatch(() -> { 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) { // Check first if town-hall room is not already active
throw new IllegalStateException("Townhall, for exam: " + examId + " already existis"); if (isTownhallRoomActive(examId)) {
throw new IllegalStateException("Townhall, for exam: " + examId + " already exists");
} }
final RemoteProctoringRoomRecord townhallRoomRecord = new RemoteProctoringRoomRecord( final RemoteProctoringRoomRecord townhallRoomRecord = new RemoteProctoringRoomRecord(
@ -159,6 +158,24 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
.onError(TransactionHandler::rollback); .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 @Override
@Transactional @Transactional
public Result<EntityKey> deleteTownhallRoom(final Long examId) { public Result<EntityKey> deleteTownhallRoom(final Long examId) {

View file

@ -58,6 +58,8 @@ public interface ExamProctoringRoomService {
* @return Result refer to the given exam or to an error when happened */ * @return Result refer to the given exam or to an error when happened */
Result<Exam> disposeRoomsForExam(Exam exam); Result<Exam> 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 /** 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 * and there must be no other town-hall room already be active. An unique room name will be
* created and returned. * created and returned.

View file

@ -318,6 +318,11 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
} }
} }
@Override
public boolean isTownhallRoomActive(final Long examId) {
return this.remoteProctoringRoomDAO.isTownhallRoomActive(examId);
}
private void closeTownhall( private void closeTownhall(
final Long examId, final Long examId,
final ProctoringServiceSettings proctoringSettings, final ProctoringServiceSettings proctoringSettings,
@ -593,4 +598,5 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
true) true)
.onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error)); .onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error));
} }
} }

View file

@ -21,6 +21,7 @@ import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier; 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.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; 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.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper; 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 POSTMapper mapper = new POSTMapper(formParams, request.getQueryString());
final String remoteAddr = request.getRemoteAddr(); final String remoteAddr = this.getClientAddress(request);
final Long institutionId = (instIdRequestParam != null) final Long institutionId = (instIdRequestParam != null)
? instIdRequestParam ? instIdRequestParam
: mapper.getLong(API.PARAM_INSTITUTION_ID); : mapper.getLong(API.PARAM_INSTITUTION_ID);
@ -163,7 +165,7 @@ public class ExamAPI_V1_Controller {
return CompletableFuture.runAsync( return CompletableFuture.runAsync(
() -> { () -> {
final String remoteAddr = request.getRemoteAddr(); final String remoteAddr = this.getClientAddress(request);
final Long institutionId = getInstitutionId(principal); final Long institutionId = getInstitutionId(principal);
this.sebClientConnectionService.updateClientConnection( this.sebClientConnectionService.updateClientConnection(
@ -193,7 +195,7 @@ public class ExamAPI_V1_Controller {
return CompletableFuture.runAsync( return CompletableFuture.runAsync(
() -> { () -> {
final String remoteAddr = request.getRemoteAddr(); final String remoteAddr = this.getClientAddress(request);
final Long institutionId = getInstitutionId(principal); final Long institutionId = getInstitutionId(principal);
this.sebClientConnectionService.establishClientConnection( this.sebClientConnectionService.establishClientConnection(
@ -220,7 +222,7 @@ public class ExamAPI_V1_Controller {
return CompletableFuture.runAsync( return CompletableFuture.runAsync(
() -> { () -> {
final String remoteAddr = request.getRemoteAddr(); final String remoteAddr = this.getClientAddress(request);
final Long institutionId = getInstitutionId(principal); final Long institutionId = getInstitutionId(principal);
if (log.isDebugEnabled()) { 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");
}
}
} }

View file

@ -234,6 +234,22 @@ public class ExamProctoringController {
.onError(error -> log.error("Failed to close remote proctoring break out room {}", roomName, error)); .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( @RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.EXAM_PROCTORING_TOWNHALL_ROOM_DATA, + API.EXAM_PROCTORING_TOWNHALL_ROOM_DATA,
@ -246,7 +262,7 @@ public class ExamProctoringController {
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId, defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@PathVariable(name = API.PARAM_MODEL_ID) final Long examId) { @PathVariable(name = API.PARAM_MODEL_ID) final Long examId) {
checkAccess(institutionId, examId); checkExamReadAccess(institutionId);
return this.examProcotringRoomService return this.examProcotringRoomService
.getTownhallRoomData(examId) .getTownhallRoomData(examId)
.getOrElse(() -> RemoteProctoringRoom.NULL_ROOM); .getOrElse(() -> RemoteProctoringRoom.NULL_ROOM);
@ -273,6 +289,13 @@ public class ExamProctoringController {
.getOrThrow(); .getOrThrow();
} }
private void checkExamReadAccess(final Long institutionId) {
this.authorization.check(
PrivilegeType.READ,
EntityType.EXAM,
institutionId);
}
private void checkAccess(final Long institutionId, final Long examId) { private void checkAccess(final Long institutionId, final Long examId) {
this.authorization.check( this.authorization.check(
PrivilegeType.READ, PrivilegeType.READ,