SEBSERV-417 adapt Moodle API calls, error handling and fixes

This commit is contained in:
anhefti 2024-06-06 10:19:01 +02:00
parent 012b0e2f99
commit f9de99d9bc
17 changed files with 110 additions and 34 deletions

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2019 ETH Zürich, IT Services
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gbl.model.user;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import com.fasterxml.jackson.annotation.JsonProperty;
public final class LoginForward {
@JsonProperty("entityKey")
public final EntityKey entityKey;
@JsonProperty("actionName")
public final String actionName;
public LoginForward(
@JsonProperty("entityKey") final EntityKey entityKey,
@JsonProperty("actionName") final String actionName) {
this.entityKey = entityKey;
this.actionName = actionName;
}
}

View file

@ -8,7 +8,6 @@
package ch.ethz.seb.sebserver.gbl.model.user; package ch.ethz.seb.sebserver.gbl.model.user;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2AccessToken;
@ -19,7 +18,7 @@ public class TokenLoginInfo {
@JsonProperty("userUUID") @JsonProperty("userUUID")
public final String userUUID; public final String userUUID;
@JsonProperty("redirect") @JsonProperty("redirect")
public final EntityKey redirect; public final LoginForward login_forward;
@JsonProperty("login") @JsonProperty("login")
public final OAuth2AccessToken login; public final OAuth2AccessToken login;
@ -27,12 +26,13 @@ public class TokenLoginInfo {
public TokenLoginInfo( public TokenLoginInfo(
@JsonProperty("username") final String username, @JsonProperty("username") final String username,
@JsonProperty("userUUID") final String userUUID, @JsonProperty("userUUID") final String userUUID,
@JsonProperty("redirect") final EntityKey redirect, @JsonProperty("redirect") final LoginForward login_forward,
@JsonProperty("login") final OAuth2AccessToken login) { @JsonProperty("login") final OAuth2AccessToken login) {
this.username = username; this.username = username;
this.userUUID = userUUID; this.userUUID = userUUID;
this.redirect = redirect; this.login_forward = login_forward;
this.login = login; this.login = login;
} }
} }

View file

@ -21,6 +21,8 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext;
import org.apache.commons.codec.binary.Base64InputStream; import org.apache.commons.codec.binary.Base64InputStream;

View file

@ -8,8 +8,16 @@
package ch.ethz.seb.sebserver.gui.content; package ch.ethz.seb.sebserver.gui.content;
import javax.servlet.http.HttpServletRequest;
import java.util.function.Consumer; import java.util.function.Consumer;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.user.LoginForward;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder;
import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.SWT; import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.custom.SashForm;
@ -54,13 +62,19 @@ public class MainPage implements TemplateComposer {
private final WidgetFactory widgetFactory; private final WidgetFactory widgetFactory;
private final PolyglotPageService polyglotPageService; private final PolyglotPageService polyglotPageService;
private final AuthorizationContextHolder authorizationContextHolder;
private final PageService pageService;
public MainPage( public MainPage(
final WidgetFactory widgetFactory, final WidgetFactory widgetFactory,
final PolyglotPageService polyglotPageService) { final PolyglotPageService polyglotPageService,
final AuthorizationContextHolder authorizationContextHolder,
final PageService pageService) {
this.widgetFactory = widgetFactory; this.widgetFactory = widgetFactory;
this.polyglotPageService = polyglotPageService; this.polyglotPageService = polyglotPageService;
this.authorizationContextHolder = authorizationContextHolder;
this.pageService = pageService;
} }
@Override @Override
@ -158,6 +172,19 @@ public class MainPage implements TemplateComposer {
pageContext.copyOf(nav)); pageContext.copyOf(nav));
mainSash.setWeights(DEFAULT_SASH_WEIGHTS); mainSash.setWeights(DEFAULT_SASH_WEIGHTS);
final LoginForward loginForward = authorizationContextHolder
.getAuthorizationContext()
.getLoginForward();
if (loginForward != null) {
final PageAction pageAction = pageService.pageActionBuilder(pageContext)
.newAction( ActionDefinition.valueOf(loginForward.actionName))
.withEntityKey(loginForward.entityKey)
.create();
pageService.executePageAction(pageAction);
}
} }
private static final class ContentActionEventListener implements ActionEventListener { private static final class ContentActionEventListener implements ActionEventListener {

View file

@ -8,6 +8,7 @@
package ch.ethz.seb.sebserver.gui.content.action; package ch.ethz.seb.sebserver.gui.content.action;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gui.content.activity.PageStateDefinitionImpl; import ch.ethz.seb.sebserver.gui.content.activity.PageStateDefinitionImpl;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageStateDefinition; import ch.ethz.seb.sebserver.gui.service.page.PageStateDefinition;

View file

@ -456,8 +456,9 @@ public class LmsSetupForm implements TemplateComposer {
} else if (formHandle != null && formHandle.handleError(result.getError())) { } else if (formHandle != null && formHandle.handleError(result.getError())) {
action.pageContext().notifyActivationError(EntityType.LMS_SETUP, error); action.pageContext().notifyActivationError(EntityType.LMS_SETUP, error);
} else { } else {
result.getOrThrow(); action.pageContext().notifyUnexpectedError(result.getError());
} }
return action;
} }
return handleTestResult( return handleTestResult(

View file

@ -15,6 +15,8 @@ import java.nio.charset.StandardCharsets;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.user.LoginForward;
import ch.ethz.seb.sebserver.gbl.model.user.TokenLoginInfo; import ch.ethz.seb.sebserver.gbl.model.user.TokenLoginInfo;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -152,6 +154,7 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol
private final String jwtTokenVerificationURI; private final String jwtTokenVerificationURI;
private Result<UserInfo> loggedInUser = null; private Result<UserInfo> loggedInUser = null;
private LoginForward loginForward = null;
OAuth2AuthorizationContext( OAuth2AuthorizationContext(
final String guiClientId, final String guiClientId,
@ -215,6 +218,11 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol
return null; return null;
} }
@Override
public LoginForward getLoginForward() {
return loginForward;
}
@Override @Override
public boolean login(final String username, final CharSequence password) { public boolean login(final String username, final CharSequence password) {
if (!this.valid || this.isLoggedIn()) { if (!this.valid || this.isLoggedIn()) {
@ -282,6 +290,7 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol
final TokenLoginInfo loginInfo = response.getBody(); final TokenLoginInfo loginInfo = response.getBody();
this.restTemplate.getOAuth2ClientContext().setAccessToken(loginInfo.login); this.restTemplate.getOAuth2ClientContext().setAccessToken(loginInfo.login);
loginForward = loginInfo.login_forward;
return this.isLoggedIn(); return this.isLoggedIn();
} catch (final Exception e) { } catch (final Exception e) {
log.warn("Autologin failed due to unexpected error: {}", e.getMessage()); log.warn("Autologin failed due to unexpected error: {}", e.getMessage());

View file

@ -8,6 +8,8 @@
package ch.ethz.seb.sebserver.gui.service.remote.webservice.auth; package ch.ethz.seb.sebserver.gui.service.remote.webservice.auth;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.user.LoginForward;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
@ -67,4 +69,5 @@ public interface SEBServerAuthorizationContext {
CharSequence getUserPassword(); CharSequence getUserPassword();
LoginForward getLoginForward();
} }

View file

@ -15,10 +15,7 @@ import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.user.TokenLoginInfo; import ch.ethz.seb.sebserver.gbl.model.user.*;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserMod;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Cryptor; import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
@ -187,11 +184,14 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
final OAuth2AccessToken token = accessToken.getBody(); final OAuth2AccessToken token = accessToken.getBody();
final String examId = claims.get(EXAM_ID_CLAIM, String.class); final String examId = claims.get(EXAM_ID_CLAIM, String.class);
final EntityKey redirectTo = (StringUtils.isNotBlank(examId)) final EntityKey key = (StringUtils.isNotBlank(examId))
? new EntityKey(examId, EntityType.EXAM) ? new EntityKey(examId, EntityType.EXAM)
: null; : null;
final LoginForward loginForward = new LoginForward(
key,
"MONITOR_EXAM_FROM_LIST");
return new TokenLoginInfo(user.username, user.uuid, redirectTo, token); return new TokenLoginInfo(user.username, user.uuid, loginForward, token);
}); });
} }

View file

