diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index 71a32884..c5c2812c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -157,6 +157,9 @@ public final class API { + LMS_SETUP_TEST_PATH_SEGMENT + LMS_SETUP_TEST_AD_HOC_PATH_SEGMENT; + public static final String LMS_FULL_INTEGRATION_REFRESH_TOKEN_ENDPOINT = "/refresh-access-token"; + public static final String LMS_FULL_INTEGRATION_LMS_UUID = "lms_uuid"; + public static final String USER_ACCOUNT_ENDPOINT = "/useraccount"; public static final String QUIZ_DISCOVERY_ENDPOINT = "/quiz"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java index 0440275d..1f8edd33 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java @@ -51,7 +51,10 @@ public final class LmsSetup implements GrantEntity, Activatable { SEB_RESTRICTION, /** Indicates if the LMS integration has some process for course recovery * after backup-restore process for example. */ - COURSE_RECOVERY + COURSE_RECOVERY, + + /** Indicates if the LMS integration has some deeper integration that involves LMS calls to SEB Server*/ + LMS_FULL_INTEGRATION } /** Defines the supported types if LMS bindings. @@ -64,7 +67,7 @@ public final class LmsSetup implements GrantEntity, Activatable { /** The Moodle binding features only the course access API so far */ MOODLE(Features.COURSE_API, Features.COURSE_RECOVERY /* , Features.SEB_RESTRICTION */), /** The Moodle binding features with SEB Server integration plugin for fully featured */ - MOODLE_PLUGIN(Features.COURSE_API, Features.COURSE_RECOVERY, Features.SEB_RESTRICTION), + MOODLE_PLUGIN(Features.COURSE_API, Features.COURSE_RECOVERY, Features.SEB_RESTRICTION, Features.LMS_FULL_INTEGRATION), /** The Ans Delft binding is on the way */ ANS_DELFT(Features.COURSE_API, Features.SEB_RESTRICTION), /** The OpenOLAT binding is on the way */ diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationAPI.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationAPI.java new file mode 100644 index 00000000..e82b0ab5 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationAPI.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2019 ETH Zürich, IT Services + * + * 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; + +import ch.ethz.seb.sebserver.gbl.util.Result; + +public interface FullLmsIntegrationAPI { + + Result createConnectionDetails(); + + Result updateConnectionDetails(); + + Result deleteConnectionDetails(); + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationService.java new file mode 100644 index 00000000..4892b7de --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationService.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019 ETH Zürich, IT Services + * + * 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; + +import java.util.Map; + +import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.util.Result; + +public interface FullLmsIntegrationService { + + Result getLmsAPITemplate(String lmsUUID); + + Result refreshAccessToken(String lmsUUID); + + Result applyFullLmsIntegration(Long lmsSetupId, boolean refreshToken); + + Result deleteFullLmsIntegration(Long lmsSetupId); + + Result> getExamTemplateSelection(); + + Result importExam(String lmsUUID, String courseId, String quizId, String examTemplateId); + + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java index a59cff35..4372c49e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPIService.java @@ -26,7 +26,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; /** Defines the LMS API access service interface with all functionality needed to access * a LMS API within a given LmsSetup configuration. - * + *

* There are LmsAPITemplate implementations for each type of supported LMS that are managed * in reference to a LmsSetup configuration within this service. This means actually that * this service caches requested LmsAPITemplate (that holds the LMS API connection) as long @@ -100,7 +100,7 @@ public interface LmsAPIService { * Now supports name and startTime filtering * * @param filterMap the FilterMap containing the filter criteria - * @return true if the given QuizzData passes the filter */ + * @return filter predicate */ static Predicate quizFilterPredicate(final FilterMap filterMap) { if (filterMap == null) { return q -> true; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java index 4970b544..daae1fe0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/LmsAPITemplate.java @@ -63,7 +63,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCour * or partial API Access and can flag missing or wrong {@link LmsSetup } attributes with the resulting * {@link LmsSetupTestResult }.
* SEB Server than uses an instance of this template to communicate with the an LMS. */ -public interface LmsAPITemplate extends CourseAccessAPI, SEBRestrictionAPI { +public interface LmsAPITemplate extends CourseAccessAPI, SEBRestrictionAPI, FullLmsIntegrationAPI { /** Get the LMS type of the concrete template implementation * diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java new file mode 100644 index 00000000..e49b0001 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019 ETH Zürich, IT Services + * + * 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.util.Map; + +import ch.ethz.seb.sebserver.gbl.model.exam.Exam; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +@Lazy +@Service +@WebServiceProfile +public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService { + @Override + public Result getLmsAPITemplate(final String lmsUUID) { + return Result.ofRuntimeError("TODO"); + } + + @Override + public Result refreshAccessToken(final String lmsUUID) { + return Result.ofRuntimeError("TODO"); + } + + @Override + public Result applyFullLmsIntegration(final Long lmsSetupId, final boolean refreshToken) { + return Result.ofRuntimeError("TODO"); + } + + @Override + public Result deleteFullLmsIntegration(final Long lmsSetupId) { + return Result.ofRuntimeError("TODO"); + } + + @Override + public Result> getExamTemplateSelection() { + return Result.ofRuntimeError("TODO"); + } + + @Override + public Result importExam( + final String lmsUUID, + final String courseId, + final String quizId, + final String examTemplateId) { + return Result.ofRuntimeError("TODO"); + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java index a40343c7..8f218ff8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java @@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; import java.util.Collection; import java.util.Set; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; @@ -28,10 +29,6 @@ import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; 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.APITemplateDataSupplier; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.CourseAccessAPI; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionAPI; public class LmsAPITemplateAdapter implements LmsAPITemplate { @@ -39,6 +36,8 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { private final CourseAccessAPI courseAccessAPI; private final SEBRestrictionAPI sebRestrictionAPI; + + private final FullLmsIntegrationAPI lmsIntegrationAPI; private final APITemplateDataSupplier apiTemplateDataSupplier; /** CircuitBreaker for protected lmsTestRequest */ @@ -57,16 +56,20 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { private final CircuitBreaker restrictionRequest; private final CircuitBreaker releaseRestrictionRequest; + private final CircuitBreaker lmsAccessRequest; + public LmsAPITemplateAdapter( final AsyncService asyncService, final Environment environment, final APITemplateDataSupplier apiTemplateDataSupplier, final CourseAccessAPI courseAccessAPI, - final SEBRestrictionAPI sebRestrictionAPI) { + final SEBRestrictionAPI sebRestrictionAPI, + final FullLmsIntegrationAPI lmsIntegrationAPI) { this.courseAccessAPI = courseAccessAPI; this.sebRestrictionAPI = sebRestrictionAPI; this.apiTemplateDataSupplier = apiTemplateDataSupplier; + this.lmsIntegrationAPI = lmsIntegrationAPI; this.lmsTestRequest = asyncService.createCircuitBreaker( environment.getProperty( @@ -82,6 +85,20 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { Long.class, 0L)); + lmsAccessRequest = asyncService.createCircuitBreaker( + environment.getProperty( + "sebserver.webservice.circuitbreaker.lmsTestRequest.attempts", + Integer.class, + 2), + environment.getProperty( + "sebserver.webservice.circuitbreaker.lmsTestRequest.blockingTime", + Long.class, + Constants.SECOND_IN_MILLIS * 20), + environment.getProperty( + "sebserver.webservice.circuitbreaker.lmsTestRequest.timeToRecover", + Long.class, + 0L)); + this.quizzesRequest = asyncService.createCircuitBreaker( environment.getProperty( "sebserver.webservice.circuitbreaker.quizzesRequest.attempts", @@ -210,7 +227,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { log.debug("Test Course Access API for LMSSetup: {}", lmsSetup()); } - return this.lmsTestRequest.protectedRun(() -> this.courseAccessAPI.testCourseAccessAPI()) + return this.lmsTestRequest.protectedRun(this.courseAccessAPI::testCourseAccessAPI) .onError(error -> log.error( "Failed to run protectedQuizzesRequest: {}", error.getMessage())) @@ -408,14 +425,12 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { log.debug("Apply course restriction: {} for LMSSetup: {}", exam, lmsSetup()); } - final Result protectedRun = this.restrictionRequest.protectedRun(() -> this.sebRestrictionAPI + return this.restrictionRequest.protectedRun(() -> this.sebRestrictionAPI .applySEBClientRestriction(exam, sebRestrictionData) .onError(error -> log.error( "Failed to apply SEB restrictions: {}", error.getMessage())) .getOrThrow()); - - return protectedRun; } @Override @@ -446,4 +461,57 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { return protectedRun; } + @Override + public Result createConnectionDetails() { + if (this.lmsIntegrationAPI == null) { + return Result.ofError( + new UnsupportedOperationException("LMS Integration API Not Supported For: " + getType().name())); + } + + if (log.isDebugEnabled()) { + log.debug("Create LMS connection details for LMSSetup: {}", lmsSetup()); + } + + return this.lmsAccessRequest.protectedRun(() -> this.lmsIntegrationAPI.createConnectionDetails() + .onError(error -> log.error( + "Failed to run protected createConnectionDetails: {}", + error.getMessage())) + .getOrThrow()); + } + + @Override + public Result updateConnectionDetails() { + if (this.lmsIntegrationAPI == null) { + return Result.ofError( + new UnsupportedOperationException("LMS Integration API Not Supported For: " + getType().name())); + } + + if (log.isDebugEnabled()) { + log.debug("Update LMS connection details for LMSSetup: {}", lmsSetup()); + } + + return this.lmsAccessRequest.protectedRun(() -> this.lmsIntegrationAPI.updateConnectionDetails() + .onError(error -> log.error( + "Failed to run protected updateConnectionDetails: {}", + error.getMessage())) + .getOrThrow()); + } + + @Override + public Result deleteConnectionDetails() { + if (this.lmsIntegrationAPI == null) { + return Result.ofError( + new UnsupportedOperationException("LMS Integration API Not Supported For: " + getType().name())); + } + + if (log.isDebugEnabled()) { + log.debug("Delete LMS connection details for LMSSetup: {}", lmsSetup()); + } + + return this.lmsAccessRequest.protectedRun(() -> this.lmsIntegrationAPI.deleteConnectionDetails() + .onError(error -> log.error( + "Failed to run protected deleteConnectionDetails: {}", + error.getMessage())) + .getOrThrow()); + } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java index 56b4e8fc..43ac9372 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java @@ -420,6 +420,21 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms .map(x -> exam); } + @Override + public Result createConnectionDetails() { + return Result.ofRuntimeError("Not Supported"); + } + + @Override + public Result updateConnectionDetails() { + return Result.ofRuntimeError("Not Supported"); + } + + @Override + public Result deleteConnectionDetails() { + return Result.ofRuntimeError("Not Supported"); + } + private enum LinkRel { FIRST, LAST, PREV, NEXT } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplateFactory.java index b3d8b59b..bc475ddf 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplateFactory.java @@ -74,7 +74,8 @@ public class AnsLmsAPITemplateFactory implements LmsAPITemplateFactory { this.environment, apiTemplateDataSupplier, ansLmsAPITemplate, - ansLmsAPITemplate); + ansLmsAPITemplate, + null); }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java index 37ddce73..8d4b2ea1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/edx/OpenEdxLmsAPITemplateFactory.java @@ -100,7 +100,8 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory { this.environment, apiTemplateDataSupplier, openEdxCourseAccess, - openEdxCourseRestriction); + openEdxCourseRestriction, + null); }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockLmsAPITemplateFactory.java index 7a45624a..d4f0ea1f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockLmsAPITemplateFactory.java @@ -53,14 +53,13 @@ public class MockLmsAPITemplateFactory implements LmsAPITemplateFactory { apiTemplateDataSupplier, this.webserviceInfo); - final MockSEBRestrictionAPI mockSEBRestrictionAPI = new MockSEBRestrictionAPI(); - return Result.tryCatch(() -> new LmsAPITemplateAdapter( this.asyncService, this.environment, apiTemplateDataSupplier, mockCourseAccessAPI, - mockSEBRestrictionAPI)); + new MockSEBRestrictionAPI(), + new MockupFullIntegration())); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupFullIntegration.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupFullIntegration.java new file mode 100644 index 00000000..4fb77805 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupFullIntegration.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019 ETH Zürich, IT Services + * + * 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.mockup; + +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationAPI; + +public class MockupFullIntegration implements FullLmsIntegrationAPI { + + + @Override + public Result createConnectionDetails() { + return Result.ofRuntimeError("TODO"); + } + + @Override + public Result updateConnectionDetails() { + return Result.ofRuntimeError("TODO"); + } + + @Override + public Result deleteConnectionDetails() { + return Result.ofRuntimeError("TODO"); + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleLmsAPITemplateFactory.java index 02672002..c6a7f5ad 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleLmsAPITemplateFactory.java @@ -91,7 +91,8 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory { this.environment, apiTemplateDataSupplier, moodleCourseAccess, - new MoodleCourseRestriction()); + new MoodleCourseRestriction(), + null); }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginFullIntegration.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginFullIntegration.java new file mode 100644 index 00000000..09652f6c --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginFullIntegration.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 ETH Zürich, IT Services + * + * 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.moodle.plugin; + +import ch.ethz.seb.sebserver.gbl.api.JSONMapper; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationAPI; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory; + +public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI { + + private final JSONMapper jsonMapper; + private final MoodleRestTemplateFactory restTemplateFactory; + + public MoodlePluginFullIntegration( + final JSONMapper jsonMapper, + final MoodleRestTemplateFactory restTemplateFactory) { + + this.jsonMapper = jsonMapper; + this.restTemplateFactory = restTemplateFactory; + } + + @Override + public Result createConnectionDetails() { + return Result.ofRuntimeError("TODO"); + } + + @Override + public Result updateConnectionDetails() { + return Result.ofRuntimeError("TODO"); + } + + @Override + public Result deleteConnectionDetails() { + return Result.ofRuntimeError("TODO"); + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MooldePluginLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MooldePluginLmsAPITemplateFactory.java index e6fc0b90..83b15985 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MooldePluginLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MooldePluginLmsAPITemplateFactory.java @@ -101,12 +101,18 @@ public class MooldePluginLmsAPITemplateFactory implements LmsAPITemplateFactory moodleRestTemplateFactory, this.examConfigurationValueService); + final MoodlePluginFullIntegration moodlePluginFullIntegration = new MoodlePluginFullIntegration( + this.jsonMapper, + moodleRestTemplateFactory + ); + return new LmsAPITemplateAdapter( this.asyncService, this.environment, apiTemplateDataSupplier, moodlePluginCourseAccess, - moodlePluginCourseRestriction); + moodlePluginCourseRestriction, + moodlePluginFullIntegration); }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java index 589f6763..59bfcaeb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java @@ -407,6 +407,21 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm .map(x -> exam); } + @Override + public Result createConnectionDetails() { + return Result.ofRuntimeError("Not Supported"); + } + + @Override + public Result updateConnectionDetails() { + return Result.ofRuntimeError("Not Supported"); + } + + @Override + public Result deleteConnectionDetails() { + return Result.ofRuntimeError("Not Supported"); + } + private T apiGet(final RestTemplate restTemplate, final String url, final Class type) { final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); final ResponseEntity res = restTemplate.exchange( @@ -489,4 +504,5 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm }); } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplateFactory.java index b84b1e34..edb1d8ed 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplateFactory.java @@ -87,7 +87,8 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory { this.environment, apiTemplateDataSupplier, olatLmsAPITemplate, - olatLmsAPITemplate); + olatLmsAPITemplate, + null); }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/WebServiceSecurityConfig.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/WebServiceSecurityConfig.java index 0624fdb9..98253975 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/WebServiceSecurityConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/WebServiceSecurityConfig.java @@ -13,7 +13,6 @@ import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import ch.ethz.seb.sebserver.gbl.api.API; import org.apache.catalina.filters.RemoteIpFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,7 +43,6 @@ import org.springframework.security.oauth2.provider.token.UserAuthenticationConv import org.springframework.security.web.AuthenticationEntryPoint; import ch.ethz.seb.sebserver.WebSecurityConfig; -import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.webservice.weblayer.oauth.PreAuthProvider; import ch.ethz.seb.sebserver.webservice.weblayer.oauth.WebClientDetailsService; @@ -85,7 +83,7 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private TokenStore tokenStore; @Autowired - private WebClientDetailsService webServiceClientDetails; + private WebClientDetailsService webClientDetailsService; @Autowired private PreAuthProvider preAuthProvider; @@ -93,6 +91,8 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter { private String adminAPIEndpoint; @Value("${sebserver.webservice.api.exam.endpoint}") private String examAPIEndpoint; + @Value("${sebserver.webservice.lms.api.endpoint}") + private String lmsAPIEndpoint; @Value("${management.endpoints.web.base-path:NONE}") private String actuatorEndpoint; @Value("${sebserver.webservice.http.redirect.gui}") @@ -104,9 +104,12 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter { private Integer adminRefreshTokenValSec; @Value("${sebserver.webservice.api.exam.accessTokenValiditySeconds:43200}") private Integer examAccessTokenValSec; + @Value("${sebserver.webservice.lms.api.accessTokenValiditySeconds:-1}") + private Integer lmsAccessTokenValSec; + /** Used to get real remote IP address by using "X-Forwarded-For" and "X-Forwarded-Proto" header. - * https://tomcat.apache.org/tomcat-7.0-doc/api/org/apache/catalina/filters/RemoteIpFilter.html + * see * * @return RemoteIpFilter instance */ @Bean @@ -132,8 +135,7 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter { @Override @Bean(AUTHENTICATION_MANAGER) public AuthenticationManager authenticationManagerBean() throws Exception { - final AuthenticationManager authenticationManagerBean = super.authenticationManagerBean(); - return authenticationManagerBean; + return super.authenticationManagerBean(); } @Override @@ -162,7 +164,7 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter { protected ResourceServerConfiguration sebServerAdminAPIResources() throws Exception { return new AdminAPIResourceServerConfiguration( this.tokenStore, - this.webServiceClientDetails, + this.webClientDetailsService, authenticationManagerBean(), this.adminAPIEndpoint, this.unauthorizedRedirect, @@ -174,30 +176,24 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter { protected ResourceServerConfiguration sebServerExamAPIResources() throws Exception { return new ExamAPIClientResourceServerConfiguration( this.tokenStore, - this.webServiceClientDetails, + this.webClientDetailsService, authenticationManagerBean(), this.examAPIEndpoint, this.examAccessTokenValSec); } @Bean - protected ResourceServerConfiguration sebServerActuatorResources() throws Exception { - if ("NONE".equals(this.actuatorEndpoint)) { - return null; - } - - return new ActuatorResourceServerConfiguration( + protected ResourceServerConfiguration sebServerLMSAPIResources() throws Exception { + return new LMSAPIClientResourceServerConfiguration( this.tokenStore, - this.webServiceClientDetails, + this.webClientDetailsService, authenticationManagerBean(), - this.actuatorEndpoint, - this.unauthorizedRedirect, - this.adminAccessTokenValSec, - this.adminRefreshTokenValSec); + this.lmsAPIEndpoint, + this.lmsAccessTokenValSec); } - // NOTE: We need two different class types here to support Spring configuration for different - // ResourceServerConfiguration. There is a class type now for the Admin API as well as for the Exam API + + // NOTE: We need different class types here to support Spring configuration for different private static final class AdminAPIResourceServerConfiguration extends WebserviceResourceConfiguration { public AdminAPIResourceServerConfiguration( @@ -223,8 +219,7 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter { } } - // NOTE: We need two different class types here to support Spring configuration for different - // ResourceServerConfiguration. There is a class type now for the Admin API as well as for the Exam API + // NOTE: We need different class types here to support Spring configuration for different private static final class ExamAPIClientResourceServerConfiguration extends WebserviceResourceConfiguration { public ExamAPIClientResourceServerConfiguration( @@ -254,40 +249,33 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter { } } - private static final class ActuatorResourceServerConfiguration extends WebserviceResourceConfiguration { + // NOTE: We need different class types here to support Spring configuration for different + private static final class LMSAPIClientResourceServerConfiguration extends WebserviceResourceConfiguration { - public ActuatorResourceServerConfiguration( + public LMSAPIClientResourceServerConfiguration( final TokenStore tokenStore, final WebClientDetailsService webServiceClientDetails, final AuthenticationManager authenticationManager, final String apiEndpoint, - final String redirect, - final int adminAccessTokenValSec, - final int adminRefreshTokenValSec) { + final int accessTokenValSec) { super( tokenStore, webServiceClientDetails, authenticationManager, - new LoginRedirectOnUnauthorized(redirect), - ADMIN_API_RESOURCE_ID, + (request, response, exception) -> { + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + log.warn("Unauthorized Request: {}", request, exception); + log.info("Redirect to login after unauthorized request"); + response.getOutputStream().println("{ \"error\": \"" + exception.getMessage() + "\" }"); + }, + EXAM_API_RESOURCE_ID, apiEndpoint, true, 4, - adminAccessTokenValSec, - adminRefreshTokenValSec); - } - - @Override - protected void addConfiguration( - final ConfigurerAdapter configurerAdapter, - final HttpSecurity http) throws Exception { - - http.antMatcher(configurerAdapter.apiEndpoint + "/**") - .authorizeRequests() - - .anyRequest() - .hasAuthority(UserRole.SEB_SERVER_ADMIN.name()); + accessTokenValSec, + 1); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsIntegrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsIntegrationController.java new file mode 100644 index 00000000..dbe12ddc --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsIntegrationController.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019 ETH Zürich, IT Services + * + * 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.weblayer.api; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.model.Entity; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@WebServiceProfile +@RestController +@RequestMapping("${sebserver.webservice.lms.api.endpoint}") +public class LmsIntegrationController { + + private static final Logger log = LoggerFactory.getLogger(LmsIntegrationController.class); + + private final FullLmsIntegrationService fullLmsIntegrationService; + + public LmsIntegrationController(final FullLmsIntegrationService fullLmsIntegrationService) { + this.fullLmsIntegrationService = fullLmsIntegrationService; + } + + @RequestMapping( + path = API.LMS_FULL_INTEGRATION_REFRESH_TOKEN_ENDPOINT, + method = RequestMethod.POST, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public void refreshAccessToken( + @RequestParam(name = API.LMS_FULL_INTEGRATION_LMS_UUID, required = true) final String lmsUUID, + final HttpServletResponse response) { + + final Result result = fullLmsIntegrationService.refreshAccessToken(lmsUUID) + .onError(e -> log.error("Failed to refresh access token for LMS Setup: {}", lmsUUID, e)); + + if (result.hasError()) { + response.setStatus(HttpStatus.NOT_FOUND.value()); + } else { + response.setStatus(HttpStatus.OK.value()); + } + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/LmsAPIClientDetails.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/LmsAPIClientDetails.java new file mode 100644 index 00000000..8939fe8f --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/LmsAPIClientDetails.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 ETH Zürich, IT Services + * + * 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.weblayer.oauth; + +import ch.ethz.seb.sebserver.WebSecurityConfig; +import ch.ethz.seb.sebserver.gbl.Constants; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; +import org.springframework.stereotype.Component; + +@Lazy +@Component +public class LmsAPIClientDetails extends BaseClientDetails { + + public LmsAPIClientDetails( + @Qualifier(WebSecurityConfig.CLIENT_PASSWORD_ENCODER_BEAN_NAME) final PasswordEncoder clientPasswordEncoder, + @Value("${sebserver.webservice.lms.api.clientId}") final String clientId, + @Value("${sebserver.webservice.api.admin.clientSecret}") final String clientSecret, + @Value("${sebserver.webservice.lms.api.accessTokenValiditySeconds:-1}") final Integer accessTokenValiditySeconds + ) { + super( + clientId, + WebserviceResourceConfiguration.LMS_API_RESOURCE_ID, + StringUtils.joinWith( + Constants.LIST_SEPARATOR, + Constants.OAUTH2_SCOPE_READ, + Constants.OAUTH2_SCOPE_WRITE), + Constants.OAUTH2_GRANT_TYPE_CLIENT_CREDENTIALS, + null + ); + super.setClientSecret(clientPasswordEncoder.encode(clientSecret)); + super.setAccessTokenValiditySeconds(accessTokenValiditySeconds); + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java index 6df81631..9b2e16c7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java @@ -34,13 +34,16 @@ public class WebClientDetailsService implements ClientDetailsService { private final ClientConfigService sebClientConfigService; private final AdminAPIClientDetails adminClientDetails; + private final LmsAPIClientDetails lmsAPIClientDetails; public WebClientDetailsService( final AdminAPIClientDetails adminClientDetails, - final ClientConfigService sebClientConfigService) { + final ClientConfigService sebClientConfigService, + final LmsAPIClientDetails lmsAPIClientDetails) { this.adminClientDetails = adminClientDetails; this.sebClientConfigService = sebClientConfigService; + this.lmsAPIClientDetails = lmsAPIClientDetails; } /** Load a client by the client id. This method must not return null. @@ -64,6 +67,10 @@ public class WebClientDetailsService implements ClientDetailsService { return this.adminClientDetails; } + if (clientId.equals(this.lmsAPIClientDetails.getClientId())) { + return this.lmsAPIClientDetails; + } + return getForExamClientAPI(clientId) .get(t -> { if (log.isDebugEnabled()) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebserviceResourceConfiguration.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebserviceResourceConfiguration.java index 04790c34..303cd2e4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebserviceResourceConfiguration.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebserviceResourceConfiguration.java @@ -32,8 +32,12 @@ public abstract class WebserviceResourceConfiguration extends ResourceServerConf public static final String ADMIN_API_RESOURCE_ID = "seb-server-administration-api"; /** The resource identifier of the Exam API resources */ public static final String EXAM_API_RESOURCE_ID = "seb-server-exam-api"; + public static final String LMS_API_RESOURCE_ID = "seb-server-lms-api"; @Value("${sebserver.webservice.api.exam.endpoint.discovery}") private String examAPIDiscoveryEndpoint; + @Value("${sebserver.webservice.lms.api.endpoint}") + private String lmsAPIEndpoint; + public WebserviceResourceConfiguration( final TokenStore tokenStore, @@ -87,6 +91,8 @@ public abstract class WebserviceResourceConfiguration extends ResourceServerConf .antMatchers(configurerAdapter.apiEndpoint + API.INFO_ENDPOINT + API.LOGO_PATH_SEGMENT + "/**").permitAll() .antMatchers(configurerAdapter.apiEndpoint + API.INFO_ENDPOINT + API.INFO_INST_PATH_SEGMENT + "/**").permitAll() .antMatchers(configurerAdapter.apiEndpoint + API.REGISTER_ENDPOINT).permitAll() + .antMatchers(this.lmsAPIEndpoint + API.LMS_FULL_INTEGRATION_REFRESH_TOKEN_ENDPOINT).permitAll() + .and() .antMatcher(configurerAdapter.apiEndpoint + "/**") .authorizeRequests() diff --git a/src/main/resources/config/application-dev-ws.properties b/src/main/resources/config/application-dev-ws.properties index 4d555ad6..15ae100f 100644 --- a/src/main/resources/config/application-dev-ws.properties +++ b/src/main/resources/config/application-dev-ws.properties @@ -27,7 +27,7 @@ sebserver.init.database.integrity.try-fix=true # webservice setup configuration sebserver.init.adminaccount.gen-on-init=false -sebserver.webservice.light.setup=true +sebserver.webservice.light.setup=false sebserver.webservice.distributed=false #sebserver.webservice.master.delay.threshold=10000 sebserver.webservice.http.external.scheme=http diff --git a/src/main/resources/config/application-ws.properties b/src/main/resources/config/application-ws.properties index 960ab45a..79adf6ce 100644 --- a/src/main/resources/config/application-ws.properties +++ b/src/main/resources/config/application-ws.properties @@ -60,6 +60,9 @@ sebserver.webservice.api.admin.request.limit.refill=2 sebserver.webservice.api.admin.create.limit=10 sebserver.webservice.api.admin.create.limit.interval.min=3600 sebserver.webservice.api.admin.create.limit.refill=10 + + +### SEB exam API sebserver.webservice.api.admin.exam.app.signature.key.enabled=false sebserver.webservice.api.exam.config.init.permittedProcesses=config/initialPermittedProcesses.xml sebserver.webservice.api.exam.config.init.prohibitedProcesses=config/initialProhibitedProcesses.xml @@ -69,6 +72,17 @@ sebserver.webservice.api.exam.endpoint.v1=${sebserver.webservice.api.exam.endpoi sebserver.webservice.api.exam.accessTokenValiditySeconds=43200 sebserver.webservice.api.exam.enable-indicator-cache=true sebserver.webservice.api.pagination.maxPageSize=500 + + +sebserver.webservice.proctoring.resetBroadcastOnLeave=true +sebserver.webservice.proctoring.zoom.enableWaitingRoom=false +sebserver.webservice.proctoring.zoom.sendRejoinForCollectingRoom=false + +### LMS integration API +sebserver.webservice.lms.api.endpoint=/lms-api/v1 +sebserver.webservice.lms.api.clientId=lmsClient +sebserver.webservice.lms.api.accessTokenValiditySeconds=-1 + # comma separated list of known possible OpenEdX API access token request endpoints sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token sebserver.webservice.lms.moodle.api.token.request.paths=/login/token.php @@ -79,10 +93,6 @@ sebserver.webservice.lms.olat.sendAdditionalAttributesWithRestriction=false sebserver.webservice.lms.address.alias= sebserver.webservice.lms.datafetch.validity.seconds=600 -sebserver.webservice.proctoring.resetBroadcastOnLeave=true -sebserver.webservice.proctoring.zoom.enableWaitingRoom=false -sebserver.webservice.proctoring.zoom.sendRejoinForCollectingRoom=false - # Default Ping indicator: sebserver.webservice.api.exam.indicator.name=Ping sebserver.webservice.api.exam.indicator.type=LAST_PING diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index 622fec38..e8380742 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -33,6 +33,10 @@ sebserver.webservice.api.admin.endpoint=/admin-api sebserver.webservice.api.admin.accessTokenValiditySeconds=1800 sebserver.webservice.api.admin.refreshTokenValiditySeconds=-1 sebserver.webservice.api.exam.endpoint=/exam-api +### LMS integration API +sebserver.webservice.lms.api.endpoint=/lms-api/v1 +sebserver.webservice.lms.api.clientId=lmsClient +sebserver.webservice.lms.api.accessTokenValiditySeconds=-1 sebserver.webservice.api.exam.endpoint.discovery=${sebserver.webservice.api.exam.endpoint}/discovery sebserver.webservice.api.exam.endpoint.v1=${sebserver.webservice.api.exam.endpoint}/v1 sebserver.webservice.api.redirect.unauthorized=none