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_PATH_SEGMENT
+ LMS_SETUP_TEST_AD_HOC_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 USER_ACCOUNT_ENDPOINT = "/useraccount";
public static final String QUIZ_DISCOVERY_ENDPOINT = "/quiz"; public static final String QUIZ_DISCOVERY_ENDPOINT = "/quiz";

View file

@ -51,7 +51,10 @@ public final class LmsSetup implements GrantEntity, Activatable {
SEB_RESTRICTION, SEB_RESTRICTION,
/** Indicates if the LMS integration has some process for course recovery /** Indicates if the LMS integration has some process for course recovery
* after backup-restore process for example. */ * 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. /** 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 */ /** The Moodle binding features only the course access API so far */
MOODLE(Features.COURSE_API, Features.COURSE_RECOVERY /* , Features.SEB_RESTRICTION */), MOODLE(Features.COURSE_API, Features.COURSE_RECOVERY /* , Features.SEB_RESTRICTION */),
/** The Moodle binding features with SEB Server integration plugin for fully featured */ /** 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 */ /** The Ans Delft binding is on the way */
ANS_DELFT(Features.COURSE_API, Features.SEB_RESTRICTION), ANS_DELFT(Features.COURSE_API, Features.SEB_RESTRICTION),
/** The OpenOLAT binding is on the way */ /** 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 /** Defines the LMS API access service interface with all functionality needed to access
* a LMS API within a given LmsSetup configuration. * a LMS API within a given LmsSetup configuration.
* * <p>
* There are LmsAPITemplate implementations for each type of supported LMS that are managed * 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 * 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 * 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 * Now supports name and startTime filtering
* *
* @param filterMap the FilterMap containing the filter criteria * @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) { static Predicate<QuizData> quizFilterPredicate(final FilterMap filterMap) {
if (filterMap == null) { if (filterMap == null) {
return q -> true; 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 * or partial API Access and can flag missing or wrong {@link LmsSetup } attributes with the resulting
* {@link LmsSetupTestResult }.</br> * {@link LmsSetupTestResult }.</br>
* SEB Server than uses an instance of this template to communicate with the an LMS. */ * 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 /** 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.Collection;
import java.util.Set; import java.util.Set;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment; 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.model.user.ExamineeAccountDetails;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; 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 { public class LmsAPITemplateAdapter implements LmsAPITemplate {
@ -39,6 +36,8 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
private final CourseAccessAPI courseAccessAPI; private final CourseAccessAPI courseAccessAPI;
private final SEBRestrictionAPI sebRestrictionAPI; private final SEBRestrictionAPI sebRestrictionAPI;
private final FullLmsIntegrationAPI lmsIntegrationAPI;
private final APITemplateDataSupplier apiTemplateDataSupplier; private final APITemplateDataSupplier apiTemplateDataSupplier;
/** CircuitBreaker for protected lmsTestRequest */ /** CircuitBreaker for protected lmsTestRequest */
@ -57,16 +56,20 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
private final CircuitBreaker<SEBRestriction> restrictionRequest; private final CircuitBreaker<SEBRestriction> restrictionRequest;
private final CircuitBreaker<Exam> releaseRestrictionRequest; private final CircuitBreaker<Exam> releaseRestrictionRequest;
private final CircuitBreaker<Void> lmsAccessRequest;
public LmsAPITemplateAdapter( public LmsAPITemplateAdapter(
final AsyncService asyncService, final AsyncService asyncService,
final Environment environment, final Environment environment,
final APITemplateDataSupplier apiTemplateDataSupplier, final APITemplateDataSupplier apiTemplateDataSupplier,
final CourseAccessAPI courseAccessAPI, final CourseAccessAPI courseAccessAPI,
final SEBRestrictionAPI sebRestrictionAPI) { final SEBRestrictionAPI sebRestrictionAPI,
final FullLmsIntegrationAPI lmsIntegrationAPI) {
this.courseAccessAPI = courseAccessAPI; this.courseAccessAPI = courseAccessAPI;
this.sebRestrictionAPI = sebRestrictionAPI; this.sebRestrictionAPI = sebRestrictionAPI;
this.apiTemplateDataSupplier = apiTemplateDataSupplier; this.apiTemplateDataSupplier = apiTemplateDataSupplier;
this.lmsIntegrationAPI = lmsIntegrationAPI;
this.lmsTestRequest = asyncService.createCircuitBreaker( this.lmsTestRequest = asyncService.createCircuitBreaker(
environment.getProperty( environment.getProperty(
@ -82,6 +85,20 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
Long.class, Long.class,
0L)); 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( this.quizzesRequest = asyncService.createCircuitBreaker(
environment.getProperty( environment.getProperty(
"sebserver.webservice.circuitbreaker.quizzesRequest.attempts", "sebserver.webservice.circuitbreaker.quizzesRequest.attempts",
@ -210,7 +227,7 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
log.debug("Test Course Access API for LMSSetup: {}", lmsSetup()); 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( .onError(error -> log.error(
"Failed to run protectedQuizzesRequest: {}", "Failed to run protectedQuizzesRequest: {}",
error.getMessage())) error.getMessage()))
@ -408,14 +425,12 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
log.debug("Apply course restriction: {} for LMSSetup: {}", exam, lmsSetup()); 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) .applySEBClientRestriction(exam, sebRestrictionData)
.onError(error -> log.error( .onError(error -> log.error(
"Failed to apply SEB restrictions: {}", "Failed to apply SEB restrictions: {}",
error.getMessage())) error.getMessage()))
.getOrThrow()); .getOrThrow());
return protectedRun;
} }
@Override @Override
@ -446,4 +461,57 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
return protectedRun; 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); .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 { private enum LinkRel {
FIRST, LAST, PREV, NEXT FIRST, LAST, PREV, NEXT
} }

View file

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

View file

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

View file

@ -53,14 +53,13 @@ public class MockLmsAPITemplateFactory implements LmsAPITemplateFactory {
apiTemplateDataSupplier, apiTemplateDataSupplier,
this.webserviceInfo); this.webserviceInfo);
final MockSEBRestrictionAPI mockSEBRestrictionAPI = new MockSEBRestrictionAPI();
return Result.tryCatch(() -> new LmsAPITemplateAdapter( return Result.tryCatch(() -> new LmsAPITemplateAdapter(
this.asyncService, this.asyncService,
this.environment, this.environment,
apiTemplateDataSupplier, apiTemplateDataSupplier,
mockCourseAccessAPI, 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, this.environment,
apiTemplateDataSupplier, apiTemplateDataSupplier,
moodleCourseAccess, 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, moodleRestTemplateFactory,
this.examConfigurationValueService); this.examConfigurationValueService);
final MoodlePluginFullIntegration moodlePluginFullIntegration = new MoodlePluginFullIntegration(
this.jsonMapper,
moodleRestTemplateFactory
);
return new LmsAPITemplateAdapter( return new LmsAPITemplateAdapter(
this.asyncService, this.asyncService,
this.environment, this.environment,
apiTemplateDataSupplier, apiTemplateDataSupplier,
moodlePluginCourseAccess, moodlePluginCourseAccess,
moodlePluginCourseRestriction); moodlePluginCourseRestriction,
moodlePluginFullIntegration);
}); });
} }

