SEBSERV-363 finished integration adaption and testing
This commit is contained in:
		
							parent
							
								
									edce7275ca
								
							
						
					
					
						commit
						4671e682a3
					
				
					 13 changed files with 209 additions and 124 deletions
				
			
		| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<PageAction, PageAction> 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 -> {
 | 
			
		||||
                    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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<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()
 | 
			
		||||
                .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()));
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -116,6 +116,10 @@ public interface ExamAdminService {
 | 
			
		|||
     * @return ExamProctoringService instance */
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
    /** This archives a finished exam and set it to archived state as well as the assigned
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -245,6 +245,9 @@ public class ExamAdminServiceImpl implements ExamAdminService {
 | 
			
		|||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Result<Exam> resetProctoringSettings(final Exam exam) {
 | 
			
		||||
 | 
			
		||||
        // first delete all proctoring settings
 | 
			
		||||
 | 
			
		||||
        return getProctoringServiceSettings(exam.id)
 | 
			
		||||
                .map(settings -> {
 | 
			
		||||
                    ProctoringServiceSettings resetSettings;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<String, String> 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,
 | 
			
		||||
            attributes.put(
 | 
			
		||||
                    ProctoringServiceSettings.ATTR_APP_KEY,
 | 
			
		||||
                    StringUtils.trim(proctoringServiceSettings.appKey));
 | 
			
		||||
 | 
			
		||||
                this.additionalAttributesDAO.saveAdditionalAttribute(
 | 
			
		||||
                        parentEntityKey.entityType,
 | 
			
		||||
                        entityId,
 | 
			
		||||
            attributes.put(
 | 
			
		||||
                    ProctoringServiceSettings.ATTR_APP_SECRET,
 | 
			
		||||
                        this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.appSecret))
 | 
			
		||||
                                .getOrThrow()
 | 
			
		||||
                                .toString());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (StringUtils.isNotBlank(proctoringServiceSettings.accountId)) {
 | 
			
		||||
                this.additionalAttributesDAO.saveAdditionalAttribute(
 | 
			
		||||
                        parentEntityKey.entityType,
 | 
			
		||||
                        entityId,
 | 
			
		||||
                    encryptSecret(Utils.trim(proctoringServiceSettings.appSecret)));
 | 
			
		||||
            attributes.put(
 | 
			
		||||
                    ProctoringServiceSettings.ATTR_ACCOUNT_ID,
 | 
			
		||||
                    StringUtils.trim(proctoringServiceSettings.accountId));
 | 
			
		||||
                this.additionalAttributesDAO.saveAdditionalAttribute(
 | 
			
		||||
                        parentEntityKey.entityType,
 | 
			
		||||
                        entityId,
 | 
			
		||||
            attributes.put(
 | 
			
		||||
                    ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_ID,
 | 
			
		||||
                    StringUtils.trim(proctoringServiceSettings.clientId));
 | 
			
		||||
 | 
			
		||||
                this.additionalAttributesDAO.saveAdditionalAttribute(
 | 
			
		||||
                        parentEntityKey.entityType,
 | 
			
		||||
                        entityId,
 | 
			
		||||
            attributes.put(
 | 
			
		||||
                    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,
 | 
			
		||||
                    encryptSecret(Utils.trim(proctoringServiceSettings.clientSecret)));
 | 
			
		||||
            attributes.put(
 | 
			
		||||
                    ProctoringServiceSettings.ATTR_SDK_KEY,
 | 
			
		||||
                    StringUtils.trim(proctoringServiceSettings.sdkKey));
 | 
			
		||||
 | 
			
		||||
                this.additionalAttributesDAO.saveAdditionalAttribute(
 | 
			
		||||
                        parentEntityKey.entityType,
 | 
			
		||||
                        entityId,
 | 
			
		||||
            attributes.put(
 | 
			
		||||
                    ProctoringServiceSettings.ATTR_SDK_SECRET,
 | 
			
		||||
                        this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.sdkSecret))
 | 
			
		||||
                                .getOrThrow()
 | 
			
		||||
                                .toString());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.additionalAttributesDAO.saveAdditionalAttribute(
 | 
			
		||||
                    parentEntityKey.entityType,
 | 
			
		||||
                    entityId,
 | 
			
		||||
                    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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -139,6 +139,11 @@ public interface ExamProctoringRoomService {
 | 
			
		|||
     * @return Result refer to void or to an error when happened */
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -125,4 +125,6 @@ public interface ExamProctoringService {
 | 
			
		|||
            RemoteProctoringRoom room,
 | 
			
		||||
            Collection<ClientConnection> clientConnections);
 | 
			
		||||
 | 
			
		||||
    public void clearRestTemplateCache(final Long examId);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<ProctoringRoomConnection> createProctoringConnection(
 | 
			
		||||
            final String connectionToken,
 | 
			
		||||
            final String url,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<String> 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<Long, ZoomRestTemplate> 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<String> exchange = exchange(url, HttpMethod.PATCH, body, headers);
 | 
			
		||||
                final ResponseEntity<String> 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<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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<Exam, Exam> {
 | 
			
		||||
 | 
			
		||||
    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<Exam, Exam> {
 | 
			
		|||
        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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.<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.tooltip=Use a Jitsi Meet server for proctoring
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue