SEBSERV-363 finished integration adaption and testing

This commit is contained in:
anhefti 2023-01-19 16:08:56 +01:00
parent edce7275ca
commit 4671e682a3
13 changed files with 209 additions and 124 deletions

View file

@ -126,7 +126,7 @@ public class ProctoringServiceSettings implements Entity {
this.examId = examId; this.examId = examId;
this.enableProctoring = BooleanUtils.isTrue(enableProctoring); this.enableProctoring = BooleanUtils.isTrue(enableProctoring);
this.serverType = (serverType != null) ? serverType : ProctoringServerType.JITSI_MEET; this.serverType = (serverType != null) ? serverType : ProctoringServerType.ZOOM;
this.serverURL = serverURL; this.serverURL = serverURL;
this.collectingRoomSize = (collectingRoomSize != null) ? collectingRoomSize : 20; this.collectingRoomSize = (collectingRoomSize != null) ? collectingRoomSize : 20;
this.enabledFeatures = enabledFeatures != null ? enabledFeatures : EnumSet.allOf(ProctoringFeature.class); this.enabledFeatures = enabledFeatures != null ? enabledFeatures : EnumSet.allOf(ProctoringFeature.class);
@ -144,7 +144,7 @@ public class ProctoringServiceSettings implements Entity {
public ProctoringServiceSettings(final Long examId) { public ProctoringServiceSettings(final Long examId) {
this.examId = examId; this.examId = examId;
this.enableProctoring = false; this.enableProctoring = false;
this.serverType = null; this.serverType = ProctoringServerType.ZOOM;
this.serverURL = null; this.serverURL = null;
this.collectingRoomSize = 20; this.collectingRoomSize = 20;
this.enabledFeatures = EnumSet.allOf(ProctoringFeature.class); this.enabledFeatures = EnumSet.allOf(ProctoringFeature.class);

View file

@ -256,7 +256,6 @@ public class ExamSignatureKeyForm implements TemplateComposer {
.newAction(ActionDefinition.EXAM_SECURITY_KEY_BACK_MODIFY) .newAction(ActionDefinition.EXAM_SECURITY_KEY_BACK_MODIFY)
.withEntityKey(entityKey) .withEntityKey(entityKey)
//.withExec(this.pageService.backToCurrentFunction())
.publishIf(() -> readonly) .publishIf(() -> readonly)
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP) .newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP)

View file

@ -111,6 +111,8 @@ public class ProctoringSettingsPopup {
new LocTextKey("sebserver.exam.proctoring.form.resetSettings"); new LocTextKey("sebserver.exam.proctoring.form.resetSettings");
private final static LocTextKey RESET_CONFIRM_KEY = private final static LocTextKey RESET_CONFIRM_KEY =
new LocTextKey("sebserver.exam.proctoring.form.resetConfirm"); new LocTextKey("sebserver.exam.proctoring.form.resetConfirm");
private final static LocTextKey RESET_ACTIVE_CON_KEY =
new LocTextKey("sebserver.exam.proctoring.form.resetActive");
Function<PageAction, PageAction> settingsFunction(final PageService pageService, final boolean modifyGrant) { Function<PageAction, PageAction> settingsFunction(final PageService pageService, final boolean modifyGrant) {
@ -171,7 +173,7 @@ public class ProctoringSettingsPopup {
pc -> new SEBProctoringPropertiesForm( pc -> new SEBProctoringPropertiesForm(
pageService, pageService,
pageContext, pageContext,
resetButtonHandler)); resetButtonHandler).compose(pc.getParent()));
} }
return action; return action;
@ -207,8 +209,14 @@ public class ProctoringSettingsPopup {
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call() .call()
.onError(error -> { .onError(error -> {
log.error("Failed to rest proctoring settings for exam: {}", entityKey, error); if (error.getMessage().contains("active connections") ||
pageContext.notifyUnexpectedError(error); (error.getCause() != null &&
error.getCause().getMessage().contains("active connections"))) {
pageContext.publishInfo(RESET_ACTIVE_CON_KEY);
} else {
log.error("Failed to rest proctoring settings for exam: {}", entityKey, error);
pageContext.notifyUnexpectedError(error);
}
}).map(settings -> true).getOr(false); }).map(settings -> true).getOr(false);
} }

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -102,10 +103,37 @@ public interface AdditionalAttributesDAO {
final Long entityId, final Long entityId,
final Map<String, String> attributes) { final Map<String, String> attributes) {
return saveAdditionalAttributes(type, entityId, attributes, false);
}
/** Use this to save an additional attributes for a specific entity.
* If an additional attribute with specified name already exists for the specified entity
* this updates just the value for this additional attribute. Otherwise create a new instance
* of additional attribute with the given data
*
* @param type the entity type
* @param entityId the entity identifier (primary key)
* @param attributes Map of attributes to save for
* @param deleteNullValues indicates if null values shall be deleted or not */
default Result<Collection<AdditionalAttributeRecord>> saveAdditionalAttributes(
final EntityType type,
final Long entityId,
final Map<String, String> attributes,
final boolean deleteNullValues) {
return Result.tryCatch(() -> attributes.entrySet() return Result.tryCatch(() -> attributes.entrySet()
.stream() .stream()
.map(attr -> saveAdditionalAttribute(type, entityId, attr.getKey(), attr.getValue()) .map(attr -> {
.onError(error -> log.warn("Failed to save additional attribute: {}", error.getMessage()))) if (deleteNullValues && attr.getValue() == null) {
delete(type, entityId, attr.getKey());
return null;
} else {
return saveAdditionalAttribute(type, entityId, attr.getKey(), attr.getValue())
.onError(error -> log.warn("Failed to save additional attribute: {}",
error.getMessage()));
}
})
.filter(Objects::nonNull)
.flatMap(Result::skipOnError) .flatMap(Result::skipOnError)
.collect(Collectors.toList())); .collect(Collectors.toList()));
} }

View file

@ -116,6 +116,10 @@ public interface ExamAdminService {
* @return ExamProctoringService instance */ * @return ExamProctoringService instance */
Result<ExamProctoringService> getExamProctoringService(final Long examId); Result<ExamProctoringService> getExamProctoringService(final Long examId);
/** This resets the proctoring settings for a given exam and stores the default settings.
*
* @param exam The exam reference
* @return Result refer to the given exam or to an error when happened */
Result<Exam> resetProctoringSettings(Exam exam); Result<Exam> resetProctoringSettings(Exam exam);
/** This archives a finished exam and set it to archived state as well as the assigned /** This archives a finished exam and set it to archived state as well as the assigned

View file

@ -245,6 +245,9 @@ public class ExamAdminServiceImpl implements ExamAdminService {
@Override @Override
public Result<Exam> resetProctoringSettings(final Exam exam) { public Result<Exam> resetProctoringSettings(final Exam exam) {
// first delete all proctoring settings
return getProctoringServiceSettings(exam.id) return getProctoringServiceSettings(exam.id)
.map(settings -> { .map(settings -> {
ProctoringServiceSettings resetSettings; ProctoringServiceSettings resetSettings;

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl;
import java.util.Arrays; import java.util.Arrays;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function; import java.util.function.Function;
@ -100,7 +101,6 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM)); ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM));
}) })
.getOrThrow(); .getOrThrow();
}); });
} }
@ -118,95 +118,53 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
testExamProctoring(proctoringServiceSettings).getOrThrow(); testExamProctoring(proctoringServiceSettings).getOrThrow();
} }
this.additionalAttributesDAO.saveAdditionalAttribute( final Map<String, String> attributes = new HashMap<>();
parentEntityKey.entityType, attributes.put(
entityId,
ProctoringServiceSettings.ATTR_ENABLE_PROCTORING, ProctoringServiceSettings.ATTR_ENABLE_PROCTORING,
String.valueOf(proctoringServiceSettings.enableProctoring)); String.valueOf(proctoringServiceSettings.enableProctoring));
attributes.put(
this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType,
entityId,
ProctoringServiceSettings.ATTR_SERVER_TYPE, ProctoringServiceSettings.ATTR_SERVER_TYPE,
proctoringServiceSettings.serverType.name()); proctoringServiceSettings.serverType.name());
attributes.put(
this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType,
entityId,
ProctoringServiceSettings.ATTR_SERVER_URL, ProctoringServiceSettings.ATTR_SERVER_URL,
StringUtils.trim(proctoringServiceSettings.serverURL)); StringUtils.trim(proctoringServiceSettings.serverURL));
attributes.put(
this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType,
entityId,
ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE, ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE,
String.valueOf(proctoringServiceSettings.collectingRoomSize)); String.valueOf(proctoringServiceSettings.collectingRoomSize));
attributes.put(
if (StringUtils.isNotBlank(proctoringServiceSettings.appKey)) { ProctoringServiceSettings.ATTR_APP_KEY,
this.additionalAttributesDAO.saveAdditionalAttribute( StringUtils.trim(proctoringServiceSettings.appKey));
parentEntityKey.entityType, attributes.put(
entityId, ProctoringServiceSettings.ATTR_APP_SECRET,
ProctoringServiceSettings.ATTR_APP_KEY, encryptSecret(Utils.trim(proctoringServiceSettings.appSecret)));
StringUtils.trim(proctoringServiceSettings.appKey)); attributes.put(
ProctoringServiceSettings.ATTR_ACCOUNT_ID,
this.additionalAttributesDAO.saveAdditionalAttribute( StringUtils.trim(proctoringServiceSettings.accountId));
parentEntityKey.entityType, attributes.put(
entityId, ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_ID,
ProctoringServiceSettings.ATTR_APP_SECRET, StringUtils.trim(proctoringServiceSettings.clientId));
this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.appSecret)) attributes.put(
.getOrThrow() ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_SECRET,
.toString()); encryptSecret(Utils.trim(proctoringServiceSettings.clientSecret)));
} attributes.put(
ProctoringServiceSettings.ATTR_SDK_KEY,
if (StringUtils.isNotBlank(proctoringServiceSettings.accountId)) { StringUtils.trim(proctoringServiceSettings.sdkKey));
this.additionalAttributesDAO.saveAdditionalAttribute( attributes.put(
parentEntityKey.entityType, ProctoringServiceSettings.ATTR_SDK_SECRET,
entityId, encryptSecret(Utils.trim(proctoringServiceSettings.sdkSecret)));
ProctoringServiceSettings.ATTR_ACCOUNT_ID, attributes.put(
StringUtils.trim(proctoringServiceSettings.accountId));
this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType,
entityId,
ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_ID,
StringUtils.trim(proctoringServiceSettings.clientId));
this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType,
entityId,
ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_SECRET,
this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.clientSecret))
.getOrThrow()
.toString());
}
if (StringUtils.isNotBlank(proctoringServiceSettings.sdkKey)) {
this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType,
entityId,
ProctoringServiceSettings.ATTR_SDK_KEY,
StringUtils.trim(proctoringServiceSettings.sdkKey));
this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType,
entityId,
ProctoringServiceSettings.ATTR_SDK_SECRET,
this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.sdkSecret))
.getOrThrow()
.toString());
}
this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType,
entityId,
ProctoringServiceSettings.ATTR_ENABLED_FEATURES, ProctoringServiceSettings.ATTR_ENABLED_FEATURES,
StringUtils.join(proctoringServiceSettings.enabledFeatures, Constants.LIST_SEPARATOR)); StringUtils.join(proctoringServiceSettings.enabledFeatures, Constants.LIST_SEPARATOR));
attributes.put(
this.additionalAttributesDAO.saveAdditionalAttribute(
parentEntityKey.entityType,
entityId,
ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM, ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM,
String.valueOf(proctoringServiceSettings.useZoomAppClientForCollectingRoom)); String.valueOf(proctoringServiceSettings.useZoomAppClientForCollectingRoom));
this.additionalAttributesDAO.saveAdditionalAttributes(
parentEntityKey.entityType,
entityId,
attributes,
true);
return proctoringServiceSettings; return proctoringServiceSettings;
}); });
} }
@ -293,4 +251,13 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
} }
} }
private String encryptSecret(final CharSequence secret) {
if (StringUtils.isBlank(secret)) {
return null;
}
return this.cryptor.encrypt(Utils.trim(secret))
.getOrThrow()
.toString();
}
} }

View file

@ -139,6 +139,11 @@ public interface ExamProctoringRoomService {
* @return Result refer to void or to an error when happened */ * @return Result refer to void or to an error when happened */
Result<Void> notifyRoomOpened(Long examId, String roomName); Result<Void> notifyRoomOpened(Long examId, String roomName);
/** Disposes all open proctoring rooms for a given exam and removes the references on the persistent storage.
* First checks if there are active SEB client connections on for the given exam and if so, throws error.
*
* @param exam The exam to cleanup the rooms
* @return Result refer to the given exam or to an error when happened */
Result<Exam> cleanupAllRooms(Exam exam); Result<Exam> cleanupAllRooms(Exam exam);
} }

View file

@ -125,4 +125,6 @@ public interface ExamProctoringService {
RemoteProctoringRoom room, RemoteProctoringRoom room,
Collection<ClientConnection> clientConnections); Collection<ClientConnection> clientConnections);
public void clearRestTemplateCache(final Long examId);
} }

View file

@ -351,6 +351,11 @@ public class JitsiProctoringService implements ExamProctoringService {
return Result.EMPTY; return Result.EMPTY;
} }
@Override
public void clearRestTemplateCache(final Long examId) {
// Nothing to do here
}
protected Result<ProctoringRoomConnection> createProctoringConnection( protected Result<ProctoringRoomConnection> createProctoringConnection(
final String connectionToken, final String connectionToken,
final String url, final String url,

View file

@ -15,7 +15,9 @@ import java.util.Base64;
import java.util.Base64.Encoder; import java.util.Base64.Encoder;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
@ -38,11 +40,22 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext; import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails; import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest; import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
import org.springframework.security.oauth2.client.token.AccessTokenProvider;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientResponseException; import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
@ -196,13 +209,10 @@ public class ZoomProctoringService implements ExamProctoringService {
final ResponseEntity<String> result = newRestTemplate.testServiceConnection(); final ResponseEntity<String> result = newRestTemplate.testServiceConnection();
if (result.getStatusCode() != HttpStatus.OK) { if (result.getStatusCode() != HttpStatus.OK) {
throw new APIMessageException(Arrays.asList( throw new RuntimeException("Invalid Zoom Service response: " + result);
APIMessage.fieldValidationError(ProctoringServiceSettings.ATTR_SERVER_URL,
"proctoringSettings:serverURL:url.invalid"),
APIMessage.ErrorMessage.EXTERNAL_SERVICE_BINDING_ERROR.of()));
} }
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to access Zoom service at: {}", proctoringSettings.serverURL, e.getMessage()); log.error("Failed to access Zoom service at: {}", proctoringSettings.serverURL, e);
throw new APIMessageException(Arrays.asList( throw new APIMessageException(Arrays.asList(
APIMessage.fieldValidationError(ProctoringServiceSettings.ATTR_SERVER_URL, APIMessage.fieldValidationError(ProctoringServiceSettings.ATTR_SERVER_URL,
"proctoringSettings:serverURL:url.noservice"), "proctoringSettings:serverURL:url.noservice"),
@ -655,35 +665,17 @@ public class ZoomProctoringService implements ExamProctoringService {
// NOTE: following is the original code that includes the exam end time but seems to make trouble for OLAT // NOTE: following is the original code that includes the exam end time but seems to make trouble for OLAT
final long nowInSeconds = Utils.getSecondsNow(); final long nowInSeconds = Utils.getSecondsNow();
// final long nowPlus30MinInSeconds = nowInSeconds + Utils.toSeconds(30 * Constants.MINUTE_IN_MILLIS);
final long nowPlusOneDayInSeconds = nowInSeconds + Utils.toSeconds(Constants.DAY_IN_MILLIS); final long nowPlusOneDayInSeconds = nowInSeconds + Utils.toSeconds(Constants.DAY_IN_MILLIS);
// final long nowPlusTwoDayInSeconds = nowInSeconds + Utils.toSeconds(2 * Constants.DAY_IN_MILLIS);
// long expTime = nowPlusOneDayInSeconds;
// if (examProctoring.examId == null && this.examSessionService.isExamRunning(examProctoring.examId)) {
// final Exam exam = this.examSessionService.getRunningExam(examProctoring.examId)
// .getOrThrow();
// if (exam.endTime != null) {
// expTime = Utils.toSeconds(exam.endTime.getMillis());
// }
// }
// // refer to https://marketplace.zoom.us/docs/sdk/native-sdks/auth
// // "exp": 0, //JWT expiration date (Min:1800 seconds greater than iat value, Max: 48 hours greater than iat value) in epoch format.
// if (expTime >= nowPlusTwoDayInSeconds) {
// expTime = nowPlusTwoDayInSeconds - 10; // Do not set to max because it is not well defined if max is included or not
// } else if (expTime < nowPlus30MinInSeconds) {
// expTime = nowPlusOneDayInSeconds;
// }
//
// log.debug("**** SDK Token exp time with exam-end-time inclusion would be: {}", expTime);
//
//
// NOTE: Set this to the maximum according to https://marketplace.zoom.us/docs/sdk/native-sdks/auth
//return nowPlusTwoDayInSeconds - 1000; // Do not set to max because it is not well defined if max is included or not;
// NOTE: It seems that since the update of web sdk to SDKToken to 1.7.0, the max is new + one day // NOTE: It seems that since the update of web sdk to SDKToken to 1.7.0, the max is new + one day
return nowPlusOneDayInSeconds - 10; return nowPlusOneDayInSeconds - 10;
} }
@Override
public synchronized void clearRestTemplateCache(final Long examId) {
this.restTemplatesCache.remove(examId);
}
private final LinkedHashMap<Long, ZoomRestTemplate> restTemplatesCache = new LinkedHashMap<>(); private final LinkedHashMap<Long, ZoomRestTemplate> restTemplatesCache = new LinkedHashMap<>();
private synchronized ZoomRestTemplate getZoomRestTemplate(final ProctoringServiceSettings proctoringSettings) { private synchronized ZoomRestTemplate getZoomRestTemplate(final ProctoringServiceSettings proctoringSettings) {
@ -828,7 +820,7 @@ public class ZoomProctoringService implements ExamProctoringService {
final HttpHeaders headers = getHeaders(); final HttpHeaders headers = getHeaders();
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
final ResponseEntity<String> exchange = exchange(url, HttpMethod.PATCH, body, headers); final ResponseEntity<String> exchange = exchange(url, HttpMethod.POST, body, headers);
return exchange; return exchange;
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to apply user settings for Zoom user: {}", userId, e); log.error("Failed to apply user settings for Zoom user: {}", userId, e);
@ -1006,17 +998,19 @@ public class ZoomProctoringService implements ExamProctoringService {
.decrypt(this.credentials.secret) .decrypt(this.credentials.secret)
.getOrThrow(); .getOrThrow();
this.resource = new BaseOAuth2ProtectedResourceDetails(); this.resource = new ClientCredentialsResourceDetails();
this.resource.setAccessTokenUri(this.proctoringSettings.serverURL + "/oauth/token"); this.resource.setAccessTokenUri(this.proctoringSettings.serverURL + "/oauth/token");
this.resource.setClientId(this.credentials.clientIdAsString()); this.resource.setClientId(this.credentials.clientIdAsString());
this.resource.setClientSecret(decryptedSecret.toString()); this.resource.setClientSecret(decryptedSecret.toString());
this.resource.setGrantType("account_credentials"); this.resource.setGrantType("account_credentials");
this.resource.setId(this.proctoringSettings.accountId);
final DefaultAccessTokenRequest defaultAccessTokenRequest = new DefaultAccessTokenRequest(); final SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
defaultAccessTokenRequest.set("account_id", this.proctoringSettings.accountId); requestFactory.setOutputStreaming(false);
this.restTemplate = new OAuth2RestTemplate( final OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(this.resource);
this.resource, oAuth2RestTemplate.setRequestFactory(requestFactory);
new DefaultOAuth2ClientContext(defaultAccessTokenRequest)); oAuth2RestTemplate.setAccessTokenProvider(new ZoomCredentialsAccessTokenProvider());
this.restTemplate = oAuth2RestTemplate;
} }
} }
@ -1116,4 +1110,63 @@ public class ZoomProctoringService implements ExamProctoringService {
} }
} }
private static final class ZoomCredentialsAccessTokenProvider extends OAuth2AccessTokenSupport
implements AccessTokenProvider {
@Override
public boolean supportsResource(final OAuth2ProtectedResourceDetails resource) {
return resource instanceof ClientCredentialsResourceDetails
&& "account_credentials".equals(resource.getGrantType());
}
@Override
public boolean supportsRefresh(final OAuth2ProtectedResourceDetails resource) {
return false;
}
@Override
public OAuth2AccessToken refreshAccessToken(final OAuth2ProtectedResourceDetails resource,
final OAuth2RefreshToken refreshToken, final AccessTokenRequest request)
throws UserRedirectRequiredException {
return null;
}
@Override
public OAuth2AccessToken obtainAccessToken(final OAuth2ProtectedResourceDetails details,
final AccessTokenRequest request)
throws UserRedirectRequiredException, AccessDeniedException, OAuth2AccessDeniedException {
final ClientCredentialsResourceDetails resource = (ClientCredentialsResourceDetails) details;
return retrieveToken(request, resource, getParametersForTokenRequest(resource), new HttpHeaders());
}
private MultiValueMap<String, String> getParametersForTokenRequest(
final ClientCredentialsResourceDetails resource) {
final MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.set("grant_type", "account_credentials");
form.set("account_id", resource.getId());
if (resource.isScoped()) {
final StringBuilder builder = new StringBuilder();
final List<String> scope = resource.getScope();
if (scope != null) {
final Iterator<String> scopeIt = scope.iterator();
while (scopeIt.hasNext()) {
builder.append(scopeIt.next());
if (scopeIt.hasNext()) {
builder.append(' ');
}
}
}
form.set("scope", builder.toString());
}
return form;
}
}
} }

View file

@ -22,6 +22,8 @@ import javax.validation.Valid;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.SqlTable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.validation.FieldError; import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@ -80,6 +82,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe
@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.EXAM_ADMINISTRATION_ENDPOINT) @RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.EXAM_ADMINISTRATION_ENDPOINT)
public class ExamAdministrationController extends EntityController<Exam, Exam> { public class ExamAdministrationController extends EntityController<Exam, Exam> {
private static final Logger log = LoggerFactory.getLogger(ExamAdministrationController.class);
private final ExamDAO examDAO; private final ExamDAO examDAO;
private final UserDAO userDAO; private final UserDAO userDAO;
private final ExamAdminService examAdminService; private final ExamAdminService examAdminService;
@ -508,9 +512,16 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
return this.entityDAO return this.entityDAO
.byPK(examId) .byPK(examId)
.flatMap(this.examProctoringRoomService::cleanupAllRooms) .flatMap(this.examProctoringRoomService::cleanupAllRooms)
.map(exam -> {
this.examAdminService.getExamProctoringService(exam.id)
.onSuccess(service -> service.clearRestTemplateCache(exam.id))
.onError(error -> log.warn(
"Failed to clear proctoring rest template cache for exam: {}",
error.getMessage()));
return exam;
})
.flatMap(this.examAdminService::resetProctoringSettings) .flatMap(this.examAdminService::resetProctoringSettings)
.getOrThrow(); .getOrThrow();
} }
// **** Proctoring // **** Proctoring

View file

@ -831,7 +831,7 @@ sebserver.exam.proctoring.form.useZoomAppClient.tooltip=If this is set SEB Serve
sebserver.exam.proctoring.form.saveSettings=Save Settings sebserver.exam.proctoring.form.saveSettings=Save Settings
sebserver.exam.proctoring.form.resetSettings=Reset Settings sebserver.exam.proctoring.form.resetSettings=Reset Settings
sebserver.exam.proctoring.form.resetConfirm=Reset will first cleanup and remove all existing proctoring rooms for this exam and then reset to default or template settings.<br/>Please make sure there are no active SEB client connections for this exam, otherwise this reset will be denied.<br/><br/>Do you want to reset proctoring for this exam now? sebserver.exam.proctoring.form.resetConfirm=Reset will first cleanup and remove all existing proctoring rooms for this exam and then reset to default or template settings.<br/>Please make sure there are no active SEB client connections for this exam, otherwise this reset will be denied.<br/><br/>Do you want to reset proctoring for this exam now?
sebserver.exam.proctoring.form.resetActive=There are still active SEB client connection for this exam. Please disconnect or cancel them first.<br/>This action is only possible when there are no active SEB client connection within this exam.
sebserver.exam.proctoring.type.servertype.JITSI_MEET=Jitsi Meet Server sebserver.exam.proctoring.type.servertype.JITSI_MEET=Jitsi Meet Server
sebserver.exam.proctoring.type.servertype.JITSI_MEET.tooltip=Use a Jitsi Meet server for proctoring sebserver.exam.proctoring.type.servertype.JITSI_MEET.tooltip=Use a Jitsi Meet server for proctoring