SEBSERV-73 finished first feature complete
This commit is contained in:
parent
0d4e5c419a
commit
6b48bca761
25 changed files with 1274 additions and 645 deletions
|
@ -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;
|
||||
|
|
|
@ -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<String> whiteListPaths;
|
||||
|
||||
@JsonProperty(ATTR_BLACKLIST_CHAPTERS)
|
||||
public final Collection<String> blacklistChapters;
|
||||
|
||||
@JsonProperty(ATTR_SEB_PERMISSION_COMPONENTS)
|
||||
public final Collection<String> permissionComponents;
|
||||
|
||||
@JsonProperty(ATTR_USER_BANNING_ENABLED)
|
||||
public final Boolean banningEnabled;
|
||||
|
||||
@JsonProperty(ATTR_BROWSER_EXAM_KEYS)
|
||||
public final Collection<String> browserExamKeys;
|
||||
|
||||
protected EdxCourseRestrictionAttributes(
|
||||
final Collection<String> whiteListPaths,
|
||||
final Collection<String> blacklistChapters,
|
||||
final Collection<String> permissionComponents,
|
||||
final Boolean banningEnabled,
|
||||
final Collection<String> 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<String> getWhiteListPaths() {
|
||||
return this.whiteListPaths;
|
||||
}
|
||||
|
||||
public Collection<String> getBlacklistChapters() {
|
||||
return this.blacklistChapters;
|
||||
}
|
||||
|
||||
public Collection<String> getPermissionComponents() {
|
||||
return this.permissionComponents;
|
||||
}
|
||||
|
||||
public Boolean getBanningEnabled() {
|
||||
return this.banningEnabled;
|
||||
}
|
||||
|
||||
public Collection<String> 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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<Error> errors;
|
||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE)
|
||||
public final List<APIMessage> missingLMSSetupAttribute;
|
||||
|
||||
@JsonProperty(ATTR_ERROR_TOKEN_REQUEST)
|
||||
public final String tokenRequestError;
|
||||
|
||||
@JsonProperty(ATTR_ERROR_QUIZ_REQUEST)
|
||||
public final String quizRequestError;
|
||||
public final Collection<APIMessage> missingLMSSetupAttribute;
|
||||
|
||||
@JsonCreator
|
||||
public LmsSetupTestResult(
|
||||
@JsonProperty(value = ATTR_OK_STATUS, required = true) final Boolean ok,
|
||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE) final Collection<APIMessage> missingLMSSetupAttribute,
|
||||
@JsonProperty(ATTR_ERROR_TOKEN_REQUEST) final String tokenRequestError,
|
||||
@JsonProperty(ATTR_ERROR_QUIZ_REQUEST) final String quizRequestError) {
|
||||
@JsonProperty(ATTR_ERRORS) final Collection<Error> errors,
|
||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE) final Collection<APIMessage> 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<APIMessage> 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<APIMessage> 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<APIMessage> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Exam> 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<Exam> 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<APIMessage> 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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -46,11 +46,17 @@ public interface LmsAPIService {
|
|||
* @return LmsAPITemplate for specified LmsSetup configuration */
|
||||
Result<LmsAPITemplate> 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
|
||||
|
|
|
@ -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<Exam> releaseSebClientRestriction(Exam exam);
|
||||
|
||||
default List<APIMessage> attributeValidation(final ClientCredentials credentials) {
|
||||
|
||||
final LmsSetup lmsSetup = lmsSetup();
|
||||
// validation of LmsSetup
|
||||
final List<APIMessage> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<CacheKey, LmsAPITemplate> 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<LmsAPITemplate> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<APIMessage> checkAttributes() {
|
||||
final List<APIMessage> 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<APIMessage> missingAttrs = attributeValidation(this.credentials);
|
||||
final List<APIMessage> 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<List<QuizData>> getQuizzes(final FilterMap filterMap) {
|
||||
|
||||
|
|
|
@ -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<String> knownTokenAccessPaths;
|
||||
private final WebserviceInfo webserviceInfo;
|
||||
|
||||
private OAuth2RestTemplate restTemplate = null;
|
||||
private final MemoizingCircuitBreaker<List<QuizData>> 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<APIMessage> 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<List<QuizData>> getQuizzes(final FilterMap filterMap) {
|
||||
return this.allQuizzesSupplier.get()
|
||||
.map(LmsAPIService.quizzesFilterFunction(filterMap));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Result<QuizData>> getQuizzes(final Set<String> 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<SebRestrictionData> applySebClientRestriction(final SebRestrictionData sebRestrictionData) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Apply SEB Client restriction: {}", sebRestrictionData);
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
return sebRestrictionData;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<SebRestrictionData> updateSebClientRestriction(final SebRestrictionData sebRestrictionData) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Update SEB Client restriction: {}", sebRestrictionData);
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
return sebRestrictionData;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Exam> releaseSebClientRestriction(final Exam exam) {
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Release SEB Client restriction for Exam: {}", exam);
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
return exam;
|
||||
});
|
||||
}
|
||||
|
||||
private Result<LmsSetup> 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<String> 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<List<QuizData>> allQuizzesSupplier() {
|
||||
return () -> {
|
||||
return initRestTemplateAndRequestAccessToken()
|
||||
.map(this::collectAllQuizzes)
|
||||
.getOrThrow();
|
||||
};
|
||||
}
|
||||
|
||||
private ArrayList<QuizData> collectAllQuizzes(final LmsSetup lmsSetup) {
|
||||
final String externalStartURI = getExternalLMSServerAddress(lmsSetup);
|
||||
return collectAllCourses(lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT)
|
||||
.stream()
|
||||
.reduce(
|
||||
new ArrayList<QuizData>(),
|
||||
(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<CourseData> collectAllCourses(final String pageURI) {
|
||||
final List<CourseData> 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<EdXPage> 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<String, String> 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<CourseData> 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<String, String> 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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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<List<QuizData>> 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<OAuth2RestTemplate> 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<List<QuizData>> getQuizzes(final FilterMap filterMap) {
|
||||
return this.allQuizzesSupplier.get()
|
||||
.map(LmsAPIService.quizzesFilterFunction(filterMap));
|
||||
}
|
||||
|
||||
private Supplier<List<QuizData>> allQuizzesSupplier() {
|
||||
return () -> {
|
||||
return getRestTemplate()
|
||||
.map(this::collectAllQuizzes)
|
||||
.getOrThrow();
|
||||
};
|
||||
}
|
||||
|
||||
private ArrayList<QuizData> 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<QuizData>(),
|
||||
(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<CourseData> collectAllCourses(final String pageURI, final OAuth2RestTemplate restTemplate) {
|
||||
final List<CourseData> 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<EdXPage> 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<String, String> 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<CourseData> 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<OAuth2RestTemplate> getRestTemplate() {
|
||||
if (this.restTemplate == null) {
|
||||
final Result<OAuth2RestTemplate> templateRequest = this.openEdxRestTemplateFactory
|
||||
.createOAuthRestTemplate();
|
||||
if (templateRequest.hasError()) {
|
||||
return templateRequest;
|
||||
} else {
|
||||
this.restTemplate = templateRequest.get();
|
||||
}
|
||||
}
|
||||
|
||||
return Result.of(this.restTemplate);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<OAuth2RestTemplate> 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<OpenEdxCourseRestrictionData> 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<Boolean> 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<Object> 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<OAuth2RestTemplate> getRestTemplate() {
|
||||
if (this.restTemplate == null) {
|
||||
final Result<OAuth2RestTemplate> templateRequest = this.openEdxRestTemplateFactory
|
||||
.createOAuthRestTemplate();
|
||||
if (templateRequest.hasError()) {
|
||||
return templateRequest;
|
||||
} else {
|
||||
this.restTemplate = templateRequest.get();
|
||||
}
|
||||
}
|
||||
|
||||
return Result.of(this.restTemplate);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<String> blacklistChapters;
|
||||
|
||||
@JsonProperty(ATTR_SEB_PERMISSION_COMPONENTS)
|
||||
@JsonProperty(ATTR_PERMISSION_COMPONENTS)
|
||||
public final Collection<String> permissionComponents;
|
||||
|
||||
@JsonProperty(ATTR_USER_BANNING_ENABLED)
|
||||
public final boolean banningEnabled;
|
||||
|
||||
@JsonCreator
|
||||
public OpenEdxSebClientRestriction(
|
||||
public OpenEdxCourseRestrictionData(
|
||||
@JsonProperty(ATTR_CONFIG_KEYS) final Collection<String> configKeys,
|
||||
@JsonProperty(ATTR_BROWSER_KEYS) final Collection<String> browserExamKeys,
|
||||
@JsonProperty(ATTR_WHITELIST_PATHS) final Collection<String> whiteListPaths,
|
||||
@JsonProperty(ATTR_BLACKLIST_CHAPTERS) final Collection<String> blacklistChapters,
|
||||
@JsonProperty(ATTR_SEB_PERMISSION_COMPONENTS) final Collection<String> permissionComponents,
|
||||
@JsonProperty(ATTR_PERMISSION_COMPONENTS) final Collection<String> 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<String> 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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<List<QuizData>> getQuizzes(final FilterMap filterMap) {
|
||||
return this.openEdxCourseAccess.getQuizzes(filterMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Result<QuizData>> getQuizzes(final Set<String> 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<SebRestrictionData> 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<SebRestrictionData> 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<Exam> 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);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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<OpenEdxLmsAPITemplate> 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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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<String> 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<APIMessage> 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<OAuth2RestTemplate> 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<OAuth2RestTemplate> 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<String, String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -233,7 +233,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
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<Exam, Exam> {
|
|||
return Result.of(exam);
|
||||
}
|
||||
|
||||
private Result<Exam> setSebRestriction(final Exam exam, final boolean sebRestriction) {
|
||||
private Result<Exam> applySebRestriction(final Exam exam, final boolean sebRestriction) {
|
||||
return Result.tryCatch(() -> {
|
||||
if (BooleanUtils.toBoolean(exam.lmsSebRestriction) == sebRestriction) {
|
||||
return exam;
|
||||
|
|
|
@ -93,7 +93,7 @@ public class LmsSetupController extends ActivatableEntityController<LmsSetup, Lm
|
|||
institutionId);
|
||||
|
||||
final LmsSetupTestResult result = this.lmsAPIService.getLmsAPITemplate(modelId)
|
||||
.map(template -> template.testLmsSetup())
|
||||
.map(this.lmsAPIService::test)
|
||||
.getOrThrow();
|
||||
|
||||
if (result.missingLMSSetupAttribute != null && !result.missingLMSSetupAttribute.isEmpty()) {
|
||||
|
|
|
@ -39,6 +39,8 @@
|
|||
|
||||
<Logger name="ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig" level="INFO" additivity="true" />
|
||||
<Logger name="ch.ethz.seb.sebserver.webservice.servicelayer.session" level="DEBUG" additivity="true" />
|
||||
<Logger name="ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx" level="DEBUG" additivity="true" />
|
||||
|
||||
<Logger name="ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.SebExamConfigServiceImpl" level="TRACE" additivity="true" />
|
||||
|
||||
</springProfile>
|
||||
|
|
|
@ -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.<br/>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
|
||||
|
|
|
@ -473,7 +473,7 @@ public class LmsSetupAPITest extends AdministrationAPIIntegrationTester {
|
|||
});
|
||||
|
||||
assertNotNull(testResult);
|
||||
assertTrue(testResult.getOkStatus());
|
||||
assertTrue(testResult.isQuizAccessOk());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue