SEBSERV-435 finished up bundle and auto login

This commit is contained in:
anhefti 2023-11-09 13:50:16 +01:00
parent 6e5d5e7710
commit 2af659dd33
23 changed files with 336 additions and 252 deletions

View file

@ -56,6 +56,7 @@ public final class API {
public static final String OAUTH_ENDPOINT = "/oauth";
public static final String OAUTH_TOKEN_ENDPOINT = OAUTH_ENDPOINT + "/token";
public static final String OAUTH_JWTTOKEN_ENDPOINT = OAUTH_ENDPOINT + "/jwttoken";
public static final String OAUTH_REVOKE_TOKEN_ENDPOINT = OAUTH_ENDPOINT + "/revoke-token";
public static final String CURRENT_USER_PATH_SEGMENT = "/me";

View file

@ -34,6 +34,8 @@ public class ScreenProctoringSettings {
public static final String ATTR_SPS_ACCOUNT_ID = "spsAccountId";
public static final String ATTR_SPS_ACCOUNT_PASSWORD = "spsAccountPassword";
public static final String ATTR_SPS_BUNDLED = "bundled";
@JsonProperty(Domain.EXAM.ATTR_ID)
public final Long examId;
@ -62,6 +64,9 @@ public class ScreenProctoringSettings {
@JsonProperty(ATTR_COLLECTING_GROUP_SIZE)
public final Integer collectingGroupSize;
@JsonProperty(ATTR_SPS_BUNDLED)
public final boolean bundled;
@JsonCreator
public ScreenProctoringSettings(
@JsonProperty(Domain.EXAM.ATTR_ID) final Long examId,
@ -72,7 +77,8 @@ public class ScreenProctoringSettings {
@JsonProperty(ATTR_SPS_ACCOUNT_ID) final String spsAccountId,
@JsonProperty(ATTR_SPS_ACCOUNT_PASSWORD) final CharSequence spsAccountPassword,
@JsonProperty(ATTR_COLLECTING_STRATEGY) final CollectingStrategy collectingStrategy,
@JsonProperty(ATTR_COLLECTING_GROUP_SIZE) final Integer collectingGroupSize) {
@JsonProperty(ATTR_COLLECTING_GROUP_SIZE) final Integer collectingGroupSize,
@JsonProperty(ATTR_SPS_BUNDLED) final boolean bundled) {
this.examId = examId;
this.enableScreenProctoring = enableScreenProctoring;
@ -83,6 +89,30 @@ public class ScreenProctoringSettings {
this.spsAccountPassword = spsAccountPassword;
this.collectingStrategy = collectingStrategy;
this.collectingGroupSize = collectingGroupSize;
this.bundled = bundled;
}
public ScreenProctoringSettings(
final Long examId,
final Boolean enableScreenProctoring,
final String spsServiceURL,
final String spsAPIKey,
final CharSequence spsAPISecret,
final String spsAccountId,
final CharSequence spsAccountPassword,
final CollectingStrategy collectingStrategy,
final Integer collectingGroupSize) {
this.examId = examId;
this.enableScreenProctoring = enableScreenProctoring;
this.spsServiceURL = spsServiceURL;
this.spsAPIKey = spsAPIKey;
this.spsAPISecret = spsAPISecret;
this.spsAccountId = spsAccountId;
this.spsAccountPassword = spsAccountPassword;
this.collectingStrategy = collectingStrategy;
this.collectingGroupSize = collectingGroupSize;
this.bundled = false;
}
public ScreenProctoringSettings(final Exam exam) {
@ -108,6 +138,7 @@ public class ScreenProctoringSettings {
this.collectingGroupSize = Integer.parseInt(exam.additionalAttributes.getOrDefault(
ATTR_COLLECTING_GROUP_SIZE,
"-1"));
this.bundled = false;
}
public Long getExamId() {
@ -151,6 +182,10 @@ public class ScreenProctoringSettings {
return Objects.hash(this.examId);
}
public boolean isBundled() {
return this.bundled;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)

View file

@ -39,6 +39,7 @@ import java.util.stream.Collectors;
import javax.validation.constraints.NotNull;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
@ -919,4 +920,12 @@ public final class Utils {
.replaceAll("[^A-Za-z0-9_]", "");
}
public static String createBasicAuthHeader(final String clientname, final CharSequence clientsecret) {
final String plainCreds = clientname + Constants.COLON + clientsecret;
final byte[] plainCredsBytes = plainCreds.getBytes();
final byte[] base64CredsBytes = Base64.encodeBase64(plainCredsBytes);
final String base64Creds = new String(base64CredsBytes);
return "Basic " + base64Creds;
}
}

View file

@ -18,7 +18,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang3.BooleanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
@ -26,21 +25,17 @@ import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService.ProctoringWindowData;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService.ScreenProctoringWindowData;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringWindowScriptResolver;
@Component
@GuiProfile
public class ProctoringServlet extends HttpServlet {
public static final String SCREEN_PROCOTRING_FLAG_PARAM = "screenproctoring";
private static final long serialVersionUID = 3475978419653411800L;
private static final Logger log = LoggerFactory.getLogger(ProctoringServlet.class);
@ -63,54 +58,12 @@ public class ProctoringServlet extends HttpServlet {
final WebApplicationContext webApplicationContext = WebApplicationContextUtils
.getRequiredWebApplicationContext(servletContext);
UserInfo user;
try {
user = isAuthenticated(httpSession, webApplicationContext);
} catch (final Exception e) {
final boolean authenticated = isAuthenticated(httpSession, webApplicationContext);
if (!authenticated) {
resp.setStatus(HttpStatus.FORBIDDEN.value());
return;
}
final String parameter = req.getParameter(SCREEN_PROCOTRING_FLAG_PARAM);
if (BooleanUtils.toBoolean(parameter)) {
openScreenProctoring(req, resp, user, httpSession);
} else {
openRemoteProctoring(resp, httpSession);
}
}
private void openScreenProctoring(
final HttpServletRequest req,
final HttpServletResponse resp,
final UserInfo user,
final HttpSession httpSession) throws IOException {
final ScreenProctoringWindowData data = (ScreenProctoringWindowData) httpSession
.getAttribute(ProctoringGUIService.SESSION_ATTR_SCREEN_PROCTORING_DATA);
// NOTE: POST on data.loginLocation seems not to work for automated login
// TODO discuss with Nadim how to make a direct login POST on the GUI client
// maybe there is a way to expose /login endpoint for directly POST credentials for login.
// https://stackoverflow.com/questions/46582/response-redirect-with-post-instead-of-get
final StringBuilder sb = new StringBuilder();
// sb.append("<html>");
// sb.append("<body onload='document.forms[\"form\"].submit()'>");
// sb.append("<form name='form' action='");
// sb.append( "" /* data.loginLocation */).append("' method='post'>");
// sb.append("</input type='hidden' name='username' value='").append("super-admin").append("'>");
// sb.append("</input type='hidden' name='password' type='password' value='").append("admin").append("'>");
// sb.append("</form>");
// sb.append("</body>");
// sb.append("</html>");
resp.getOutputStream().println(sb.toString());
}
private void openRemoteProctoring(
final HttpServletResponse resp,
final HttpSession httpSession) throws IOException {
final ProctoringWindowData proctoringData =
(ProctoringWindowData) httpSession
.getAttribute(ProctoringGUIService.SESSION_ATTR_PROCTORING_DATA);
@ -136,7 +89,7 @@ public class ProctoringServlet extends HttpServlet {
resp.setStatus(HttpServletResponse.SC_OK);
}
private UserInfo isAuthenticated(
private boolean isAuthenticated(
final HttpSession httpSession,
final WebApplicationContext webApplicationContext) {
@ -144,11 +97,7 @@ public class ProctoringServlet extends HttpServlet {
.getBean(AuthorizationContextHolder.class);
final SEBServerAuthorizationContext authorizationContext = authorizationContextHolder
.getAuthorizationContext(httpSession);
if (!authorizationContext.isValid() || !authorizationContext.isLoggedIn()) {
throw new RuntimeException("No authentication found");
}
return authorizationContext.getLoggedInUser().getOrThrow();
return authorizationContext.isValid() && authorizationContext.isLoggedIn();
}
}

View file

@ -199,7 +199,13 @@ public class ScreenProctoringSettingsPopup {
new ActionEvent(action),
action.pageContext());
return true;
} else {
final String bundled = formHandle.getForm().getStaticValue(ScreenProctoringSettings.ATTR_SPS_BUNDLED);
if (bundled != null) {
pageContext.notifyActivationError(EntityType.SCREEN_PROCTORING_GROUP, saveRequest.getError());
}
}
return false;
}
@ -243,11 +249,14 @@ public class ScreenProctoringSettingsPopup {
this.pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
final FormHandle<Entity> form = this.pageService.formBuilder(formContext)
.putStaticValueIf(
() -> settings.bundled,
ScreenProctoringSettings.ATTR_SPS_BUNDLED,
Constants.TRUE_STRING)
.withDefaultSpanInput(5)
.withEmptyCellSeparation(true)
.withDefaultSpanEmptyCell(1)
.readonly(isReadonly)
.addField(FormBuilder.text(
"Info",
FORM_INFO_TITLE,
@ -265,33 +274,39 @@ public class ScreenProctoringSettingsPopup {
ScreenProctoringSettings.ATTR_SPS_SERVICE_URL,
FORM_URL,
settings.spsServiceURL)
.mandatory())
.mandatory()
.readonly(settings.bundled))
.addField(FormBuilder.text(
ScreenProctoringSettings.ATTR_SPS_API_KEY,
FORM_APPKEY_SPS,
settings.spsAPIKey))
settings.spsAPIKey)
.readonly(settings.bundled))
.withEmptyCellSeparation(false)
.addField(FormBuilder.password(
ScreenProctoringSettings.ATTR_SPS_API_SECRET,
FORM_APPSECRET_SPS,
(settings.spsAPISecret != null)
? String.valueOf(settings.spsAPISecret)
: null))
.addFieldIf(
() -> !settings.bundled,
() -> FormBuilder.password(
ScreenProctoringSettings.ATTR_SPS_API_SECRET,
FORM_APPSECRET_SPS,
(settings.spsAPISecret != null)
? String.valueOf(settings.spsAPISecret)
: null))
.addField(FormBuilder.text(
ScreenProctoringSettings.ATTR_SPS_ACCOUNT_ID,
FORM_ACCOUNT_ID_SPS,
settings.spsAccountId))
settings.spsAccountId)
.readonly(settings.bundled))
.withEmptyCellSeparation(false)
.addField(FormBuilder.password(
ScreenProctoringSettings.ATTR_SPS_ACCOUNT_PASSWORD,
FORM_ACCOUNT_SECRET_SPS,
(settings.spsAccountPassword != null)
? String.valueOf(settings.spsAccountPassword)
: null))
.addFieldIf(
() -> !settings.bundled,
() -> FormBuilder.password(
ScreenProctoringSettings.ATTR_SPS_ACCOUNT_PASSWORD,
FORM_ACCOUNT_SECRET_SPS,
(settings.spsAccountPassword != null)
? String.valueOf(settings.spsAccountPassword)
: null))
.build();

View file

@ -108,6 +108,10 @@ public final class Form implements FormBinding {
}
}
public String getStaticValue(final String name) {
return this.staticValues.get(name);
}
public void addToGroup(final String groupName, final String fieldName) {
if (this.formFields.containsKey(fieldName)) {
this.groups.computeIfAbsent(groupName, k -> new HashSet<>())

View file

@ -212,6 +212,14 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol
return true;
}
@Override
public CharSequence getUserPassword() {
if (isLoggedIn()) {
return this.resource.getPassword();
}
return null;
}
@Override
public boolean login(final String username, final CharSequence password) {
if (!this.valid || this.isLoggedIn()) {
@ -363,6 +371,5 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol
}
}
}
}
}

View file

@ -63,4 +63,6 @@ public interface SEBServerAuthorizationContext {
* @return the underling RestTemplate to connect and communicate with the SEB Server webservice */
RestTemplate getRestTemplate();
CharSequence getUserPassword();
}

View file

@ -27,7 +27,10 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@ -47,10 +50,10 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.gbl.util.Tuple;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.GuiServiceInfo;
import ch.ethz.seb.sebserver.gui.ProctoringServlet;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.content.action.ActionPane;
import ch.ethz.seb.sebserver.gui.content.monitoring.ProctorRoomConnectionsPopup;
@ -98,16 +101,16 @@ public class MonitoringProctoringService {
private final JSONMapper jsonMapper;
private final Resource openRoomScriptRes;
private final String remoteProctoringEndpoint;
private final String remoteProctoringViewServletEndpoint;
private final Cryptor cryptor;
public MonitoringProctoringService(
final PageService pageService,
final GuiServiceInfo guiServiceInfo,
final ProctorRoomConnectionsPopup proctorRoomConnectionsPopup,
final JSONMapper jsonMapper,
final Cryptor cryptor,
@Value(OPEN_ROOM_SCRIPT_RES) final Resource openRoomScript,
@Value("${sebserver.gui.remote.proctoring.entrypoint:/remote-proctoring}") final String remoteProctoringEndpoint,
@Value("${sebserver.gui.remote.proctoring.api-servler.endpoint:/remote-view-servlet}") final String remoteProctoringViewServletEndpoint) {
@Value("${sebserver.gui.remote.proctoring.entrypoint:/remote-proctoring}") final String remoteProctoringEndpoint) {
this.pageService = pageService;
this.guiServiceInfo = guiServiceInfo;
@ -115,7 +118,7 @@ public class MonitoringProctoringService {
this.jsonMapper = jsonMapper;
this.openRoomScriptRes = openRoomScript;
this.remoteProctoringEndpoint = remoteProctoringEndpoint;
this.remoteProctoringViewServletEndpoint = remoteProctoringViewServletEndpoint;
this.cryptor = cryptor;
}
public boolean isTownhallRoomActive(final String examModelId) {
@ -314,34 +317,55 @@ public class MonitoringProctoringService {
final ScreenProctoringGroup group,
final PageAction _action) {
// TODO make this configurable or static
final String serviceRedirect = settings.spsServiceURL + "/gui-redirect-location";
final ResponseEntity<String> redirect = new RestTemplate().exchange(
serviceRedirect,
HttpMethod.GET,
null,
String.class);
try {
// Get login Token for user login from SPS service
final RestTemplate restTemplate = new RestTemplate();
final String serviceRedirect = settings.spsServiceURL + "/gui-redirect-location";
final ResponseEntity<String> redirect = restTemplate.exchange(
serviceRedirect,
HttpMethod.GET,
null,
String.class);
final String redirectLocation = redirect.getBody();
final CurrentUser currentUser = this.pageService.getCurrentUser();
// JWT token request URL
final String jwtTokenURL = settings.spsServiceURL + API.OAUTH_JWTTOKEN_ENDPOINT;
ProctoringGUIService.setCurrentScreenProctoringWindowData(
group.uuid,
redirectLocation,
currentUser.get().username,
"admin");
// Basic Auth header and content type header
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(
HttpHeaders.AUTHORIZATION,
Utils.createBasicAuthHeader(
settings.spsAPIKey,
this.cryptor.decrypt(settings.getSpsAPISecret()).getOrThrow()));
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
final UrlLauncher launcher = RWT.getClient().getService(UrlLauncher.class);
final String url = this.guiServiceInfo.getExternalServerURIBuilder().toUriString()
+ this.remoteProctoringEndpoint
+ this.remoteProctoringViewServletEndpoint
+ Constants.SLASH
+ Constants.QUERY
+ ProctoringServlet.SCREEN_PROCOTRING_FLAG_PARAM
+ Constants.EQUALITY_SIGN
+ Constants.TRUE_STRING;
// user credential and redirect info for jwt token request in body - form URL encoded format
final CurrentUser currentUser = this.pageService.getCurrentUser();
final CharSequence userPassword = currentUser
.getAuthorizationContextHolder()
.getAuthorizationContext()
.getUserPassword();
final String body = "username=" + currentUser.get().username
+ "&password=" + userPassword.toString()
+ "&redirect=/galleryView/" + group.uuid;
launcher.openURL(url);
// apply jwt token request
final HttpEntity<String> httpEntity = new HttpEntity<>(body, httpHeaders);
final ResponseEntity<String> tokenRequest = restTemplate.exchange(
jwtTokenURL,
HttpMethod.POST,
httpEntity,
String.class);
// Open SPS Gui redirect URL with login token (jwt token) in new browser tab
final String redirectLocation = redirect.getBody() + "/jwt?token=" + tokenRequest.getBody();
final UrlLauncher launcher = RWT.getClient().getService(UrlLauncher.class);
launcher.openURL(redirectLocation);
} catch (final Exception e) {
log.error("Failed to open screen proctoring service group gallery view: ", e);
_action.pageContext()
.notifyError(new LocTextKey("Failed to open screen proctoring service group gallery view"), e);
}
return _action;
}

View file

@ -30,6 +30,7 @@ import org.springframework.web.util.UriComponentsBuilder;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.WebserviceInfoDAO;
@Lazy
@ -80,9 +81,12 @@ public class WebserviceInfo {
@Value("${sebserver.webservice.api.exam.accessTokenValiditySeconds:43200}")
private int examAPITokenValiditySeconds;
private final ScreenProctoringServiceBundle screenProctoringServiceBundle;
public WebserviceInfo(
final WebserviceInfoDAO webserviceInfoDAO,
final Environment environment) {
final Environment environment,
final Cryptor cryptor) {
this.webserviceInfoDAO = webserviceInfoDAO;
this.sebServerVersion = environment.getRequiredProperty(VERSION_KEY);
@ -145,6 +149,24 @@ public class WebserviceInfo {
} else {
this.lmsExternalAddressAlias = Collections.emptyMap();
}
final boolean spsBundled = BooleanUtils.toBoolean(environment.getProperty(
"sebserver.feature.seb.screenProctoring.bundled",
Constants.FALSE_STRING));
if (spsBundled) {
this.screenProctoringServiceBundle = new ScreenProctoringServiceBundle(
environment.getProperty("sebserver.feature.seb.screenProctoring.bundled.url"),
environment.getProperty("sebserver.feature.seb.screenProctoring.bundled.clientId"),
cryptor.encrypt(
environment.getProperty("sebserver.feature.seb.screenProctoring.bundled.clientPassword"))
.getOrThrow(),
environment.getProperty("sebserver.feature.seb.screenProctoring.bundled.sebserveraccount.username"),
cryptor.encrypt(environment
.getProperty("sebserver.feature.seb.screenProctoring.bundled.sebserveraccount.password"))
.getOrThrow());
} else {
this.screenProctoringServiceBundle = new ScreenProctoringServiceBundle();
}
}
public boolean isMaster() {
@ -207,6 +229,10 @@ public class WebserviceInfo {
return this.distributedUpdateInterval;
}
public ScreenProctoringServiceBundle getScreenProctoringServiceBundle() {
return this.screenProctoringServiceBundle;
}
public String getLocalHostName() {
try {
return InetAddress.getLocalHost().getHostName();
@ -300,4 +326,53 @@ public class WebserviceInfo {
return builder.toString();
}
public static final class ScreenProctoringServiceBundle {
public final boolean bundled;
public final String serviceURL;
public final String clientId;
public final CharSequence clientSecret;
public final String apiAccountName;
public final CharSequence apiAccountPassword;
public ScreenProctoringServiceBundle(
final String serviceURL,
final String clientId,
final CharSequence clientSecret,
final String apiAccountName,
final CharSequence apiAccountPassword) {
this.bundled = true;
this.serviceURL = serviceURL;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.apiAccountName = apiAccountName;
this.apiAccountPassword = apiAccountPassword;
}
public ScreenProctoringServiceBundle() {
this.bundled = false;
this.serviceURL = null;
this.clientId = null;
this.clientSecret = null;
this.apiAccountName = null;
this.apiAccountPassword = null;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("ScreenProctoringServiceBundle [bundled=");
builder.append(this.bundled);
builder.append(", serviceURL=");
builder.append(this.serviceURL);
builder.append(", clientId=");
builder.append(this.clientId);
builder.append(", apiAccountName=");
builder.append(this.apiAccountName);
builder.append("]");
return builder.toString();
}
}
}

View file

@ -22,8 +22,8 @@ import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.SEBServerInit;
import ch.ethz.seb.sebserver.SEBServerInitEvent;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo.ScreenProctoringServiceBundle;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.WebserviceInfoDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.SEBClientPingServiceFactory;
@Component
@WebServiceProfile
@ -39,7 +39,6 @@ public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent
private final WebserviceInfoDAO webserviceInfoDAO;
private final DBIntegrityChecker dbIntegrityChecker;
private final SEBServerMigrationStrategy sebServerMigrationStrategy;
private final SEBClientPingServiceFactory sebClientPingServiceFactory;
protected WebserviceInit(
final SEBServerInit sebServerInit,
@ -49,8 +48,7 @@ public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent
final WebserviceInfoDAO webserviceInfoDAO,
final DBIntegrityChecker dbIntegrityChecker,
final ApplicationContext applicationContext,
final SEBServerMigrationStrategy sebServerMigrationStrategy,
final SEBClientPingServiceFactory sebClientPingServiceFactory) {
final SEBServerMigrationStrategy sebServerMigrationStrategy) {
this.applicationContext = applicationContext;
this.sebServerInit = sebServerInit;
@ -61,7 +59,6 @@ public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent
this.webserviceInfoDAO = webserviceInfoDAO;
this.dbIntegrityChecker = dbIntegrityChecker;
this.sebServerMigrationStrategy = sebServerMigrationStrategy;
this.sebClientPingServiceFactory = sebClientPingServiceFactory;
}
public ApplicationContext getApplicationContext() {
@ -126,7 +123,7 @@ public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent
SEBServerInit.INIT_LOGGER.info("----> ");
SEBServerInit.INIT_LOGGER.info("----> Working with ping service: {}",
this.sebClientPingServiceFactory.getWorkingServiceType());
this.environment.getProperty("sebserver.webservice.ping.service.strategy"));
SEBServerInit.INIT_LOGGER.info("----> ");
SEBServerInit.INIT_LOGGER.info("----> Server address: {}", this.environment.getProperty("server.address"));
@ -153,6 +150,14 @@ public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent
"----> admin API refresh token validity: " + this.webserviceInfo.getAdminRefreshTokenValSec() + "s");
SEBServerInit.INIT_LOGGER.info(
"----> exam API access token validity: " + this.webserviceInfo.getExamAPITokenValiditySeconds() + "s");
final ScreenProctoringServiceBundle spsBundle = this.webserviceInfo.getScreenProctoringServiceBundle();
SEBServerInit.INIT_LOGGER.info("----> ");
SEBServerInit.INIT_LOGGER.info("----> Screen Proctoring Bundle enabled: {}", spsBundle.bundled);
if (spsBundle.bundled) {
SEBServerInit.INIT_LOGGER.info("------> {}", spsBundle);
}
SEBServerInit.INIT_LOGGER.info("----> ");
SEBServerInit.INIT_LOGGER.info("----> Property Override Test: {}", this.webserviceInfo.getTestProperty());

View file

@ -21,7 +21,7 @@ public interface ProctoringSettingsDAO {
EntityKey entityKey,
ProctoringServiceSettings proctoringServiceSettings);
Result<ScreenProctoringSettings> getScreenProctoringSettings(EntityKey entityKey);
Result<ScreenProctoringSettings> getScreenProctoringSettings(EntityKey entityKeyp);
Result<ScreenProctoringSettings> storeScreenProctoringSettings(
final EntityKey entityKey,

View file

@ -18,7 +18,10 @@ import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo.ScreenProctoringServiceBundle;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ProctoringSettingsDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ProctoringSettingsDAOImpl;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ProctoringAdminService;
@ -36,17 +39,23 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
private final RemoteProctoringServiceFactory remoteProctoringServiceFactory;
private final ScreenProctoringService screenProctoringService;
private final ExamSessionCacheService examSessionCacheService;
private final ScreenProctoringServiceBundle screenProctoringServiceBundle;
private final Cryptor cryptor;
public ProctoringAdminServiceImpl(
final ProctoringSettingsDAOImpl proctoringSettingsDAO,
final RemoteProctoringServiceFactory remoteProctoringServiceFactory,
final ScreenProctoringService screenProctoringService,
final ExamSessionCacheService examSessionCacheService) {
final ExamSessionCacheService examSessionCacheService,
final WebserviceInfo webserviceInfo,
final Cryptor cryptor) {
this.proctoringSettingsDAO = proctoringSettingsDAO;
this.remoteProctoringServiceFactory = remoteProctoringServiceFactory;
this.screenProctoringService = screenProctoringService;
this.examSessionCacheService = examSessionCacheService;
this.screenProctoringServiceBundle = webserviceInfo.getScreenProctoringServiceBundle();
this.cryptor = cryptor;
}
@Override
@ -91,9 +100,25 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
checkType(parentEntityKey);
return this.proctoringSettingsDAO
ScreenProctoringSettings settings = this.proctoringSettingsDAO
.getScreenProctoringSettings(parentEntityKey)
.getOrThrow();
if (this.screenProctoringServiceBundle.bundled) {
settings = new ScreenProctoringSettings(
settings.examId,
settings.enableScreenProctoring,
this.screenProctoringServiceBundle.serviceURL,
this.screenProctoringServiceBundle.clientId,
null,
this.screenProctoringServiceBundle.apiAccountName,
null,
settings.collectingStrategy,
settings.collectingGroupSize,
true);
}
return settings;
});
}
@ -106,17 +131,30 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
checkType(parentEntityKey);
ScreenProctoringSettings settings = screenProctoringSettings;
if (this.screenProctoringServiceBundle.bundled) {
settings = new ScreenProctoringSettings(
screenProctoringSettings.examId,
screenProctoringSettings.enableScreenProctoring,
this.screenProctoringServiceBundle.serviceURL,
this.screenProctoringServiceBundle.clientId,
this.cryptor.decrypt(this.screenProctoringServiceBundle.clientSecret).getOrThrow(),
this.screenProctoringServiceBundle.apiAccountName,
this.cryptor.decrypt(this.screenProctoringServiceBundle.apiAccountPassword).getOrThrow(),
screenProctoringSettings.collectingStrategy,
screenProctoringSettings.collectingGroupSize,
true);
}
this.screenProctoringService
.testSettings(screenProctoringSettings)
.flatMap(settings -> this.proctoringSettingsDAO.storeScreenProctoringSettings(
parentEntityKey,
screenProctoringSettings))
.testSettings(settings)
.flatMap(s -> this.proctoringSettingsDAO.storeScreenProctoringSettings(parentEntityKey, s))
.getOrThrow();
if (parentEntityKey.entityType == EntityType.EXAM) {
this.screenProctoringService
.applyScreenProctoingForExam(screenProctoringSettings.examId)
.applyScreenProctoingForExam(settings.examId)
.onError(error -> this.proctoringSettingsDAO
.disableScreenProctoring(screenProctoringSettings.examId))
.getOrThrow();
@ -128,7 +166,7 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
}
}
return screenProctoringSettings;
return settings;
});
}

View file

@ -21,6 +21,7 @@ import org.ehcache.impl.internal.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Component;
@ -33,6 +34,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientPingServic
@Lazy
@Component
@WebServiceProfile
@ConditionalOnExpression("'${sebserver.webservice.ping.service.strategy}'.equals('BATCH')")
public class SEBClientPingBatchService implements SEBClientPingService {
private static final Logger log = LoggerFactory.getLogger(SEBClientPingBatchService.class);
@ -116,9 +118,9 @@ public class SEBClientPingBatchService implements SEBClientPingService {
+ this.instructions);
this.pings.put(connectionToken, instructionConfirm);
// // TODO is this a good idea or is there another better way to deal with instruction confirm synchronization?
// if (instruction != null && instruction.contains("\"instruction-confirm\":\"" + instructionConfirm + "\"")) {
// return null;
// }
if (instruction != null && instruction.contains("\"instruction-confirm\":\"" + instructionConfirm + "\"")) {
return null;
}
} else if (!this.pings.containsKey(connectionToken)) {
this.pings.put(connectionToken, StringUtils.EMPTY);
}

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@ -22,6 +23,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientPingServic
@Lazy
@Component
@WebServiceProfile
@ConditionalOnExpression("'${sebserver.webservice.ping.service.strategy}'.equals('BLOCKING')")
public class SEBClientPingBlockingService implements SEBClientPingService {
private static final Logger log = LoggerFactory.getLogger(SEBClientPingBlockingService.class);

View file

@ -1,73 +0,0 @@
/*
* Copyright (c) 2023 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.util.Collection;
import java.util.EnumMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientPingService;
@Lazy
@Component
@WebServiceProfile
public class SEBClientPingServiceFactory {
private static final Logger log = LoggerFactory.getLogger(SEBClientPingServiceFactory.class);
private final EnumMap<SEBClientPingService.PingServiceType, SEBClientPingService> serviceMapping =
new EnumMap<>(SEBClientPingService.PingServiceType.class);
private final SEBClientPingService.PingServiceType workingServiceType;
public SEBClientPingServiceFactory(
final Collection<SEBClientPingService> serviceBeans,
@Value("${sebserver.webservice.api.exam.session.ping.service.strategy:BLOCKING}") final String serviceType) {
SEBClientPingService.PingServiceType serviceTypeToSet = SEBClientPingService.PingServiceType.BLOCKING;
try {
serviceTypeToSet = SEBClientPingService.PingServiceType.valueOf(serviceType);
} catch (final Exception e) {
serviceTypeToSet = SEBClientPingService.PingServiceType.BLOCKING;
}
this.workingServiceType = serviceTypeToSet;
serviceBeans.stream().forEach(service -> this.serviceMapping.putIfAbsent(service.pingServiceType(), service));
}
public SEBClientPingService.PingServiceType getWorkingServiceType() {
return this.workingServiceType;
}
public SEBClientPingService getSEBClientPingService() {
log.info("Work with SEBClientPingService of type: {}", this.workingServiceType);
switch (this.workingServiceType) {
case BATCH: {
final SEBClientPingService service =
this.serviceMapping.get(SEBClientPingService.PingServiceType.BATCH);
if (service != null) {
((SEBClientPingBatchService) service).init();
return service;
} else {
return this.serviceMapping.get(SEBClientPingService.PingServiceType.BLOCKING);
}
}
default:
return this.serviceMapping.get(SEBClientPingService.PingServiceType.BLOCKING);
}
}
}

View file

@ -60,7 +60,7 @@ public class SEBClientSessionServiceImpl implements SEBClientSessionService {
final InternalClientConnectionDataFactory internalClientConnectionDataFactory,
final SecurityKeyService securityKeyService,
final SEBClientVersionService sebClientVersionService,
final SEBClientPingServiceFactory sebClientPingServiceFactory) {
final SEBClientPingService sebClientPingService) {
this.clientConnectionDAO = clientConnectionDAO;
this.examSessionService = examSessionService;
@ -70,7 +70,7 @@ public class SEBClientSessionServiceImpl implements SEBClientSessionService {
this.internalClientConnectionDataFactory = internalClientConnectionDataFactory;
this.securityKeyService = securityKeyService;
this.sebClientVersionService = sebClientVersionService;
this.sebClientPingService = sebClientPingServiceFactory.getSEBClientPingService();
this.sebClientPingService = sebClientPingService;
}
@Override

View file

@ -223,6 +223,10 @@ class ScreenProctoringAPIBinding {
if (result.getStatusCode() != HttpStatus.OK) {
if (result.getStatusCode().is4xxClientError()) {
log.warn(
"Failed to establish REST connection to: {}. status: {}",
screenProctoringSettings.spsServiceURL, result.getStatusCode());
throw new FieldValidationException(
"serverURL",
"screenProctoringSettings:spsServiceURL:url.noAccess");
@ -302,7 +306,6 @@ class ScreenProctoringAPIBinding {
final SPSData spsData = this.getSPSData(exam.id);
// re-activate all needed entities on SPS side
activation(exam, SPS_API.SEB_ACCESS_ENDPOINT, spsData.spsSEBAccesUUID, true, apiTemplate);
activation(exam, SPS_API.EXAM_ENDPOINT, spsData.spsExamUUID, true, apiTemplate);
// mark successfully activated on SPS side
this.additionalAttributesDAO.saveAdditionalAttribute(
@ -367,7 +370,7 @@ class ScreenProctoringAPIBinding {
final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(exam.id);
final String uri = UriComponentsBuilder
.fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL)
.fromUriString(apiTemplate.screenProctoringSettings.spsServiceURL)
.path(SPS_API.EXAM_ENDPOINT)
.pathSegment(spsData.spsExamUUID)
.build()
@ -410,8 +413,8 @@ class ScreenProctoringAPIBinding {
}
final SPSData spsData = this.getSPSData(exam.id);
activation(exam, SPS_API.EXAM_ENDPOINT, spsData.spsExamUUID, false, this.apiTemplate);
activation(exam, SPS_API.SEB_ACCESS_ENDPOINT, spsData.spsSEBAccesUUID, false, this.apiTemplate);
final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(exam.id);
activation(exam, SPS_API.SEB_ACCESS_ENDPOINT, spsData.spsSEBAccesUUID, false, apiTemplate);
// mark successfully dispose on SPS side
this.additionalAttributesDAO.saveAdditionalAttribute(
@ -490,7 +493,7 @@ class ScreenProctoringAPIBinding {
final String token = clientConnection.getConnectionToken();
final ScreenProctoringServiceOAuthTemplate apiTemplate = this.getAPITemplate(examId);
final String uri = UriComponentsBuilder
.fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL)
.fromUriString(apiTemplate.screenProctoringSettings.spsServiceURL)
.path(SPS_API.SESSION_ENDPOINT)
.build()
@ -567,7 +570,7 @@ class ScreenProctoringAPIBinding {
userInfo.roles);
final String uri = UriComponentsBuilder
.fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL)
.fromUriString(apiTemplate.screenProctoringSettings.spsServiceURL)
.path(SPS_API.USERSYNC_SEBSERVER_ENDPOINT)
.build()
.toUriString();
@ -603,7 +606,7 @@ class ScreenProctoringAPIBinding {
.getOrThrow();
final String uri = UriComponentsBuilder
.fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL)
.fromUriString(apiTemplate.screenProctoringSettings.spsServiceURL)
.path(SPS_API.ENTIY_PRIVILEGES_ENDPOINT)
.build()
.toUriString();
@ -696,7 +699,7 @@ class ScreenProctoringAPIBinding {
throws JsonMappingException, JsonProcessingException {
final String uri = UriComponentsBuilder
.fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL)
.fromUriString(apiTemplate.screenProctoringSettings.spsServiceURL)
.path(SPS_API.GROUP_ENDPOINT)
.build()
.toUriString();
@ -729,7 +732,7 @@ class ScreenProctoringAPIBinding {
try {
final String uri = UriComponentsBuilder
.fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL)
.fromUriString(apiTemplate.screenProctoringSettings.spsServiceURL)
.path(SPS_API.EXAM_ENDPOINT)
.build().toUriString();
@ -773,7 +776,7 @@ class ScreenProctoringAPIBinding {
final String description = "This SEB access was auto-generated by SEB Server";
final String uri = UriComponentsBuilder
.fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL)
.fromUriString(apiTemplate.screenProctoringSettings.spsServiceURL)
.path(SPS_API.SEB_ACCESS_ENDPOINT)
.build()
.toUriString();
@ -816,7 +819,7 @@ class ScreenProctoringAPIBinding {
try {
final String uri = UriComponentsBuilder
.fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL)
.fromUriString(apiTemplate.screenProctoringSettings.spsServiceURL)
.path(domainPath)
.pathSegment(uuid)
.pathSegment(activate ? SPS_API.ACTIVE_PATH_SEGMENT : SPS_API.INACTIVE_PATH_SEGMENT)
@ -840,7 +843,7 @@ class ScreenProctoringAPIBinding {
try {
final String uri = UriComponentsBuilder
.fromUriString(this.apiTemplate.screenProctoringSettings.spsServiceURL)
.fromUriString(apiTemplate.screenProctoringSettings.spsServiceURL)
.path(domainPath)
.pathSegment(uuid)
.build()

View file

@ -14,6 +14,7 @@ import java.util.List;
import java.util.concurrent.CompletionException;
import java.util.stream.Collectors;
import org.apache.catalina.connector.ClientAbortException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
@ -281,4 +282,14 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
}
@ExceptionHandler(ClientAbortException.class)
public ResponseEntity<Object> handleClientAbortException(
final ClientAbortException ex,
final WebRequest request) {
log.warn("Client aborted: {}", ex.getMessage());
return null;
}
}

View file

@ -1,35 +0,0 @@
/*
* Copyright (c) 2019 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.webservice.weblayer.api;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
//@EnableAsync
//@Configuration
//@WebServiceProfile
@Deprecated
public class ControllerConfig implements WebMvcConfigurer {
// @Override
// public void configureAsyncSupport(final AsyncSupportConfigurer configurer) {
// configurer.setTaskExecutor(threadPoolTaskExecutor());
// configurer.setDefaultTimeout(30000);
// }
//
// public AsyncTaskExecutor threadPoolTaskExecutor() {
// final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// executor.setCorePoolSize(7);
// executor.setMaxPoolSize(42);
// executor.setQueueCapacity(11);
// executor.setThreadNamePrefix("mvc-");
// executor.initialize();
// return executor;
// }
}

View file

@ -23,7 +23,7 @@ sebserver.webservice.distributed.updateInterval=1000
sebserver.webservice.distributed.connectionUpdate=2000
sebserver.webservice.clean-db-on-startup=false
# webservice configuration
# webservice setup configuration
sebserver.init.adminaccount.gen-on-init=false
sebserver.webservice.distributed=true
#sebserver.webservice.master.delay.threshold=10000
@ -31,6 +31,7 @@ sebserver.webservice.http.external.scheme=http
sebserver.webservice.http.external.servername=localhost
sebserver.webservice.http.external.port=${server.port}
sebserver.webservice.http.redirect.gui=/gui
sebserver.webservice.ping.service.strategy=BATCH
sebserver.webservice.api.admin.endpoint=/admin-api/v1
@ -44,8 +45,6 @@ sebserver.webservice.api.exam.time-suffix=0
sebserver.webservice.api.exam.endpoint=/exam-api
sebserver.webservice.api.exam.endpoint.discovery=${sebserver.webservice.api.exam.endpoint}/discovery
sebserver.webservice.api.exam.endpoint.v1=${sebserver.webservice.api.exam.endpoint}/v1
sebserver.webservice.api.exam.event-handling-strategy=ASYNC_BATCH_STORE_STRATEGY
sebserver.webservice.api.exam.session.ping.service.strategy=BATCH
sebserver.webservice.api.exam.enable-indicator-cache=true
sebserver.webservice.api.exam.defaultPingInterval=1000
sebserver.webservice.api.pagination.maxPageSize=500
@ -62,4 +61,8 @@ springdoc.swagger-ui.enabled=true
management.server.port=${server.port}
management.endpoints.web.base-path=/management
management.endpoints.web.exposure.include=logfile,loggers,jolokia
management.endpoints.web.path-mapping.jolokia=jmx
management.endpoints.web.path-mapping.jolokia=jmx
sebserver.feature.seb.screenProctoring.bundled.url=localhost:8090
sebserver.feature.seb.screenProctoring.bundled.clientId=sebserverClient
sebserver.feature.seb.screenProctoring.bundled.sebserveraccount.username=SEBServerAPIAccount

View file

@ -37,7 +37,7 @@ spring.datasource.password=${sebserver.mariadb.password}
sebserver.webservice.api.admin.clientSecret=${sebserver.password}
sebserver.webservice.internalSecret=${sebserver.password}
### webservice networking
### webservice setup configuration
sebserver.webservice.forceMaster=false
sebserver.webservice.distributed=false
sebserver.webservice.distributed.updateInterval=2000
@ -45,6 +45,8 @@ sebserver.webservice.http.external.scheme=https
sebserver.webservice.http.external.servername=
sebserver.webservice.http.external.port=
sebserver.webservice.http.redirect.gui=/gui
sebserver.webservice.ping.service.strategy=BLOCKING
### Open API Documentation
springdoc.api-docs.enabled=false
@ -55,6 +57,8 @@ springdoc.swagger-ui.oauth.clientSecret=${sebserver.password}
#springdoc.default-consumes-media-type=application/x-www-form-urlencoded
springdoc.paths-to-exclude=/exam-api,/exam-api/discovery,/sebserver/error,/sebserver/check,/oauth,/exam-api/v1/*
### webservice API
sebserver.webservice.api.admin.clientId=guiClient
sebserver.webservice.api.admin.endpoint=/admin-api/v1
@ -72,8 +76,7 @@ sebserver.webservice.api.exam.config.init.prohibitedProcesses=config/initialProh
sebserver.webservice.api.exam.endpoint=/exam-api
sebserver.webservice.api.exam.endpoint.discovery=${sebserver.webservice.api.exam.endpoint}/discovery
sebserver.webservice.api.exam.endpoint.v1=${sebserver.webservice.api.exam.endpoint}/v1
sebserver.webservice.api.exam.accessTokenValiditySeconds=43200
sebserver.webservice.api.exam.event-handling-strategy=SINGLE_EVENT_STORE_STRATEGY
sebserver.webservice.api.exam.accessTokenValiditySeconds=43200
sebserver.webservice.api.exam.enable-indicator-cache=true
sebserver.webservice.api.pagination.maxPageSize=500
# comma separated list of known possible OpenEdX API access token request endpoints

View file

@ -66,6 +66,10 @@ sebserver.ssl.redirect.html.port=8080
# features
sebserver.feature.seb.screenProctoring=false
sebserver.feature.seb.screenProctoring.bundled=true
sebserver.feature.seb.screenProctoring.bundled.url=sps-service:8090
sebserver.feature.seb.screenProctoring.bundled.clientId=sebserverClient
sebserver.feature.seb.screenProctoring.bundled.clientPassword=${sebserver.password}
sebserver.feature.seb.screenProctoring.bundled.clientPassword=${sps.sebserver.client.secret}
sebserver.feature.seb.screenProctoring.bundled.sebserveraccount.username=SEBServerAPIAccount
sebserver.feature.seb.screenProctoring.bundled.sebserveraccount.password=${sps.sebserver.password}
sebserver.feature.CollectingRoomStrategy.SEB-GROUP=false