From 6b48bca7618b2865887104ebcac6bfa9859e09be Mon Sep 17 00:00:00 2001 From: anhefti Date: Mon, 11 Nov 2019 16:39:48 +0100 Subject: [PATCH] SEBSERV-73 finished first feature complete --- .../seb/sebserver/gbl/model/exam/Exam.java | 2 +- .../EdxCourseRestrictionAttributes.java | 106 ++++ .../model/institution/LmsSetupTestResult.java | 137 ++--- .../seb/sebserver/gui/content/ExamForm.java | 38 +- .../sebserver/gui/content/LmsSetupForm.java | 60 ++- .../gui/content/action/ActionDefinition.java | 2 +- .../gui/service/page/impl/PageAction.java | 2 + .../client/ClientCredentials.java | 6 +- .../servicelayer/lms/LmsAPIService.java | 8 +- .../servicelayer/lms/LmsAPITemplate.java | 43 +- .../lms/impl/LmsAPIServiceImpl.java | 47 +- .../lms/impl/MockupLmsAPITemplate.java | 33 +- .../lms/impl/OpenEdxLmsAPITemplate.java | 477 ------------------ .../lms/impl/edx/OpenEdxCourseAccess.java | 263 ++++++++++ .../impl/edx/OpenEdxCourseRestriction.java | 180 +++++++ .../OpenEdxCourseRestrictionData.java} | 49 +- .../lms/impl/edx/OpenEdxLmsAPITemplate.java | 120 +++++ .../edx/OpenEdxLmsAPITemplateFactory.java | 82 +++ .../impl/edx/OpenEdxRestTemplateFactory.java | 208 ++++++++ .../api/ExamAdministrationController.java | 4 +- .../weblayer/api/LmsSetupController.java | 2 +- src/main/resources/logback-spring.xml | 2 + src/main/resources/messages.properties | 1 + .../api/admin/LmsSetupAPITest.java | 2 +- .../edx/OpenEdxCourseRestrictionDataTest.java | 45 ++ 25 files changed, 1274 insertions(+), 645 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/EdxCourseRestrictionAttributes.java delete mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/OpenEdxLmsAPITemplate.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java rename src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/{OpenEdxSebClientRestriction.java => edx/OpenEdxCourseRestrictionData.java} (68%) create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplate.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxRestTemplateFactory.java create mode 100644 src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestrictionDataTest.java diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java index f31d680f..eabf6899 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java @@ -162,7 +162,7 @@ public final class Exam implements GrantEntity { this.quitPassword = quitPassword; this.owner = owner; this.status = (status != null) ? status : getStatusFromDate(startTime, endTime); - this.lmsSebRestriction = (lmsSebRestriction != null) ? lmsSebRestriction : Boolean.TRUE; + this.lmsSebRestriction = (lmsSebRestriction != null) ? lmsSebRestriction : Boolean.FALSE; this.browserExamKeys = browserExamKeys; this.active = (active != null) ? active : Boolean.TRUE; this.lastUpdate = lastUpdate; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/EdxCourseRestrictionAttributes.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/EdxCourseRestrictionAttributes.java new file mode 100644 index 00000000..710035a6 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/EdxCourseRestrictionAttributes.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gbl.model.institution; + +import java.util.Collection; + +import org.apache.commons.lang3.BooleanUtils; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import ch.ethz.seb.sebserver.gbl.util.Utils; + +public final class EdxCourseRestrictionAttributes { + + public enum PermissionComponents { + ALWAYS_ALLOW_STUFF("AlwaysAllowStaff"), + CHECK_BROWSER_EXAM_KEY("CheckSEBHashBrowserExamKey"), + CHECK_CONFIG_KEY("CheckSEBHashConfigKey"), + CHECK_BROWSER_EXAM_KEY_AND_CONFIG_KEY("CheckSEBHashBrowserExamKeyOrConfigKey"); + + public final String name; + + private PermissionComponents(final String name) { + this.name = name; + } + } + + public static final String ATTR_USER_BANNING_ENABLED = "userBanningEnabled"; + public static final String ATTR_SEB_PERMISSION_COMPONENTS = "permissionComponents"; + public static final String ATTR_BLACKLIST_CHAPTERS = "blacklistChapters"; + public static final String ATTR_WHITELIST_PATHS = "whitelistPaths"; + public static final String ATTR_BROWSER_EXAM_KEYS = "browserExamKeys"; + + @JsonProperty(ATTR_WHITELIST_PATHS) + public final Collection whiteListPaths; + + @JsonProperty(ATTR_BLACKLIST_CHAPTERS) + public final Collection blacklistChapters; + + @JsonProperty(ATTR_SEB_PERMISSION_COMPONENTS) + public final Collection permissionComponents; + + @JsonProperty(ATTR_USER_BANNING_ENABLED) + public final Boolean banningEnabled; + + @JsonProperty(ATTR_BROWSER_EXAM_KEYS) + public final Collection browserExamKeys; + + protected EdxCourseRestrictionAttributes( + final Collection whiteListPaths, + final Collection blacklistChapters, + final Collection permissionComponents, + final Boolean banningEnabled, + final Collection browserExamKeys) { + + this.whiteListPaths = Utils.immutableCollectionOf(whiteListPaths); + this.blacklistChapters = Utils.immutableCollectionOf(blacklistChapters); + this.permissionComponents = Utils.immutableCollectionOf(permissionComponents); + this.banningEnabled = BooleanUtils.isTrue(banningEnabled); + this.browserExamKeys = Utils.immutableCollectionOf(browserExamKeys); + } + + public Collection getWhiteListPaths() { + return this.whiteListPaths; + } + + public Collection getBlacklistChapters() { + return this.blacklistChapters; + } + + public Collection getPermissionComponents() { + return this.permissionComponents; + } + + public Boolean getBanningEnabled() { + return this.banningEnabled; + } + + public Collection getBrowserExamKeys() { + return this.browserExamKeys; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("EdxCourseRestrictionAttributes [whiteListPaths="); + builder.append(this.whiteListPaths); + builder.append(", blacklistChapters="); + builder.append(this.blacklistChapters); + builder.append(", permissionComponents="); + builder.append(this.permissionComponents); + builder.append(", banningEnabled="); + builder.append(this.banningEnabled); + builder.append(", browserExamKeys="); + builder.append(this.browserExamKeys); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetupTestResult.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetupTestResult.java index b0674ed3..56079a41 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetupTestResult.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetupTestResult.java @@ -11,10 +11,8 @@ package ch.ethz.seb.sebserver.gbl.model.institution; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.List; - -import javax.validation.constraints.NotNull; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -23,94 +21,109 @@ import ch.ethz.seb.sebserver.gbl.util.Utils; public final class LmsSetupTestResult { - public static final String ATTR_OK_STATUS = "okStatus"; + public static final String ATTR_ERROR_TYPE = "errorType"; + public static final String ATTR_ERROR_MESSAGE = "errorMessage"; + public static final String ATTR_ERRORS = "errors"; public static final String ATTR_MISSING_ATTRIBUTE = "missingLMSSetupAttribute"; - public static final String ATTR_ERROR_TOKEN_REQUEST = "tokenRequestError"; - public static final String ATTR_ERROR_QUIZ_REQUEST = "quizRequestError"; - @JsonProperty(ATTR_OK_STATUS) - @NotNull - public final Boolean okStatus; + public enum ErrorType { + MISSING_ATTRIBUTE, + TOKEN_REQUEST, + QUIZ_ACCESS_API_REQUEST, + QUIZ_RESTRICTION_API_REQUEST + } + @JsonProperty(ATTR_ERRORS) + public final Collection errors; @JsonProperty(ATTR_MISSING_ATTRIBUTE) - public final List missingLMSSetupAttribute; - - @JsonProperty(ATTR_ERROR_TOKEN_REQUEST) - public final String tokenRequestError; - - @JsonProperty(ATTR_ERROR_QUIZ_REQUEST) - public final String quizRequestError; + public final Collection missingLMSSetupAttribute; + @JsonCreator public LmsSetupTestResult( - @JsonProperty(value = ATTR_OK_STATUS, required = true) final Boolean ok, - @JsonProperty(ATTR_MISSING_ATTRIBUTE) final Collection missingLMSSetupAttribute, - @JsonProperty(ATTR_ERROR_TOKEN_REQUEST) final String tokenRequestError, - @JsonProperty(ATTR_ERROR_QUIZ_REQUEST) final String quizRequestError) { + @JsonProperty(ATTR_ERRORS) final Collection errors, + @JsonProperty(ATTR_MISSING_ATTRIBUTE) final Collection missingLMSSetupAttribute) { - this.okStatus = ok; - // TODO - this.missingLMSSetupAttribute = Utils.immutableListOf(missingLMSSetupAttribute); - this.tokenRequestError = tokenRequestError; - this.quizRequestError = quizRequestError; + this.errors = Utils.immutableCollectionOf(errors); + this.missingLMSSetupAttribute = Utils.immutableCollectionOf(missingLMSSetupAttribute); + } + + protected LmsSetupTestResult() { + this( + Collections.emptyList(), + Collections.emptyList()); + } + + protected LmsSetupTestResult(final Error error) { + this( + Utils.immutableCollectionOf(Arrays.asList(error)), + Collections.emptyList()); + } + + protected LmsSetupTestResult(final Error error, final Collection missingLMSSetupAttribute) { + this( + Utils.immutableCollectionOf(Arrays.asList(error)), + Utils.immutableCollectionOf(missingLMSSetupAttribute)); } @JsonIgnore public boolean isOk() { - return this.okStatus != null && this.okStatus.booleanValue(); + return this.errors == null || this.errors.isEmpty(); } - public Boolean getOkStatus() { - return this.okStatus; + @JsonIgnore + public boolean isQuizAccessOk() { + return isOk() || hasError(ErrorType.QUIZ_RESTRICTION_API_REQUEST); } - public List getMissingLMSSetupAttribute() { - return this.missingLMSSetupAttribute; - } - - public String getTokenRequestError() { - return this.tokenRequestError; - } - - public String getQuizRequestError() { - return this.quizRequestError; - } - - @Override - public String toString() { - final StringBuilder builder = new StringBuilder(); - builder.append("LmsSetupTestResult [okStatus="); - builder.append(this.okStatus); - builder.append(", missingLMSSetupAttribute="); - builder.append(this.missingLMSSetupAttribute); - builder.append(", tokenRequestError="); - builder.append(this.tokenRequestError); - builder.append(", quizRequestError="); - builder.append(this.quizRequestError); - builder.append("]"); - return builder.toString(); + @JsonIgnore + public boolean hasError(final ErrorType type) { + return this.errors + .stream() + .filter(error -> error.errorType == type) + .findFirst() + .isPresent(); } public static final LmsSetupTestResult ofOkay() { - return new LmsSetupTestResult(true, Collections.emptyList(), null, null); + return new LmsSetupTestResult(); } public static final LmsSetupTestResult ofMissingAttributes(final Collection attrs) { - return new LmsSetupTestResult(false, attrs, null, null); + return new LmsSetupTestResult(new Error(ErrorType.MISSING_ATTRIBUTE, "missing attribute(s)"), attrs); } public static final LmsSetupTestResult ofMissingAttributes(final APIMessage... attrs) { - if (attrs == null) { - return new LmsSetupTestResult(false, Collections.emptyList(), null, null); - } - return new LmsSetupTestResult(false, Arrays.asList(attrs), null, null); + return new LmsSetupTestResult(new Error(ErrorType.MISSING_ATTRIBUTE, "missing attribute(s)"), + Arrays.asList(attrs)); } public static final LmsSetupTestResult ofTokenRequestError(final String message) { - return new LmsSetupTestResult(false, Collections.emptyList(), message, null); + return new LmsSetupTestResult(new Error(ErrorType.TOKEN_REQUEST, message)); } - public static final LmsSetupTestResult ofQuizRequestError(final String message) { - return new LmsSetupTestResult(false, Collections.emptyList(), null, message); + public static final LmsSetupTestResult ofQuizAccessAPIError(final String message) { + return new LmsSetupTestResult(new Error(ErrorType.QUIZ_ACCESS_API_REQUEST, message)); + } + + public static final LmsSetupTestResult ofQuizRestrictionAPIError(final String message) { + return new LmsSetupTestResult(new Error(ErrorType.QUIZ_RESTRICTION_API_REQUEST, message)); + } + + public final static class Error { + + @JsonProperty(ATTR_ERROR_TYPE) + public final ErrorType errorType; + @JsonProperty(ATTR_ERROR_MESSAGE) + public final String message; + + @JsonCreator + protected Error( + @JsonProperty(ATTR_ERROR_TYPE) final ErrorType errorType, + @JsonProperty(ATTR_ERROR_MESSAGE) final String message) { + + this.errorType = errorType; + this.message = message; + } } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java index c1c02c70..b0d6a6e2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamForm.java @@ -40,6 +40,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus; import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -68,6 +69,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfi import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicatorPage; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SetExamSebRestriction; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.TestLmsSetup; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.GetQuizData; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.quiz.ImportAsExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; @@ -215,6 +217,7 @@ public class ExamForm implements TemplateComposer { final boolean editable = examStatus == ExamStatus.UP_COMING || examStatus == ExamStatus.RUNNING && currentUser.get().hasRole(UserRole.EXAM_ADMIN); + final boolean sebRestrictionAvailable = testSebRestrictionAPI(exam); // The Exam form final FormHandle formHandle = this.pageService.formBuilder( @@ -301,7 +304,9 @@ public class ExamForm implements TemplateComposer { .publishIf(() -> modifyGrant && readonly && editable) .newAction(ActionDefinition.EXAM_SAVE) - .withExec(formHandle::processFormSave) + .withExec(action -> (importFromQuizData) + ? importExam(action, formHandle, sebRestrictionAvailable && exam.status == ExamStatus.RUNNING) + : formHandle.processFormSave(action)) .ignoreMoveAwayFromEdit() .publishIf(() -> !readonly && modifyGrant) @@ -314,12 +319,12 @@ public class ExamForm implements TemplateComposer { .newAction(ActionDefinition.EXAM_ENABLE_SEB_RESTRICTION) .withEntityKey(entityKey) .withExec(action -> setSebRestriction(action, true)) - .publishIf(() -> readonly && BooleanUtils.isFalse(exam.lmsSebRestriction)) + .publishIf(() -> sebRestrictionAvailable && readonly && BooleanUtils.isFalse(exam.lmsSebRestriction)) .newAction(ActionDefinition.EXAM_DISABLE_SEB_RESTRICTION) .withEntityKey(entityKey) .withExec(action -> setSebRestriction(action, false)) - .publishIf(() -> readonly && BooleanUtils.isTrue(exam.lmsSebRestriction)); + .publishIf(() -> sebRestrictionAvailable && readonly && BooleanUtils.isTrue(exam.lmsSebRestriction)); // additional data in read-only view if (readonly && !importFromQuizData) { @@ -473,6 +478,31 @@ public class ExamForm implements TemplateComposer { } } + private PageAction importExam( + final PageAction action, + final FormHandle formHandle, + final boolean applySebRestriction) { + + // process normal save first + final PageAction processFormSave = formHandle.processFormSave(action); + + // when okay and the exam sebRestriction is true + if (applySebRestriction) { + setSebRestriction(processFormSave, true); + } + + return processFormSave; + } + + private boolean testSebRestrictionAPI(final Exam exam) { + return this.restService.getBuilder(TestLmsSetup.class) + .withURIVariable(API.PARAM_MODEL_ID, String.valueOf(exam.lmsSetupId)) + .call() + .onError(t -> log.error("Failed to check SEB restriction API: ", t)) + .map(result -> !result.hasError(LmsSetupTestResult.ErrorType.QUIZ_RESTRICTION_API_REQUEST)) + .getOr(false); + } + private void showConsistencyChecks(final Collection result, final Composite parent) { if (result == null || result.isEmpty()) { return; @@ -506,7 +536,7 @@ public class ExamForm implements TemplateComposer { this.restService.getBuilder(SetExamSebRestriction.class) .withURIVariable( API.PARAM_MODEL_ID, - action.pageContext().getAttribute(AttributeKeys.ENTITY_ID)) + action.getEntityKey().modelId) .withQueryParam( Domain.EXAM.ATTR_LMS_SEB_RESTRICTION, sebRestriction ? Constants.TRUE_STRING : Constants.FALSE_STRING) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/LmsSetupForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/LmsSetupForm.java index 6cfd01b4..6ccb9b4b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/LmsSetupForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/LmsSetupForm.java @@ -11,7 +11,6 @@ package ch.ethz.seb.sebserver.gui.content; import java.util.function.BooleanSupplier; import java.util.function.Function; -import org.apache.commons.lang3.StringUtils; import org.eclipse.swt.widgets.Composite; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,6 +41,7 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; import ch.ethz.seb.sebserver.gui.service.page.impl.PageUtils; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCallError; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution.GetInstitution; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.ActivateLmsSetup; @@ -177,7 +177,7 @@ public class LmsSetupForm implements TemplateComposer { .addField(FormBuilder.singleSelection( Domain.LMS_SETUP.ATTR_LMS_TYPE, FORM_TYPE_TEXT_KEY, - (lmsType != null) ? lmsType.name() : null, + (lmsType != null) ? lmsType.name() : LmsType.MOCKUP.name(), this.resourceService::lmsTypeResources) .readonlyIf(isNotNew)) .addField(FormBuilder.text( @@ -225,17 +225,11 @@ public class LmsSetupForm implements TemplateComposer { .withEntityKey(entityKey) .publishIf(() -> modifyGrant && readonly && institutionActive) - .newAction(ActionDefinition.LMS_SETUP_SAVE_AND_TEST) - .withEntityKey(entityKey) - .withExec(action -> this.testLmsSetup(action, formHandle, true)) - .ignoreMoveAwayFromEdit() - .publishIf(() -> modifyGrant && isNotNew.getAsBoolean() && !readonly) - .newAction(ActionDefinition.LMS_SETUP_TEST_AND_SAVE) .withEntityKey(entityKey) .withExec(action -> this.testAdHoc(action, formHandle)) .ignoreMoveAwayFromEdit() - .publishIf(() -> modifyGrant && isNew.getAsBoolean() && !readonly) + .publishIf(() -> modifyGrant && !readonly) .newAction(ActionDefinition.LMS_SETUP_DEACTIVATE) .withEntityKey(entityKey) @@ -292,8 +286,12 @@ public class LmsSetupForm implements TemplateComposer { // ... and handle the response if (result.hasError()) { if (formHandle.handleError(result.getError())) { - throw new PageMessageException( - new LocTextKey("sebserver.lmssetup.action.test.missingParameter")); + final Throwable error = result.getError(); + if (error instanceof RestCallError) { + throw (RestCallError) error; + } else { + throw new RuntimeException("Cause: ", error); + } } } @@ -355,17 +353,37 @@ public class LmsSetupForm implements TemplateComposer { if (testResult.isOk()) { return onOK.apply(action); - } else if (StringUtils.isNotBlank(testResult.tokenRequestError)) { - throw new PageMessageException( - new LocTextKey("sebserver.lmssetup.action.test.tokenRequestError", - testResult.tokenRequestError)); - } else if (StringUtils.isNotBlank(testResult.quizRequestError)) { - throw new PageMessageException( - new LocTextKey("sebserver.lmssetup.action.test.quizRequestError", testResult.quizRequestError)); - } else { - throw new PageMessageException( - new LocTextKey("sebserver.lmssetup.action.test.unknownError", testResult)); } + + testResult.errors + .stream() + .findFirst() + .ifPresent(error -> { + switch (error.errorType) { + case TOKEN_REQUEST: { + throw new PageMessageException(new LocTextKey( + "sebserver.lmssetup.action.test.tokenRequestError", + error.message)); + } + case QUIZ_ACCESS_API_REQUEST: { + throw new PageMessageException(new LocTextKey( + "sebserver.lmssetup.action.test.quizRequestError", + error.message)); + } + case QUIZ_RESTRICTION_API_REQUEST: { + // NOTE: quiz restriction is not mandatory for functional LmsSetup + // so this error is ignored here + break; + } + default: { + throw new PageMessageException(new LocTextKey( + "sebserver.lmssetup.action.test.unknownError", + error.message)); + } + } + }); + + return onOK.apply(action); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java index 8276294b..c9974eb0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java @@ -232,7 +232,7 @@ public enum ActionDefinition { PageStateDefinitionImpl.EXAM_VIEW, ActionCategory.FORM), EXAM_DISABLE_SEB_RESTRICTION( - new LocTextKey("sebserver.exam.action.deasebrestriction.disable"), + new LocTextKey("sebserver.exam.action.sebrestriction.disable"), ImageIcon.TOGGLE_ON, PageStateDefinitionImpl.EXAM_VIEW, ActionCategory.FORM), diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java index a0b2993c..129f7c6a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageAction.java @@ -113,6 +113,8 @@ public final class PageAction { } return Collections.emptySet(); + } catch (final PageMessageException e) { + throw e; } catch (final Exception e) { log.error("Unexpected error while trying to get current selection: ", e); throw new PageMessageException( diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentials.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentials.java index dd34f003..d25d6f89 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentials.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/client/ClientCredentials.java @@ -36,15 +36,15 @@ public final class ClientCredentials { } public boolean hasClientId() { - return this.clientId != null && this.clientId.length() >= 0; + return this.clientId != null && this.clientId.length() > 0; } public boolean hasSecret() { - return this.secret != null && this.secret.length() >= 0; + return this.secret != null && this.secret.length() > 0; } public boolean hasAccessToken() { - return this.accessToken != null && this.accessToken.length() >= 0; + return this.accessToken != null && this.accessToken.length() > 0; } public String clientIdAsString() { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java index 9d9c9db2..fe85eff9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java @@ -46,11 +46,17 @@ public interface LmsAPIService { * @return LmsAPITemplate for specified LmsSetup configuration */ Result getLmsAPITemplate(String lmsSetupId); + /** use this to the the specified LmsAPITemplate. + * + * @param template the LmsAPITemplate + * @return LmsSetupTestResult containing list of errors if happened */ + LmsSetupTestResult test(LmsAPITemplate template); + /** This can be used to test an LmsSetup connection parameter without saving or heaving * an already persistent version of an LmsSetup. * * @param lmsSetup - * @return */ + * @return LmsSetupTestResult containing list of errors if happened */ LmsSetupTestResult testAdHoc(LmsSetup lmsSetup); /** Get a LmsAPITemplate for specified LmsSetup configuration by primary key diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java index 9ac665a7..98601ec0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java @@ -8,7 +8,6 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; @@ -17,15 +16,12 @@ import java.util.Set; import org.apache.commons.lang3.StringUtils; -import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.EntityType; -import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; @@ -43,12 +39,16 @@ public interface LmsAPITemplate { * @return the underling LMSSetup configuration for this LmsAPITemplate */ LmsSetup lmsSetup(); - /** Performs a test for the underling LmsSetup configuration and checks if the - * LMS and the core API of the LMS can be accessed or if there are some difficulties, - * missing configuration data or connection/authentication errors. - * - * @return LmsSetupTestResult instance with the test result report */ - LmsSetupTestResult testLmsSetup(); +// /** Performs a test for the underling LmsSetup configuration and checks if the +// * LMS and the core API of the LMS can be accessed or if there are some difficulties, +// * missing configuration data or connection/authentication errors. +// * +// * @return LmsSetupTestResult instance with the test result report */ +// LmsSetupTestResult testLmsSetup(); + + LmsSetupTestResult testCourseAccessAPI(); + + LmsSetupTestResult testCourseRestrictionAPI(); /** Get a Result of an unsorted List of filtered QuizData from the LMS course/quiz API * @@ -101,27 +101,4 @@ public interface LmsAPITemplate { * @return Result refer to the given Exam if successful or to an error if not */ Result releaseSebClientRestriction(Exam exam); - default List attributeValidation(final ClientCredentials credentials) { - - final LmsSetup lmsSetup = lmsSetup(); - // validation of LmsSetup - final List missingAttrs = new ArrayList<>(); - if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) { - missingAttrs.add(APIMessage.fieldValidationError( - LMS_SETUP.ATTR_LMS_URL, - "lmsSetup:lmsUrl:notNull")); - } - if (!credentials.hasClientId()) { - missingAttrs.add(APIMessage.fieldValidationError( - LMS_SETUP.ATTR_LMS_CLIENTNAME, - "lmsSetup:lmsClientname:notNull")); - } - if (!credentials.hasSecret()) { - missingAttrs.add(APIMessage.fieldValidationError( - LMS_SETUP.ATTR_LMS_CLIENTSECRET, - "lmsSetup:lmsClientsecret:notNull")); - } - return missingAttrs; - } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java index 5f50ad13..4d9822d1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java @@ -14,17 +14,13 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; -import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; -import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; import ch.ethz.seb.sebserver.gbl.Constants; -import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; @@ -38,6 +34,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx.OpenEdxLmsAPITemplateFactory; @Lazy @Service @@ -46,32 +43,23 @@ public class LmsAPIServiceImpl implements LmsAPIService { private static final Logger log = LoggerFactory.getLogger(LmsAPIServiceImpl.class); - private final AsyncService asyncService; private final LmsSetupDAO lmsSetupDAO; private final ClientCredentialService clientCredentialService; - private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; - private final String[] openEdxAlternativeTokenRequestPaths; private final WebserviceInfo webserviceInfo; + private final OpenEdxLmsAPITemplateFactory openEdxLmsAPITemplateFactory; private final Map cache = new ConcurrentHashMap<>(); public LmsAPIServiceImpl( - final AsyncService asyncService, + final OpenEdxLmsAPITemplateFactory openEdxLmsAPITemplateFactory, final LmsSetupDAO lmsSetupDAO, final ClientCredentialService clientCredentialService, - final ClientHttpRequestFactoryService clientHttpRequestFactoryService, - final WebserviceInfo webserviceInfo, - @Value("${sebserver.webservice.lms.openedx.api.token.request.paths}") final String alternativeTokenRequestPaths) { + final WebserviceInfo webserviceInfo) { - this.asyncService = asyncService; + this.openEdxLmsAPITemplateFactory = openEdxLmsAPITemplateFactory; this.lmsSetupDAO = lmsSetupDAO; this.clientCredentialService = clientCredentialService; - this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.webserviceInfo = webserviceInfo; - - this.openEdxAlternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) - ? StringUtils.split(alternativeTokenRequestPaths, Constants.LIST_SEPARATOR) - : null; } /** Listen to LmsSetupChangeEvent to release an affected LmsAPITemplate from cache @@ -145,6 +133,16 @@ public class LmsAPIServiceImpl implements LmsAPIService { .flatMap(this::getLmsAPITemplate); } + @Override + public LmsSetupTestResult test(final LmsAPITemplate template) { + final LmsSetupTestResult testCourseAccessAPI = template.testCourseAccessAPI(); + if (!testCourseAccessAPI.isOk()) { + return testCourseAccessAPI; + } + + return template.testCourseRestrictionAPI(); + } + @Override public LmsSetupTestResult testAdHoc(final LmsSetup lmsSetup) { final ClientCredentials lmsCredentials = this.clientCredentialService.encryptClientCredentials( @@ -152,7 +150,7 @@ public class LmsAPIServiceImpl implements LmsAPIService { lmsSetup.lmsAuthSecret, lmsSetup.lmsRestApiToken); - return createLmsSetupTemplate(lmsSetup, lmsCredentials).testLmsSetup(); + return test(createLmsSetupTemplate(lmsSetup, lmsCredentials)); } private Result getLmsAPITemplate(final LmsSetup lmsSetup) { @@ -202,14 +200,10 @@ public class LmsAPIServiceImpl implements LmsAPIService { credentials, this.webserviceInfo); case OPEN_EDX: - return new OpenEdxLmsAPITemplate( - this.asyncService, - lmsSetup, - credentials, - this.clientCredentialService, - this.clientHttpRequestFactoryService, - this.openEdxAlternativeTokenRequestPaths, - this.webserviceInfo); + return this.openEdxLmsAPITemplateFactory + .create(lmsSetup, credentials) + .getOrThrow(); + default: throw new UnsupportedOperationException("No support for LMS Type: " + lmsSetup.lmsType); } @@ -251,4 +245,5 @@ public class LmsAPIServiceImpl implements LmsAPIService { return true; } } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockupLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockupLmsAPITemplate.java index 5b0f9f0c..d2863b0e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockupLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/MockupLmsAPITemplate.java @@ -22,6 +22,7 @@ import org.slf4j.LoggerFactory; import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; @@ -94,12 +95,32 @@ final class MockupLmsAPITemplate implements LmsAPITemplate { return this.lmsSetup; } - @Override - public LmsSetupTestResult testLmsSetup() { + private List checkAttributes() { + final List missingAttrs = new ArrayList<>(); + if (StringUtils.isBlank(this.lmsSetup.lmsApiUrl)) { + missingAttrs.add(APIMessage.fieldValidationError( + LMS_SETUP.ATTR_LMS_URL, + "lmsSetup:lmsUrl:notNull")); + } + if (!this.credentials.hasClientId()) { + missingAttrs.add(APIMessage.fieldValidationError( + LMS_SETUP.ATTR_LMS_CLIENTNAME, + "lmsSetup:lmsClientname:notNull")); + } + if (!this.credentials.hasSecret()) { + missingAttrs.add(APIMessage.fieldValidationError( + LMS_SETUP.ATTR_LMS_CLIENTSECRET, + "lmsSetup:lmsClientsecret:notNull")); + } + return missingAttrs; + } + @Override + public LmsSetupTestResult testCourseAccessAPI() { log.info("Test Lms Binding for Mockup and LmsSetup: {}", this.lmsSetup); - final List missingAttrs = attributeValidation(this.credentials); + final List missingAttrs = checkAttributes(); + if (!missingAttrs.isEmpty()) { return LmsSetupTestResult.ofMissingAttributes(missingAttrs); } @@ -111,6 +132,12 @@ final class MockupLmsAPITemplate implements LmsAPITemplate { } } + @Override + public LmsSetupTestResult testCourseRestrictionAPI() { + // TODO Auto-generated method stub + return LmsSetupTestResult.ofQuizRestrictionAPIError("unsupported"); + } + @Override public Result> getQuizzes(final FilterMap filterMap) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/OpenEdxLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/OpenEdxLmsAPITemplate.java deleted file mode 100644 index 9289093b..00000000 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/OpenEdxLmsAPITemplate.java +++ /dev/null @@ -1,477 +0,0 @@ -/* - * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; - -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.http.client.ClientHttpRequest; -import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.oauth2.client.OAuth2ClientContext; -import org.springframework.security.oauth2.client.OAuth2RequestAuthenticator; -import org.springframework.security.oauth2.client.OAuth2RestTemplate; -import org.springframework.security.oauth2.client.http.AccessTokenRequiredException; -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.AccessTokenRequest; -import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider; -import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; -import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; -import ch.ethz.seb.sebserver.gbl.Constants; -import ch.ethz.seb.sebserver.gbl.api.APIMessage; -import ch.ethz.seb.sebserver.gbl.api.ProxyData; -import ch.ethz.seb.sebserver.gbl.api.ProxyData.ProxyAuthType; -import ch.ethz.seb.sebserver.gbl.async.AsyncService; -import ch.ethz.seb.sebserver.gbl.async.MemoizingCircuitBreaker; -import ch.ethz.seb.sebserver.gbl.model.exam.Exam; -import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; -import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; -import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.webservice.WebserviceInfo; -import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService; -import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SebRestrictionData; - -/** Implements the LmsAPITemplate for Open edX LMS Course API access. - * - * See also: https://course-catalog-api-guide.readthedocs.io */ -final class OpenEdxLmsAPITemplate implements LmsAPITemplate { - - private static final Logger log = LoggerFactory.getLogger(OpenEdxLmsAPITemplate.class); - - private static final String OPEN_EDX_DEFAULT_TOKEN_REQUEST_PATH = "/oauth2/access_token"; - private static final String OPEN_EDX_DEFAULT_COURSE_ENDPOINT = "/api/courses/v1/courses/"; - private static final String OPEN_EDX_DEFAULT_COURSE_START_URL_PREFIX = "/courses/"; - private static final String OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH = - "/seb-openedx/api/v1/course/%s/configuration/"; - - private final LmsSetup lmsSetup; - private final ClientCredentials credentials; - private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; - private final ClientCredentialService clientCredentialService; - private final Set knownTokenAccessPaths; - private final WebserviceInfo webserviceInfo; - - private OAuth2RestTemplate restTemplate = null; - private final MemoizingCircuitBreaker> allQuizzesSupplier; - - OpenEdxLmsAPITemplate( - final AsyncService asyncService, - final LmsSetup lmsSetup, - final ClientCredentials credentials, - final ClientCredentialService clientCredentialService, - final ClientHttpRequestFactoryService clientHttpRequestFactoryService, - final String[] alternativeTokenRequestPaths, - final WebserviceInfo webserviceInfo) { - - this.lmsSetup = lmsSetup; - this.clientCredentialService = clientCredentialService; - this.credentials = credentials; - this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; - this.webserviceInfo = webserviceInfo; - this.knownTokenAccessPaths = new HashSet<>(); - this.knownTokenAccessPaths.add(OPEN_EDX_DEFAULT_TOKEN_REQUEST_PATH); - if (alternativeTokenRequestPaths != null) { - this.knownTokenAccessPaths.addAll(Arrays.asList(alternativeTokenRequestPaths)); - } - - this.allQuizzesSupplier = asyncService.createMemoizingCircuitBreaker( - allQuizzesSupplier(), - 3, - Constants.MINUTE_IN_MILLIS, - Constants.MINUTE_IN_MILLIS, - true, - Constants.HOUR_IN_MILLIS); - } - - @Override - public LmsSetup lmsSetup() { - return this.lmsSetup; - } - - @Override - public LmsSetupTestResult testLmsSetup() { - - log.info("Test Lms Binding for OpenEdX and LmsSetup: {}", this.lmsSetup); - - final List missingAttrs = attributeValidation(this.credentials); - if (!missingAttrs.isEmpty()) { - return LmsSetupTestResult.ofMissingAttributes(missingAttrs); - } - - // request OAuth2 access token on OpenEdx API - initRestTemplateAndRequestAccessToken(); - if (this.restTemplate == null) { - return LmsSetupTestResult.ofTokenRequestError( - "Failed to gain access token from OpenEdX Rest API:\n tried token endpoints: " + - this.knownTokenAccessPaths); - } - - try { - this.getEdxPage(this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT); - } catch (final RuntimeException e) { - if (this.restTemplate != null) { - this.restTemplate.setAuthenticator(new EdxOAuth2RequestAuthenticator()); - } - try { - this.getEdxPage(this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT); - } catch (final RuntimeException ee) { - return LmsSetupTestResult.ofQuizRequestError(ee.getMessage()); - } - } - - return LmsSetupTestResult.ofOkay(); - } - - @Override - public Result> getQuizzes(final FilterMap filterMap) { - return this.allQuizzesSupplier.get() - .map(LmsAPIService.quizzesFilterFunction(filterMap)); - } - - @Override - public Collection> getQuizzes(final Set ids) { - // TODO this can be improved in the future - return getQuizzes(new FilterMap()) - .getOrElse(() -> Collections.emptyList()) - .stream() - .filter(quiz -> ids.contains(quiz.id)) - .map(quiz -> Result.of(quiz)) - .collect(Collectors.toList()); - } - - @Override - public Result applySebClientRestriction(final SebRestrictionData sebRestrictionData) { - return Result.tryCatch(() -> { - - if (log.isDebugEnabled()) { - log.debug("Apply SEB Client restriction: {}", sebRestrictionData); - } - - // TODO - - return sebRestrictionData; - }); - } - - @Override - public Result updateSebClientRestriction(final SebRestrictionData sebRestrictionData) { - return Result.tryCatch(() -> { - - if (log.isDebugEnabled()) { - log.debug("Update SEB Client restriction: {}", sebRestrictionData); - } - - // TODO - - return sebRestrictionData; - }); - } - - @Override - public Result releaseSebClientRestriction(final Exam exam) { - return Result.tryCatch(() -> { - - if (log.isDebugEnabled()) { - log.debug("Release SEB Client restriction for Exam: {}", exam); - } - - // TODO - - return exam; - }); - } - - private Result initRestTemplateAndRequestAccessToken() { - - return Result.tryCatch(() -> { - if (this.restTemplate != null) { - try { - this.restTemplate.getAccessToken(); - return this.lmsSetup; - } catch (final Exception e) { - log.warn( - "Error while trying to get access token within already existing OAuth2RestTemplate instance. Try to create new one.", - e); - this.restTemplate = null; - } - } - - log.info("Initialize Rest Template for OpenEdX API access. LmsSetup: {}", this.lmsSetup); - - final Iterator tokenAccessPaths = this.knownTokenAccessPaths.iterator(); - while (tokenAccessPaths.hasNext()) { - final String accessTokenRequestPath = tokenAccessPaths.next(); - try { - - final OAuth2RestTemplate template = createRestTemplate( - this.lmsSetup, - this.credentials, - accessTokenRequestPath); - - final OAuth2AccessToken accessToken = template.getAccessToken(); - if (accessToken != null) { - this.restTemplate = template; - return this.lmsSetup; - } - } catch (final Exception e) { - log.info("Failed to request access token on access token request path: {}", accessTokenRequestPath, - e); - } - } - - throw new IllegalArgumentException( - "Unable to establish OpenEdX API connection for lmsSetup: " + this.lmsSetup); - }); - } - - private OAuth2RestTemplate createRestTemplate( - final LmsSetup lmsSetup, - final ClientCredentials credentials, - final String accessTokenRequestPath) throws URISyntaxException { - - final CharSequence plainClientId = credentials.clientId; - final CharSequence plainClientSecret = this.clientCredentialService.getPlainClientSecret(credentials); - - final ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails(); - details.setAccessTokenUri(lmsSetup.lmsApiUrl + accessTokenRequestPath); - details.setClientId(plainClientId.toString()); - details.setClientSecret(plainClientSecret.toString()); - - ClientHttpRequestFactory clientHttpRequestFactory = null; - if (lmsSetup.proxyAuthType != ProxyAuthType.NONE) { - final ClientCredentials proxyCredentials = new ClientCredentials( - lmsSetup.proxyAuthUsername, - lmsSetup.proxyAuthSecret); - - // TODO check where we have to encrypt/decrypt the secret internally - CharSequence proxySecretPlain; - try { - proxySecretPlain = this.clientCredentialService.getPlainClientSecret(proxyCredentials); - } catch (final Exception e) { - proxySecretPlain = lmsSetup.proxyAuthSecret; - } - - final URI uri = new URI(lmsSetup.lmsApiUrl); - final ProxyData proxyData = new ProxyData( - lmsSetup.proxyAuthType, - uri.getHost(), - uri.getPort(), - proxyCredentials.clientId, - proxySecretPlain); - - clientHttpRequestFactory = this.clientHttpRequestFactoryService - .getClientHttpRequestFactory(proxyData) - .getOrThrow(); - } else { - clientHttpRequestFactory = this.clientHttpRequestFactoryService - .getClientHttpRequestFactory() - .getOrThrow(); - } - - final OAuth2RestTemplate template = new OAuth2RestTemplate(details); - template.setRequestFactory(clientHttpRequestFactory); - template.setAccessTokenProvider(new EdxClientCredentialsAccessTokenProvider()); - - return template; - } - - private Supplier> allQuizzesSupplier() { - return () -> { - return initRestTemplateAndRequestAccessToken() - .map(this::collectAllQuizzes) - .getOrThrow(); - }; - } - - private ArrayList collectAllQuizzes(final LmsSetup lmsSetup) { - final String externalStartURI = getExternalLMSServerAddress(lmsSetup); - return collectAllCourses(lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT) - .stream() - .reduce( - new ArrayList(), - (list, courseData) -> { - list.add(quizDataOf(lmsSetup, courseData, externalStartURI)); - return list; - }, - (list1, list2) -> { - list1.addAll(list2); - return list1; - }); - } - - private String getExternalLMSServerAddress(final LmsSetup lmsSetup) { - final String externalAddressAlias = this.webserviceInfo.getExternalAddressAlias(lmsSetup.lmsApiUrl); - String _externalStartURI = lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_START_URL_PREFIX; - if (StringUtils.isNoneBlank(externalAddressAlias)) { - try { - final URL url = new URL(lmsSetup.lmsApiUrl); - final int port = url.getPort(); - _externalStartURI = url.getProtocol() + - Constants.URL_ADDRESS_SEPARATOR + externalAddressAlias + - ((port >= 0) - ? Constants.URL_PORT_SEPARATOR + port - : StringUtils.EMPTY) - + OPEN_EDX_DEFAULT_COURSE_START_URL_PREFIX; - - if (log.isDebugEnabled()) { - log.debug("Use external address for course access: {}", _externalStartURI); - } - } catch (final Exception e) { - log.error("Failed to create external address from alias: ", e); - } - } - return _externalStartURI; - } - - private List collectAllCourses(final String pageURI) { - final List collector = new ArrayList<>(); - EdXPage page = getEdxPage(pageURI).getBody(); - if (page != null) { - collector.addAll(page.results); - while (page != null && StringUtils.isNotBlank(page.next)) { - page = getEdxPage(page.next).getBody(); - if (page != null) { - collector.addAll(page.results); - } - } - } - - return collector; - } - - private ResponseEntity getEdxPage(final String pageURI) { - final HttpHeaders httpHeaders = new HttpHeaders(); - return this.restTemplate.exchange( - pageURI, - HttpMethod.GET, - new HttpEntity<>(httpHeaders), - EdXPage.class); - } - - private static QuizData quizDataOf( - final LmsSetup lmsSetup, - final CourseData courseData, - final String uriPrefix) { - - final String startURI = uriPrefix + courseData.id; - final Map additionalAttrs = new HashMap<>(); - additionalAttrs.put("blocks_url", courseData.blocks_url); - return new QuizData( - courseData.id, - lmsSetup.getInstitutionId(), - lmsSetup.id, - lmsSetup.getLmsType(), - courseData.name, - courseData.short_description, - courseData.start, - courseData.end, - startURI); - } - - /** Maps a OpenEdX course API course page */ - static final class EdXPage { - public Integer count; - public String previous; - public Integer num_pages; - public String next; - public List results; - } - - /** Maps the OpenEdX course API course data */ - static final class CourseData { - public String id; - public String name; - public String short_description; - public String blocks_url; - public String start; - public String end; - } - - /** A custom ClientCredentialsAccessTokenProvider that adapts the access token request to Open edX - * access token request protocol using a form-URL-encoded POST request according to: - * https://course-catalog-api-guide.readthedocs.io/en/latest/authentication/index.html#getting-an-access-token */ - private class EdxClientCredentialsAccessTokenProvider extends ClientCredentialsAccessTokenProvider { - - @Override - public OAuth2AccessToken obtainAccessToken( - final OAuth2ProtectedResourceDetails details, - final AccessTokenRequest request) - throws UserRedirectRequiredException, - AccessDeniedException, - OAuth2AccessDeniedException { - - if (details instanceof ClientCredentialsResourceDetails) { - final ClientCredentialsResourceDetails resource = (ClientCredentialsResourceDetails) details; - final HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); - - final MultiValueMap params = new LinkedMultiValueMap<>(); - params.add("grant_type", "client_credentials"); - params.add("client_id", resource.getClientId()); - params.add("client_secret", resource.getClientSecret()); - - final OAuth2AccessToken retrieveToken = retrieveToken(request, resource, params, headers); - return retrieveToken; - } else { - return super.obtainAccessToken(details, request); - } - } - } - - private class EdxOAuth2RequestAuthenticator implements OAuth2RequestAuthenticator { - - @Override - public void authenticate( - final OAuth2ProtectedResourceDetails resource, - final OAuth2ClientContext clientContext, - final ClientHttpRequest request) { - - final OAuth2AccessToken accessToken = clientContext.getAccessToken(); - if (accessToken == null) { - throw new AccessTokenRequiredException(resource); - } - - request.getHeaders().set("Authorization", String.format("%s %s", "Bearer", accessToken.getValue())); - } - - } - -} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java new file mode 100644 index 00000000..902ce17e --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseAccess.java @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx; + +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.OAuth2RequestAuthenticator; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.client.http.AccessTokenRequiredException; +import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; +import org.springframework.security.oauth2.common.OAuth2AccessToken; + +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.async.AsyncService; +import ch.ethz.seb.sebserver.gbl.async.MemoizingCircuitBreaker; +import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.WebserviceInfo; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; + +/** Implements the LmsAPITemplate for Open edX LMS Course API access. + * + * See also: https://course-catalog-api-guide.readthedocs.io */ +final class OpenEdxCourseAccess { + + private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseAccess.class); + + private static final String OPEN_EDX_DEFAULT_COURSE_ENDPOINT = "/api/courses/v1/courses/"; + private static final String OPEN_EDX_DEFAULT_COURSE_START_URL_PREFIX = "/courses/"; + + private final LmsSetup lmsSetup; + private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; + private final WebserviceInfo webserviceInfo; + private final MemoizingCircuitBreaker> allQuizzesSupplier; + + private OAuth2RestTemplate restTemplate; + + public OpenEdxCourseAccess( + final LmsSetup lmsSetup, + final OpenEdxRestTemplateFactory openEdxRestTemplateFactory, + final WebserviceInfo webserviceInfo, + final AsyncService asyncService) { + + this.lmsSetup = lmsSetup; + this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; + this.webserviceInfo = webserviceInfo; + this.allQuizzesSupplier = asyncService.createMemoizingCircuitBreaker( + allQuizzesSupplier(), + 3, + Constants.MINUTE_IN_MILLIS, + Constants.MINUTE_IN_MILLIS, + true, + Constants.HOUR_IN_MILLIS); + } + + LmsSetupTestResult initAPIAccess() { + + final LmsSetupTestResult attributesCheck = this.openEdxRestTemplateFactory.test(); + if (!attributesCheck.isOk()) { + return attributesCheck; + } + + final Result restTemplateRequest = getRestTemplate(); + if (restTemplateRequest.hasError()) { + final String message = "Failed to gain access token from OpenEdX Rest API:\n tried token endpoints: " + + this.openEdxRestTemplateFactory.knownTokenAccessPaths; + log.error(message, restTemplateRequest.getError()); + return LmsSetupTestResult.ofTokenRequestError(message); + } + + final OAuth2RestTemplate restTemplate = restTemplateRequest.get(); + + try { + this.getEdxPage(this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, restTemplate); + } catch (final RuntimeException e) { + + restTemplate.setAuthenticator(new EdxOAuth2RequestAuthenticator()); + + try { + this.getEdxPage(this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, restTemplate); + } catch (final RuntimeException ee) { + return LmsSetupTestResult.ofQuizAccessAPIError(ee.getMessage()); + } + } + + return LmsSetupTestResult.ofOkay(); + } + + Result> getQuizzes(final FilterMap filterMap) { + return this.allQuizzesSupplier.get() + .map(LmsAPIService.quizzesFilterFunction(filterMap)); + } + + private Supplier> allQuizzesSupplier() { + return () -> { + return getRestTemplate() + .map(this::collectAllQuizzes) + .getOrThrow(); + }; + } + + private ArrayList collectAllQuizzes(final OAuth2RestTemplate restTemplate) { + final String externalStartURI = getExternalLMSServerAddress(this.lmsSetup); + return collectAllCourses( + this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT, + restTemplate) + .stream() + .reduce( + new ArrayList(), + (list, courseData) -> { + list.add(quizDataOf(this.lmsSetup, courseData, externalStartURI)); + return list; + }, + (list1, list2) -> { + list1.addAll(list2); + return list1; + }); + } + + private String getExternalLMSServerAddress(final LmsSetup lmsSetup) { + final String externalAddressAlias = this.webserviceInfo.getExternalAddressAlias(lmsSetup.lmsApiUrl); + String _externalStartURI = lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_START_URL_PREFIX; + if (StringUtils.isNoneBlank(externalAddressAlias)) { + try { + final URL url = new URL(lmsSetup.lmsApiUrl); + final int port = url.getPort(); + _externalStartURI = url.getProtocol() + + Constants.URL_ADDRESS_SEPARATOR + externalAddressAlias + + ((port >= 0) + ? Constants.URL_PORT_SEPARATOR + port + : StringUtils.EMPTY) + + OPEN_EDX_DEFAULT_COURSE_START_URL_PREFIX; + + if (log.isDebugEnabled()) { + log.debug("Use external address for course access: {}", _externalStartURI); + } + } catch (final Exception e) { + log.error("Failed to create external address from alias: ", e); + } + } + return _externalStartURI; + } + + private List collectAllCourses(final String pageURI, final OAuth2RestTemplate restTemplate) { + final List collector = new ArrayList<>(); + EdXPage page = getEdxPage(pageURI, restTemplate).getBody(); + if (page != null) { + collector.addAll(page.results); + while (page != null && StringUtils.isNotBlank(page.next)) { + page = getEdxPage(page.next, restTemplate).getBody(); + if (page != null) { + collector.addAll(page.results); + } + } + } + + return collector; + } + + private ResponseEntity getEdxPage(final String pageURI, final OAuth2RestTemplate restTemplate) { + final HttpHeaders httpHeaders = new HttpHeaders(); + return restTemplate.exchange( + pageURI, + HttpMethod.GET, + new HttpEntity<>(httpHeaders), + EdXPage.class); + } + + private static QuizData quizDataOf( + final LmsSetup lmsSetup, + final CourseData courseData, + final String uriPrefix) { + + final String startURI = uriPrefix + courseData.id; + final Map additionalAttrs = new HashMap<>(); + additionalAttrs.put("blocks_url", courseData.blocks_url); + return new QuizData( + courseData.id, + lmsSetup.getInstitutionId(), + lmsSetup.id, + lmsSetup.getLmsType(), + courseData.name, + courseData.short_description, + courseData.start, + courseData.end, + startURI); + } + + /** Maps a OpenEdX course API course page */ + static final class EdXPage { + public Integer count; + public String previous; + public Integer num_pages; + public String next; + public List results; + } + + /** Maps the OpenEdX course API course data */ + static final class CourseData { + public String id; + public String name; + public String short_description; + public String blocks_url; + public String start; + public String end; + } + + private class EdxOAuth2RequestAuthenticator implements OAuth2RequestAuthenticator { + + @Override + public void authenticate( + final OAuth2ProtectedResourceDetails resource, + final OAuth2ClientContext clientContext, + final ClientHttpRequest request) { + + final OAuth2AccessToken accessToken = clientContext.getAccessToken(); + if (accessToken == null) { + throw new AccessTokenRequiredException(resource); + } + + request.getHeaders().set("Authorization", String.format("%s %s", "Bearer", accessToken.getValue())); + } + + } + + private Result getRestTemplate() { + if (this.restTemplate == null) { + final Result templateRequest = this.openEdxRestTemplateFactory + .createOAuthRestTemplate(); + if (templateRequest.hasError()) { + return templateRequest; + } else { + this.restTemplate = templateRequest.get(); + } + } + + return Result.of(this.restTemplate); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java new file mode 100644 index 00000000..b713ec09 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestriction.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +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.OAuth2RestTemplate; +import org.springframework.web.client.HttpClientErrorException; + +import ch.ethz.seb.sebserver.gbl.api.JSONMapper; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; +import ch.ethz.seb.sebserver.gbl.util.Result; + +public class OpenEdxCourseRestriction { + + private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseRestriction.class); + + private static final String OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO = "/seb-openedx/seb-info"; + private static final String OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH = + "/seb-openedx/api/v1/course/%s/configuration/"; + + private final LmsSetup lmsSetup; + private final JSONMapper jsonMapper; + private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory; + + private OAuth2RestTemplate restTemplate; + + protected OpenEdxCourseRestriction( + final LmsSetup lmsSetup, + final JSONMapper jsonMapper, + final OpenEdxRestTemplateFactory openEdxRestTemplateFactory) { + + this.lmsSetup = lmsSetup; + this.jsonMapper = jsonMapper; + this.openEdxRestTemplateFactory = openEdxRestTemplateFactory; + } + + LmsSetupTestResult initAPIAccess() { + + final LmsSetupTestResult attributesCheck = this.openEdxRestTemplateFactory.test(); + if (!attributesCheck.isOk()) { + return attributesCheck; + } + + final Result restTemplateRequest = getRestTemplate(); + if (restTemplateRequest.hasError()) { + return LmsSetupTestResult.ofTokenRequestError( + "Failed to gain access token from OpenEdX Rest API:\n tried token endpoints: " + + this.openEdxRestTemplateFactory.knownTokenAccessPaths); + } + + final OAuth2RestTemplate restTemplate = restTemplateRequest.get(); + + // NOTE: since the OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO endpoint is + // not accessible within OAuth2 authentication (just with user - authentication), + // we can only check if the endpoint is available for now. This is checked + // if there is no 404 response. + // TODO: Ask eduNEXT to implement also OAuth2 API access for this endpoint to be able + // to check the version of the installed plugin. + final String url = this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_INFO; + try { + + restTemplate.exchange( + url, + HttpMethod.GET, + new HttpEntity<>(new HttpHeaders()), + Object.class); + + } catch (final HttpClientErrorException e) { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) { + return LmsSetupTestResult.ofQuizRestrictionAPIError( + "Failed to verify course restriction API: " + e.getMessage()); + } + + if (log.isDebugEnabled()) { + log.debug("Sucessfully checked SEB Open edX integration Plugin"); + } + } + + return LmsSetupTestResult.ofOkay(); + } + + Result pushSebRestriction( + final String courseId, + final OpenEdxCourseRestrictionData restriction) { + + if (log.isDebugEnabled()) { + log.debug("PUT SEB Client restriction on course: {} : {}", courseId, restriction); + } + + return getRestTemplate() + .map(restTemplate -> { + + final String url = this.lmsSetup.lmsApiUrl + getSebRestrictionUrl(courseId); + final HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + + try { + + final String json = this.jsonMapper.writeValueAsString(restriction); + final OpenEdxCourseRestrictionData confirm = restTemplate.exchange( + url, + HttpMethod.PUT, + new HttpEntity<>(json, httpHeaders), + OpenEdxCourseRestrictionData.class) + .getBody(); + + if (log.isDebugEnabled()) { + log.debug("Successfully PUT SEB Client restriction on course: {} : {}", courseId, confirm); + } + + return confirm; + + } catch (final Exception e) { + throw new RuntimeException("Unexpected: ", e); + } + }); + } + + Result deleteSebRestriction(final String courseId) { + + if (log.isDebugEnabled()) { + log.debug("DELETE SEB Client restriction on course: {}", courseId); + } + + return getRestTemplate() + .map(restTemplate -> { + + final String url = this.lmsSetup.lmsApiUrl + getSebRestrictionUrl(courseId); + final ResponseEntity exchange = restTemplate.exchange( + url, + HttpMethod.DELETE, + new HttpEntity<>(new HttpHeaders()), + Object.class); + + if (exchange.getStatusCode() == HttpStatus.NO_CONTENT) { + if (log.isDebugEnabled()) { + log.debug("Successfully PUT SEB Client restriction on course: {}", courseId); + } + + return true; + } else { + throw new RuntimeException("Unexpected response for deletion: " + exchange); + } + + }); + } + + private String getSebRestrictionUrl(final String courseId) { + return String.format(OPEN_EDX_DEFAULT_COURSE_RESTRICTION_API_PATH, courseId); + } + + private Result getRestTemplate() { + if (this.restTemplate == null) { + final Result templateRequest = this.openEdxRestTemplateFactory + .createOAuthRestTemplate(); + if (templateRequest.hasError()) { + return templateRequest; + } else { + this.restTemplate = templateRequest.get(); + } + } + + return Result.of(this.restTemplate); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/OpenEdxSebClientRestriction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestrictionData.java similarity index 68% rename from src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/OpenEdxSebClientRestriction.java rename to src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestrictionData.java index 2213b7a8..5cc5a0bb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/OpenEdxSebClientRestriction.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestrictionData.java @@ -6,8 +6,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; +package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -20,14 +21,15 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.model.institution.EdxCourseRestrictionAttributes; import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SebRestrictionData; @JsonIgnoreProperties(ignoreUnknown = true) -public class OpenEdxSebClientRestriction { +public class OpenEdxCourseRestrictionData { private static final String ATTR_USER_BANNING_ENABLED = "USER_BANNING_ENABLED"; - private static final String ATTR_SEB_PERMISSION_COMPONENTS = "SEB_PERMISSION_COMPONENTS"; + private static final String ATTR_PERMISSION_COMPONENTS = "PERMISSION_COMPONENTS"; private static final String ATTR_BLACKLIST_CHAPTERS = "BLACKLIST_CHAPTERS"; private static final String ATTR_WHITELIST_PATHS = "WHITELIST_PATHS"; private static final String ATTR_BROWSER_KEYS = "BROWSER_KEYS"; @@ -45,19 +47,19 @@ public class OpenEdxSebClientRestriction { @JsonProperty(ATTR_BLACKLIST_CHAPTERS) public final Collection blacklistChapters; - @JsonProperty(ATTR_SEB_PERMISSION_COMPONENTS) + @JsonProperty(ATTR_PERMISSION_COMPONENTS) public final Collection permissionComponents; @JsonProperty(ATTR_USER_BANNING_ENABLED) public final boolean banningEnabled; @JsonCreator - public OpenEdxSebClientRestriction( + public OpenEdxCourseRestrictionData( @JsonProperty(ATTR_CONFIG_KEYS) final Collection configKeys, @JsonProperty(ATTR_BROWSER_KEYS) final Collection browserExamKeys, @JsonProperty(ATTR_WHITELIST_PATHS) final Collection whiteListPaths, @JsonProperty(ATTR_BLACKLIST_CHAPTERS) final Collection blacklistChapters, - @JsonProperty(ATTR_SEB_PERMISSION_COMPONENTS) final Collection permissionComponents, + @JsonProperty(ATTR_PERMISSION_COMPONENTS) final Collection permissionComponents, @JsonProperty(ATTR_USER_BANNING_ENABLED) final boolean banningEnabled) { this.configKeys = Utils.immutableCollectionOf(configKeys); @@ -68,7 +70,7 @@ public class OpenEdxSebClientRestriction { this.banningEnabled = banningEnabled; } - public OpenEdxSebClientRestriction(final SebRestrictionData data) { + public OpenEdxCourseRestrictionData(final SebRestrictionData data) { this.configKeys = Utils.immutableCollectionOf(data.configKeys); this.browserExamKeys = Utils.immutableCollectionOf(data.browserExamKeys); @@ -88,12 +90,21 @@ public class OpenEdxSebClientRestriction { this.blacklistChapters = Collections.emptyList(); } - final String permissionComponents = data.additionalAttributes.get(ATTR_SEB_PERMISSION_COMPONENTS); + final String permissionComponents = data.additionalAttributes.get(ATTR_PERMISSION_COMPONENTS); if (StringUtils.isNotBlank(permissionComponents)) { this.permissionComponents = Utils.immutableCollectionOf(Arrays.asList( StringUtils.split(permissionComponents, Constants.LIST_SEPARATOR))); } else { - this.permissionComponents = Collections.emptyList(); + final Collection defaultPermissions = new ArrayList<>(); + defaultPermissions.add(EdxCourseRestrictionAttributes.PermissionComponents.ALWAYS_ALLOW_STUFF.name); + if (!this.configKeys.isEmpty()) { + defaultPermissions.add(EdxCourseRestrictionAttributes.PermissionComponents.CHECK_CONFIG_KEY.name); + } + if (!this.browserExamKeys.isEmpty()) { + defaultPermissions.add(EdxCourseRestrictionAttributes.PermissionComponents.CHECK_BROWSER_EXAM_KEY.name); + } + + this.permissionComponents = Utils.immutableCollectionOf(defaultPermissions); } this.banningEnabled = BooleanUtils.toBoolean(data.additionalAttributes.get(ATTR_USER_BANNING_ENABLED)); @@ -122,4 +133,24 @@ public class OpenEdxSebClientRestriction { public boolean isBanningEnabled() { return this.banningEnabled; } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("OpenEdxCourseRestrictionData [configKeys="); + builder.append(this.configKeys); + builder.append(", browserExamKeys="); + builder.append(this.browserExamKeys); + builder.append(", whiteListPaths="); + builder.append(this.whiteListPaths); + builder.append(", blacklistChapters="); + builder.append(this.blacklistChapters); + builder.append(", permissionComponents="); + builder.append(this.permissionComponents); + builder.append(", banningEnabled="); + builder.append(this.banningEnabled); + builder.append("]"); + return builder.toString(); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplate.java new file mode 100644 index 00000000..955bc92b --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplate.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SebRestrictionData; + +final class OpenEdxLmsAPITemplate implements LmsAPITemplate { + + private static final Logger log = LoggerFactory.getLogger(OpenEdxLmsAPITemplate.class); + + private final LmsSetup lmsSetup; + private final OpenEdxCourseAccess openEdxCourseAccess; + private final OpenEdxCourseRestriction openEdxCourseRestriction; + + OpenEdxLmsAPITemplate( + final LmsSetup lmsSetup, + final OpenEdxCourseAccess openEdxCourseAccess, + final OpenEdxCourseRestriction openEdxCourseRestriction) { + + this.lmsSetup = lmsSetup; + this.openEdxCourseAccess = openEdxCourseAccess; + this.openEdxCourseRestriction = openEdxCourseRestriction; + } + + @Override + public LmsSetup lmsSetup() { + return this.lmsSetup; + } + + @Override + public LmsSetupTestResult testCourseAccessAPI() { + return this.openEdxCourseAccess.initAPIAccess(); + } + + @Override + public LmsSetupTestResult testCourseRestrictionAPI() { + return this.openEdxCourseRestriction.initAPIAccess(); + } + + @Override + public Result> getQuizzes(final FilterMap filterMap) { + return this.openEdxCourseAccess.getQuizzes(filterMap); + } + + @Override + public Collection> getQuizzes(final Set ids) { + // TODO this can be improved in the future + return getQuizzes(new FilterMap()) + .getOrElse(() -> Collections.emptyList()) + .stream() + .filter(quiz -> ids.contains(quiz.id)) + .map(quiz -> Result.of(quiz)) + .collect(Collectors.toList()); + } + + @Override + public Result applySebClientRestriction(final SebRestrictionData sebRestrictionData) { + + if (log.isDebugEnabled()) { + log.debug("Apply SEB Client restriction: {}", sebRestrictionData); + } + + return Result.tryCatch(() -> { + + this.openEdxCourseRestriction.pushSebRestriction( + sebRestrictionData.exam.externalId, + new OpenEdxCourseRestrictionData(sebRestrictionData)) + .getOrThrow(); + + return sebRestrictionData; + }); + } + + @Override + public Result updateSebClientRestriction(final SebRestrictionData sebRestrictionData) { + if (log.isDebugEnabled()) { + log.debug("Apply SEB Client restriction: {}", sebRestrictionData); + } + + return this.openEdxCourseRestriction.pushSebRestriction( + sebRestrictionData.exam.externalId, + new OpenEdxCourseRestrictionData(sebRestrictionData)) + .map(result -> sebRestrictionData); + } + + @Override + public Result releaseSebClientRestriction(final Exam exam) { + + if (log.isDebugEnabled()) { + log.debug("Release SEB Client restriction for Exam: {}", exam); + } + + return this.openEdxCourseRestriction.deleteSebRestriction(exam.externalId) + .map(result -> exam); + + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java new file mode 100644 index 00000000..d985d055 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.JSONMapper; +import ch.ethz.seb.sebserver.gbl.async.AsyncService; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.WebserviceInfo; +import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService; +import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials; + +@Lazy +@Service +@WebServiceProfile +public class OpenEdxLmsAPITemplateFactory { + + private final JSONMapper jsonMapper; + private final WebserviceInfo webserviceInfo; + private final AsyncService asyncService; + private final ClientCredentialService clientCredentialService; + private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; + private final String[] alternativeTokenRequestPaths; + + protected OpenEdxLmsAPITemplateFactory( + final JSONMapper jsonMapper, + final WebserviceInfo webserviceInfo, + final AsyncService asyncService, + final ClientCredentialService clientCredentialService, + final ClientHttpRequestFactoryService clientHttpRequestFactoryService, + @Value("${sebserver.webservice.lms.openedx.api.token.request.paths}") final String alternativeTokenRequestPaths) { + + this.jsonMapper = jsonMapper; + this.webserviceInfo = webserviceInfo; + this.asyncService = asyncService; + this.clientCredentialService = clientCredentialService; + this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; + this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null) + ? StringUtils.split(alternativeTokenRequestPaths, Constants.LIST_SEPARATOR) + : null; + } + + public Result create(final LmsSetup lmsSetup, final ClientCredentials credentials) { + return Result.tryCatch(() -> { + final OpenEdxRestTemplateFactory openEdxRestTemplateFactory = new OpenEdxRestTemplateFactory( + lmsSetup, + credentials, + this.clientCredentialService, + this.clientHttpRequestFactoryService, + this.alternativeTokenRequestPaths); + final OpenEdxCourseAccess openEdxCourseAccess = new OpenEdxCourseAccess( + lmsSetup, + openEdxRestTemplateFactory, + this.webserviceInfo, + this.asyncService); + final OpenEdxCourseRestriction openEdxCourseRestriction = new OpenEdxCourseRestriction( + lmsSetup, + this.jsonMapper, + openEdxRestTemplateFactory); + + return new OpenEdxLmsAPITemplate( + lmsSetup, + openEdxCourseAccess, + openEdxCourseRestriction); + }); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxRestTemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxRestTemplateFactory.java new file mode 100644 index 00000000..8a30ddf1 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxRestTemplateFactory.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +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.AccessTokenRequest; +import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider; +import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; +import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gbl.api.ProxyData; +import ch.ethz.seb.sebserver.gbl.api.ProxyData.ProxyAuthType; +import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService; +import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials; + +final class OpenEdxRestTemplateFactory { + + private static final String OPEN_EDX_DEFAULT_TOKEN_REQUEST_PATH = "/oauth2/access_token"; + + final LmsSetup lmsSetup; + final ClientCredentials credentials; + final ClientHttpRequestFactoryService clientHttpRequestFactoryService; + final ClientCredentialService clientCredentialService; + final Set knownTokenAccessPaths; + + OpenEdxRestTemplateFactory( + final LmsSetup lmsSetup, + final ClientCredentials credentials, + final ClientCredentialService clientCredentialService, + final ClientHttpRequestFactoryService clientHttpRequestFactoryService, + final String[] alternativeTokenRequestPaths) { + + this.lmsSetup = lmsSetup; + this.clientCredentialService = clientCredentialService; + this.credentials = credentials; + this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; + + this.knownTokenAccessPaths = new HashSet<>(); + this.knownTokenAccessPaths.add(OPEN_EDX_DEFAULT_TOKEN_REQUEST_PATH); + if (alternativeTokenRequestPaths != null) { + this.knownTokenAccessPaths.addAll(Arrays.asList(alternativeTokenRequestPaths)); + } + } + + public LmsSetupTestResult test() { + final List missingAttrs = new ArrayList<>(); + if (StringUtils.isBlank(this.lmsSetup.lmsApiUrl)) { + missingAttrs.add(APIMessage.fieldValidationError( + LMS_SETUP.ATTR_LMS_URL, + "lmsSetup:lmsUrl:notNull")); + } + if (!this.credentials.hasClientId()) { + missingAttrs.add(APIMessage.fieldValidationError( + LMS_SETUP.ATTR_LMS_CLIENTNAME, + "lmsSetup:lmsClientname:notNull")); + } + if (!this.credentials.hasSecret()) { + missingAttrs.add(APIMessage.fieldValidationError( + LMS_SETUP.ATTR_LMS_CLIENTSECRET, + "lmsSetup:lmsClientsecret:notNull")); + } + + if (!missingAttrs.isEmpty()) { + return LmsSetupTestResult.ofMissingAttributes(missingAttrs); + } + + return LmsSetupTestResult.ofOkay(); + } + + Result createOAuthRestTemplate() { + return this.knownTokenAccessPaths + .stream() + .map(this::createOAuthRestTemplate) + .filter(Result::hasValue) + .findFirst() + .orElse(Result.ofRuntimeError( + "Failed to find gain any access on paths: " + this.knownTokenAccessPaths)); + } + + Result createOAuthRestTemplate(final String accessTokenPath) { + return Result.tryCatch(() -> { + final OAuth2RestTemplate template = createRestTemplate( + this.lmsSetup, + this.credentials, + accessTokenPath); + + final OAuth2AccessToken accessToken = template.getAccessToken(); + if (accessToken == null) { + throw new RuntimeException("Failed to gain access token on path: " + accessTokenPath); + } + + return template; + }); + } + + private OAuth2RestTemplate createRestTemplate( + final LmsSetup lmsSetup, + final ClientCredentials credentials, + final String accessTokenRequestPath) throws URISyntaxException { + + final CharSequence plainClientId = credentials.clientId; + final CharSequence plainClientSecret = this.clientCredentialService.getPlainClientSecret(credentials); + + final ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails(); + details.setAccessTokenUri(lmsSetup.lmsApiUrl + accessTokenRequestPath); + details.setClientId(plainClientId.toString()); + details.setClientSecret(plainClientSecret.toString()); + + ClientHttpRequestFactory clientHttpRequestFactory = null; + if (lmsSetup.proxyAuthType != ProxyAuthType.NONE) { + final ClientCredentials proxyCredentials = new ClientCredentials( + lmsSetup.proxyAuthUsername, + lmsSetup.proxyAuthSecret); + + // TODO check where we have to encrypt/decrypt the secret internally + CharSequence proxySecretPlain; + try { + proxySecretPlain = this.clientCredentialService.getPlainClientSecret(proxyCredentials); + } catch (final Exception e) { + proxySecretPlain = lmsSetup.proxyAuthSecret; + } + + final URI uri = new URI(lmsSetup.lmsApiUrl); + final ProxyData proxyData = new ProxyData( + lmsSetup.proxyAuthType, + uri.getHost(), + uri.getPort(), + proxyCredentials.clientId, + proxySecretPlain); + + clientHttpRequestFactory = this.clientHttpRequestFactoryService + .getClientHttpRequestFactory(proxyData) + .getOrThrow(); + } else { + clientHttpRequestFactory = this.clientHttpRequestFactoryService + .getClientHttpRequestFactory() + .getOrThrow(); + } + + final OAuth2RestTemplate template = new OAuth2RestTemplate(details); + template.setRequestFactory(clientHttpRequestFactory); + template.setAccessTokenProvider(new EdxClientCredentialsAccessTokenProvider()); + + return template; + } + + /** A custom ClientCredentialsAccessTokenProvider that adapts the access token request to Open edX + * access token request protocol using a form-URL-encoded POST request according to: + * https://course-catalog-api-guide.readthedocs.io/en/latest/authentication/index.html#getting-an-access-token */ + private class EdxClientCredentialsAccessTokenProvider extends ClientCredentialsAccessTokenProvider { + + @Override + public OAuth2AccessToken obtainAccessToken( + final OAuth2ProtectedResourceDetails details, + final AccessTokenRequest request) + throws UserRedirectRequiredException, + AccessDeniedException, + OAuth2AccessDeniedException { + + if (details instanceof ClientCredentialsResourceDetails) { + final ClientCredentialsResourceDetails resource = (ClientCredentialsResourceDetails) details; + final HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + + final MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "client_credentials"); + params.add("client_id", resource.getClientId()); + params.add("client_secret", resource.getClientSecret()); + + final OAuth2AccessToken retrieveToken = retrieveToken(request, resource, params, headers); + return retrieveToken; + } else { + return super.obtainAccessToken(details, request); + } + } + } + +} 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 f0376fcd..9c5a5205 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 @@ -233,7 +233,7 @@ public class ExamAdministrationController extends EntityController { return this.entityDAO.byPK(modelId) .flatMap(this.authorization::checkModify) .flatMap(this::checkNoActiveSebClientConnections) - .flatMap(exam -> this.setSebRestriction(exam, sebRestriction)) + .flatMap(exam -> this.applySebRestriction(exam, sebRestriction)) .flatMap(this.userActivityLogDAO::logModify) .getOrThrow(); } @@ -336,7 +336,7 @@ public class ExamAdministrationController extends EntityController { return Result.of(exam); } - private Result setSebRestriction(final Exam exam, final boolean sebRestriction) { + private Result applySebRestriction(final Exam exam, final boolean sebRestriction) { return Result.tryCatch(() -> { if (BooleanUtils.toBoolean(exam.lmsSebRestriction) == sebRestriction) { return exam; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java index 8db75729..1b02580b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java @@ -93,7 +93,7 @@ public class LmsSetupController extends ActivatableEntityController template.testLmsSetup()) + .map(this.lmsAPIService::test) .getOrThrow(); if (result.missingLMSSetupAttribute != null && !result.missingLMSSetupAttribute.isEmpty()) { diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 03b5abd1..ed1d257d 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -39,6 +39,8 @@ + + diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 34058dd0..9bae484b 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -218,6 +218,7 @@ sebserver.lmssetup.action.testsave=Test And Save sebserver.lmssetup.action.test.ok=Successfully connected to the course API sebserver.lmssetup.action.test.tokenRequestError=The API access was denied: {0} sebserver.lmssetup.action.test.quizRequestError=Unable to request courses or quizzes from the course API of the LMS. {0} +sebserver.lmssetup.action.test.quizRestrictionError=Unable to access course restriction API of the LMS. {0} sebserver.lmssetup.action.test.missingParameter=There is one or more missing connection parameter.
Please check the connection parameter for this LMS Setup sebserver.lmssetup.action.test.unknownError=An unexpected error happened while trying to connect to the LMS course API. {0} sebserver.lmssetup.action.save=Save LMS Setup diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/LmsSetupAPITest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/LmsSetupAPITest.java index aca0884b..cfcebe03 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/LmsSetupAPITest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/LmsSetupAPITest.java @@ -473,7 +473,7 @@ public class LmsSetupAPITest extends AdministrationAPIIntegrationTester { }); assertNotNull(testResult); - assertTrue(testResult.getOkStatus()); + assertTrue(testResult.isQuizAccessOk()); } } diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestrictionDataTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestrictionDataTest.java new file mode 100644 index 00000000..84e7bf78 --- /dev/null +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxCourseRestrictionDataTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx; + +import static org.junit.Assert.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import ch.ethz.seb.sebserver.gbl.api.JSONMapper; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SebRestrictionData; + +public class OpenEdxCourseRestrictionDataTest { + + @Test + public void testEmpty1() throws JsonProcessingException { + final JSONMapper mapper = new JSONMapper(); + + final OpenEdxCourseRestrictionData data = new OpenEdxCourseRestrictionData(null, null, null, null, null, false); + final String json = mapper.writeValueAsString(data); + assertEquals( + "{\"CONFIG_KEYS\":[],\"BROWSER_KEYS\":[],\"WHITELIST_PATHS\":[],\"BLACKLIST_CHAPTERS\":[],\"PERMISSION_COMPONENTS\":[\"AlwaysAllowStaff\"],\"USER_BANNING_ENABLED\":false}", + json); + } + + @Test + public void testEmpty2() throws JsonProcessingException { + final JSONMapper mapper = new JSONMapper(); + + final OpenEdxCourseRestrictionData data = + new OpenEdxCourseRestrictionData(new SebRestrictionData(null, null, null, null)); + final String json = mapper.writeValueAsString(data); + assertEquals( + "{\"CONFIG_KEYS\":[],\"BROWSER_KEYS\":[],\"WHITELIST_PATHS\":[],\"BLACKLIST_CHAPTERS\":[],\"PERMISSION_COMPONENTS\":[\"AlwaysAllowStaff\"],\"USER_BANNING_ENABLED\":false}", + json); + } + +}