diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java index ca8fff0f..2604a889 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java @@ -126,7 +126,7 @@ public class ProctoringServiceSettings implements Entity { this.examId = examId; this.enableProctoring = BooleanUtils.isTrue(enableProctoring); - this.serverType = (serverType != null) ? serverType : ProctoringServerType.JITSI_MEET; + this.serverType = (serverType != null) ? serverType : ProctoringServerType.ZOOM; this.serverURL = serverURL; this.collectingRoomSize = (collectingRoomSize != null) ? collectingRoomSize : 20; this.enabledFeatures = enabledFeatures != null ? enabledFeatures : EnumSet.allOf(ProctoringFeature.class); @@ -144,7 +144,7 @@ public class ProctoringServiceSettings implements Entity { public ProctoringServiceSettings(final Long examId) { this.examId = examId; this.enableProctoring = false; - this.serverType = null; + this.serverType = ProctoringServerType.ZOOM; this.serverURL = null; this.collectingRoomSize = 20; this.enabledFeatures = EnumSet.allOf(ProctoringFeature.class); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamSignatureKeyForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamSignatureKeyForm.java index 057ed392..2e1d92b3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamSignatureKeyForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ExamSignatureKeyForm.java @@ -256,7 +256,6 @@ public class ExamSignatureKeyForm implements TemplateComposer { .newAction(ActionDefinition.EXAM_SECURITY_KEY_BACK_MODIFY) .withEntityKey(entityKey) - //.withExec(this.pageService.backToCurrentFunction()) .publishIf(() -> readonly) .newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ProctoringSettingsPopup.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ProctoringSettingsPopup.java index ed1a6659..36b92781 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ProctoringSettingsPopup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ProctoringSettingsPopup.java @@ -111,6 +111,8 @@ public class ProctoringSettingsPopup { new LocTextKey("sebserver.exam.proctoring.form.resetSettings"); private final static LocTextKey RESET_CONFIRM_KEY = new LocTextKey("sebserver.exam.proctoring.form.resetConfirm"); + private final static LocTextKey RESET_ACTIVE_CON_KEY = + new LocTextKey("sebserver.exam.proctoring.form.resetActive"); Function settingsFunction(final PageService pageService, final boolean modifyGrant) { @@ -171,7 +173,7 @@ public class ProctoringSettingsPopup { pc -> new SEBProctoringPropertiesForm( pageService, pageContext, - resetButtonHandler)); + resetButtonHandler).compose(pc.getParent())); } return action; @@ -207,8 +209,14 @@ public class ProctoringSettingsPopup { .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) .call() .onError(error -> { - log.error("Failed to rest proctoring settings for exam: {}", entityKey, error); - pageContext.notifyUnexpectedError(error); + if (error.getMessage().contains("active connections") || + (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); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/AdditionalAttributesDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/AdditionalAttributesDAO.java index acc8f327..da3ec62e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/AdditionalAttributesDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/AdditionalAttributesDAO.java @@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao; import java.util.Collection; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -102,10 +103,37 @@ public interface AdditionalAttributesDAO { final Long entityId, final Map 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> saveAdditionalAttributes( + final EntityType type, + final Long entityId, + final Map attributes, + final boolean deleteNullValues) { + return Result.tryCatch(() -> attributes.entrySet() .stream() - .map(attr -> saveAdditionalAttribute(type, entityId, attr.getKey(), attr.getValue()) - .onError(error -> log.warn("Failed to save additional attribute: {}", error.getMessage()))) + .map(attr -> { + 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) .collect(Collectors.toList())); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java index 75f4c1e5..57b3347f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java @@ -116,6 +116,10 @@ public interface ExamAdminService { * @return ExamProctoringService instance */ Result 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 resetProctoringSettings(Exam exam); /** This archives a finished exam and set it to archived state as well as the assigned diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java index c4465646..19f443c1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java @@ -245,6 +245,9 @@ public class ExamAdminServiceImpl implements ExamAdminService { @Override public Result resetProctoringSettings(final Exam exam) { + + // first delete all proctoring settings + return getProctoringServiceSettings(exam.id) .map(settings -> { ProctoringServiceSettings resetSettings; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ProctoringAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ProctoringAdminServiceImpl.java index f0c242c8..d630d150 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ProctoringAdminServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ProctoringAdminServiceImpl.java @@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.exam.impl; import java.util.Arrays; import java.util.EnumSet; +import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.function.Function; @@ -100,7 +101,6 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService { ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM)); }) .getOrThrow(); - }); } @@ -118,95 +118,53 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService { testExamProctoring(proctoringServiceSettings).getOrThrow(); } - this.additionalAttributesDAO.saveAdditionalAttribute( - parentEntityKey.entityType, - entityId, + final Map attributes = new HashMap<>(); + attributes.put( ProctoringServiceSettings.ATTR_ENABLE_PROCTORING, String.valueOf(proctoringServiceSettings.enableProctoring)); - - this.additionalAttributesDAO.saveAdditionalAttribute( - parentEntityKey.entityType, - entityId, + attributes.put( ProctoringServiceSettings.ATTR_SERVER_TYPE, proctoringServiceSettings.serverType.name()); - - this.additionalAttributesDAO.saveAdditionalAttribute( - parentEntityKey.entityType, - entityId, + attributes.put( ProctoringServiceSettings.ATTR_SERVER_URL, StringUtils.trim(proctoringServiceSettings.serverURL)); - - this.additionalAttributesDAO.saveAdditionalAttribute( - parentEntityKey.entityType, - entityId, + attributes.put( ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE, String.valueOf(proctoringServiceSettings.collectingRoomSize)); - - if (StringUtils.isNotBlank(proctoringServiceSettings.appKey)) { - this.additionalAttributesDAO.saveAdditionalAttribute( - parentEntityKey.entityType, - entityId, - ProctoringServiceSettings.ATTR_APP_KEY, - StringUtils.trim(proctoringServiceSettings.appKey)); - - this.additionalAttributesDAO.saveAdditionalAttribute( - parentEntityKey.entityType, - entityId, - ProctoringServiceSettings.ATTR_APP_SECRET, - this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.appSecret)) - .getOrThrow() - .toString()); - } - - if (StringUtils.isNotBlank(proctoringServiceSettings.accountId)) { - this.additionalAttributesDAO.saveAdditionalAttribute( - parentEntityKey.entityType, - entityId, - ProctoringServiceSettings.ATTR_ACCOUNT_ID, - 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, + attributes.put( + ProctoringServiceSettings.ATTR_APP_KEY, + StringUtils.trim(proctoringServiceSettings.appKey)); + attributes.put( + ProctoringServiceSettings.ATTR_APP_SECRET, + encryptSecret(Utils.trim(proctoringServiceSettings.appSecret))); + attributes.put( + ProctoringServiceSettings.ATTR_ACCOUNT_ID, + StringUtils.trim(proctoringServiceSettings.accountId)); + attributes.put( + ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_ID, + StringUtils.trim(proctoringServiceSettings.clientId)); + attributes.put( + ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_SECRET, + encryptSecret(Utils.trim(proctoringServiceSettings.clientSecret))); + attributes.put( + ProctoringServiceSettings.ATTR_SDK_KEY, + StringUtils.trim(proctoringServiceSettings.sdkKey)); + attributes.put( + ProctoringServiceSettings.ATTR_SDK_SECRET, + encryptSecret(Utils.trim(proctoringServiceSettings.sdkSecret))); + attributes.put( ProctoringServiceSettings.ATTR_ENABLED_FEATURES, StringUtils.join(proctoringServiceSettings.enabledFeatures, Constants.LIST_SEPARATOR)); - - this.additionalAttributesDAO.saveAdditionalAttribute( - parentEntityKey.entityType, - entityId, + attributes.put( ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM, String.valueOf(proctoringServiceSettings.useZoomAppClientForCollectingRoom)); + this.additionalAttributesDAO.saveAdditionalAttributes( + parentEntityKey.entityType, + entityId, + attributes, + true); + 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(); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java index f8f91247..08490f1a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java @@ -139,6 +139,11 @@ public interface ExamProctoringRoomService { * @return Result refer to void or to an error when happened */ Result 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 cleanupAllRooms(Exam exam); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringService.java index 67723096..f9cfa7c5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringService.java @@ -125,4 +125,6 @@ public interface ExamProctoringService { RemoteProctoringRoom room, Collection clientConnections); + public void clearRestTemplateCache(final Long examId); + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/JitsiProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/JitsiProctoringService.java index 34ca5f18..f5b29fb3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/JitsiProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/JitsiProctoringService.java @@ -351,6 +351,11 @@ public class JitsiProctoringService implements ExamProctoringService { return Result.EMPTY; } + @Override + public void clearRestTemplateCache(final Long examId) { + // Nothing to do here + } + protected Result createProctoringConnection( final String connectionToken, final String url, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java index e5737062..139c0650 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java @@ -15,7 +15,9 @@ import java.util.Base64; import java.util.Base64.Encoder; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -38,11 +40,22 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; 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.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.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClientResponseException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; @@ -196,13 +209,10 @@ public class ZoomProctoringService implements ExamProctoringService { final ResponseEntity result = newRestTemplate.testServiceConnection(); if (result.getStatusCode() != HttpStatus.OK) { - throw new APIMessageException(Arrays.asList( - APIMessage.fieldValidationError(ProctoringServiceSettings.ATTR_SERVER_URL, - "proctoringSettings:serverURL:url.invalid"), - APIMessage.ErrorMessage.EXTERNAL_SERVICE_BINDING_ERROR.of())); + throw new RuntimeException("Invalid Zoom Service response: " + result); } } 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( APIMessage.fieldValidationError(ProctoringServiceSettings.ATTR_SERVER_URL, "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 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 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 return nowPlusOneDayInSeconds - 10; } + @Override + public synchronized void clearRestTemplateCache(final Long examId) { + this.restTemplatesCache.remove(examId); + } + private final LinkedHashMap restTemplatesCache = new LinkedHashMap<>(); private synchronized ZoomRestTemplate getZoomRestTemplate(final ProctoringServiceSettings proctoringSettings) { @@ -828,7 +820,7 @@ public class ZoomProctoringService implements ExamProctoringService { final HttpHeaders headers = getHeaders(); headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); - final ResponseEntity exchange = exchange(url, HttpMethod.PATCH, body, headers); + final ResponseEntity exchange = exchange(url, HttpMethod.POST, body, headers); return exchange; } catch (final Exception 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) .getOrThrow(); - this.resource = new BaseOAuth2ProtectedResourceDetails(); + this.resource = new ClientCredentialsResourceDetails(); this.resource.setAccessTokenUri(this.proctoringSettings.serverURL + "/oauth/token"); this.resource.setClientId(this.credentials.clientIdAsString()); this.resource.setClientSecret(decryptedSecret.toString()); this.resource.setGrantType("account_credentials"); + this.resource.setId(this.proctoringSettings.accountId); - final DefaultAccessTokenRequest defaultAccessTokenRequest = new DefaultAccessTokenRequest(); - defaultAccessTokenRequest.set("account_id", this.proctoringSettings.accountId); - this.restTemplate = new OAuth2RestTemplate( - this.resource, - new DefaultOAuth2ClientContext(defaultAccessTokenRequest)); + final SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + requestFactory.setOutputStreaming(false); + final OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(this.resource); + oAuth2RestTemplate.setRequestFactory(requestFactory); + 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 getParametersForTokenRequest( + final ClientCredentialsResourceDetails resource) { + + final MultiValueMap 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 scope = resource.getScope(); + + if (scope != null) { + final Iterator scopeIt = scope.iterator(); + while (scopeIt.hasNext()) { + builder.append(scopeIt.next()); + if (scopeIt.hasNext()) { + builder.append(' '); + } + } + } + + form.set("scope", builder.toString()); + } + return form; + } + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index f8bf51f7..31a3f9d7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -22,6 +22,8 @@ import javax.validation.Valid; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.mybatis.dynamic.sql.SqlTable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.validation.FieldError; 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) public class ExamAdministrationController extends EntityController { + private static final Logger log = LoggerFactory.getLogger(ExamAdministrationController.class); + private final ExamDAO examDAO; private final UserDAO userDAO; private final ExamAdminService examAdminService; @@ -508,9 +512,16 @@ public class ExamAdministrationController extends EntityController { return this.entityDAO .byPK(examId) .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) .getOrThrow(); - } // **** Proctoring diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index f0ab6a7f..a3a18229 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -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.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.
Please make sure there are no active SEB client connections for this exam, otherwise this reset will be denied.

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.
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.tooltip=Use a Jitsi Meet server for proctoring