Merge branch 'SEBSERV-417' into development
This commit is contained in:
commit
845e29ed17
26 changed files with 487 additions and 72 deletions
|
@ -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";
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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<Void> createConnectionDetails();
|
||||
|
||||
Result<Void> updateConnectionDetails();
|
||||
|
||||
Result<Void> deleteConnectionDetails();
|
||||
|
||||
}
|
|
@ -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<LmsAPITemplate> getLmsAPITemplate(String lmsUUID);
|
||||
|
||||
Result<Void> refreshAccessToken(String lmsUUID);
|
||||
|
||||
Result<Void> applyFullLmsIntegration(Long lmsSetupId, boolean refreshToken);
|
||||
|
||||
Result<Void> deleteFullLmsIntegration(Long lmsSetupId);
|
||||
|
||||
Result<Map<String, String>> getExamTemplateSelection();
|
||||
|
||||
Result<Exam> importExam(String lmsUUID, String courseId, String quizId, String examTemplateId);
|
||||
|
||||
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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<QuizData> quizFilterPredicate(final FilterMap filterMap) {
|
||||
if (filterMap == null) {
|
||||
return q -> true;
|
||||
|
|
|
@ -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 }.</br>
|
||||
* 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
|
||||
*
|
||||
|
|
|
@ -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<LmsAPITemplate> getLmsAPITemplate(final String lmsUUID) {
|
||||
return Result.ofRuntimeError("TODO");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> refreshAccessToken(final String lmsUUID) {
|
||||
return Result.ofRuntimeError("TODO");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> applyFullLmsIntegration(final Long lmsSetupId, final boolean refreshToken) {
|
||||
return Result.ofRuntimeError("TODO");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> deleteFullLmsIntegration(final Long lmsSetupId) {
|
||||
return Result.ofRuntimeError("TODO");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Map<String, String>> getExamTemplateSelection() {
|
||||
return Result.ofRuntimeError("TODO");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Exam> importExam(
|
||||
final String lmsUUID,
|
||||
final String courseId,
|
||||
final String quizId,
|
||||
final String examTemplateId) {
|
||||
return Result.ofRuntimeError("TODO");
|
||||
}
|
||||
}
|
|
@ -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<SEBRestriction> restrictionRequest;
|
||||
private final CircuitBreaker<Exam> releaseRestrictionRequest;
|
||||
|
||||
private final CircuitBreaker<Void> 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<SEBRestriction> 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<Void> 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<Void> 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<Void> 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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -420,6 +420,21 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms
|
|||
.map(x -> exam);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> createConnectionDetails() {
|
||||
return Result.ofRuntimeError("Not Supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> updateConnectionDetails() {
|
||||
return Result.ofRuntimeError("Not Supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> deleteConnectionDetails() {
|
||||
return Result.ofRuntimeError("Not Supported");
|
||||
}
|
||||
|
||||
private enum LinkRel {
|
||||
FIRST, LAST, PREV, NEXT
|
||||
}
|
||||
|
|
|
@ -74,7 +74,8 @@ public class AnsLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
this.environment,
|
||||
apiTemplateDataSupplier,
|
||||
ansLmsAPITemplate,
|
||||
ansLmsAPITemplate);
|
||||
ansLmsAPITemplate,
|
||||
null);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -100,7 +100,8 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
this.environment,
|
||||
apiTemplateDataSupplier,
|
||||
openEdxCourseAccess,
|
||||
openEdxCourseRestriction);
|
||||
openEdxCourseRestriction,
|
||||
null);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Void> createConnectionDetails() {
|
||||
return Result.ofRuntimeError("TODO");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> updateConnectionDetails() {
|
||||
return Result.ofRuntimeError("TODO");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> deleteConnectionDetails() {
|
||||
return Result.ofRuntimeError("TODO");
|
||||
}
|
||||
}
|
|
@ -91,7 +91,8 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
this.environment,
|
||||
apiTemplateDataSupplier,
|
||||
moodleCourseAccess,
|
||||
new MoodleCourseRestriction());
|
||||
new MoodleCourseRestriction(),
|
||||
null);
|
||||
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<Void> createConnectionDetails() {
|
||||
return Result.ofRuntimeError("TODO");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> updateConnectionDetails() {
|
||||
return Result.ofRuntimeError("TODO");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> deleteConnectionDetails() {
|
||||
return Result.ofRuntimeError("TODO");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -407,6 +407,21 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
|
|||
.map(x -> exam);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> createConnectionDetails() {
|
||||
return Result.ofRuntimeError("Not Supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> updateConnectionDetails() {
|
||||
return Result.ofRuntimeError("Not Supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> deleteConnectionDetails() {
|
||||
return Result.ofRuntimeError("Not Supported");
|
||||
}
|
||||
|
||||
private <T> T apiGet(final RestTemplate restTemplate, final String url, final Class<T> type) {
|
||||
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||
final ResponseEntity<T> res = restTemplate.exchange(
|
||||
|
@ -489,4 +504,5 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -87,7 +87,8 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
|
|||
this.environment,
|
||||
apiTemplateDataSupplier,
|
||||
olatLmsAPITemplate,
|
||||
olatLmsAPITemplate);
|
||||
olatLmsAPITemplate,
|
||||
null);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
* <a href="https://tomcat.apache.org/tomcat-7.0-doc/api/org/apache/catalina/filters/RemoteIpFilter.html">see</a>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Void> 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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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()) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue