Merge branch 'SEBSERV-417' into development

This commit is contained in:
anhefti 2024-04-04 08:07:36 +02:00
commit 845e29ed17
26 changed files with 487 additions and 72 deletions

View file

@ -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";

View file

@ -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 */

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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;

View file

@ -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
*

View file

@ -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");
}
}

View file

@ -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());
}
}

View file

@ -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
}

View file

@ -74,7 +74,8 @@ public class AnsLmsAPITemplateFactory implements LmsAPITemplateFactory {
this.environment,
apiTemplateDataSupplier,
ansLmsAPITemplate,
ansLmsAPITemplate);
ansLmsAPITemplate,
null);
});
}

View file

@ -100,7 +100,8 @@ public class OpenEdxLmsAPITemplateFactory implements LmsAPITemplateFactory {
this.environment,
apiTemplateDataSupplier,
openEdxCourseAccess,
openEdxCourseRestriction);
openEdxCourseRestriction,
null);
});
}

View file

@ -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()));
}
}

View file

@ -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");
}
}

View file

@ -91,7 +91,8 @@ public class MoodleLmsAPITemplateFactory implements LmsAPITemplateFactory {
this.environment,
apiTemplateDataSupplier,
moodleCourseAccess,
new MoodleCourseRestriction());
new MoodleCourseRestriction(),
null);
});
}

View file

@ -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");
}
}

View file

@ -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);
});
}

View file

@ -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
});
}
}

View file

@ -87,7 +87,8 @@ public class OlatLmsAPITemplateFactory implements LmsAPITemplateFactory {
this.environment,
apiTemplateDataSupplier,
olatLmsAPITemplate,
olatLmsAPITemplate);
olatLmsAPITemplate,
null);
});
}

View file

@ -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);
}
}

View file

@ -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());
}
}
}

View file

@ -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);
}
}

View file

@ -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()) {

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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