View file

@ -407,6 +407,21 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
.map(x -> exam); .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) { private <T> T apiGet(final RestTemplate restTemplate, final String url, final Class<T> type) {
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
final ResponseEntity<T> res = restTemplate.exchange( 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, this.environment,
apiTemplateDataSupplier, apiTemplateDataSupplier,
olatLmsAPITemplate, olatLmsAPITemplate,
olatLmsAPITemplate); olatLmsAPITemplate,
null);
}); });
} }

View file

@ -13,7 +13,6 @@ import java.io.IOException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import ch.ethz.seb.sebserver.gbl.api.API;
import org.apache.catalina.filters.RemoteIpFilter; import org.apache.catalina.filters.RemoteIpFilter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -44,7 +43,6 @@ import org.springframework.security.oauth2.provider.token.UserAuthenticationConv
import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.AuthenticationEntryPoint;
import ch.ethz.seb.sebserver.WebSecurityConfig; 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.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.weblayer.oauth.PreAuthProvider; import ch.ethz.seb.sebserver.webservice.weblayer.oauth.PreAuthProvider;
import ch.ethz.seb.sebserver.webservice.weblayer.oauth.WebClientDetailsService; import ch.ethz.seb.sebserver.webservice.weblayer.oauth.WebClientDetailsService;
@ -85,7 +83,7 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired @Autowired
private TokenStore tokenStore; private TokenStore tokenStore;
@Autowired @Autowired
private WebClientDetailsService webServiceClientDetails; private WebClientDetailsService webClientDetailsService;
@Autowired @Autowired
private PreAuthProvider preAuthProvider; private PreAuthProvider preAuthProvider;
@ -93,6 +91,8 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter {
private String adminAPIEndpoint; private String adminAPIEndpoint;
@Value("${sebserver.webservice.api.exam.endpoint}") @Value("${sebserver.webservice.api.exam.endpoint}")
private String examAPIEndpoint; private String examAPIEndpoint;
@Value("${sebserver.webservice.lms.api.endpoint}")
private String lmsAPIEndpoint;
@Value("${management.endpoints.web.base-path:NONE}") @Value("${management.endpoints.web.base-path:NONE}")
private String actuatorEndpoint; private String actuatorEndpoint;
@Value("${sebserver.webservice.http.redirect.gui}") @Value("${sebserver.webservice.http.redirect.gui}")
@ -104,9 +104,12 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter {
private Integer adminRefreshTokenValSec; private Integer adminRefreshTokenValSec;
@Value("${sebserver.webservice.api.exam.accessTokenValiditySeconds:43200}") @Value("${sebserver.webservice.api.exam.accessTokenValiditySeconds:43200}")
private Integer examAccessTokenValSec; 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. /** 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 */ * @return RemoteIpFilter instance */
@Bean @Bean
@ -132,8 +135,7 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter {
@Override @Override
@Bean(AUTHENTICATION_MANAGER) @Bean(AUTHENTICATION_MANAGER)
public AuthenticationManager authenticationManagerBean() throws Exception { public AuthenticationManager authenticationManagerBean() throws Exception {
final AuthenticationManager authenticationManagerBean = super.authenticationManagerBean(); return super.authenticationManagerBean();
return authenticationManagerBean;
} }
@Override @Override
@ -162,7 +164,7 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter {
protected ResourceServerConfiguration sebServerAdminAPIResources() throws Exception { protected ResourceServerConfiguration sebServerAdminAPIResources() throws Exception {
return new AdminAPIResourceServerConfiguration( return new AdminAPIResourceServerConfiguration(
this.tokenStore, this.tokenStore,
this.webServiceClientDetails, this.webClientDetailsService,
authenticationManagerBean(), authenticationManagerBean(),
this.adminAPIEndpoint, this.adminAPIEndpoint,
this.unauthorizedRedirect, this.unauthorizedRedirect,
@ -174,30 +176,24 @@ public class WebServiceSecurityConfig extends WebSecurityConfigurerAdapter {
protected ResourceServerConfiguration sebServerExamAPIResources() throws Exception { protected ResourceServerConfiguration sebServerExamAPIResources() throws Exception {
return new ExamAPIClientResourceServerConfiguration( return new ExamAPIClientResourceServerConfiguration(
this.tokenStore, this.tokenStore,
this.webServiceClientDetails, this.webClientDetailsService,
authenticationManagerBean(), authenticationManagerBean(),
this.examAPIEndpoint, this.examAPIEndpoint,
this.examAccessTokenValSec); this.examAccessTokenValSec);
} }
@Bean @Bean
protected ResourceServerConfiguration sebServerActuatorResources() throws Exception { protected ResourceServerConfiguration sebServerLMSAPIResources() throws Exception {
if ("NONE".equals(this.actuatorEndpoint)) { return new LMSAPIClientResourceServerConfiguration(
return null;
}
return new ActuatorResourceServerConfiguration(
this.tokenStore, this.tokenStore,
this.webServiceClientDetails, this.webClientDetailsService,
authenticationManagerBean(), authenticationManagerBean(),
this.actuatorEndpoint, this.lmsAPIEndpoint,
this.unauthorizedRedirect, this.lmsAccessTokenValSec);
this.adminAccessTokenValSec,
this.adminRefreshTokenValSec);
} }
// 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 { private static final class AdminAPIResourceServerConfiguration extends WebserviceResourceConfiguration {
public AdminAPIResourceServerConfiguration( 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 // NOTE: We need 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
private static final class ExamAPIClientResourceServerConfiguration extends WebserviceResourceConfiguration { private static final class ExamAPIClientResourceServerConfiguration extends WebserviceResourceConfiguration {
public ExamAPIClientResourceServerConfiguration( 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 TokenStore tokenStore,
final WebClientDetailsService webServiceClientDetails, final WebClientDetailsService webServiceClientDetails,
final AuthenticationManager authenticationManager, final AuthenticationManager authenticationManager,
final String apiEndpoint, final String apiEndpoint,
final String redirect, final int accessTokenValSec) {
final int adminAccessTokenValSec,
final int adminRefreshTokenValSec) {
super( super(
tokenStore, tokenStore,
webServiceClientDetails, webServiceClientDetails,
authenticationManager, authenticationManager,
new LoginRedirectOnUnauthorized(redirect), (request, response, exception) -> {
ADMIN_API_RESOURCE_ID, 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, apiEndpoint,
true, true,
4, 4,
adminAccessTokenValSec, accessTokenValSec,
adminRefreshTokenValSec); 1);
}
@Override
protected void addConfiguration(
final ConfigurerAdapter configurerAdapter,
final HttpSecurity http) throws Exception {
http.antMatcher(configurerAdapter.apiEndpoint + "/**")
.authorizeRequests()
.anyRequest()
.hasAuthority(UserRole.SEB_SERVER_ADMIN.name());
} }
} }

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 ClientConfigService sebClientConfigService;
private final AdminAPIClientDetails adminClientDetails; private final AdminAPIClientDetails adminClientDetails;
private final LmsAPIClientDetails lmsAPIClientDetails;
public WebClientDetailsService( public WebClientDetailsService(
final AdminAPIClientDetails adminClientDetails, final AdminAPIClientDetails adminClientDetails,
final ClientConfigService sebClientConfigService) { final ClientConfigService sebClientConfigService,
final LmsAPIClientDetails lmsAPIClientDetails) {
this.adminClientDetails = adminClientDetails; this.adminClientDetails = adminClientDetails;
this.sebClientConfigService = sebClientConfigService; this.sebClientConfigService = sebClientConfigService;
this.lmsAPIClientDetails = lmsAPIClientDetails;
} }
/** Load a client by the client id. This method must not return null. /** 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; return this.adminClientDetails;
} }
if (clientId.equals(this.lmsAPIClientDetails.getClientId())) {
return this.lmsAPIClientDetails;
}
return getForExamClientAPI(clientId) return getForExamClientAPI(clientId)
.get(t -> { .get(t -> {
if (log.isDebugEnabled()) { 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"; public static final String ADMIN_API_RESOURCE_ID = "seb-server-administration-api";
/** The resource identifier of the Exam API resources */ /** The resource identifier of the Exam API resources */
public static final String EXAM_API_RESOURCE_ID = "seb-server-exam-api"; 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}") @Value("${sebserver.webservice.api.exam.endpoint.discovery}")
private String examAPIDiscoveryEndpoint; private String examAPIDiscoveryEndpoint;
@Value("${sebserver.webservice.lms.api.endpoint}")
private String lmsAPIEndpoint;
public WebserviceResourceConfiguration( public WebserviceResourceConfiguration(
final TokenStore tokenStore, 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.LOGO_PATH_SEGMENT + "/**").permitAll()
.antMatchers(configurerAdapter.apiEndpoint + API.INFO_ENDPOINT + API.INFO_INST_PATH_SEGMENT + "/**").permitAll() .antMatchers(configurerAdapter.apiEndpoint + API.INFO_ENDPOINT + API.INFO_INST_PATH_SEGMENT + "/**").permitAll()
.antMatchers(configurerAdapter.apiEndpoint + API.REGISTER_ENDPOINT).permitAll() .antMatchers(configurerAdapter.apiEndpoint + API.REGISTER_ENDPOINT).permitAll()
.antMatchers(this.lmsAPIEndpoint + API.LMS_FULL_INTEGRATION_REFRESH_TOKEN_ENDPOINT).permitAll()
.and() .and()
.antMatcher(configurerAdapter.apiEndpoint + "/**") .antMatcher(configurerAdapter.apiEndpoint + "/**")
.authorizeRequests() .authorizeRequests()

View file

@ -27,7 +27,7 @@ sebserver.init.database.integrity.try-fix=true
# webservice setup configuration # webservice setup configuration
sebserver.init.adminaccount.gen-on-init=false sebserver.init.adminaccount.gen-on-init=false
sebserver.webservice.light.setup=true sebserver.webservice.light.setup=false
sebserver.webservice.distributed=false sebserver.webservice.distributed=false
#sebserver.webservice.master.delay.threshold=10000 #sebserver.webservice.master.delay.threshold=10000
sebserver.webservice.http.external.scheme=http 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=10
sebserver.webservice.api.admin.create.limit.interval.min=3600 sebserver.webservice.api.admin.create.limit.interval.min=3600
sebserver.webservice.api.admin.create.limit.refill=10 sebserver.webservice.api.admin.create.limit.refill=10
### SEB exam API
sebserver.webservice.api.admin.exam.app.signature.key.enabled=false 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.permittedProcesses=config/initialPermittedProcesses.xml
sebserver.webservice.api.exam.config.init.prohibitedProcesses=config/initialProhibitedProcesses.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.accessTokenValiditySeconds=43200
sebserver.webservice.api.exam.enable-indicator-cache=true sebserver.webservice.api.exam.enable-indicator-cache=true
sebserver.webservice.api.pagination.maxPageSize=500 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 # 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.openedx.api.token.request.paths=/oauth2/access_token
sebserver.webservice.lms.moodle.api.token.request.paths=/login/token.php 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.address.alias=
sebserver.webservice.lms.datafetch.validity.seconds=600 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: # Default Ping indicator:
sebserver.webservice.api.exam.indicator.name=Ping sebserver.webservice.api.exam.indicator.name=Ping
sebserver.webservice.api.exam.indicator.type=LAST_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.accessTokenValiditySeconds=1800
sebserver.webservice.api.admin.refreshTokenValiditySeconds=-1 sebserver.webservice.api.admin.refreshTokenValiditySeconds=-1
sebserver.webservice.api.exam.endpoint=/exam-api 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.discovery=${sebserver.webservice.api.exam.endpoint}/discovery
sebserver.webservice.api.exam.endpoint.v1=${sebserver.webservice.api.exam.endpoint}/v1 sebserver.webservice.api.exam.endpoint.v1=${sebserver.webservice.api.exam.endpoint}/v1
sebserver.webservice.api.redirect.unauthorized=none sebserver.webservice.api.redirect.unauthorized=none