SEBSERV-417 finished first part

This commit is contained in:
anhefti 2024-05-08 14:29:53 +02:00
parent 4b675bc717
commit 3501c5de05
14 changed files with 204 additions and 40 deletions

View file

@ -175,7 +175,7 @@ public final class API {
public static final String LMS_FULL_INTEGRATION_EXAM_TEMPLATE_ID = "exam_template_id";
public static final String LMS_FULL_INTEGRATION_QUIT_PASSWORD = "quit_password";
public static final String LMS_FULL_INTEGRATION_QUIT_LINK = "quit_link";
public static final String LMS_FULL_INTEGRATION_TIME_ZONE = "account_time_zone";
public static final String USER_ACCOUNT_ENDPOINT = "/useraccount";

View file

@ -21,6 +21,7 @@ import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
@ -95,6 +96,12 @@ public final class UserMod implements UserAccount {
@JsonProperty(PasswordChange.ATTR_NAME_CONFIRM_NEW_PASSWORD)
private final CharSequence confirmNewPassword;
@JsonProperty(USER.ATTR_LOCAL_ACCOUNT)
private final Boolean isLocalAccount;
@JsonProperty(USER.ATTR_DIRECT_LOGIN)
private final Boolean hasDirectLogin;
@JsonCreator
public UserMod(
@JsonProperty(USER.ATTR_UUID) final String uuid,
@ -107,6 +114,8 @@ public final class UserMod implements UserAccount {
@JsonProperty(USER.ATTR_EMAIL) final String email,
@JsonProperty(USER.ATTR_LANGUAGE) final Locale language,
@JsonProperty(USER.ATTR_TIMEZONE) final DateTimeZone timeZone,
@JsonProperty(USER.ATTR_LOCAL_ACCOUNT) final Boolean isLocalAccount,
@JsonProperty(USER.ATTR_DIRECT_LOGIN) final Boolean hasDirectLogin,
@JsonProperty(USER_ROLE.REFERENCE_NAME) final Set<String> roles) {
this.uuid = uuid;
@ -119,6 +128,8 @@ public final class UserMod implements UserAccount {
this.email = email;
this.language = (language != null) ? language : Locale.ENGLISH;
this.timeZone = (timeZone != null) ? timeZone : DateTimeZone.UTC;
this.isLocalAccount = BooleanUtils.isNotFalse(isLocalAccount);
this.hasDirectLogin = BooleanUtils.isNotFalse(hasDirectLogin);
this.roles = (roles != null)
? Collections.unmodifiableSet(roles)
: Collections.emptySet();
@ -136,6 +147,8 @@ public final class UserMod implements UserAccount {
this.language = postAttrMapper.getLocale(USER.ATTR_LANGUAGE);
this.timeZone = postAttrMapper.getDateTimeZone(USER.ATTR_TIMEZONE);
this.roles = postAttrMapper.getStringSet(USER_ROLE.REFERENCE_NAME);
this.isLocalAccount = BooleanUtils.isNotFalse(postAttrMapper.getBoolean(USER.ATTR_LOCAL_ACCOUNT));
this.hasDirectLogin = BooleanUtils.isNotFalse(postAttrMapper.getBoolean(USER.ATTR_DIRECT_LOGIN));
}
@Override
@ -237,6 +250,15 @@ public final class UserMod implements UserAccount {
return false;
}
public Boolean isLocalAccount() {
return isLocalAccount;
}
public Boolean hasDirectLogin() {
return hasDirectLogin;
}
@JsonIgnore
@Override
public EntityKey getEntityKey() {
@ -279,7 +301,7 @@ public final class UserMod implements UserAccount {
return new UserMod(
UUID.randomUUID().toString(),
institutionId,
null, null, null, null, null, null, null, null, null);
null, null, null, null, null, null, null, null, true, true, null);
}
}

View file

@ -145,6 +145,8 @@ class AdminUserInitializer {
null,
null,
null,
true,
true,
new HashSet<>(this.webserviceInfo.isLightSetup() ?
UserRole.getLightSetupRoles() :
List.of(UserRole.SEB_SERVER_ADMIN.name())

View file

@ -50,6 +50,7 @@ public class WebserviceInfo {
"sebserver.webservice.api.exam.endpoint.discovery";
private static final String WEB_SERVICE_EXTERNAL_ADDRESS_ALIAS = "sebserver.webservice.lms.address.alias";
private static final String WEB_SERVICE_CONTEXT_PATH = "server.servlet.context-path";
public static final String SEBSERVER_WEBSERVICE_AUTOLOGIN_ENDPOINT = "sebserver.webservice.autologin.endpoint";
private final String sebServerVersion;
private final String testProperty;
@ -61,6 +62,8 @@ public class WebserviceInfo {
private final String discoveryEndpoint;
private final String contextPath;
private final String autoLoginEndpoint;
private final boolean isLightSetup;
private final String serverURLPrefix;
private final boolean isDistributed;
@ -104,6 +107,9 @@ public class WebserviceInfo {
this.webserviceUUID = UUID.randomUUID().toString()
+ Constants.UNDERLINE
+ this.sebServerVersion;
this.autoLoginEndpoint = environment.getProperty(
SEBSERVER_WEBSERVICE_AUTOLOGIN_ENDPOINT,
"/auto_login");
this.distributedUpdateInterval = environment.getProperty(
"sebserver.webservice.distributed.updateInterval",
@ -237,6 +243,10 @@ public class WebserviceInfo {
return this.discoveryEndpoint;
}
public String getAutoLoginEndpoint() {
return autoLoginEndpoint;
}
public String getDiscoveryEndpointAddress() {
return this.serverURLPrefix + this.discoveryEndpoint;
}

View file

@ -113,16 +113,6 @@ public interface EntityDAO<T extends Entity, M extends ModelIdAware> {
* happened */
Result<Collection<EntityKey>> delete(Set<EntityKey> all);
@Transactional
default Result<EntityKey> deleteOne(final Long examId) {
if (examId == null) {
return Result.ofRuntimeError("exam Id has null reference");
}
return delete( new HashSet<>(Arrays.asList(new EntityKey(examId, EntityType.EXAM))))
.map(set -> set.iterator().next())
.onError(TransactionHandler::rollback);
}
/** Get a (unordered) collection of all Entities that matches the given filter criteria.
* The possible filter criteria for a specific Entity type is defined by the entity type.
* <p>

View file

@ -252,8 +252,8 @@ public class UserDAOImpl implements UserDAO {
userMod.language.toLanguageTag(),
userMod.timeZone.getID(),
BooleanUtils.toInteger(false),
BooleanUtils.toInteger(true),
BooleanUtils.toInteger(true));
BooleanUtils.toInteger(userMod.hasDirectLogin()),
BooleanUtils.toInteger(userMod.isLocalAccount()));
this.userRecordMapper.insert(recordToSave);
final Long newUserPK = recordToSave.getId();

View file

@ -42,7 +42,8 @@ public interface FullLmsIntegrationService {
String quizId,
String examTemplateId,
String quitPassword,
String quitLink);
String quitLink,
String timezone);
Result<EntityKey> deleteExam(
String lmsUUID,
@ -66,6 +67,9 @@ public interface FullLmsIntegrationService {
public final String name;
@JsonProperty("url")
public final String url;
@JsonProperty("autologin_url")
public final String autoLoginURL;
@JsonProperty("access_token")
public final String access_token;
@JsonProperty("exam_templates")
@ -76,12 +80,14 @@ public interface FullLmsIntegrationService {
@JsonProperty("id") final String id,
@JsonProperty("name") final String name,
@JsonProperty("url") final String url,
@JsonProperty("autologin_url") final String autoLoginURL,
@JsonProperty("access_token") final String access_token,
@JsonProperty("exam_templates") final Collection<ExamTemplateSelection> exam_templates) {
this.id = id;
this.name = name;
this.url = url;
this.autoLoginURL = autoLoginURL;
this.access_token = access_token;
this.exam_templates = Utils.immutableCollectionOf(exam_templates);
}

View file

@ -12,12 +12,15 @@ import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.Locale;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
@ -26,8 +29,12 @@ import ch.ethz.seb.sebserver.gbl.model.exam.ExamTemplate;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.SEBClientConfig;
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.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser;
@ -44,6 +51,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigServi
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
@ -62,6 +70,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
private final LmsSetupDAO lmsSetupDAO;
private final UserActivityLogDAO userActivityLogDAO;
private final UserDAO userDAO;
private final SEBClientConfigDAO sebClientConfigDAO;
private final ClientConfigService clientConfigService;
private final DeleteExamAction deleteExamAction;
@ -79,6 +88,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
public FullLmsIntegrationServiceImpl(
final LmsSetupDAO lmsSetupDAO,
final UserActivityLogDAO userActivityLogDAO,
final UserDAO userDAO,
final SEBClientConfigDAO sebClientConfigDAO,
final ClientConfigService clientConfigService,
final DeleteExamAction deleteExamAction,
@ -96,6 +106,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
this.lmsSetupDAO = lmsSetupDAO;
this.userActivityLogDAO = userActivityLogDAO;
this.userDAO = userDAO;
this.sebClientConfigDAO = sebClientConfigDAO;
this.clientConfigService = clientConfigService;
this.deleteExamAction = deleteExamAction;
@ -126,6 +137,11 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
.add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
}
@Override
public void notifyExamDeletion(final ExamDeletionEvent event) {
event.ids.forEach(this::deleteAdHocAccount);
}
@Override
public void notifyLmsSetupChange(final LmsSetupChangeEvent event) {
final LmsSetup lmsSetup = event.getLmsSetup();
@ -187,6 +203,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
connectionId,
lmsSetup.name,
getAPIRootURL(),
getAutoLoginURL(),
accessToken,
this.getIntegrationTemplates(lmsSetup.institutionId)
);
@ -238,13 +255,14 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
final String quizId,
final String examTemplateId,
final String quitPassword,
final String quitLink) {
final String quitLink,
final String timezone) {
return lmsSetupDAO
.getLmsSetupIdByConnectionId(lmsUUID)
.flatMap(lmsAPIService::getLmsAPITemplate)
.map(findQuizData(courseId, quizId))
.map(createAccountAndExam(examTemplateId, quitPassword));
.map(createAccountAndExam(examTemplateId, quitPassword, timezone));
}
@Override
@ -260,6 +278,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
.flatMap(this::findExam)
.map(this::checkDeletion)
.map(this::logExamDeleted)
.map(this::deleteAdHocAccount)
.flatMap(deleteExamAction::deleteExamFromLMSIntegration);
}
@ -276,10 +295,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
return exam;
}
@Override
public void notifyExamDeletion(final ExamDeletionEvent event) {
event.ids.forEach(this::deleteAdHocAccount);
}
@Override
public Result<Void> streamConnectionConfiguration(
@ -306,12 +322,15 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
if (StringUtils.isBlank(connectionConfigId)) {
connectionConfigId = this.sebClientConfigDAO
.all(exam.institutionId, true)
.map(all -> all.iterator().next())
.map(all -> all.stream().filter(config -> config.configPurpose == SEBClientConfig.ConfigPurpose.START_EXAM)
.findFirst()
.orElseThrow(() -> new APIMessage.APIMessageException(
APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT.of("No active Connection Configuration found"))))
.map(SEBClientConfig::getModelId)
.getOr(null);
}
if (StringUtils.isBlank(connectionConfigId)) {
return Result.ofRuntimeError("No active Connection Configuration found");
throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.ILLEGAL_API_ARGUMENT.of("No active Connection Configuration found"));
}
this.clientConfigService.exportSEBClientConfiguration(out, connectionConfigId, exam.id);
@ -358,7 +377,8 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
private Function<QuizData, Exam> createAccountAndExam(
final String examTemplateId,
final String quitPassword) {
final String quitPassword,
final String timezone) {
return quizData -> {
@ -378,8 +398,13 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
post.putIfAbsent(Domain.EXAM.ATTR_QUIT_PASSWORD, quitPassword);
}
final String accountUUID = createAdHocSupporterAccount(quizData);
post.putIfAbsent(Domain.EXAM.ATTR_OWNER, accountUUID);
final String accountUUID = createAdHocSupporterAccount(quizData, timezone);
if (accountUUID != null) {
post.putIfAbsent(Domain.EXAM.ATTR_OWNER, accountUUID);
// TODO do we need to apply the ad-hoc teacher account also as supporter?
} else {
post.putIfAbsent(Domain.EXAM.ATTR_OWNER, userService.getCurrentUser().uuid());
}
final Exam exam = new Exam(null, quizData, post);
return examDAO
@ -390,16 +415,81 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
};
}
private String createAdHocSupporterAccount(final QuizData data) {
// TODO create an ad hoc supporter account for this exam and apply it to the exam
return "mockAccountUUID";
private String createAdHocSupporterAccount(final QuizData data, final String timezone) {
try {
final String uuid = UUID.randomUUID().toString();
final String name = "teacher-" + uuid;
DateTimeZone dtz = DateTimeZone.UTC;
if (StringUtils.isNotBlank(timezone)) {
try {
dtz = DateTimeZone.forID(timezone);
} catch (final Exception e) {
log.warn("Failed to set requested time zone for ad-hoc teacher account: {}", timezone);
}
}
final UserMod adHocTeacherUser = new UserMod(
uuid,
data.institutionId,
name,
data.id,
name,
uuid,
uuid,
null,
Locale.ENGLISH,
dtz,
true,
false,
Utils.immutableSetOf(UserRole.TEACHER.name()));
userDAO.createNew(adHocTeacherUser)
.flatMap(account -> userDAO.setActive(account, true))
.getOrThrow();
return uuid;
} catch (final Exception e) {
log.error("Failed to create ad-hoc teacher account for importing exam: {}", data, e);
return null;
}
}
private Exam deleteAdHocAccount(final Exam exam) {
deleteAdHocAccount(exam.id);
return exam;
}
private void deleteAdHocAccount(final Long examId) {
try {
// TODO check if exam has an ad-hoc account and if true, delete it
final Result<Exam> examResult = examDAO.byPK(examId);
if (examResult.hasError()) {
log.warn("Failed to get exam for id: {}", examId);
return;
}
final String externalId = examResult.get().externalId;
final FilterMap filter = new FilterMap();
filter.putIfAbsent(Domain.USER.ATTR_SURNAME, externalId);
final Collection<UserInfo> accounts = userDAO.allMatching(filter).getOrThrow();
if (accounts.isEmpty()) {
return;
}
if (accounts.size() > 1) {
log.error("Too many accounts found!?... ad-hoc teacher account mapping: {}", externalId);
return;
}
userDAO.delete(Utils.immutableSetOf(new EntityKey(
accounts.iterator().next().uuid,
EntityType.USER)))
.getOrThrow();
} catch (final Exception e) {
log.error("Failed to delete ad hoc account for exam: {}", examId, e);
log.error("Failed to delete ad-hoc account for exam: {}", examId, e);
}
}
@ -420,6 +510,10 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
return webserviceInfo.getExternalServerURL() + lmsAPIEndpoint;
}
private String getAutoLoginURL() {
return webserviceInfo.getExternalServerURL() + webserviceInfo.getAutoLoginEndpoint();
}
private Exam logExamCreated(final Exam exam) {
this.userActivityLogDAO
.logCreate(exam)

View file

@ -637,8 +637,8 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
public void streamLightExamConfig(final String modelId, final HttpServletResponse response) throws IOException{
final ServletOutputStream outputStream = response.getOutputStream();
PipedOutputStream pout;
PipedInputStream pin;
PipedOutputStream pout = null;
PipedInputStream pin= null;
try {
pout = new PipedOutputStream();

View file

@ -670,6 +670,8 @@ class ScreenProctoringAPIBinding {
userInfo.email,
userInfo.language,
userInfo.timeZone,
true,
true,
spsUserRoles);
}

View file

@ -8,18 +8,23 @@
package ch.ethz.seb.sebserver.webservice.weblayer.api;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
@ -53,9 +58,17 @@ public class LmsIntegrationController {
@RequestParam(name = API.LMS_FULL_INTEGRATION_EXAM_TEMPLATE_ID, required = true) final String templateId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIT_PASSWORD, required = false) final String quitPassword,
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIT_LINK, required = false) final String quitLink,
@RequestParam(name = API.LMS_FULL_INTEGRATION_TIME_ZONE, required = false) final String timezone,
final HttpServletResponse response) {
final Exam exam = fullLmsIntegrationService.importExam(lmsUUId, courseId, quizId, templateId, quitPassword, quitLink)
final Exam exam = fullLmsIntegrationService.importExam(
lmsUUId,
courseId,
quizId,
templateId,
quitPassword,
quitLink,
timezone)
.onError(e -> log.error(
"Failed to create/import exam: lmsId:{}, courseId: {}, quizId: {}, templateId: {}",
lmsUUId, courseId, quizId, templateId, e))
@ -94,11 +107,33 @@ public class LmsIntegrationController {
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIZ_ID, required = true) final String quizId,
final HttpServletResponse response) throws IOException {
fullLmsIntegrationService.streamConnectionConfiguration(lmsUUId, courseId, quizId, response.getOutputStream())
.onError(e -> log.error(
"Failed to stream connection configuration for exam: lmsId:{}, courseId: {}, quizId: {}",
lmsUUId, courseId, quizId, e))
.getOrThrow();
final ServletOutputStream outputStream = response.getOutputStream();
final PipedOutputStream pout;
final PipedInputStream pin;
try {
pout = new PipedOutputStream();
pin = new PipedInputStream(pout);
}
fullLmsIntegrationService
.streamConnectionConfiguration(lmsUUId, courseId, quizId, pout)
.getOrThrow();
IOUtils.copyLarge(pin, outputStream);
response.setStatus(HttpStatus.OK.value());
outputStream.flush();
} catch (final APIMessage.APIMessageException me) {
response.setStatus(HttpStatus.BAD_REQUEST.value());
throw me;
} catch (final Exception e) {
log.error(
"Failed to stream connection configuration for exam: lmsId:{}, courseId: {}, quizId: {}",
lmsUUId, courseId, quizId, e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
} finally {
outputStream.flush();
outputStream.close();
}
}
}

View file

@ -19,6 +19,7 @@ sebserver.gui.date.displayformat=de
sebserver.gui.http.external.scheme=${sebserver.webservice.http.external.scheme}
sebserver.gui.http.external.servername=${sebserver.webservice.http.external.servername}
sebserver.gui.http.external.port=${sebserver.webservice.http.external.port}
sebserver.gui.http.external.autologin.endpoint=/auto_login
sebserver.gui.http.webservice.scheme=http
sebserver.gui.http.webservice.servername=localhost

View file

@ -47,6 +47,7 @@ sebserver.webservice.http.external.servername=
sebserver.webservice.http.external.port=
sebserver.webservice.http.redirect.gui=/gui
sebserver.webservice.ping.service.strategy=BLOCKING
sebserver.webservice.autologin.endpoint=/auto_login
### webservice API

View file

@ -96,6 +96,7 @@ public class ModelObjectJSONGenerator {
domainObject = new UserMod(
"UUID", 1L, "NAME", "SURNAME", "USERNAME", "newPassword", "confirmNewPassword", "EMAIL",
Locale.ENGLISH, DateTimeZone.UTC,
true, true,
new HashSet<>(Arrays.asList(UserRole.EXAM_ADMIN.name(), UserRole.EXAM_SUPPORTER.name())));
System.out.println(domainObject.getClass().getSimpleName() + ":");
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));