From cdd393aecdfc9111c0690a9bde68300473dff2fb Mon Sep 17 00:00:00 2001 From: Carol Alexandru Date: Wed, 28 Jul 2021 15:21:22 +0200 Subject: [PATCH 01/11] Implement OlatLms integration --- .../lms/impl/olat/OlatLmsAPITemplate.java | 286 ++++++++++++------ .../lms/impl/olat/OlatLmsData.java | 82 +++++ .../lms/impl/olat/OlatLmsRestTemplate.java | 59 ++++ 3 files changed, 332 insertions(+), 95 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsData.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java 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 7ecfa6a2..98d1da1e 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 @@ -10,19 +10,33 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.Map; +import java.util.HashMap; import java.util.List; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.springframework.cache.CacheManager; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.core.env.Environment; import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.web.client.RestTemplate; import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.web.util.DefaultUriBuilderFactory; +import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode; + import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; import ch.ethz.seb.sebserver.gbl.api.APIMessage; @@ -47,19 +61,21 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.APITemplateDataSupplier import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsData.AssessmentData; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsData.UserData; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsData.RestrictionData; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsData.RestrictionDataPost; public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements LmsAPITemplate { - // TODO add needed dependencies here + private static final Logger log = LoggerFactory.getLogger(OlatLmsAPITemplate.class); + private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; private final ClientCredentialService clientCredentialService; private final APITemplateDataSupplier apiTemplateDataSupplier; private final Long lmsSetupId; protected OlatLmsAPITemplate( - - // TODO if you need more dependencies inject them here and set the reference - final ClientHttpRequestFactoryService clientHttpRequestFactoryService, final ClientCredentialService clientCredentialService, final APITemplateDataSupplier apiTemplateDataSupplier, @@ -95,32 +111,33 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm final LmsSetupTestResult testLmsSetupSettings = testLmsSetupSettings(); if (testLmsSetupSettings.hasAnyError()) { return testLmsSetupSettings; - } else { - } - - // TODO check if the course API of the remote LMS is available - // if not, create corresponding LmsSetupTestResult error - return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.OPEN_OLAT, "TODO: implement LMS access check"); - - //return LmsSetupTestResult.ofOkay(LmsType.OPEN_OLAT); + // TODO: improve error handling + try { + final RestTemplate restTemplate = this.getRestTemplate().get(); + } + catch (Exception e) { + return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.OPEN_OLAT, "Unspecific error connecting to OLAT API"); + } + return LmsSetupTestResult.ofOkay(LmsType.OPEN_OLAT); } @Override public LmsSetupTestResult testCourseRestrictionAPI() { - final LmsSetupTestResult testLmsSetupSettings = testLmsSetupSettings(); + // TODO: Any reason to implement a separate check or is this good enough? + return testCourseAccessAPI(); + + /*final LmsSetupTestResult testLmsSetupSettings = testLmsSetupSettings(); if (testLmsSetupSettings.hasAnyError()) { return testLmsSetupSettings; } if (LmsType.OPEN_OLAT.features.contains(Features.SEB_RESTRICTION)) { - // TODO check if the course API of the remote LMS is available - // if not, create corresponding LmsSetupTestResult error - } return LmsSetupTestResult.ofOkay(LmsType.OPEN_OLAT); + */ } private LmsSetupTestResult testLmsSetupSettings() { @@ -207,62 +224,96 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm @Override protected Supplier> allQuizzesSupplier(final FilterMap filterMap) { - - @SuppressWarnings("unused") - final String quizName = filterMap.getString(QuizData.FILTER_ATTR_QUIZ_NAME); - @SuppressWarnings("unused") - final DateTime quizFromTime = (filterMap != null) ? filterMap.getQuizFromTime() : null; - return () -> { - - // TODO Get all course / quiz data from remote LMS that matches the filter criteria. - // If the LMS API uses paging, go through all pages using the filter criteria - // and collect the course data. - // Transform the data from courses / quizzes from LMS into QuizData objects - // Put loaded QuizData objects to the cache: super.putToCache(quizDataCollection); - // before returning it. - - throw new RuntimeException("TODO"); + List res = getRestTemplate() + .map(t -> this.collectAllQuizzes(t, filterMap)) + .getOrThrow(); + super.putToCache(res); + return res; }; } + private String examUrl(long olatTestId) { + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + // TODO: at the moment, we don't know olatTestId because we get the assessment mode id (a.key), not the test id. + return lmsSetup.lmsApiUrl + "/auth/RepositoryEntry/" + olatTestId; + } + + private List collectAllQuizzes(final OlatLmsRestTemplate restTemplate, final FilterMap filterMap) { + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + final String quizName = filterMap.getString(QuizData.FILTER_ATTR_QUIZ_NAME); + final DateTime quizFromTime = (filterMap != null) ? filterMap.getQuizFromTime() : null; + final long fromCutTime = (quizFromTime != null) ? Utils.toUnixTimeInSeconds(quizFromTime) : -1; + + String url = "/restapi/assessment_modes/seb?"; + if (fromCutTime != -1) { url = String.format("%sdateFrom=%s&", url, fromCutTime); } + if (quizName != null) { url = String.format("%sname=%s&", url, quizName); } + + final List as = this.apiGetList(restTemplate, url, new ParameterizedTypeReference>(){}); + return as.stream() + .map(a -> new QuizData( + String.format("%d", a.key), + lmsSetup.getInstitutionId(), + lmsSetup.id, + lmsSetup.getLmsType(), + a.name, + a.description, + Utils.toDateTimeUTC(a.dateFrom), + Utils.toDateTimeUTC(a.dateTo), + examUrl(a.key), + new HashMap())) + .collect(Collectors.toList()); + } + @Override protected Supplier> quizzesSupplier(final Set ids) { - - return () -> { - - // TODO get all quiz / course data for specified identifiers from remote LMS - // Transform the data from courses / quizzes from LMS into QuizData objects - // and put it to the cache: super.putToCache(quizDataCollection); - // before returning it. - - throw new RuntimeException("TODO"); - }; + return () -> ids.stream().map(id -> quizSupplier(id).get()).collect(Collectors.toList()); } @Override protected Supplier quizSupplier(final String id) { - - return () -> { - - // TODO get the specified quiz / course data for specified identifier from remote LMS - // and put it to the cache: super.putToCache(quizDataCollection); - // before returning it. - - throw new RuntimeException("TODO"); - }; + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + return () -> getRestTemplate() + .map(t -> this.quizById(t, id)) + .getOrThrow(); } + private QuizData quizById(final OlatLmsRestTemplate restTemplate, final String id) { + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + final String url = String.format("/restapi/assessment_modes/%s", id); + final AssessmentData a = this.apiGet(restTemplate, url, AssessmentData.class); + return new QuizData( + String.format("%d", a.key), + lmsSetup.getInstitutionId(), + lmsSetup.id, + lmsSetup.getLmsType(), + a.name, + a.description, + Utils.toDateTimeUTC(a.dateFrom), + Utils.toDateTimeUTC(a.dateTo), + examUrl(a.key), + new HashMap()); + } + + private ExamineeAccountDetails getExamineeById(final RestTemplate restTemplate, final String id) { + final String url = String.format("/restapi/users/%s/name_username", id); + final UserData u = this.apiGet(restTemplate, url, UserData.class); + final Map attrs = new HashMap<>(); + return new ExamineeAccountDetails( + String.valueOf(u.key), + u.lastName + ", " + u.firstName, + u.username, + // TODO: other placeholder value? null? + "OLAT does not provide email addresses", + attrs); + } + + @Override - protected Supplier accountDetailsSupplier(final String examineeSessionId) { - - return () -> { - - // TODO get the examinee's account details by the given examineeSessionId from remote LMS. - // Currently only the name is needed to display on monitoring view. - - throw new RuntimeException("TODO"); - }; + protected Supplier accountDetailsSupplier(final String id) { + return () -> getRestTemplate() + .map(t -> this.getExamineeById(t, id)) + .getOrThrow(); } @Override @@ -272,33 +323,43 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm }; } + private SEBRestriction getRestrictionForAssignmentId(final RestTemplate restTemplate, String id) { + final String url = String.format("/restapi/assessment_modes/%s/seb_restriction", id); + final RestrictionData r = this.apiGet(restTemplate, url, RestrictionData.class); + return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap()); + } + + private SEBRestriction setRestrictionForAssignmentId(final RestTemplate restTemplate, String id, SEBRestriction restriction) { + final String url = String.format("/restapi/assessment_modes/%s/seb_restriction", id); + final RestrictionDataPost post = new RestrictionDataPost(); + post.browserExamKeys = new ArrayList<>(restriction.browserExamKeys); + post.configKeys = new ArrayList<>(restriction.configKeys); + final RestrictionData r = this.apiPost(restTemplate, url, post, RestrictionDataPost.class, RestrictionData.class); + return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap()); + } + + private SEBRestriction deleteRestrictionForAssignmentId(final RestTemplate restTemplate, String id) { + final String url = String.format("/restapi/assessment_modes/%s/seb_restriction", id); + final RestrictionData r = this.apiDelete(restTemplate, url, RestrictionData.class); + // OLAT returns RestrictionData with null values upon deletion. + // We return it here for consistency, even though SEB server does not need it + return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap()); + } + @Override public Result getSEBClientRestriction(final Exam exam) { - @SuppressWarnings("unused") - final String quizId = exam.externalId; - - return Result.tryCatch(() -> { - - // TODO get the SEB client restrictions that are currently set on the remote LMS for - // the given quiz / course derived from the given exam - - throw new RuntimeException("TODO"); - }); + return Result.of(getRestTemplate() + .map(t -> this.getRestrictionForAssignmentId(t, exam.externalId)) + .getOrThrow()); } @Override public Result applySEBClientRestriction( final String externalExamId, final SEBRestriction sebRestrictionData) { - - return Result.tryCatch(() -> { - - // TODO apply the given sebRestrictionData settings as current SEB client restriction setting - // to the remote LMS for the given quiz / course. - // Mainly SEBRestriction.configKeys and SEBRestriction.browserExamKeys - - throw new RuntimeException("TODO"); - }); + return Result.of(getRestTemplate() + .map(t -> this.setRestrictionForAssignmentId(t, externalExamId, sebRestrictionData)) + .getOrThrow()); } @Override @@ -306,22 +367,57 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm @SuppressWarnings("unused") final String quizId = exam.externalId; - return Result.tryCatch(() -> { - - // TODO Release respectively delete all SEB client restrictions for the given - // course / quize on the remote LMS. - - throw new RuntimeException("TODO"); - }); + return Result.of(getRestTemplate() + .map(t -> this.deleteRestrictionForAssignmentId(t, exam.externalId)) + .map(x -> exam) + .getOrThrow()); } - // TODO: This is an example of how to create a RestTemplate for the service to access the LMS API - // The example deals with a Http based API that is secured by an OAuth2 client-credential flow. - // You might need some different template, then you have to adapt this code - // To your needs. - @SuppressWarnings("unused") - private OAuth2RestTemplate createRestTemplate(final String accessTokenRequestPath) { + private T apiGet(final RestTemplate restTemplate, String url, Class type) { + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + final ResponseEntity res = restTemplate.exchange( + lmsSetup.lmsApiUrl + url, + HttpMethod.GET, + HttpEntity.EMPTY, + type); + return res.getBody(); + } + private List apiGetList(final RestTemplate restTemplate, String url, ParameterizedTypeReference> type) { + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + final ResponseEntity> res = restTemplate.exchange( + lmsSetup.lmsApiUrl + url, + HttpMethod.GET, + HttpEntity.EMPTY, + type); + return res.getBody(); + } + + private R apiPost(final RestTemplate restTemplate, String url, P post, Class

postType, Class responseType) { + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + final HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.set("content-type", "application/json"); + HttpEntity

requestEntity = new HttpEntity<>(post, httpHeaders); + final ResponseEntity res = restTemplate.exchange( + lmsSetup.lmsApiUrl + url, + HttpMethod.POST, + requestEntity, + responseType); + return res.getBody(); + } + + private T apiDelete(final RestTemplate restTemplate, String url, Class type) { + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + final ResponseEntity res = restTemplate.exchange( + lmsSetup.lmsApiUrl + url, + HttpMethod.DELETE, + HttpEntity.EMPTY, + type); + return res.getBody(); + } + + private Result getRestTemplate() { + // TODO: cache/reuse authenticated template for more than 1 request? final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData(); @@ -332,7 +428,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm .getOrThrow(); final ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails(); - details.setAccessTokenUri(lmsSetup.lmsApiUrl + accessTokenRequestPath); + details.setAccessTokenUri(lmsSetup.lmsApiUrl + "/restapi/auth/"); details.setClientId(plainClientId.toString()); details.setClientSecret(plainClientSecret.toString()); @@ -340,10 +436,10 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm .getClientHttpRequestFactory(proxyData) .getOrThrow(); - final OAuth2RestTemplate template = new OAuth2RestTemplate(details); + final OlatLmsRestTemplate template = new OlatLmsRestTemplate(details); template.setRequestFactory(clientHttpRequestFactory); - return template; + return Result.of(template); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsData.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsData.java new file mode 100644 index 00000000..9fe7b961 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsData.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET) + * + * 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.olat; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +public final class OlatLmsData { + + @JsonIgnoreProperties(ignoreUnknown = true) + static public final class AssessmentData { + /* OLAT API example: + { + "key":7405568, + "name":"Test1", + "description":"", + "courseName":"test", + "dateFrom":1626515100000, + "dateTo":1626523260000 + } + */ + public long key; + public String name; + public String description; + public String courseName; + public long dateFrom; + public long dateTo; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static final class UserData { + /* OLAT API example: + { + "firstName": "OpenOLAT", + "key": 360448, + "lastName": "Administrator", + "username": "administrator" + } + */ + public long key; + public String firstName; + public String lastName; + public String username; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static final class RestrictionData { + /* OLAT API example: + { + "browserExamKeys": [ "1" ], + "configKeys": null, + "key": 8028160 + } + */ + public long key; + public List browserExamKeys; + public List configKeys; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static final class RestrictionDataPost { + /* OLAT API example: + { + "configKeys": ["a", "b"], + "browserExamKeys": ["1", "2"] + } + */ + public List browserExamKeys; + public List configKeys; + } + +} + + diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java new file mode 100644 index 00000000..65112dd6 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET) + * + * 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.olat; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.web.client.RestTemplate; +import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; + +public class OlatLmsRestTemplate extends RestTemplate { + + private static final Logger log = LoggerFactory.getLogger(OlatLmsRestTemplate.class); + + public String token; + + public OlatLmsRestTemplate(ClientCredentialsResourceDetails details) { + super(); + + // Authenticate with OLAT and store the received X-OLAT-TOKEN + final String authUrl = String.format("%s%s?password=%s", + details.getAccessTokenUri(), + details.getClientId(), + details.getClientSecret()); + final HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.set("accept", "application/json"); + ResponseEntity response = this.getForEntity(authUrl, String.class); + HttpHeaders responseHeaders = response.getHeaders(); + log.debug("OLAT Auth Response Headers: {}", responseHeaders); + token = responseHeaders.getFirst("X-OLAT-TOKEN"); + + // Add X-OLAT-TOKEN request header to every request done using this RestTemplate + this.getInterceptors().add(new ClientHttpRequestInterceptor(){ + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { + request.getHeaders().set("X-OLAT-TOKEN", token); + request.getHeaders().set("accept", "application/json"); + HttpHeaders responseHeaders = response.getHeaders(); + return execution.execute(request, body); + } + }); + } +} + From ae5149226a5fddebef4cf908963f430eb83fbea9 Mon Sep 17 00:00:00 2001 From: Carol Alexandru Date: Wed, 28 Jul 2021 22:47:49 +0200 Subject: [PATCH 02/11] generate proper exam url using updated OLAT api --- .../lms/impl/olat/OlatLmsAPITemplate.java | 8 ++++---- .../servicelayer/lms/impl/olat/OlatLmsData.java | 16 +++++++++------- 2 files changed, 13 insertions(+), 11 deletions(-) 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 98d1da1e..70f38b23 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 @@ -252,7 +252,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm final List as = this.apiGetList(restTemplate, url, new ParameterizedTypeReference>(){}); return as.stream() .map(a -> new QuizData( - String.format("%d", a.key), + String.format("%d", a.assessmentModeKey), lmsSetup.getInstitutionId(), lmsSetup.id, lmsSetup.getLmsType(), @@ -260,7 +260,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm a.description, Utils.toDateTimeUTC(a.dateFrom), Utils.toDateTimeUTC(a.dateTo), - examUrl(a.key), + examUrl(a.repositoryEntryKey), new HashMap())) .collect(Collectors.toList()); } @@ -283,7 +283,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm final String url = String.format("/restapi/assessment_modes/%s", id); final AssessmentData a = this.apiGet(restTemplate, url, AssessmentData.class); return new QuizData( - String.format("%d", a.key), + String.format("%d", a.assessmentModeKey), lmsSetup.getInstitutionId(), lmsSetup.id, lmsSetup.getLmsType(), @@ -291,7 +291,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm a.description, Utils.toDateTimeUTC(a.dateFrom), Utils.toDateTimeUTC(a.dateTo), - examUrl(a.key), + examUrl(a.repositoryEntryKey), new HashMap()); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsData.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsData.java index 9fe7b961..5698080b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsData.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsData.java @@ -19,15 +19,17 @@ public final class OlatLmsData { static public final class AssessmentData { /* OLAT API example: { - "key":7405568, - "name":"Test1", - "description":"", - "courseName":"test", - "dateFrom":1626515100000, - "dateTo":1626523260000 + "courseName": "course 1", + "dateFrom": 1624420800000, + "dateTo": 1624658400000, + "description": "", + "assessmentModeKey": 6356992, + “repositoryEntryKey”: 462324, + "name": "SEB test" } */ - public long key; + public long assessmentModeKey; + public long repositoryEntryKey; public String name; public String description; public String courseName; From 59da4bcf4e24ca8a72f6e00efc6029bbb5ce86eb Mon Sep 17 00:00:00 2001 From: Carol Alexandru Date: Fri, 30 Jul 2021 13:23:25 +0200 Subject: [PATCH 03/11] minor improvements, re-use OlatLmsRestTemplate with existing token --- .../lms/impl/olat/OlatLmsAPITemplate.java | 90 ++++++++----------- .../lms/impl/olat/OlatLmsData.java | 4 +- .../lms/impl/olat/OlatLmsRestTemplate.java | 59 +++++++----- 3 files changed, 80 insertions(+), 73 deletions(-) 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 70f38b23..68cb949d 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 @@ -75,6 +75,8 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm private final APITemplateDataSupplier apiTemplateDataSupplier; private final Long lmsSetupId; + private OlatLmsRestTemplate cachedRestTemplate; + protected OlatLmsAPITemplate( final ClientHttpRequestFactoryService clientHttpRequestFactoryService, final ClientCredentialService clientCredentialService, @@ -112,9 +114,8 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm if (testLmsSetupSettings.hasAnyError()) { return testLmsSetupSettings; } - // TODO: improve error handling try { - final RestTemplate restTemplate = this.getRestTemplate().get(); + this.getRestTemplate().get(); } catch (Exception e) { return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.OPEN_OLAT, "Unspecific error connecting to OLAT API"); @@ -124,20 +125,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm @Override public LmsSetupTestResult testCourseRestrictionAPI() { - // TODO: Any reason to implement a separate check or is this good enough? return testCourseAccessAPI(); - - /*final LmsSetupTestResult testLmsSetupSettings = testLmsSetupSettings(); - if (testLmsSetupSettings.hasAnyError()) { - return testLmsSetupSettings; - } - - if (LmsType.OPEN_OLAT.features.contains(Features.SEB_RESTRICTION)) { - - } - - return LmsSetupTestResult.ofOkay(LmsType.OPEN_OLAT); - */ } private LmsSetupTestResult testLmsSetupSettings() { @@ -233,10 +221,9 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm }; } - private String examUrl(long olatTestId) { + private String examUrl(long olatRepositoryId) { final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); - // TODO: at the moment, we don't know olatTestId because we get the assessment mode id (a.key), not the test id. - return lmsSetup.lmsApiUrl + "/auth/RepositoryEntry/" + olatTestId; + return lmsSetup.lmsApiUrl + "/auth/RepositoryEntry/" + olatRepositoryId; } private List collectAllQuizzes(final OlatLmsRestTemplate restTemplate, final FilterMap filterMap) { @@ -251,8 +238,9 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm final List as = this.apiGetList(restTemplate, url, new ParameterizedTypeReference>(){}); return as.stream() - .map(a -> new QuizData( - String.format("%d", a.assessmentModeKey), + .map(a -> { + return new QuizData( + String.format("%d", a.key), lmsSetup.getInstitutionId(), lmsSetup.id, lmsSetup.getLmsType(), @@ -261,7 +249,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm Utils.toDateTimeUTC(a.dateFrom), Utils.toDateTimeUTC(a.dateTo), examUrl(a.repositoryEntryKey), - new HashMap())) + new HashMap());}) .collect(Collectors.toList()); } @@ -283,7 +271,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm final String url = String.format("/restapi/assessment_modes/%s", id); final AssessmentData a = this.apiGet(restTemplate, url, AssessmentData.class); return new QuizData( - String.format("%d", a.assessmentModeKey), + String.format("%d", a.key), lmsSetup.getInstitutionId(), lmsSetup.id, lmsSetup.getLmsType(), @@ -303,8 +291,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm String.valueOf(u.key), u.lastName + ", " + u.firstName, u.username, - // TODO: other placeholder value? null? - "OLAT does not provide email addresses", + "OLAT API does not provide email addresses", attrs); } @@ -348,18 +335,16 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm @Override public Result getSEBClientRestriction(final Exam exam) { - return Result.of(getRestTemplate() - .map(t -> this.getRestrictionForAssignmentId(t, exam.externalId)) - .getOrThrow()); + return getRestTemplate() + .map(t -> this.getRestrictionForAssignmentId(t, exam.externalId)); } @Override public Result applySEBClientRestriction( final String externalExamId, final SEBRestriction sebRestrictionData) { - return Result.of(getRestTemplate() - .map(t -> this.setRestrictionForAssignmentId(t, externalExamId, sebRestrictionData)) - .getOrThrow()); + return getRestTemplate() + .map(t -> this.setRestrictionForAssignmentId(t, externalExamId, sebRestrictionData)); } @Override @@ -367,10 +352,9 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm @SuppressWarnings("unused") final String quizId = exam.externalId; - return Result.of(getRestTemplate() + return getRestTemplate() .map(t -> this.deleteRestrictionForAssignmentId(t, exam.externalId)) - .map(x -> exam) - .getOrThrow()); + .map(x -> exam); } private T apiGet(final RestTemplate restTemplate, String url, Class type) { @@ -417,29 +401,33 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm } private Result getRestTemplate() { - // TODO: cache/reuse authenticated template for more than 1 request? - final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); - final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); - final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData(); + return Result.tryCatch(() -> { + if (this.cachedRestTemplate != null) { return this.cachedRestTemplate; } + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); + final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); + final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData(); - final CharSequence plainClientId = credentials.clientId; - final CharSequence plainClientSecret = this.clientCredentialService - .getPlainClientSecret(credentials) - .getOrThrow(); + final CharSequence plainClientId = credentials.clientId; + final CharSequence plainClientSecret = this.clientCredentialService + .getPlainClientSecret(credentials) + .getOrThrow(); - final ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails(); - details.setAccessTokenUri(lmsSetup.lmsApiUrl + "/restapi/auth/"); - details.setClientId(plainClientId.toString()); - details.setClientSecret(plainClientSecret.toString()); + final ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails(); + details.setAccessTokenUri(lmsSetup.lmsApiUrl + "/restapi/auth/"); + details.setClientId(plainClientId.toString()); + details.setClientSecret(plainClientSecret.toString()); - final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService - .getClientHttpRequestFactory(proxyData) - .getOrThrow(); + final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService + .getClientHttpRequestFactory(proxyData) + .getOrThrow(); - final OlatLmsRestTemplate template = new OlatLmsRestTemplate(details); - template.setRequestFactory(clientHttpRequestFactory); + final OlatLmsRestTemplate template = new OlatLmsRestTemplate(details); + template.setRequestFactory(clientHttpRequestFactory); - return Result.of(template); + this.cachedRestTemplate = template; + + return this.cachedRestTemplate; + }); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsData.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsData.java index 5698080b..9f7dff08 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsData.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsData.java @@ -23,12 +23,12 @@ public final class OlatLmsData { "dateFrom": 1624420800000, "dateTo": 1624658400000, "description": "", - "assessmentModeKey": 6356992, + "key": 6356992, “repositoryEntryKey”: 462324, "name": "SEB test" } */ - public long assessmentModeKey; + public long key; public long repositoryEntryKey; public String name; public String description; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java index 65112dd6..18e2a7a0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java @@ -16,6 +16,7 @@ import org.slf4j.LoggerFactory; import org.springframework.web.client.RestTemplate; import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; import org.springframework.http.HttpRequest; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; @@ -25,35 +26,53 @@ import org.springframework.http.client.ClientHttpResponse; public class OlatLmsRestTemplate extends RestTemplate { - private static final Logger log = LoggerFactory.getLogger(OlatLmsRestTemplate.class); + private static final Logger log = LoggerFactory.getLogger(OlatLmsRestTemplate.class); - public String token; + public String token; + private ClientCredentialsResourceDetails details; - public OlatLmsRestTemplate(ClientCredentialsResourceDetails details) { + public OlatLmsRestTemplate(ClientCredentialsResourceDetails details) { super(); - - // Authenticate with OLAT and store the received X-OLAT-TOKEN - final String authUrl = String.format("%s%s?password=%s", - details.getAccessTokenUri(), - details.getClientId(), - details.getClientSecret()); - final HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.set("accept", "application/json"); - ResponseEntity response = this.getForEntity(authUrl, String.class); - HttpHeaders responseHeaders = response.getHeaders(); - log.debug("OLAT Auth Response Headers: {}", responseHeaders); - token = responseHeaders.getFirst("X-OLAT-TOKEN"); + this.details = details; + authenticate(); // Add X-OLAT-TOKEN request header to every request done using this RestTemplate this.getInterceptors().add(new ClientHttpRequestInterceptor(){ @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { - request.getHeaders().set("X-OLAT-TOKEN", token); - request.getHeaders().set("accept", "application/json"); - HttpHeaders responseHeaders = response.getHeaders(); - return execution.execute(request, body); + request.getHeaders().set("accept", "application/json"); + // if we don't have a token (this is normal during authentication), just do the call + if (token == null) { return execution.execute(request, body); } + // otherwise, add the X-OLAT-TOKEN + request.getHeaders().set("X-OLAT-TOKEN", token); + ClientHttpResponse response = execution.execute(request, body); + log.debug("OLAT [regular API call] Response Headers: {}", response.getHeaders()); + // If we get a 401, re-authenticate and try once more + if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) { + authenticate(); + request.getHeaders().set("X-OLAT-TOKEN", token); + response = execution.execute(request, body); + log.debug("OLAT [retry API call] Response Headers: {}", response.getHeaders()); + } + return response; } }); - } + } + + private void authenticate() { + // Authenticate with OLAT and store the received X-OLAT-TOKEN + token = null; + final String authUrl = String.format("%s%s?password=%s", + details.getAccessTokenUri(), + details.getClientId(), + details.getClientSecret()); + final HttpHeaders httpHeaders = new HttpHeaders(); + ResponseEntity response = this.getForEntity(authUrl, String.class); + HttpHeaders responseHeaders = response.getHeaders(); + log.debug("OLAT [authenticate] Response Headers: {}", responseHeaders); + token = responseHeaders.getFirst("X-OLAT-TOKEN"); + } + + } From 0029cd4ec32511ad805ad12a11ba7f49ab762b5f Mon Sep 17 00:00:00 2001 From: Carol Alexandru Date: Fri, 30 Jul 2021 14:19:53 +0200 Subject: [PATCH 04/11] get OLAT token lazily when first needed, not upon instantiation --- .../lms/impl/olat/OlatLmsRestTemplate.java | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java index 18e2a7a0..fd2982e5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java @@ -34,25 +34,26 @@ public class OlatLmsRestTemplate extends RestTemplate { public OlatLmsRestTemplate(ClientCredentialsResourceDetails details) { super(); this.details = details; - authenticate(); // Add X-OLAT-TOKEN request header to every request done using this RestTemplate this.getInterceptors().add(new ClientHttpRequestInterceptor(){ @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { - request.getHeaders().set("accept", "application/json"); - // if we don't have a token (this is normal during authentication), just do the call - if (token == null) { return execution.execute(request, body); } + // if there's no token, authenticate first + if (token == null) { authenticate(); } + // when authenticating, just do a normal call + else if (token == "authenticating") { return execution.execute(request, body); } // otherwise, add the X-OLAT-TOKEN + request.getHeaders().set("accept", "application/json"); request.getHeaders().set("X-OLAT-TOKEN", token); ClientHttpResponse response = execution.execute(request, body); - log.debug("OLAT [regular API call] Response Headers: {}", response.getHeaders()); + log.debug("OLAT [regular API call] {} Headers: {}", response.getStatusCode(), response.getHeaders()); // If we get a 401, re-authenticate and try once more if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) { authenticate(); request.getHeaders().set("X-OLAT-TOKEN", token); response = execution.execute(request, body); - log.debug("OLAT [retry API call] Response Headers: {}", response.getHeaders()); + log.debug("OLAT [retry API call] {} Headers: {}", response.getStatusCode(), response.getHeaders()); } return response; } @@ -61,16 +62,21 @@ public class OlatLmsRestTemplate extends RestTemplate { private void authenticate() { // Authenticate with OLAT and store the received X-OLAT-TOKEN - token = null; + token = "authenticating"; final String authUrl = String.format("%s%s?password=%s", details.getAccessTokenUri(), details.getClientId(), details.getClientSecret()); - final HttpHeaders httpHeaders = new HttpHeaders(); - ResponseEntity response = this.getForEntity(authUrl, String.class); - HttpHeaders responseHeaders = response.getHeaders(); - log.debug("OLAT [authenticate] Response Headers: {}", responseHeaders); - token = responseHeaders.getFirst("X-OLAT-TOKEN"); + try { + ResponseEntity response = this.getForEntity(authUrl, String.class); + HttpHeaders responseHeaders = response.getHeaders(); + log.debug("OLAT [authenticate] {} Headers: {}", response.getStatusCode(), responseHeaders); + token = responseHeaders.getFirst("X-OLAT-TOKEN"); + } + catch (Exception e) { + token = null; + throw e; + } } From 8376a1c3fd2eb9a9d515776275017fdc5e3729e8 Mon Sep 17 00:00:00 2001 From: Carol Alexandru Date: Tue, 3 Aug 2021 07:27:57 +0200 Subject: [PATCH 05/11] minor cleanups --- .../servicelayer/lms/impl/olat/OlatLmsAPITemplate.java | 7 +++---- .../servicelayer/lms/impl/olat/OlatLmsRestTemplate.java | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) 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 68cb949d..92dd4b55 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 @@ -118,7 +118,8 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm this.getRestTemplate().get(); } catch (Exception e) { - return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.OPEN_OLAT, "Unspecific error connecting to OLAT API"); + log.error("Failed to access OLAT course API: ", e); + return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.OPEN_OLAT, e.getMessage()); } return LmsSetupTestResult.ofOkay(LmsType.OPEN_OLAT); } @@ -349,9 +350,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm @Override public Result releaseSEBClientRestriction(final Exam exam) { - @SuppressWarnings("unused") final String quizId = exam.externalId; - return getRestTemplate() .map(t -> this.deleteRestrictionForAssignmentId(t, exam.externalId)) .map(x -> exam); @@ -403,6 +402,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm private Result getRestTemplate() { return Result.tryCatch(() -> { if (this.cachedRestTemplate != null) { return this.cachedRestTemplate; } + final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData(); @@ -425,7 +425,6 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm template.setRequestFactory(clientHttpRequestFactory); this.cachedRestTemplate = template; - return this.cachedRestTemplate; }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java index fd2982e5..1f8e11d5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java @@ -28,7 +28,7 @@ public class OlatLmsRestTemplate extends RestTemplate { private static final Logger log = LoggerFactory.getLogger(OlatLmsRestTemplate.class); - public String token; + private String token; private ClientCredentialsResourceDetails details; public OlatLmsRestTemplate(ClientCredentialsResourceDetails details) { @@ -68,8 +68,8 @@ public class OlatLmsRestTemplate extends RestTemplate { details.getClientId(), details.getClientSecret()); try { - ResponseEntity response = this.getForEntity(authUrl, String.class); - HttpHeaders responseHeaders = response.getHeaders(); + final ResponseEntity response = this.getForEntity(authUrl, String.class); + final HttpHeaders responseHeaders = response.getHeaders(); log.debug("OLAT [authenticate] {} Headers: {}", response.getStatusCode(), responseHeaders); token = responseHeaders.getFirst("X-OLAT-TOKEN"); } From d627bb7edbed93c96c7997818257c560dede2069 Mon Sep 17 00:00:00 2001 From: Carol Alexandru Date: Tue, 3 Aug 2021 09:52:02 +0200 Subject: [PATCH 06/11] fix silly beginner mistake --- .../servicelayer/lms/impl/olat/OlatLmsRestTemplate.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java index 1f8e11d5..f270a648 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java @@ -42,7 +42,7 @@ public class OlatLmsRestTemplate extends RestTemplate { // if there's no token, authenticate first if (token == null) { authenticate(); } // when authenticating, just do a normal call - else if (token == "authenticating") { return execution.execute(request, body); } + else if (token.equals("authenticating")) { return execution.execute(request, body); } // otherwise, add the X-OLAT-TOKEN request.getHeaders().set("accept", "application/json"); request.getHeaders().set("X-OLAT-TOKEN", token); From 9d27dbfbcf0efa9d2757aff9c1073f03943ddcd3 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 5 Aug 2021 09:37:14 +0200 Subject: [PATCH 07/11] Implement SDKToken for Zoom proctoring with SEB client for iOS and MacOS --- .../model/exam/ProctoringRoomConnection.java | 10 +++++ .../model/exam/ProctoringServiceSettings.java | 28 +++++++++++++- .../gbl/model/session/ClientInstruction.java | 1 + .../gui/content/ExamProctoringSettings.java | 37 +++++++++++++++++- .../exam/impl/ExamAdminServiceImpl.java | 20 +++++++++- .../proctoring/JitsiProctoringService.java | 1 + .../proctoring/ZoomProctoringService.java | 38 +++++++++++++++++++ .../config/application-dev-gui.properties | 4 +- src/main/resources/messages.properties | 8 +++- .../JitsiWindowScriptResolverTest.java | 2 + .../ZoomWindowScriptResolverTest.java | 2 + .../admin/ExamProctoringRoomServiceTest.java | 2 +- 12 files changed, 143 insertions(+), 10 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringRoomConnection.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringRoomConnection.java index 264858ef..8a8ac806 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringRoomConnection.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringRoomConnection.java @@ -23,6 +23,7 @@ public class ProctoringRoomConnection { public static final String ATTR_ROOM_NAME = "roomName"; public static final String ATTR_SUBJECT = "subject"; public static final String ATTR_ACCESS_TOKEN = "accessToken"; + public static final String ATTR_SDK_TOKEN = "sdkToken"; public static final String ATTR_CONNECTION_URL = "connectionURL"; public static final String ATTR_USER_NAME = "userName"; public static final String ATTR_ROOM_KEY = "roomKey"; @@ -50,6 +51,9 @@ public class ProctoringRoomConnection { @JsonProperty(ATTR_ACCESS_TOKEN) public final CharSequence accessToken; + @JsonProperty(ATTR_SDK_TOKEN) + public final CharSequence sdkToken; + @JsonProperty(ATTR_ROOM_KEY) public final CharSequence roomKey; @@ -71,6 +75,7 @@ public class ProctoringRoomConnection { @JsonProperty(ATTR_ROOM_NAME) final String roomName, @JsonProperty(ATTR_SUBJECT) final String subject, @JsonProperty(ATTR_ACCESS_TOKEN) final CharSequence accessToken, + @JsonProperty(ATTR_SDK_TOKEN) final CharSequence sdkToken, @JsonProperty(ATTR_ROOM_KEY) final CharSequence roomKey, @JsonProperty(ATTR_API_KEY) final CharSequence apiKey, @JsonProperty(ATTR_MEETING_ID) final String meetingId, @@ -83,6 +88,7 @@ public class ProctoringRoomConnection { this.roomName = roomName; this.subject = subject; this.accessToken = accessToken; + this.sdkToken = sdkToken; this.roomKey = roomKey; this.apiKey = apiKey; this.meetingId = meetingId; @@ -105,6 +111,10 @@ public class ProctoringRoomConnection { return this.accessToken; } + public CharSequence getSdkToken() { + return this.sdkToken; + } + public CharSequence getRoomKey() { return this.roomKey; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java index 4d65b4fc..92c094fe 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java @@ -43,6 +43,8 @@ public class ProctoringServiceSettings implements Entity { public static final String ATTR_SERVER_URL = "serverURL"; public static final String ATTR_APP_KEY = "appKey"; public static final String ATTR_APP_SECRET = "appSecret"; + public static final String ATTR_SDK_KEY = "sdkKey"; + public static final String ATTR_SDK_SECRET = "sdkSecret"; public static final String ATTR_COLLECTING_ROOM_SIZE = "collectingRoomSize"; public static final String ATTR_ENABLED_FEATURES = "enabledFeatures"; public static final String ATTR_COLLECT_ALL_ROOM_NAME = "collectAllRoomName"; @@ -67,6 +69,12 @@ public class ProctoringServiceSettings implements Entity { @JsonProperty(ATTR_APP_SECRET) public final CharSequence appSecret; + @JsonProperty(ATTR_SDK_KEY) + public final String sdkKey; + + @JsonProperty(ATTR_SDK_SECRET) + public final CharSequence sdkSecret; + @JsonProperty(ATTR_COLLECTING_ROOM_SIZE) public final Integer collectingRoomSize; @@ -86,7 +94,9 @@ public class ProctoringServiceSettings implements Entity { @JsonProperty(ATTR_ENABLED_FEATURES) final EnumSet enabledFeatures, @JsonProperty(ATTR_SERVICE_IN_USE) final Boolean serviceInUse, @JsonProperty(ATTR_APP_KEY) final String appKey, - @JsonProperty(ATTR_APP_SECRET) final CharSequence appSecret) { + @JsonProperty(ATTR_APP_SECRET) final CharSequence appSecret, + @JsonProperty(ATTR_SDK_KEY) final String sdkKey, + @JsonProperty(ATTR_SDK_SECRET) final CharSequence sdkSecret) { this.examId = examId; this.enableProctoring = BooleanUtils.isTrue(enableProctoring); @@ -97,6 +107,8 @@ public class ProctoringServiceSettings implements Entity { this.serviceInUse = serviceInUse; this.appKey = appKey; this.appSecret = appSecret; + this.sdkKey = sdkKey; + this.sdkSecret = sdkSecret; } @@ -147,6 +159,14 @@ public class ProctoringServiceSettings implements Entity { return this.appSecret; } + public String getSdkKey() { + return this.sdkKey; + } + + public CharSequence getSdkSecret() { + return this.sdkSecret; + } + public Boolean getServiceInUse() { return this.serviceInUse; } @@ -193,7 +213,11 @@ public class ProctoringServiceSettings implements Entity { builder.append(", appKey="); builder.append(this.appKey); builder.append(", appSecret="); - builder.append(this.appSecret); + builder.append("--"); + builder.append(", sdkKey="); + builder.append(this.sdkKey); + builder.append(", sdkSecret="); + builder.append("--"); builder.append(", collectingRoomSize="); builder.append(this.collectingRoomSize); builder.append(", enabledFeatures="); diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientInstruction.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientInstruction.java index b75f02bc..aa111164 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientInstruction.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/session/ClientInstruction.java @@ -54,6 +54,7 @@ public final class ClientInstruction { public static final String ZOOM_USER_NAME = "zoomUserName"; public static final String ZOOM_API_KEY = "zoomAPIKey"; public static final String ZOOM_TOKEN = "zoomToken"; + public static final String ZOOM_SDK_TOKEN = "zoomSDKToken"; public static final String ZOOM_MEETING_KEY = "zoomMeetingKey"; public static final String ZOOM_RECEIVE_AUDIO = "zoomReceiveAudio"; public static final String ZOOM_RECEIVE_VIDEO = "zoomReceiveVideo"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamProctoringSettings.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamProctoringSettings.java index a8771350..c2a07821 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamProctoringSettings.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamProctoringSettings.java @@ -72,6 +72,11 @@ public class ExamProctoringSettings { new LocTextKey("sebserver.exam.proctoring.form.appkey"); private final static LocTextKey SEB_PROCTORING_FORM_SECRET = new LocTextKey("sebserver.exam.proctoring.form.secret"); + private final static LocTextKey SEB_PROCTORING_FORM_SDKKEY = + new LocTextKey("sebserver.exam.proctoring.form.sdkkey"); + private final static LocTextKey SEB_PROCTORING_FORM_SDKSECRET = + new LocTextKey("sebserver.exam.proctoring.form.sdksecret"); + private final static LocTextKey SEB_PROCTORING_FORM_FEATURES = new LocTextKey("sebserver.exam.proctoring.form.features"); @@ -155,7 +160,9 @@ public class ExamProctoringSettings { featureFlags, false, form.getFieldValue(ProctoringServiceSettings.ATTR_APP_KEY), - form.getFieldValue(ProctoringServiceSettings.ATTR_APP_SECRET)); + form.getFieldValue(ProctoringServiceSettings.ATTR_APP_SECRET), + form.getFieldValue(ProctoringServiceSettings.ATTR_SDK_KEY), + form.getFieldValue(ProctoringServiceSettings.ATTR_SDK_SECRET)); } catch (final Exception e) { log.error("Unexpected error while trying to get settings from form: ", e); @@ -225,6 +232,8 @@ public class ExamProctoringSettings { .copyOf(content) .clearEntityKeys(); + final boolean isZoom = proctoringSettings.serverType == ProctoringServerType.ZOOM; + final FormHandle formHandle = this.pageService.formBuilder( formContext) .withDefaultSpanInput(5) @@ -249,7 +258,8 @@ public class ExamProctoringSettings { ProctoringServiceSettings.ATTR_SERVER_TYPE, SEB_PROCTORING_FORM_TYPE, proctoringSettings.serverType.name(), - resourceService::examProctoringTypeResources)) + resourceService::examProctoringTypeResources) + .withSelectionListener(this::serviceSelection)) .addField(FormBuilder.text( ProctoringServiceSettings.ATTR_SERVER_URL, @@ -269,6 +279,21 @@ public class ExamProctoringSettings { ? String.valueOf(proctoringSettings.appSecret) : null)) + .addField(FormBuilder.text( + ProctoringServiceSettings.ATTR_SDK_KEY, + SEB_PROCTORING_FORM_SDKKEY, + proctoringSettings.sdkKey) + .visibleIf(isZoom)) + .withEmptyCellSeparation(false) + + .addField(FormBuilder.password( + ProctoringServiceSettings.ATTR_SDK_SECRET, + SEB_PROCTORING_FORM_SDKSECRET, + (proctoringSettings.sdkSecret != null) + ? String.valueOf(proctoringSettings.sdkSecret) + : null) + .visibleIf(isZoom)) + .withDefaultSpanInput(1) .addField(FormBuilder.text( ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE, @@ -294,6 +319,14 @@ public class ExamProctoringSettings { return () -> formHandle; } + + private void serviceSelection(final Form form) { + final boolean isZoom = ProctoringServerType.ZOOM.name() + .equals(form.getFieldValue(ProctoringServiceSettings.ATTR_SERVER_TYPE)); + + form.setFieldVisible(isZoom, ProctoringServiceSettings.ATTR_SDK_KEY); + form.setFieldVisible(isZoom, ProctoringServiceSettings.ATTR_SDK_SECRET); + } } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java index bacadf7b..9bef43bd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java @@ -213,7 +213,9 @@ public class ExamAdminServiceImpl implements ExamAdminService { getEnabledFeatures(mapping), this.remoteProctoringRoomDAO.isServiceInUse(examId).getOr(true), getString(mapping, ProctoringServiceSettings.ATTR_APP_KEY), - getString(mapping, ProctoringServiceSettings.ATTR_APP_SECRET)); + getString(mapping, ProctoringServiceSettings.ATTR_APP_SECRET), + getString(mapping, ProctoringServiceSettings.ATTR_SDK_KEY), + getString(mapping, ProctoringServiceSettings.ATTR_SDK_SECRET)); }); } @@ -263,6 +265,22 @@ public class ExamAdminServiceImpl implements ExamAdminService { .getOrThrow() .toString()); + if (StringUtils.isNotBlank(proctoringServiceSettings.appKey)) { + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.EXAM, + examId, + ProctoringServiceSettings.ATTR_SDK_KEY, + proctoringServiceSettings.sdkKey); + + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.EXAM, + examId, + ProctoringServiceSettings.ATTR_SDK_SECRET, + this.cryptor.encrypt(proctoringServiceSettings.sdkSecret) + .getOrThrow() + .toString()); + } + this.additionalAttributesDAO.saveAdditionalAttribute( EntityType.EXAM, examId, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/JitsiProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/JitsiProctoringService.java index 3adb9627..df4b3fc8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/JitsiProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/JitsiProctoringService.java @@ -378,6 +378,7 @@ public class JitsiProctoringService implements ExamProctoringService { null, null, null, + null, clientName); }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java index 20e38042..836bcc91 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java @@ -214,6 +214,9 @@ public class ZoomProctoringService implements ExamProctoringService { attributes.put( ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_TOKEN, String.valueOf(proctoringConnection.accessToken)); + attributes.put( + ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_SDK_TOKEN, + String.valueOf(proctoringConnection.sdkToken)); if (StringUtils.isNotBlank(proctoringConnection.apiKey)) { attributes.put( ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_API_KEY, @@ -262,6 +265,19 @@ public class ZoomProctoringService implements ExamProctoringService { String.valueOf(additionalZoomRoomData.meeting_id), true); + String sdkJWT = null; + if (StringUtils.isNotBlank(proctoringSettings.sdkKey)) { + + final ClientCredentials sdkCredentials = new ClientCredentials( + proctoringSettings.sdkKey, + proctoringSettings.sdkSecret, + remoteProctoringRoom.joinKey); + + sdkJWT = this.createJWTForSDKAccess( + sdkCredentials, + String.valueOf(additionalZoomRoomData.meeting_id)); + } + return new ProctoringRoomConnection( ProctoringServerType.ZOOM, null, @@ -270,6 +286,7 @@ public class ZoomProctoringService implements ExamProctoringService { roomName, subject, jwt, + sdkJWT, credentials.accessToken, credentials.clientId, String.valueOf(additionalZoomRoomData.meeting_id), @@ -308,6 +325,19 @@ public class ZoomProctoringService implements ExamProctoringService { .getConnectionData(connectionToken) .getOrThrow(); + String sdkJWT = null; + if (StringUtils.isNotBlank(proctoringSettings.sdkKey)) { + + final ClientCredentials sdkCredentials = new ClientCredentials( + proctoringSettings.sdkKey, + proctoringSettings.sdkSecret, + remoteProctoringRoom.joinKey); + + sdkJWT = this.createJWTForSDKAccess( + sdkCredentials, + String.valueOf(additionalZoomRoomData.meeting_id)); + } + return new ProctoringRoomConnection( ProctoringServerType.ZOOM, connectionToken, @@ -316,6 +346,7 @@ public class ZoomProctoringService implements ExamProctoringService { roomName, subject, jwt, + sdkJWT, credentials.accessToken, credentials.clientId, String.valueOf(additionalZoomRoomData.meeting_id), @@ -613,6 +644,13 @@ public class ZoomProctoringService implements ExamProctoringService { } } + private String createJWTForSDKAccess( + final ClientCredentials sdkCredentials, + final String meetingId) { + + return createJWTForMeetingAccess(sdkCredentials, meetingId, false); + } + private String createJWTForMeetingAccess( final ClientCredentials credentials, final String meetingId, diff --git a/src/main/resources/config/application-dev-gui.properties b/src/main/resources/config/application-dev-gui.properties index a22c98bc..8069b264 100644 --- a/src/main/resources/config/application-dev-gui.properties +++ b/src/main/resources/config/application-dev-gui.properties @@ -1,11 +1,11 @@ server.address=localhost -server.port=8090 +server.port=8080 sebserver.gui.http.external.scheme=http sebserver.gui.entrypoint=/gui sebserver.gui.webservice.protocol=http sebserver.gui.webservice.address=localhost -sebserver.gui.webservice.port=8090 +sebserver.gui.webservice.port=8080 sebserver.gui.webservice.apipath=/admin-api/v1 # defines the polling interval that is used to poll the webservice for client connection data on a monitored exam page sebserver.gui.webservice.poll-interval=1000 diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 99e790a8..a37f09cd 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -658,10 +658,14 @@ sebserver.exam.proctoring.form.url=Server URL sebserver.exam.proctoring.form.url.tooltip=The proctoring server URL sebserver.exam.proctoring.form.collectingRoomSize=Collecting Room Size sebserver.exam.proctoring.form.collectingRoomSize.tooltip=The size of proctor rooms to collect connecting SEB clients into. -sebserver.exam.proctoring.form.appkey=Application Key +sebserver.exam.proctoring.form.appkey=App Key sebserver.exam.proctoring.form.appkey.tooltip=The application key of the proctoring service server -sebserver.exam.proctoring.form.secret=Secret +sebserver.exam.proctoring.form.secret=App Secret sebserver.exam.proctoring.form.secret.tooltip=The secret used to access the proctoring service +sebserver.exam.proctoring.form.sdkkey=SDK Key (Zoom) +sebserver.exam.proctoring.form.sdkkey.tooltip=The SDK key and secret are used for live proctoring with SEB clients for iOS and/or MacOS +sebserver.exam.proctoring.form.sdksecret=SDK Secret (Zoom) +sebserver.exam.proctoring.form.sdksecret.tooltip=The SDK key and secret are used for live proctoring with SEB clients for iOS and/or MacOS sebserver.exam.proctoring.form.features=Enabled Features sebserver.exam.proctoring.form.features.TOWN_HALL=Town-Hall Room sebserver.exam.proctoring.form.features.ONE_TO_ONE=One to One Room diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/JitsiWindowScriptResolverTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/JitsiWindowScriptResolverTest.java index bda05e45..749c24a4 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/JitsiWindowScriptResolverTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/JitsiWindowScriptResolverTest.java @@ -37,6 +37,7 @@ public class JitsiWindowScriptResolverTest { "ROOM", "SUBJECT", "ACCESS_TOKEN", + null, "API_KEY", "ROOM_KEY", "MEETING_ID", @@ -53,6 +54,7 @@ public class JitsiWindowScriptResolverTest { "ROOM", "SUBJECT", "ACCESS_TOKEN", + null, "API_KEY", "ROOM_KEY", "MEETING_ID", diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ZoomWindowScriptResolverTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ZoomWindowScriptResolverTest.java index a2a20887..5c503575 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ZoomWindowScriptResolverTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ZoomWindowScriptResolverTest.java @@ -37,6 +37,7 @@ public class ZoomWindowScriptResolverTest { "ROOM", "SUBJECT", "ACCESS_TOKEN", + "SDK_TOKEN", "API_KEY", "ROOM_KEY", "MEETING_ID", @@ -53,6 +54,7 @@ public class ZoomWindowScriptResolverTest { "ROOM", "SUBJECT", "ACCESS_TOKEN", + "SDK_TOKEN", "API_KEY", "ROOM_KEY", "MEETING_ID", diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamProctoringRoomServiceTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamProctoringRoomServiceTest.java index ca0e9cee..282352c5 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamProctoringRoomServiceTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamProctoringRoomServiceTest.java @@ -63,7 +63,7 @@ public class ExamProctoringRoomServiceTest extends AdministrationAPIIntegrationT 2L, new ProctoringServiceSettings( 2L, true, ProctoringServerType.JITSI_MEET, "http://jitsi.ch", 1, null, false, - "app-key", "app.secret")); + "app-key", "app.secret", "sdk-key", "sdk.secret")); assertTrue(this.examAdminService.isProctoringEnabled(2L).get()); } From d1685f4675ce770c66675bc196a3ac2fc9280e0f Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 12 Aug 2021 10:55:28 +0200 Subject: [PATCH 08/11] some code cleanup --- .../lms/impl/olat/OlatLmsAPITemplate.java | 122 +++++++++--------- 1 file changed, 64 insertions(+), 58 deletions(-) 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 92dd4b55..27408b08 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 @@ -10,33 +10,28 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Map; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.cache.CacheManager; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.env.Environment; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; -import org.springframework.core.env.Environment; import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.web.client.RestTemplate; import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.web.util.DefaultUriBuilderFactory; -import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode; - +import org.springframework.web.client.RestTemplate; import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; import ch.ethz.seb.sebserver.gbl.api.APIMessage; @@ -50,7 +45,6 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; -import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.Features; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails; @@ -62,9 +56,9 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.AbstractCachedCourseAccess; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsData.AssessmentData; -import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsData.UserData; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsData.RestrictionData; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsData.RestrictionDataPost; +import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsData.UserData; public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements LmsAPITemplate { @@ -116,8 +110,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm } try { this.getRestTemplate().get(); - } - catch (Exception e) { + } catch (final Exception e) { log.error("Failed to access OLAT course API: ", e); return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.OPEN_OLAT, e.getMessage()); } @@ -214,15 +207,15 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm @Override protected Supplier> allQuizzesSupplier(final FilterMap filterMap) { return () -> { - List res = getRestTemplate() - .map(t -> this.collectAllQuizzes(t, filterMap)) - .getOrThrow(); - super.putToCache(res); - return res; + final List res = getRestTemplate() + .map(t -> this.collectAllQuizzes(t, filterMap)) + .getOrThrow(); + super.putToCache(res); + return res; }; } - private String examUrl(long olatRepositoryId) { + private String examUrl(final long olatRepositoryId) { final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); return lmsSetup.lmsApiUrl + "/auth/RepositoryEntry/" + olatRepositoryId; } @@ -234,24 +227,31 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm final long fromCutTime = (quizFromTime != null) ? Utils.toUnixTimeInSeconds(quizFromTime) : -1; String url = "/restapi/assessment_modes/seb?"; - if (fromCutTime != -1) { url = String.format("%sdateFrom=%s&", url, fromCutTime); } - if (quizName != null) { url = String.format("%sname=%s&", url, quizName); } + if (fromCutTime != -1) { + url = String.format("%sdateFrom=%s&", url, fromCutTime); + } + if (quizName != null) { + url = String.format("%sname=%s&", url, quizName); + } - final List as = this.apiGetList(restTemplate, url, new ParameterizedTypeReference>(){}); + final List as = + this.apiGetList(restTemplate, url, new ParameterizedTypeReference>() { + }); return as.stream() - .map(a -> { - return new QuizData( - String.format("%d", a.key), - lmsSetup.getInstitutionId(), - lmsSetup.id, - lmsSetup.getLmsType(), - a.name, - a.description, - Utils.toDateTimeUTC(a.dateFrom), - Utils.toDateTimeUTC(a.dateTo), - examUrl(a.repositoryEntryKey), - new HashMap());}) - .collect(Collectors.toList()); + .map(a -> { + return new QuizData( + String.format("%d", a.key), + lmsSetup.getInstitutionId(), + lmsSetup.id, + lmsSetup.getLmsType(), + a.name, + a.description, + Utils.toDateTimeUTC(a.dateFrom), + Utils.toDateTimeUTC(a.dateTo), + examUrl(a.repositoryEntryKey), + new HashMap()); + }) + .collect(Collectors.toList()); } @Override @@ -261,10 +261,9 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm @Override protected Supplier quizSupplier(final String id) { - final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); return () -> getRestTemplate() - .map(t -> this.quizById(t, id)) - .getOrThrow(); + .map(t -> this.quizById(t, id)) + .getOrThrow(); } private QuizData quizById(final OlatLmsRestTemplate restTemplate, final String id) { @@ -289,14 +288,13 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm final UserData u = this.apiGet(restTemplate, url, UserData.class); final Map attrs = new HashMap<>(); return new ExamineeAccountDetails( - String.valueOf(u.key), - u.lastName + ", " + u.firstName, - u.username, - "OLAT API does not provide email addresses", - attrs); + String.valueOf(u.key), + u.lastName + ", " + u.firstName, + u.username, + "OLAT API does not provide email addresses", + attrs); } - @Override protected Supplier accountDetailsSupplier(final String id) { return () -> getRestTemplate() @@ -311,22 +309,27 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm }; } - private SEBRestriction getRestrictionForAssignmentId(final RestTemplate restTemplate, String id) { + private SEBRestriction getRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) { final String url = String.format("/restapi/assessment_modes/%s/seb_restriction", id); final RestrictionData r = this.apiGet(restTemplate, url, RestrictionData.class); return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap()); } - private SEBRestriction setRestrictionForAssignmentId(final RestTemplate restTemplate, String id, SEBRestriction restriction) { + private SEBRestriction setRestrictionForAssignmentId( + final RestTemplate restTemplate, + final String id, + final SEBRestriction restriction) { + final String url = String.format("/restapi/assessment_modes/%s/seb_restriction", id); final RestrictionDataPost post = new RestrictionDataPost(); post.browserExamKeys = new ArrayList<>(restriction.browserExamKeys); post.configKeys = new ArrayList<>(restriction.configKeys); - final RestrictionData r = this.apiPost(restTemplate, url, post, RestrictionDataPost.class, RestrictionData.class); + final RestrictionData r = + this.apiPost(restTemplate, url, post, RestrictionDataPost.class, RestrictionData.class); return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap()); } - private SEBRestriction deleteRestrictionForAssignmentId(final RestTemplate restTemplate, String id) { + private SEBRestriction deleteRestrictionForAssignmentId(final RestTemplate restTemplate, final String id) { final String url = String.format("/restapi/assessment_modes/%s/seb_restriction", id); final RestrictionData r = this.apiDelete(restTemplate, url, RestrictionData.class); // OLAT returns RestrictionData with null values upon deletion. @@ -350,13 +353,12 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm @Override public Result releaseSEBClientRestriction(final Exam exam) { - final String quizId = exam.externalId; return getRestTemplate() .map(t -> this.deleteRestrictionForAssignmentId(t, exam.externalId)) .map(x -> exam); } - private T apiGet(final RestTemplate restTemplate, String url, Class type) { + private T apiGet(final RestTemplate restTemplate, final String url, final Class type) { final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); final ResponseEntity res = restTemplate.exchange( lmsSetup.lmsApiUrl + url, @@ -366,7 +368,8 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm return res.getBody(); } - private List apiGetList(final RestTemplate restTemplate, String url, ParameterizedTypeReference> type) { + private List apiGetList(final RestTemplate restTemplate, final String url, + final ParameterizedTypeReference> type) { final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); final ResponseEntity> res = restTemplate.exchange( lmsSetup.lmsApiUrl + url, @@ -376,11 +379,12 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm return res.getBody(); } - private R apiPost(final RestTemplate restTemplate, String url, P post, Class

postType, Class responseType) { + private R apiPost(final RestTemplate restTemplate, final String url, final P post, final Class

postType, + final Class responseType) { final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); final HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.set("content-type", "application/json"); - HttpEntity

requestEntity = new HttpEntity<>(post, httpHeaders); + final HttpEntity

requestEntity = new HttpEntity<>(post, httpHeaders); final ResponseEntity res = restTemplate.exchange( lmsSetup.lmsApiUrl + url, HttpMethod.POST, @@ -389,7 +393,7 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm return res.getBody(); } - private T apiDelete(final RestTemplate restTemplate, String url, Class type) { + private T apiDelete(final RestTemplate restTemplate, final String url, final Class type) { final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); final ResponseEntity res = restTemplate.exchange( lmsSetup.lmsApiUrl + url, @@ -401,7 +405,9 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm private Result getRestTemplate() { return Result.tryCatch(() -> { - if (this.cachedRestTemplate != null) { return this.cachedRestTemplate; } + if (this.cachedRestTemplate != null) { + return this.cachedRestTemplate; + } final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup(); final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials(); From dea65b70d22da57196160b1a66f1c32d490bdf8a Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 12 Aug 2021 11:10:58 +0200 Subject: [PATCH 09/11] more code cleaup --- .../lms/impl/olat/OlatLmsData.java | 63 ++++++++++--------- .../lms/impl/olat/OlatLmsRestTemplate.java | 46 +++++++------- 2 files changed, 55 insertions(+), 54 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsData.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsData.java index 9f7dff08..7aab3e28 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsData.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsData.java @@ -11,22 +11,22 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat; import java.util.List; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; public final class OlatLmsData { @JsonIgnoreProperties(ignoreUnknown = true) static public final class AssessmentData { - /* OLAT API example: - { - "courseName": "course 1", - "dateFrom": 1624420800000, - "dateTo": 1624658400000, - "description": "", - "key": 6356992, - “repositoryEntryKey”: 462324, - "name": "SEB test" - } + /* + * OLAT API example: + * { + * "courseName": "course 1", + * "dateFrom": 1624420800000, + * "dateTo": 1624658400000, + * "description": "", + * "key": 6356992, + * “repositoryEntryKey”: 462324, + * "name": "SEB test" + * } */ public long key; public long repositoryEntryKey; @@ -39,13 +39,14 @@ public final class OlatLmsData { @JsonIgnoreProperties(ignoreUnknown = true) static final class UserData { - /* OLAT API example: - { - "firstName": "OpenOLAT", - "key": 360448, - "lastName": "Administrator", - "username": "administrator" - } + /* + * OLAT API example: + * { + * "firstName": "OpenOLAT", + * "key": 360448, + * "lastName": "Administrator", + * "username": "administrator" + * } */ public long key; public String firstName; @@ -55,12 +56,13 @@ public final class OlatLmsData { @JsonIgnoreProperties(ignoreUnknown = true) static final class RestrictionData { - /* OLAT API example: - { - "browserExamKeys": [ "1" ], - "configKeys": null, - "key": 8028160 - } + /* + * OLAT API example: + * { + * "browserExamKeys": [ "1" ], + * "configKeys": null, + * "key": 8028160 + * } */ public long key; public List browserExamKeys; @@ -69,16 +71,15 @@ public final class OlatLmsData { @JsonIgnoreProperties(ignoreUnknown = true) static final class RestrictionDataPost { - /* OLAT API example: - { - "configKeys": ["a", "b"], - "browserExamKeys": ["1", "2"] - } + /* + * OLAT API example: + * { + * "configKeys": ["a", "b"], + * "browserExamKeys": ["1", "2"] + * } */ public List browserExamKeys; public List configKeys; } } - - diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java index f270a648..6c326124 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java @@ -12,17 +12,15 @@ import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import org.springframework.web.client.RestTemplate; -import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpStatus; -import org.springframework.http.HttpRequest; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; +import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; +import org.springframework.web.client.RestTemplate; public class OlatLmsRestTemplate extends RestTemplate { @@ -31,27 +29,32 @@ public class OlatLmsRestTemplate extends RestTemplate { private String token; private ClientCredentialsResourceDetails details; - public OlatLmsRestTemplate(ClientCredentialsResourceDetails details) { + public OlatLmsRestTemplate(final ClientCredentialsResourceDetails details) { super(); this.details = details; // Add X-OLAT-TOKEN request header to every request done using this RestTemplate - this.getInterceptors().add(new ClientHttpRequestInterceptor(){ + this.getInterceptors().add(new ClientHttpRequestInterceptor() { @Override - public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { + public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, + final ClientHttpRequestExecution execution) throws IOException { // if there's no token, authenticate first - if (token == null) { authenticate(); } + if (OlatLmsRestTemplate.this.token == null) { + authenticate(); + } // when authenticating, just do a normal call - else if (token.equals("authenticating")) { return execution.execute(request, body); } + else if (OlatLmsRestTemplate.this.token.equals("authenticating")) { + return execution.execute(request, body); + } // otherwise, add the X-OLAT-TOKEN request.getHeaders().set("accept", "application/json"); - request.getHeaders().set("X-OLAT-TOKEN", token); + request.getHeaders().set("X-OLAT-TOKEN", OlatLmsRestTemplate.this.token); ClientHttpResponse response = execution.execute(request, body); log.debug("OLAT [regular API call] {} Headers: {}", response.getStatusCode(), response.getHeaders()); // If we get a 401, re-authenticate and try once more if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) { authenticate(); - request.getHeaders().set("X-OLAT-TOKEN", token); + request.getHeaders().set("X-OLAT-TOKEN", OlatLmsRestTemplate.this.token); response = execution.execute(request, body); log.debug("OLAT [retry API call] {} Headers: {}", response.getStatusCode(), response.getHeaders()); } @@ -62,23 +65,20 @@ public class OlatLmsRestTemplate extends RestTemplate { private void authenticate() { // Authenticate with OLAT and store the received X-OLAT-TOKEN - token = "authenticating"; + this.token = "authenticating"; final String authUrl = String.format("%s%s?password=%s", - details.getAccessTokenUri(), - details.getClientId(), - details.getClientSecret()); + this.details.getAccessTokenUri(), + this.details.getClientId(), + this.details.getClientSecret()); try { final ResponseEntity response = this.getForEntity(authUrl, String.class); final HttpHeaders responseHeaders = response.getHeaders(); log.debug("OLAT [authenticate] {} Headers: {}", response.getStatusCode(), responseHeaders); - token = responseHeaders.getFirst("X-OLAT-TOKEN"); - } - catch (Exception e) { - token = null; + this.token = responseHeaders.getFirst("X-OLAT-TOKEN"); + } catch (final Exception e) { + this.token = null; throw e; } } - } - From ebf0fab2f0bec8c2e9c64f3a7a3baa5168dc4610 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 12 Aug 2021 13:29:53 +0200 Subject: [PATCH 10/11] added OLAT LMS for selection --- .../ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java index d6dc48e8..223a53f3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java @@ -63,7 +63,7 @@ public final class LmsSetup implements GrantEntity, Activatable { /** The Ans Delft binding is on the way */ ANS_DELFT(), /** The OpenOLAT binding is on the way */ - OPEN_OLAT(); + OPEN_OLAT(Features.COURSE_API, Features.SEB_RESTRICTION); public final EnumSet features; From 6abfeb17a82b3739141dde67093c2b37a8b40674 Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 12 Aug 2021 15:31:25 +0200 Subject: [PATCH 11/11] correct messages --- src/main/resources/messages.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index a37f09cd..1b11ebaa 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -662,9 +662,9 @@ sebserver.exam.proctoring.form.appkey=App Key sebserver.exam.proctoring.form.appkey.tooltip=The application key of the proctoring service server sebserver.exam.proctoring.form.secret=App Secret sebserver.exam.proctoring.form.secret.tooltip=The secret used to access the proctoring service -sebserver.exam.proctoring.form.sdkkey=SDK Key (Zoom) +sebserver.exam.proctoring.form.sdkkey=SDK Key (MacOS/iOS) sebserver.exam.proctoring.form.sdkkey.tooltip=The SDK key and secret are used for live proctoring with SEB clients for iOS and/or MacOS -sebserver.exam.proctoring.form.sdksecret=SDK Secret (Zoom) +sebserver.exam.proctoring.form.sdksecret=SDK Secret (MacOS/iOS) sebserver.exam.proctoring.form.sdksecret.tooltip=The SDK key and secret are used for live proctoring with SEB clients for iOS and/or MacOS sebserver.exam.proctoring.form.features=Enabled Features sebserver.exam.proctoring.form.features.TOWN_HALL=Town-Hall Room