From 6e2feafc5a1f89a5cbe6ed46baf126ff246f081e Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 18 Apr 2024 16:12:09 +0200 Subject: [PATCH] SEBSERV-417 access token for Moodle --- .../sebserver/webservice/WebserviceInfo.java | 2 +- .../servicelayer/dao/LmsSetupDAO.java | 2 + .../dao/impl/LmsSetupDAOImpl.java | 21 +++++++++ .../lms/FullLmsIntegrationAPI.java | 9 ++++ .../impl/FullLmsIntegrationServiceImpl.java | 25 ++++++++-- .../lms/impl/LmsAPIServiceImpl.java | 26 +++++++++-- .../lms/impl/LmsAPITemplateAdapter.java | 14 ++++-- .../lms/impl/ans/AnsLmsAPITemplate.java | 5 ++ .../impl/mockup/MockupFullIntegration.java | 6 +++ .../moodle/MoodleRestTemplateFactory.java | 2 + .../moodle/MoodleRestTemplateFactoryImpl.java | 21 +++++++-- .../lms/impl/moodle/MoodleUtils.java | 4 +- .../moodle/legacy/MoodleCourseAccess.java | 15 ++---- .../plugin/MoodlePluginCourseAccess.java | 19 ++------ .../plugin/MoodlePluginCourseRestriction.java | 19 ++------ .../plugin/MoodlePluginFullIntegration.java | 45 +++++++++++++----- .../lms/impl/olat/OlatLmsAPITemplate.java | 5 ++ .../weblayer/oauth/LmsAPIClientDetails.java | 46 +++++++++++++++---- .../oauth/WebClientDetailsService.java | 2 +- .../MoodleMockupRestTemplateFactory.java | 17 ++++++- .../moodle/legacy/MoodleCourseAccessTest.java | 8 ++++ .../plugin/MoodlePluginCourseAccessTest.java | 12 +++-- .../MoodlePluginCourseRestrictionTest.java | 2 +- 23 files changed, 241 insertions(+), 86 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInfo.java b/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInfo.java index 8f46656e..cbfe3a3f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInfo.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInfo.java @@ -286,7 +286,7 @@ public class WebserviceInfo { } public String getOAuthTokenURI() { - return getExternalServerURL() + API.OAUTH_ENDPOINT + API.OAUTH_TOKEN_ENDPOINT; + return getExternalServerURL() + API.OAUTH_TOKEN_ENDPOINT; } public boolean isLightSetup() { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java index cb5759f9..d57eb032 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java @@ -53,4 +53,6 @@ public interface LmsSetupDAO extends ActivatableEntityDAO, B Result setIntegrationActive(Long lmsSetupId, boolean active); Result> idsOfActiveWithFullIntegration(Long institutionId); + + Result> allIdsFullIntegration(); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java index efefaf57..a81d3b87 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java @@ -133,6 +133,27 @@ public class LmsSetupDAOImpl implements LmsSetupDAO { .execute()); } + @Override + @Transactional(readOnly = true) + public Result> allIdsFullIntegration() { + return Result.tryCatch(() -> { + final List types = Arrays.stream(LmsType.values()) + .filter(type -> type.features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) + .map(LmsType::name) + .collect(Collectors.toList()); + + return this.lmsSetupRecordMapper.selectIdsByExample() + .where( + LmsSetupRecordDynamicSqlSupport.active, + isEqualTo(1)) + .and( + lmsType, + isIn(types)) + .build() + .execute(); + }); + } + @Override @Transactional(readOnly = true) public Result> allMatching( diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationAPI.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationAPI.java index 53d20703..08c0abd1 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationAPI.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/FullLmsIntegrationAPI.java @@ -8,11 +8,20 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService.IntegrationData; public interface FullLmsIntegrationAPI { + /** Performs a test for the underling {@link LmsSetup } configuration and checks if the + * LMS and the full LMS integration API of the LMS can be accessed or if there are some difficulties, + * missing API functions + * + * @return {@link LmsSetupTestResult } instance with the test result report */ + LmsSetupTestResult testFullIntegrationAPI(); + Result applyConnectionDetails(IntegrationData data); Result deleteConnectionDetails(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java index ac42bcbe..79ab1bdc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java @@ -10,6 +10,8 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.function.Function; import java.util.stream.Collectors; @@ -61,7 +63,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService private final ExamTemplateDAO examTemplateDAO; private final WebserviceInfo webserviceInfo; - //private final ClientCredentialsResourceDetails resource; + private final ClientCredentialsResourceDetails resource; private final OAuth2RestTemplate restTemplate; @@ -83,7 +85,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService this.examTemplateDAO = examTemplateDAO; this.webserviceInfo = webserviceInfo; - final ClientCredentialsResourceDetails resource = new ClientCredentialsResourceDetails(); + resource = new ClientCredentialsResourceDetails(); resource.setAccessTokenUri(webserviceInfo.getOAuthTokenURI()); resource.setClientId(clientId); resource.setClientSecret(clientSecret); @@ -104,10 +106,18 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService @Override public void notifyLmsSetupChange(final LmsSetupChangeEvent event) { final LmsSetup lmsSetup = event.getLmsSetup(); - if (lmsSetup.integrationActive) { + if (!lmsSetup.getLmsType().features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) { + return; + } + + if (lmsSetup.active) { applyFullLmsIntegration(lmsSetup.id) .onError(error -> log.warn("Failed to update LMS integration for: {}", lmsSetup, error)) - .onSuccess( data -> log.debug("Successfully updated LMS integration for: {} data: {}", lmsSetup, data)); + .onSuccess(data -> log.debug("Successfully updated LMS integration for: {} data: {}", lmsSetup, data)); + } else if (lmsSetup.integrationActive) { + deleteFullLmsIntegration(lmsSetup.id) + .onError(error -> log.warn("Failed to delete LMS integration for: {}", lmsSetup, error)) + .onSuccess(data -> log.debug("Successfully deleted LMS integration for: {} data: {}", lmsSetup, data)); } } @@ -144,11 +154,16 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService throw new IllegalStateException("No LMS Setup connectionId available for: " + lmsSetup); } + // reset old token to get actual one + resource.setScope(Arrays.asList(String.valueOf(lmsSetupId))); + restTemplate.getOAuth2ClientContext().setAccessToken(null); + final String accessToken = restTemplate.getAccessToken().getValue(); + final IntegrationData data = new IntegrationData( connectionId, lmsSetup.name, webserviceInfo.getExternalServerURL(), - restTemplate.getAccessToken().getValue(), + accessToken, this.getIntegrationTemplates(lmsSetup.institutionId) ); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java index 0eb9c889..bb1e69a7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPIServiceImpl.java @@ -156,8 +156,17 @@ public class LmsAPIServiceImpl implements LmsAPIService { final LmsSetupTestResult lmsSetupTestResult = template.testCourseRestrictionAPI(); if (!lmsSetupTestResult.isOk()) { this.cache.remove(new CacheKey(template.lmsSetup().getModelId(), 0)); + return lmsSetupTestResult; + } + + } + + if (template.lmsSetup().getLmsType().features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) { + final LmsSetupTestResult lmsSetupTestResult = template.testFullIntegrationAPI(); + if (!lmsSetupTestResult.isOk()) { + this.cache.remove(new CacheKey(template.lmsSetup().getModelId(), 0)); + return lmsSetupTestResult; } - return lmsSetupTestResult; } return LmsSetupTestResult.ofOkay(template.lmsSetup().getLmsType()); @@ -184,8 +193,19 @@ public class LmsAPIServiceImpl implements LmsAPIService { return testCourseAccessAPI; } - if (lmsSetupTemplate.lmsSetup().getLmsType().features.contains(LmsSetup.Features.SEB_RESTRICTION)) { - return lmsSetupTemplate.testCourseRestrictionAPI(); + final LmsType lmsType = lmsSetupTemplate.lmsSetup().getLmsType(); + if (lmsType.features.contains(LmsSetup.Features.SEB_RESTRICTION)) { + final LmsSetupTestResult lmsSetupTestResult = lmsSetupTemplate.testCourseRestrictionAPI(); + if (!lmsSetupTestResult.isOk()) { + return lmsSetupTestResult; + } + } + + if (lmsType.features.contains(LmsSetup.Features.LMS_FULL_INTEGRATION)) { + final LmsSetupTestResult lmsSetupTestResult = lmsSetupTemplate.testFullIntegrationAPI(); + if (!lmsSetupTestResult.isOk()) { + return lmsSetupTestResult; + } } return LmsSetupTestResult.ofOkay(lmsSetupTemplate.lmsSetup().getLmsType()); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java index fd0e525b..dfe3b57f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/LmsAPITemplateAdapter.java @@ -397,10 +397,6 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { return this.sebRestrictionAPI.testCourseRestrictionAPI(); } - if (log.isDebugEnabled()) { - log.debug("Test course restriction API for LMSSetup: {}", lmsSetup()); - } - return LmsSetupTestResult.ofAPINotSupported(getType()); } @@ -476,6 +472,16 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate { return protectedRun; } + + @Override + public LmsSetupTestResult testFullIntegrationAPI() { + if (this.lmsIntegrationAPI != null) { + return this.lmsIntegrationAPI.testFullIntegrationAPI(); + } + + return LmsSetupTestResult.ofAPINotSupported(getType()); + } + @Override public Result applyConnectionDetails(final IntegrationData data) { if (this.lmsIntegrationAPI == null) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java index c3f39851..99f641b3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/ans/AnsLmsAPITemplate.java @@ -421,6 +421,11 @@ public class AnsLmsAPITemplate extends AbstractCachedCourseAccess implements Lms .map(x -> exam); } + @Override + public LmsSetupTestResult testFullIntegrationAPI() { + return LmsSetupTestResult.ofAPINotSupported(LmsType.ANS_DELFT); + } + @Override public Result applyConnectionDetails(final IntegrationData data) { return Result.ofRuntimeError("Not Supported"); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupFullIntegration.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupFullIntegration.java index 5a13b70f..d15f5922 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupFullIntegration.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/mockup/MockupFullIntegration.java @@ -8,12 +8,18 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.mockup; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationAPI; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService.IntegrationData; public class MockupFullIntegration implements FullLmsIntegrationAPI { + @Override + public LmsSetupTestResult testFullIntegrationAPI() { + return LmsSetupTestResult.ofAPINotSupported(LmsSetup.LmsType.MOODLE_PLUGIN); + } @Override public Result applyConnectionDetails(final IntegrationData data) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactory.java index bdb102f8..c99d757f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactory.java @@ -32,6 +32,8 @@ public interface MoodleRestTemplateFactory { * @return Set of known and configured API access token paths */ Set getKnownTokenAccessPaths(); + Result getRestTemplate(); + /** Creates a MoodleAPIRestTemplate for the bundled LMSSetup of this factory. * * @param service The moodle web service name to within requesting an access token for diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactoryImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactoryImpl.java index 781adaa0..f11d2d87 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactoryImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactoryImpl.java @@ -63,6 +63,8 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory public final ClientCredentialService clientCredentialService; public final Set knownTokenAccessPaths; + private Result activeRestTemplate = Result.ofRuntimeError("Not Initialized"); + public MoodleRestTemplateFactoryImpl( final JSONMapper jsonMapper, final APITemplateDataSupplier apiTemplateDataSupplier, @@ -88,6 +90,11 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory return this.knownTokenAccessPaths; } + @Override + public Result getRestTemplate() { + return activeRestTemplate; + } + @Override public APITemplateDataSupplier getApiTemplateDataSupplier() { return this.apiTemplateDataSupplier; @@ -139,13 +146,12 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory public Result createRestTemplate(final String service) { final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); - - return this.knownTokenAccessPaths + this. activeRestTemplate = this.knownTokenAccessPaths .stream() .map(path -> this.createRestTemplate(service, path)) .map(result -> { if (result.hasError()) { - log.warn("Failed to get access token for LMS: {}({})", + log.warn("Failed to get access token for LMS: {}({}), error {}", lmsSetup.name, lmsSetup.id, result.getError().getMessage()); @@ -158,6 +164,10 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory "Failed to gain any access for LMS " + lmsSetup.name + "(" + lmsSetup.id + ") on paths: " + this.knownTokenAccessPaths)); + + log.info("Created new MoodleAPIRestTemplate for service: {} factory: {}", service, this.hashCode()); + + return activeRestTemplate; } @Override @@ -199,6 +209,7 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory ") on path: " + accessTokenPath); } + activeRestTemplate = Result.of(restTemplate); return restTemplate; }); } @@ -266,7 +277,7 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory WebserviceInfo.class); if (StringUtils.isBlank(webserviceInfo.username) || StringUtils.isBlank(webserviceInfo.userid)) { - throw new RuntimeException("Invalid WebserviceInfo: " + webserviceInfo); + throw new RuntimeException("Invalid WebserviceInfo Response"); } if (functions != null) { @@ -281,8 +292,10 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory } } catch (final RuntimeException re) { + log.warn("Failed to Moodle API access: {}", re.getMessage()); throw re; } catch (final Exception e) { + log.warn("Failed to Moodle API access: {}", e.getMessage()); throw new RuntimeException("Failed to test Moodle rest API: ", e); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleUtils.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleUtils.java index 83cf6d0f..bfb08287 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleUtils.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleUtils.java @@ -118,7 +118,7 @@ public abstract class MoodleUtils { return idNumber.equals(Constants.EMPTY_NOTE) ? null : idNumber; } - public static final void logMoodleWarning( + public static void logMoodleWarning( final Collection warnings, final String lmsSetupName, final String function) { @@ -148,7 +148,7 @@ public abstract class MoodleUtils { private static final Pattern ACCESS_DENIED_PATTERN_2 = Pattern.compile(Pattern.quote("access denied"), Pattern.CASE_INSENSITIVE); - public static final boolean checkAccessDeniedError(final String courseKeyPageJSON) { + public static boolean checkAccessDeniedError(final String courseKeyPageJSON) { return ACCESS_DENIED_PATTERN_1 .matcher(courseKeyPageJSON) .find() || diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java index 43d25fd4..1046d7a5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccess.java @@ -20,6 +20,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin.MooldePluginLmsAPITemplateFactory; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -103,7 +104,6 @@ public class MoodleCourseAccess implements CourseAccessAPI { private final int pageSize; private final int maxSize; - private MoodleAPIRestTemplate restTemplate; public MoodleCourseAccess( final JSONMapper jsonMapper, @@ -583,17 +583,12 @@ public class MoodleCourseAccess implements CourseAccessAPI { } private Result getRestTemplate() { - if (this.restTemplate == null) { - final Result templateRequest = this.restTemplateFactory - .createRestTemplate(MoodleLmsAPITemplateFactory.MOODLE_MOBILE_APP_SERVICE); - if (templateRequest.hasError()) { - return templateRequest; - } else { - this.restTemplate = templateRequest.get(); - } + final Result result = this.restTemplateFactory.getRestTemplate(); + if (!result.hasError()) { + return result; } - return Result.of(this.restTemplate); + return this.restTemplateFactory.createRestTemplate(MoodleLmsAPITemplateFactory.MOODLE_MOBILE_APP_SERVICE); } private Collection getCoursesPage( diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java index fbe0b1ec..005e0ee0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccess.java @@ -98,8 +98,6 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme private final int cutoffTimeOffset; private final boolean applyNameCriteria; - private MoodleAPIRestTemplate restTemplate; - public MoodlePluginCourseAccess( final JSONMapper jsonMapper, final AsyncService asyncService, @@ -595,19 +593,12 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme } private Result getRestTemplate() { - - if (this.restTemplate == null) { - - final Result templateRequest = this.restTemplateFactory - .createRestTemplate(MooldePluginLmsAPITemplateFactory.SEB_SERVER_SERVICE_NAME); - if (templateRequest.hasError()) { - return templateRequest; - } else { - this.restTemplate = templateRequest.get(); - } + final Result result = this.restTemplateFactory.getRestTemplate(); + if (!result.hasError()) { + return result; } - return Result.of(this.restTemplate); + return this.restTemplateFactory.createRestTemplate(MooldePluginLmsAPITemplateFactory.SEB_SERVER_SERVICE_NAME); } protected String toTestString() { @@ -619,7 +610,7 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme builder.append(", cutoffTimeOffset="); builder.append(this.cutoffTimeOffset); builder.append(", restTemplate="); - builder.append(this.restTemplate); + builder.append(this.getRestTemplate().getOr(null)); builder.append("]"); return builder.toString(); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestriction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestriction.java index 38dd7ade..5448e4bf 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestriction.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestriction.java @@ -55,8 +55,6 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI { private final MoodleRestTemplateFactory restTemplateFactory; private final ExamConfigurationValueService examConfigurationValueService; - private MoodleAPIRestTemplate restTemplate; - public MoodlePluginCourseRestriction( final JSONMapper jsonMapper, final MoodleRestTemplateFactory restTemplateFactory, @@ -90,7 +88,6 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI { RESTRICTION_SET_FUNCTION_NAME); } catch (final RuntimeException e) { - log.error("Failed to access Moodle course API: ", e); return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.MOODLE_PLUGIN, e.getMessage()); } @@ -285,24 +282,18 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI { } private Result getRestTemplate() { - if (this.restTemplate == null) { - - final Result templateRequest = this.restTemplateFactory - .createRestTemplate(MooldePluginLmsAPITemplateFactory.SEB_SERVER_SERVICE_NAME); - if (templateRequest.hasError()) { - return templateRequest; - } else { - this.restTemplate = templateRequest.get(); - } + final Result result = this.restTemplateFactory.getRestTemplate(); + if (!result.hasError()) { + return result; } - return Result.of(this.restTemplate); + return this.restTemplateFactory.createRestTemplate(MooldePluginLmsAPITemplateFactory.SEB_SERVER_SERVICE_NAME); } public String toTestString() { final StringBuilder builder = new StringBuilder(); builder.append("MoodlePluginCourseRestriction [restTemplate="); - builder.append(this.restTemplate); + builder.append(this.getRestTemplate().getOr(null)); builder.append("]"); return builder.toString(); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginFullIntegration.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginFullIntegration.java index 9b0141c9..62533838 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginFullIntegration.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginFullIntegration.java @@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin; import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService.IntegrationData; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationAPI; @@ -32,8 +33,6 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI { private final JSONMapper jsonMapper; private final MoodleRestTemplateFactory restTemplateFactory; - private MoodleAPIRestTemplate restTemplate; - public MoodlePluginFullIntegration( final JSONMapper jsonMapper, final MoodleRestTemplateFactory restTemplateFactory) { @@ -42,6 +41,34 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI { this.restTemplateFactory = restTemplateFactory; } + @Override + public LmsSetupTestResult testFullIntegrationAPI() { + final LmsSetupTestResult attributesCheck = this.restTemplateFactory.test(); + if (!attributesCheck.isOk()) { + return attributesCheck; + } + + final Result restTemplateRequest = getRestTemplate(); + if (restTemplateRequest.hasError()) { + final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " + + this.restTemplateFactory.getKnownTokenAccessPaths(); + log.error(message + " cause: {}", restTemplateRequest.getError().getMessage()); + return LmsSetupTestResult.ofTokenRequestError(LmsSetup.LmsType.MOODLE_PLUGIN, message); + } + + try { + final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get(); + restTemplate.testAPIConnection( + FUNCTION_NAME_SEBSERVER_CONNECTION, + FUNCTION_NAME_SEBSERVER_CONNECTION_DELETE); + + } catch (final RuntimeException e) { + return LmsSetupTestResult.ofQuizAccessAPIError(LmsSetup.LmsType.MOODLE_PLUGIN, e.getMessage()); + } + + return LmsSetupTestResult.ofOkay(LmsSetup.LmsType.MOODLE_PLUGIN); + } + @Override public Result applyConnectionDetails(final IntegrationData data) { return Result.tryCatch(() -> { @@ -116,17 +143,11 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI { private Result getRestTemplate() { - if (this.restTemplate == null) { - - final Result templateRequest = this.restTemplateFactory - .createRestTemplate(MooldePluginLmsAPITemplateFactory.SEB_SERVER_SERVICE_NAME); - if (templateRequest.hasError()) { - return templateRequest; - } else { - this.restTemplate = templateRequest.get(); - } + final Result result = this.restTemplateFactory.getRestTemplate(); + if (!result.hasError()) { + return result; } - return Result.of(this.restTemplate); + return this.restTemplateFactory.createRestTemplate(MooldePluginLmsAPITemplateFactory.SEB_SERVER_SERVICE_NAME); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java index 73e2190d..2c8a58eb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsAPITemplate.java @@ -408,6 +408,11 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm .map(x -> exam); } + @Override + public LmsSetupTestResult testFullIntegrationAPI() { + return LmsSetupTestResult.ofAPINotSupported(LmsType.OPEN_OLAT); + } + @Override public Result applyConnectionDetails(final IntegrationData data) { return Result.ofRuntimeError("Not Supported"); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/LmsAPIClientDetails.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/LmsAPIClientDetails.java index 8939fe8f..1a2a3ed3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/LmsAPIClientDetails.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/LmsAPIClientDetails.java @@ -10,35 +10,65 @@ package ch.ethz.seb.sebserver.webservice.weblayer.oauth; import ch.ethz.seb.sebserver.WebSecurityConfig; import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; 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.ClientDetails; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.stereotype.Component; @Lazy @Component -public class LmsAPIClientDetails extends BaseClientDetails { +public class LmsAPIClientDetails { + + private final PasswordEncoder clientPasswordEncoder; + private final String clientId; + private final String clientSecret; + private final Integer accessTokenValiditySeconds; + private final LmsSetupDAO lmsSetupDAO; public LmsAPIClientDetails( @Qualifier(WebSecurityConfig.CLIENT_PASSWORD_ENCODER_BEAN_NAME) final PasswordEncoder clientPasswordEncoder, + final LmsSetupDAO lmsSetupDAO, @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( + this.clientPasswordEncoder = clientPasswordEncoder; + this.lmsSetupDAO = lmsSetupDAO; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.accessTokenValiditySeconds = accessTokenValiditySeconds; + } + + public String getClientId() { + return clientId; + } + + // It seems this get called multiple times per token request + // TODO do we need very short time caching here? + + public ClientDetails getClientDetails() { + + final String joinIds = StringUtils.join( + lmsSetupDAO.allIdsFullIntegration().getOrThrow(), + Constants.LIST_SEPARATOR + ); + + final BaseClientDetails clientDetails = new BaseClientDetails( clientId, WebserviceResourceConfiguration.LMS_API_RESOURCE_ID, - StringUtils.joinWith( - Constants.LIST_SEPARATOR, - Constants.OAUTH2_SCOPE_READ, - Constants.OAUTH2_SCOPE_WRITE), + joinIds, Constants.OAUTH2_GRANT_TYPE_CLIENT_CREDENTIALS, null ); - super.setClientSecret(clientPasswordEncoder.encode(clientSecret)); - super.setAccessTokenValiditySeconds(accessTokenValiditySeconds); + clientDetails.setClientSecret(clientPasswordEncoder.encode(clientSecret)); + clientDetails.setAccessTokenValiditySeconds(accessTokenValiditySeconds); + return clientDetails; } + + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java index 9b2e16c7..1da9a942 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java @@ -68,7 +68,7 @@ public class WebClientDetailsService implements ClientDetailsService { } if (clientId.equals(this.lmsAPIClientDetails.getClientId())) { - return this.lmsAPIClientDetails; + return this.lmsAPIClientDetails.getClientDetails(); } return getForExamClientAPI(clientId) diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleMockupRestTemplateFactory.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleMockupRestTemplateFactory.java index b6710f51..0df6fe1b 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleMockupRestTemplateFactory.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleMockupRestTemplateFactory.java @@ -46,6 +46,8 @@ public class MoodleMockupRestTemplateFactory implements MoodleRestTemplateFactor private final APITemplateDataSupplier apiTemplateDataSupplier; + private Result template = null; + public MoodleMockupRestTemplateFactory(final APITemplateDataSupplier apiTemplateDataSupplier) { this.apiTemplateDataSupplier = apiTemplateDataSupplier; } @@ -67,14 +69,25 @@ public class MoodleMockupRestTemplateFactory implements MoodleRestTemplateFactor return paths; } + @Override + public Result getRestTemplate() { + return createRestTemplate(""); + } + @Override public Result createRestTemplate(final String service) { - return Result.of(new MockupMoodleRestTemplate(this.apiTemplateDataSupplier.getLmsSetup().lmsApiUrl)); + if (template == null) { + template = Result.of(new MockupMoodleRestTemplate(this.apiTemplateDataSupplier.getLmsSetup().lmsApiUrl)); + } + return template; } @Override public Result createRestTemplate(final String service, final String accessTokenPath) { - return Result.of(new MockupMoodleRestTemplate(this.apiTemplateDataSupplier.getLmsSetup().lmsApiUrl)); + if (template == null) { + template = Result.of(new MockupMoodleRestTemplate(this.apiTemplateDataSupplier.getLmsSetup().lmsApiUrl)); + } + return template; } public static final class MockupMoodleRestTemplate implements MoodleAPIRestTemplate { diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccessTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccessTest.java index 30ced5fb..734312c7 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccessTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/legacy/MoodleCourseAccessTest.java @@ -47,6 +47,8 @@ public class MoodleCourseAccessTest { final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class); when(moodleRestTemplateFactory.createRestTemplate(Mockito.anyString())) .thenReturn(Result.of(moodleAPIRestTemplate)); + when(moodleRestTemplateFactory.getRestTemplate()) + .thenReturn(Result.of(moodleAPIRestTemplate)); when(moodleAPIRestTemplate.callMoodleAPIFunction( anyString(), any())).thenReturn("[\r\n" + @@ -122,6 +124,8 @@ public class MoodleCourseAccessTest { final MoodleRestTemplateFactoryImpl moodleRestTemplateFactory = mock(MoodleRestTemplateFactoryImpl.class); when(moodleRestTemplateFactory.createRestTemplate(Mockito.anyString())) .thenReturn(Result.ofRuntimeError("Error1")); + when(moodleRestTemplateFactory.getRestTemplate()) + .thenReturn(Result.ofRuntimeError("Error1")); when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE)); final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( @@ -143,6 +147,8 @@ public class MoodleCourseAccessTest { final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class); when(moodleRestTemplateFactory.createRestTemplate(Mockito.anyString())) .thenReturn(Result.of(moodleAPIRestTemplate)); + when(moodleRestTemplateFactory.getRestTemplate()) + .thenReturn(Result.of(moodleAPIRestTemplate)); doThrow(RuntimeException.class).when(moodleAPIRestTemplate).testAPIConnection(any()); when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE)); @@ -165,6 +171,8 @@ public class MoodleCourseAccessTest { final MoodleAPIRestTemplateImpl moodleAPIRestTemplate = mock(MoodleAPIRestTemplateImpl.class); when(moodleRestTemplateFactory.createRestTemplate(Mockito.anyString())) .thenReturn(Result.of(moodleAPIRestTemplate)); + when(moodleRestTemplateFactory.getRestTemplate()) + .thenReturn(Result.of(moodleAPIRestTemplate)); when(moodleRestTemplateFactory.test()).thenReturn(LmsSetupTestResult.ofOkay(LmsType.MOODLE)); final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess( diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccessTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccessTest.java index a4a18403..1a42947a 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccessTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseAccessTest.java @@ -47,11 +47,13 @@ public class MoodlePluginCourseAccessTest { public void testSetup() { final MoodlePluginCourseAccess candidate = crateMockup(); - assertEquals("MoodlePluginCourseAccess [" - + "pageSize=500, " - + "maxSize=10000, " - + "cutoffTimeOffset=3, " - + "restTemplate=null]", candidate.toTestString()); + assertEquals( + "MoodlePluginCourseAccess [" + + "pageSize=500, " + + "maxSize=10000, " + + "cutoffTimeOffset=3, " + + "restTemplate=MockupMoodleRestTemplate [accessToken=MockupMoodleRestTemplate-Test-Token, url=https://test.org/, testLog=[], callLog=[]]]", + candidate.toTestString()); final LmsSetupTestResult testCourseAccessAPI = candidate.testCourseAccessAPI(); diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestrictionTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestrictionTest.java index af5ddf15..db85cc90 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestrictionTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/plugin/MoodlePluginCourseRestrictionTest.java @@ -36,7 +36,7 @@ public class MoodlePluginCourseRestrictionTest { final MoodlePluginCourseRestriction candidate = crateMockup(); - assertEquals("MoodlePluginCourseRestriction [restTemplate=null]", candidate.toTestString()); + assertEquals("MoodlePluginCourseRestriction [restTemplate=MockupMoodleRestTemplate [accessToken=MockupMoodleRestTemplate-Test-Token, url=https://test.org/, testLog=[], callLog=[]]]", candidate.toTestString()); final LmsSetupTestResult testCourseRestrictionAPI = candidate.testCourseRestrictionAPI();