@ -144,8 +144,6 @@ public interface FullLmsIntegrationService {
public final String name; public final String name;
@JsonProperty("url") @JsonProperty("url")
public final String url; public final String url;
@JsonProperty("autologin_url")
public final String autoLoginURL;
@JsonProperty("access_token") @JsonProperty("access_token")
public final String access_token; public final String access_token;
@JsonProperty("exam_templates") @JsonProperty("exam_templates")
@ -156,14 +154,12 @@ public interface FullLmsIntegrationService {
@JsonProperty("id") final String id, @JsonProperty("id") final String id,
@JsonProperty("name") final String name, @JsonProperty("name") final String name,
@JsonProperty("url") final String url, @JsonProperty("url") final String url,
@JsonProperty("autologin_url") final String autoLoginURL,
@JsonProperty("access_token") final String access_token, @JsonProperty("access_token") final String access_token,
@JsonProperty("exam_templates") final Collection<ExamTemplateSelection> exam_templates) { @JsonProperty("exam_templates") final Collection<ExamTemplateSelection> exam_templates) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.url = url; this.url = url;
this.autoLoginURL = autoLoginURL;
this.access_token = access_token; this.access_token = access_token;
this.exam_templates = Utils.immutableCollectionOf(exam_templates); this.exam_templates = Utils.immutableCollectionOf(exam_templates);
} }

View file

@ -8,6 +8,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms; package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
@ -30,7 +31,7 @@ public interface LmsAPITemplateCacheService {
* @return LmsAPITemplate for specified LmsSetup configuration */ * @return LmsAPITemplate for specified LmsSetup configuration */
Result<LmsAPITemplate> getLmsAPITemplate(String lmsSetupId); Result<LmsAPITemplate> getLmsAPITemplate(String lmsSetupId);
Result<LmsAPITemplate> getLmsAPITemplateForTesting(String lmsSetupId); Result<LmsAPITemplate> createInMemoryLmsAPITemplate(LmsSetup lmsSetup);
void clearCache(String lmsSetupId); void clearCache(String lmsSetupId);

View file

@ -49,7 +49,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplateCacheServ
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationChangeEvent; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationChangeEvent;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationService; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -241,7 +240,6 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
connectionId, connectionId,
lmsSetup.name, lmsSetup.name,
getAPIRootURL(), getAPIRootURL(),
getAutoLoginURL(),
accessToken, accessToken,
this.getIntegrationTemplates(lmsSetup.institutionId) this.getIntegrationTemplates(lmsSetup.institutionId)
); );
@ -317,8 +315,6 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
.flatMap(this::findExam) .flatMap(this::findExam)
.map(this::checkDeletion) .map(this::checkDeletion)
.map(this::logExamDeleted) .map(this::logExamDeleted)
.flatMap(teacherAccountServiceImpl::deactivateTeacherAccountsForExam)
.map(exam -> applyExamData(exam, true))
.flatMap(deleteExamAction::deleteExamInternal); .flatMap(deleteExamAction::deleteExamInternal);
} }

View file

@ -251,6 +251,16 @@ public class LmsAPITemplateAdapter implements LmsAPITemplate {
}).getOrThrow(); }).getOrThrow();
} }
@Override
public String getCourseIdFromExam(final Exam exam) {
return this.courseAccessAPI.getCourseIdFromExam(exam);
}
@Override
public String getQuizIdFromExam(final Exam exam) {
return this.courseAccessAPI.getQuizIdFromExam(exam);
}
@Override @Override
public LmsSetupTestResult testCourseAccessAPI() { public LmsSetupTestResult testCourseAccessAPI() {
if (this.courseAccessAPI != null) { if (this.courseAccessAPI != null) {

View file

@ -8,6 +8,8 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl; package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
import static ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport.lmsSetupId;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.EnumMap; import java.util.EnumMap;
@ -94,12 +96,13 @@ public class LmsAPITemplateCacheServiceImpl implements LmsAPITemplateCacheServic
} }
@Override @Override
public Result<LmsAPITemplate> getLmsAPITemplateForTesting(final String lmsSetupId) { public Result<LmsAPITemplate> createInMemoryLmsAPITemplate(final LmsSetup lmsSetup) {
return lmsSetupDAO.byModelId(lmsSetupId) return Result.tryCatch(() -> {
.map(lmsSetup -> new AdHocAPITemplateDataSupplier( final AdHocAPITemplateDataSupplier adHocAPITemplateDataSupplier = new AdHocAPITemplateDataSupplier(
lmsSetup, lmsSetup,
this.clientCredentialService)) this.clientCredentialService);
.flatMap(this::createLmsSetupTemplate); return this.createLmsSetupTemplate(adHocAPITemplateDataSupplier).getOrThrow();
});
} }
@Override @Override

View file

@ -87,7 +87,7 @@ public class LmsTestServiceImpl implements LmsTestService {
public LmsSetupTestResult testAdHoc(final LmsSetup lmsSetup) { public LmsSetupTestResult testAdHoc(final LmsSetup lmsSetup) {
final Result<LmsAPITemplate> createLmsSetupTemplate = lmsAPITemplateCacheService final Result<LmsAPITemplate> createLmsSetupTemplate = lmsAPITemplateCacheService
.getLmsAPITemplateForTesting(lmsSetup.getModelId()); .createInMemoryLmsAPITemplate(lmsSetup);
if (createLmsSetupTemplate.hasError()) { if (createLmsSetupTemplate.hasError()) {
return new LmsSetupTestResult( return new LmsSetupTestResult(
lmsSetup.lmsType, lmsSetup.lmsType,

View file

@ -379,9 +379,9 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
queryParam.queryParams(queryParams); queryParam.queryParams(queryParams);
} }
final boolean usePOST = queryAttributes != null && !queryAttributes.isEmpty(); //final boolean usePOST = queryAttributes != null && !queryAttributes.isEmpty();
final HttpEntity<?> functionReqEntity; final HttpEntity<?> functionReqEntity;
if (usePOST) { if ( queryAttributes != null && !queryAttributes.isEmpty()) {
final HttpHeaders headers = new HttpHeaders(); final HttpHeaders headers = new HttpHeaders();
headers.set( headers.set(
HttpHeaders.CONTENT_TYPE, HttpHeaders.CONTENT_TYPE,
@ -394,7 +394,7 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
functionReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>()); functionReqEntity = new HttpEntity<>(new LinkedMultiValueMap<>());
} }
return doRequest(functionName, queryParam, usePOST, functionReqEntity); return doRequest(functionName, queryParam, true, functionReqEntity);
} }
@Override @Override
@ -465,7 +465,7 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
final ResponseEntity<String> response = super.exchange( final ResponseEntity<String> response = super.exchange(
this.serverURL + this.tokenPath, this.serverURL + this.tokenPath,
HttpMethod.GET, HttpMethod.POST,
this.tokenReqEntity, this.tokenReqEntity,
String.class, String.class,
this.tokenReqURIVars); this.tokenReqURIVars);

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin; package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
@ -156,7 +157,7 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI {
final MoodleAPIRestTemplate rest = getRestTemplate().getOrThrow(); final MoodleAPIRestTemplate rest = getRestTemplate().getOrThrow();
final Map<String, Map<String, String>> attributes = new HashMap<>(); final Map<String, Map<String, String>> attributes = new HashMap<>();
final Map<String, String> data_mapping = new HashMap<>(); final Map<String, String> data_mapping = new LinkedHashMap<>();
attributes.put(ATTRIBUTE_EXAM_DATA, data_mapping); attributes.put(ATTRIBUTE_EXAM_DATA, data_mapping);
// data[quizid]= int // data[quizid]= int
@ -168,7 +169,7 @@ public class MoodlePluginFullIntegration implements FullLmsIntegrationAPI {
if (BooleanUtils.isTrue(examData.exam_created)) { if (BooleanUtils.isTrue(examData.exam_created)) {
data_mapping.put("addordelete", "1"); data_mapping.put("addordelete", "1");
data_mapping.put("templateid", examData.template_id); data_mapping.put("templateid", examData.template_id);
data_mapping.put("showquitlink", BooleanUtils.isTrue(examData.show_quit_link) ? "1" : "2"); data_mapping.put("showquitlink", BooleanUtils.isTrue(examData.show_quit_link) ? "1" : "0");
data_mapping.put("quitsecret", examData.quit_password); data_mapping.put("quitsecret", examData.quit_password);
} else { } else {
data_mapping.put("addordelete", "0"); data_mapping.put("addordelete", "0");