Merge branch 'dev-1.2' into development
This commit is contained in:
commit
6dacca72c3
15 changed files with 523 additions and 132 deletions
|
@ -23,6 +23,7 @@ public class ProctoringRoomConnection {
|
||||||
public static final String ATTR_ROOM_NAME = "roomName";
|
public static final String ATTR_ROOM_NAME = "roomName";
|
||||||
public static final String ATTR_SUBJECT = "subject";
|
public static final String ATTR_SUBJECT = "subject";
|
||||||
public static final String ATTR_ACCESS_TOKEN = "accessToken";
|
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_CONNECTION_URL = "connectionURL";
|
||||||
public static final String ATTR_USER_NAME = "userName";
|
public static final String ATTR_USER_NAME = "userName";
|
||||||
public static final String ATTR_ROOM_KEY = "roomKey";
|
public static final String ATTR_ROOM_KEY = "roomKey";
|
||||||
|
@ -50,6 +51,9 @@ public class ProctoringRoomConnection {
|
||||||
@JsonProperty(ATTR_ACCESS_TOKEN)
|
@JsonProperty(ATTR_ACCESS_TOKEN)
|
||||||
public final CharSequence accessToken;
|
public final CharSequence accessToken;
|
||||||
|
|
||||||
|
@JsonProperty(ATTR_SDK_TOKEN)
|
||||||
|
public final CharSequence sdkToken;
|
||||||
|
|
||||||
@JsonProperty(ATTR_ROOM_KEY)
|
@JsonProperty(ATTR_ROOM_KEY)
|
||||||
public final CharSequence roomKey;
|
public final CharSequence roomKey;
|
||||||
|
|
||||||
|
@ -71,6 +75,7 @@ public class ProctoringRoomConnection {
|
||||||
@JsonProperty(ATTR_ROOM_NAME) final String roomName,
|
@JsonProperty(ATTR_ROOM_NAME) final String roomName,
|
||||||
@JsonProperty(ATTR_SUBJECT) final String subject,
|
@JsonProperty(ATTR_SUBJECT) final String subject,
|
||||||
@JsonProperty(ATTR_ACCESS_TOKEN) final CharSequence accessToken,
|
@JsonProperty(ATTR_ACCESS_TOKEN) final CharSequence accessToken,
|
||||||
|
@JsonProperty(ATTR_SDK_TOKEN) final CharSequence sdkToken,
|
||||||
@JsonProperty(ATTR_ROOM_KEY) final CharSequence roomKey,
|
@JsonProperty(ATTR_ROOM_KEY) final CharSequence roomKey,
|
||||||
@JsonProperty(ATTR_API_KEY) final CharSequence apiKey,
|
@JsonProperty(ATTR_API_KEY) final CharSequence apiKey,
|
||||||
@JsonProperty(ATTR_MEETING_ID) final String meetingId,
|
@JsonProperty(ATTR_MEETING_ID) final String meetingId,
|
||||||
|
@ -83,6 +88,7 @@ public class ProctoringRoomConnection {
|
||||||
this.roomName = roomName;
|
this.roomName = roomName;
|
||||||
this.subject = subject;
|
this.subject = subject;
|
||||||
this.accessToken = accessToken;
|
this.accessToken = accessToken;
|
||||||
|
this.sdkToken = sdkToken;
|
||||||
this.roomKey = roomKey;
|
this.roomKey = roomKey;
|
||||||
this.apiKey = apiKey;
|
this.apiKey = apiKey;
|
||||||
this.meetingId = meetingId;
|
this.meetingId = meetingId;
|
||||||
|
@ -105,6 +111,10 @@ public class ProctoringRoomConnection {
|
||||||
return this.accessToken;
|
return this.accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CharSequence getSdkToken() {
|
||||||
|
return this.sdkToken;
|
||||||
|
}
|
||||||
|
|
||||||
public CharSequence getRoomKey() {
|
public CharSequence getRoomKey() {
|
||||||
return this.roomKey;
|
return this.roomKey;
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,8 @@ public class ProctoringServiceSettings implements Entity {
|
||||||
public static final String ATTR_SERVER_URL = "serverURL";
|
public static final String ATTR_SERVER_URL = "serverURL";
|
||||||
public static final String ATTR_APP_KEY = "appKey";
|
public static final String ATTR_APP_KEY = "appKey";
|
||||||
public static final String ATTR_APP_SECRET = "appSecret";
|
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_COLLECTING_ROOM_SIZE = "collectingRoomSize";
|
||||||
public static final String ATTR_ENABLED_FEATURES = "enabledFeatures";
|
public static final String ATTR_ENABLED_FEATURES = "enabledFeatures";
|
||||||
public static final String ATTR_COLLECT_ALL_ROOM_NAME = "collectAllRoomName";
|
public static final String ATTR_COLLECT_ALL_ROOM_NAME = "collectAllRoomName";
|
||||||
|
@ -67,6 +69,12 @@ public class ProctoringServiceSettings implements Entity {
|
||||||
@JsonProperty(ATTR_APP_SECRET)
|
@JsonProperty(ATTR_APP_SECRET)
|
||||||
public final CharSequence appSecret;
|
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)
|
@JsonProperty(ATTR_COLLECTING_ROOM_SIZE)
|
||||||
public final Integer collectingRoomSize;
|
public final Integer collectingRoomSize;
|
||||||
|
|
||||||
|
@ -86,7 +94,9 @@ public class ProctoringServiceSettings implements Entity {
|
||||||
@JsonProperty(ATTR_ENABLED_FEATURES) final EnumSet<ProctoringFeature> enabledFeatures,
|
@JsonProperty(ATTR_ENABLED_FEATURES) final EnumSet<ProctoringFeature> enabledFeatures,
|
||||||
@JsonProperty(ATTR_SERVICE_IN_USE) final Boolean serviceInUse,
|
@JsonProperty(ATTR_SERVICE_IN_USE) final Boolean serviceInUse,
|
||||||
@JsonProperty(ATTR_APP_KEY) final String appKey,
|
@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.examId = examId;
|
||||||
this.enableProctoring = BooleanUtils.isTrue(enableProctoring);
|
this.enableProctoring = BooleanUtils.isTrue(enableProctoring);
|
||||||
|
@ -97,6 +107,8 @@ public class ProctoringServiceSettings implements Entity {
|
||||||
this.serviceInUse = serviceInUse;
|
this.serviceInUse = serviceInUse;
|
||||||
this.appKey = appKey;
|
this.appKey = appKey;
|
||||||
this.appSecret = appSecret;
|
this.appSecret = appSecret;
|
||||||
|
this.sdkKey = sdkKey;
|
||||||
|
this.sdkSecret = sdkSecret;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,6 +159,14 @@ public class ProctoringServiceSettings implements Entity {
|
||||||
return this.appSecret;
|
return this.appSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSdkKey() {
|
||||||
|
return this.sdkKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CharSequence getSdkSecret() {
|
||||||
|
return this.sdkSecret;
|
||||||
|
}
|
||||||
|
|
||||||
public Boolean getServiceInUse() {
|
public Boolean getServiceInUse() {
|
||||||
return this.serviceInUse;
|
return this.serviceInUse;
|
||||||
}
|
}
|
||||||
|
@ -193,7 +213,11 @@ public class ProctoringServiceSettings implements Entity {
|
||||||
builder.append(", appKey=");
|
builder.append(", appKey=");
|
||||||
builder.append(this.appKey);
|
builder.append(this.appKey);
|
||||||
builder.append(", appSecret=");
|
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(", collectingRoomSize=");
|
||||||
builder.append(this.collectingRoomSize);
|
builder.append(this.collectingRoomSize);
|
||||||
builder.append(", enabledFeatures=");
|
builder.append(", enabledFeatures=");
|
||||||
|
|
|
@ -63,7 +63,7 @@ public final class LmsSetup implements GrantEntity, Activatable {
|
||||||
/** The Ans Delft binding is on the way */
|
/** The Ans Delft binding is on the way */
|
||||||
ANS_DELFT(),
|
ANS_DELFT(),
|
||||||
/** The OpenOLAT binding is on the way */
|
/** The OpenOLAT binding is on the way */
|
||||||
OPEN_OLAT();
|
OPEN_OLAT(Features.COURSE_API, Features.SEB_RESTRICTION);
|
||||||
|
|
||||||
public final EnumSet<Features> features;
|
public final EnumSet<Features> features;
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,7 @@ public final class ClientInstruction {
|
||||||
public static final String ZOOM_USER_NAME = "zoomUserName";
|
public static final String ZOOM_USER_NAME = "zoomUserName";
|
||||||
public static final String ZOOM_API_KEY = "zoomAPIKey";
|
public static final String ZOOM_API_KEY = "zoomAPIKey";
|
||||||
public static final String ZOOM_TOKEN = "zoomToken";
|
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_MEETING_KEY = "zoomMeetingKey";
|
||||||
public static final String ZOOM_RECEIVE_AUDIO = "zoomReceiveAudio";
|
public static final String ZOOM_RECEIVE_AUDIO = "zoomReceiveAudio";
|
||||||
public static final String ZOOM_RECEIVE_VIDEO = "zoomReceiveVideo";
|
public static final String ZOOM_RECEIVE_VIDEO = "zoomReceiveVideo";
|
||||||
|
|
|
@ -72,6 +72,11 @@ public class ExamProctoringSettings {
|
||||||
new LocTextKey("sebserver.exam.proctoring.form.appkey");
|
new LocTextKey("sebserver.exam.proctoring.form.appkey");
|
||||||
private final static LocTextKey SEB_PROCTORING_FORM_SECRET =
|
private final static LocTextKey SEB_PROCTORING_FORM_SECRET =
|
||||||
new LocTextKey("sebserver.exam.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 =
|
private final static LocTextKey SEB_PROCTORING_FORM_FEATURES =
|
||||||
new LocTextKey("sebserver.exam.proctoring.form.features");
|
new LocTextKey("sebserver.exam.proctoring.form.features");
|
||||||
|
|
||||||
|
@ -155,7 +160,9 @@ public class ExamProctoringSettings {
|
||||||
featureFlags,
|
featureFlags,
|
||||||
false,
|
false,
|
||||||
form.getFieldValue(ProctoringServiceSettings.ATTR_APP_KEY),
|
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) {
|
} catch (final Exception e) {
|
||||||
log.error("Unexpected error while trying to get settings from form: ", e);
|
log.error("Unexpected error while trying to get settings from form: ", e);
|
||||||
|
@ -225,6 +232,8 @@ public class ExamProctoringSettings {
|
||||||
.copyOf(content)
|
.copyOf(content)
|
||||||
.clearEntityKeys();
|
.clearEntityKeys();
|
||||||
|
|
||||||
|
final boolean isZoom = proctoringSettings.serverType == ProctoringServerType.ZOOM;
|
||||||
|
|
||||||
final FormHandle<ProctoringServiceSettings> formHandle = this.pageService.formBuilder(
|
final FormHandle<ProctoringServiceSettings> formHandle = this.pageService.formBuilder(
|
||||||
formContext)
|
formContext)
|
||||||
.withDefaultSpanInput(5)
|
.withDefaultSpanInput(5)
|
||||||
|
@ -249,7 +258,8 @@ public class ExamProctoringSettings {
|
||||||
ProctoringServiceSettings.ATTR_SERVER_TYPE,
|
ProctoringServiceSettings.ATTR_SERVER_TYPE,
|
||||||
SEB_PROCTORING_FORM_TYPE,
|
SEB_PROCTORING_FORM_TYPE,
|
||||||
proctoringSettings.serverType.name(),
|
proctoringSettings.serverType.name(),
|
||||||
resourceService::examProctoringTypeResources))
|
resourceService::examProctoringTypeResources)
|
||||||
|
.withSelectionListener(this::serviceSelection))
|
||||||
|
|
||||||
.addField(FormBuilder.text(
|
.addField(FormBuilder.text(
|
||||||
ProctoringServiceSettings.ATTR_SERVER_URL,
|
ProctoringServiceSettings.ATTR_SERVER_URL,
|
||||||
|
@ -269,6 +279,21 @@ public class ExamProctoringSettings {
|
||||||
? String.valueOf(proctoringSettings.appSecret)
|
? String.valueOf(proctoringSettings.appSecret)
|
||||||
: null))
|
: 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)
|
.withDefaultSpanInput(1)
|
||||||
.addField(FormBuilder.text(
|
.addField(FormBuilder.text(
|
||||||
ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE,
|
ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE,
|
||||||
|
@ -294,6 +319,14 @@ public class ExamProctoringSettings {
|
||||||
|
|
||||||
return () -> formHandle;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -213,7 +213,9 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
||||||
getEnabledFeatures(mapping),
|
getEnabledFeatures(mapping),
|
||||||
this.remoteProctoringRoomDAO.isServiceInUse(examId).getOr(true),
|
this.remoteProctoringRoomDAO.isServiceInUse(examId).getOr(true),
|
||||||
getString(mapping, ProctoringServiceSettings.ATTR_APP_KEY),
|
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()
|
.getOrThrow()
|
||||||
.toString());
|
.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(
|
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||||
EntityType.EXAM,
|
EntityType.EXAM,
|
||||||
examId,
|
examId,
|
||||||
|
|
|
@ -10,19 +10,28 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.cache.CacheManager;
|
import org.springframework.cache.CacheManager;
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.core.env.Environment;
|
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.http.client.ClientHttpRequestFactory;
|
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||||
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
|
|
||||||
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
|
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
|
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||||
|
@ -36,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.QuizData;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
|
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;
|
||||||
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.LmsSetup.LmsType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
||||||
|
@ -47,19 +55,23 @@ 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.LmsAPIService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
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.AbstractCachedCourseAccess;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat.OlatLmsData.AssessmentData;
|
||||||
|
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 {
|
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 ClientHttpRequestFactoryService clientHttpRequestFactoryService;
|
||||||
private final ClientCredentialService clientCredentialService;
|
private final ClientCredentialService clientCredentialService;
|
||||||
private final APITemplateDataSupplier apiTemplateDataSupplier;
|
private final APITemplateDataSupplier apiTemplateDataSupplier;
|
||||||
private final Long lmsSetupId;
|
private final Long lmsSetupId;
|
||||||
|
|
||||||
|
private OlatLmsRestTemplate cachedRestTemplate;
|
||||||
|
|
||||||
protected OlatLmsAPITemplate(
|
protected OlatLmsAPITemplate(
|
||||||
|
|
||||||
// TODO if you need more dependencies inject them here and set the reference
|
|
||||||
|
|
||||||
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
||||||
final ClientCredentialService clientCredentialService,
|
final ClientCredentialService clientCredentialService,
|
||||||
final APITemplateDataSupplier apiTemplateDataSupplier,
|
final APITemplateDataSupplier apiTemplateDataSupplier,
|
||||||
|
@ -95,32 +107,19 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
|
||||||
final LmsSetupTestResult testLmsSetupSettings = testLmsSetupSettings();
|
final LmsSetupTestResult testLmsSetupSettings = testLmsSetupSettings();
|
||||||
if (testLmsSetupSettings.hasAnyError()) {
|
if (testLmsSetupSettings.hasAnyError()) {
|
||||||
return testLmsSetupSettings;
|
return testLmsSetupSettings;
|
||||||
} else {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
// TODO check if the course API of the remote LMS is available
|
this.getRestTemplate().get();
|
||||||
// if not, create corresponding LmsSetupTestResult error
|
} catch (final Exception e) {
|
||||||
return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.OPEN_OLAT, "TODO: implement LMS access check");
|
log.error("Failed to access OLAT course API: ", e);
|
||||||
|
return LmsSetupTestResult.ofQuizAccessAPIError(LmsType.OPEN_OLAT, e.getMessage());
|
||||||
//return LmsSetupTestResult.ofOkay(LmsType.OPEN_OLAT);
|
}
|
||||||
|
return LmsSetupTestResult.ofOkay(LmsType.OPEN_OLAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LmsSetupTestResult testCourseRestrictionAPI() {
|
public LmsSetupTestResult testCourseRestrictionAPI() {
|
||||||
final LmsSetupTestResult testLmsSetupSettings = testLmsSetupSettings();
|
return testCourseAccessAPI();
|
||||||
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() {
|
private LmsSetupTestResult testLmsSetupSettings() {
|
||||||
|
@ -207,62 +206,100 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<List<QuizData>> allQuizzesSupplier(final FilterMap filterMap) {
|
protected Supplier<List<QuizData>> 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 () -> {
|
return () -> {
|
||||||
|
final List<QuizData> res = getRestTemplate()
|
||||||
// TODO Get all course / quiz data from remote LMS that matches the filter criteria.
|
.map(t -> this.collectAllQuizzes(t, filterMap))
|
||||||
// If the LMS API uses paging, go through all pages using the filter criteria
|
.getOrThrow();
|
||||||
// and collect the course data.
|
super.putToCache(res);
|
||||||
// Transform the data from courses / quizzes from LMS into QuizData objects
|
return res;
|
||||||
// Put loaded QuizData objects to the cache: super.putToCache(quizDataCollection);
|
|
||||||
// before returning it.
|
|
||||||
|
|
||||||
throw new RuntimeException("TODO");
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String examUrl(final long olatRepositoryId) {
|
||||||
|
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||||
|
return lmsSetup.lmsApiUrl + "/auth/RepositoryEntry/" + olatRepositoryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<QuizData> 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<AssessmentData> as =
|
||||||
|
this.apiGetList(restTemplate, url, new ParameterizedTypeReference<List<AssessmentData>>() {
|
||||||
|
});
|
||||||
|
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<String, String>());
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) {
|
protected Supplier<Collection<QuizData>> quizzesSupplier(final Set<String> ids) {
|
||||||
|
return () -> ids.stream().map(id -> quizSupplier(id).get()).collect(Collectors.toList());
|
||||||
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");
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<QuizData> quizSupplier(final String id) {
|
protected Supplier<QuizData> quizSupplier(final String id) {
|
||||||
|
return () -> getRestTemplate()
|
||||||
|
.map(t -> this.quizById(t, id))
|
||||||
|
.getOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
return () -> {
|
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.repositoryEntryKey),
|
||||||
|
new HashMap<String, String>());
|
||||||
|
}
|
||||||
|
|
||||||
// TODO get the specified quiz / course data for specified identifier from remote LMS
|
private ExamineeAccountDetails getExamineeById(final RestTemplate restTemplate, final String id) {
|
||||||
// and put it to the cache: super.putToCache(quizDataCollection);
|
final String url = String.format("/restapi/users/%s/name_username", id);
|
||||||
// before returning it.
|
final UserData u = this.apiGet(restTemplate, url, UserData.class);
|
||||||
|
final Map<String, String> attrs = new HashMap<>();
|
||||||
throw new RuntimeException("TODO");
|
return new ExamineeAccountDetails(
|
||||||
};
|
String.valueOf(u.key),
|
||||||
|
u.lastName + ", " + u.firstName,
|
||||||
|
u.username,
|
||||||
|
"OLAT API does not provide email addresses",
|
||||||
|
attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String examineeSessionId) {
|
protected Supplier<ExamineeAccountDetails> accountDetailsSupplier(final String id) {
|
||||||
|
return () -> getRestTemplate()
|
||||||
return () -> {
|
.map(t -> this.getExamineeById(t, id))
|
||||||
|
.getOrThrow();
|
||||||
// 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");
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -272,78 +309,130 @@ public class OlatLmsAPITemplate extends AbstractCachedCourseAccess implements Lm
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<String, String>());
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
return new SEBRestriction(Long.valueOf(id), r.configKeys, r.browserExamKeys, new HashMap<String, String>());
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
// 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<String, String>());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
|
public Result<SEBRestriction> getSEBClientRestriction(final Exam exam) {
|
||||||
@SuppressWarnings("unused")
|
return getRestTemplate()
|
||||||
final String quizId = exam.externalId;
|
.map(t -> this.getRestrictionForAssignmentId(t, 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");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<SEBRestriction> applySEBClientRestriction(
|
public Result<SEBRestriction> applySEBClientRestriction(
|
||||||
final String externalExamId,
|
final String externalExamId,
|
||||||
final SEBRestriction sebRestrictionData) {
|
final SEBRestriction sebRestrictionData) {
|
||||||
|
return getRestTemplate()
|
||||||
return Result.tryCatch(() -> {
|
.map(t -> this.setRestrictionForAssignmentId(t, externalExamId, sebRestrictionData));
|
||||||
|
|
||||||
// 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");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
|
public Result<Exam> releaseSEBClientRestriction(final Exam exam) {
|
||||||
@SuppressWarnings("unused")
|
return getRestTemplate()
|
||||||
final String quizId = exam.externalId;
|
.map(t -> this.deleteRestrictionForAssignmentId(t, exam.externalId))
|
||||||
|
.map(x -> exam);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T apiGet(final RestTemplate restTemplate, final String url, final Class<T> type) {
|
||||||
|
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||||
|
final ResponseEntity<T> res = restTemplate.exchange(
|
||||||
|
lmsSetup.lmsApiUrl + url,
|
||||||
|
HttpMethod.GET,
|
||||||
|
HttpEntity.EMPTY,
|
||||||
|
type);
|
||||||
|
return res.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> List<T> apiGetList(final RestTemplate restTemplate, final String url,
|
||||||
|
final ParameterizedTypeReference<List<T>> type) {
|
||||||
|
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||||
|
final ResponseEntity<List<T>> res = restTemplate.exchange(
|
||||||
|
lmsSetup.lmsApiUrl + url,
|
||||||
|
HttpMethod.GET,
|
||||||
|
HttpEntity.EMPTY,
|
||||||
|
type);
|
||||||
|
return res.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
private <P, R> R apiPost(final RestTemplate restTemplate, final String url, final P post, final Class<P> postType,
|
||||||
|
final Class<R> responseType) {
|
||||||
|
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||||
|
final HttpHeaders httpHeaders = new HttpHeaders();
|
||||||
|
httpHeaders.set("content-type", "application/json");
|
||||||
|
final HttpEntity<P> requestEntity = new HttpEntity<>(post, httpHeaders);
|
||||||
|
final ResponseEntity<R> res = restTemplate.exchange(
|
||||||
|
lmsSetup.lmsApiUrl + url,
|
||||||
|
HttpMethod.POST,
|
||||||
|
requestEntity,
|
||||||
|
responseType);
|
||||||
|
return res.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T apiDelete(final RestTemplate restTemplate, final String url, final Class<T> type) {
|
||||||
|
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||||
|
final ResponseEntity<T> res = restTemplate.exchange(
|
||||||
|
lmsSetup.lmsApiUrl + url,
|
||||||
|
HttpMethod.DELETE,
|
||||||
|
HttpEntity.EMPTY,
|
||||||
|
type);
|
||||||
|
return res.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<OlatLmsRestTemplate> getRestTemplate() {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
|
if (this.cachedRestTemplate != null) {
|
||||||
|
return this.cachedRestTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Release respectively delete all SEB client restrictions for the given
|
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
|
||||||
// course / quize on the remote LMS.
|
final ClientCredentials credentials = this.apiTemplateDataSupplier.getLmsClientCredentials();
|
||||||
|
final ProxyData proxyData = this.apiTemplateDataSupplier.getProxyData();
|
||||||
|
|
||||||
throw new RuntimeException("TODO");
|
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 ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService
|
||||||
|
.getClientHttpRequestFactory(proxyData)
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
final OlatLmsRestTemplate template = new OlatLmsRestTemplate(details);
|
||||||
|
template.setRequestFactory(clientHttpRequestFactory);
|
||||||
|
|
||||||
|
this.cachedRestTemplate = template;
|
||||||
|
return this.cachedRestTemplate;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
|
||||||
|
|
||||||
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 ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
|
|
||||||
details.setAccessTokenUri(lmsSetup.lmsApiUrl + accessTokenRequestPath);
|
|
||||||
details.setClientId(plainClientId.toString());
|
|
||||||
details.setClientSecret(plainClientSecret.toString());
|
|
||||||
|
|
||||||
final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService
|
|
||||||
.getClientHttpRequestFactory(proxyData)
|
|
||||||
.getOrThrow();
|
|
||||||
|
|
||||||
final OAuth2RestTemplate template = new OAuth2RestTemplate(details);
|
|
||||||
template.setRequestFactory(clientHttpRequestFactory);
|
|
||||||
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
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"
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public long key;
|
||||||
|
public long repositoryEntryKey;
|
||||||
|
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<String> browserExamKeys;
|
||||||
|
public List<String> configKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
static final class RestrictionDataPost {
|
||||||
|
/*
|
||||||
|
* OLAT API example:
|
||||||
|
* {
|
||||||
|
* "configKeys": ["a", "b"],
|
||||||
|
* "browserExamKeys": ["1", "2"]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public List<String> browserExamKeys;
|
||||||
|
public List<String> configKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* 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.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 {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(OlatLmsRestTemplate.class);
|
||||||
|
|
||||||
|
private String token;
|
||||||
|
private 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() {
|
||||||
|
@Override
|
||||||
|
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
|
||||||
|
final ClientHttpRequestExecution execution) throws IOException {
|
||||||
|
// if there's no token, authenticate first
|
||||||
|
if (OlatLmsRestTemplate.this.token == null) {
|
||||||
|
authenticate();
|
||||||
|
}
|
||||||
|
// when authenticating, just do a normal call
|
||||||
|
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", 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", OlatLmsRestTemplate.this.token);
|
||||||
|
response = execution.execute(request, body);
|
||||||
|
log.debug("OLAT [retry API call] {} Headers: {}", response.getStatusCode(), response.getHeaders());
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void authenticate() {
|
||||||
|
// Authenticate with OLAT and store the received X-OLAT-TOKEN
|
||||||
|
this.token = "authenticating";
|
||||||
|
final String authUrl = String.format("%s%s?password=%s",
|
||||||
|
this.details.getAccessTokenUri(),
|
||||||
|
this.details.getClientId(),
|
||||||
|
this.details.getClientSecret());
|
||||||
|
try {
|
||||||
|
final ResponseEntity<String> response = this.getForEntity(authUrl, String.class);
|
||||||
|
final HttpHeaders responseHeaders = response.getHeaders();
|
||||||
|
log.debug("OLAT [authenticate] {} Headers: {}", response.getStatusCode(), responseHeaders);
|
||||||
|
this.token = responseHeaders.getFirst("X-OLAT-TOKEN");
|
||||||
|
} catch (final Exception e) {
|
||||||
|
this.token = null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -378,6 +378,7 @@ public class JitsiProctoringService implements ExamProctoringService {
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
clientName);
|
clientName);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -214,6 +214,9 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
attributes.put(
|
attributes.put(
|
||||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_TOKEN,
|
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_TOKEN,
|
||||||
String.valueOf(proctoringConnection.accessToken));
|
String.valueOf(proctoringConnection.accessToken));
|
||||||
|
attributes.put(
|
||||||
|
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_SDK_TOKEN,
|
||||||
|
String.valueOf(proctoringConnection.sdkToken));
|
||||||
if (StringUtils.isNotBlank(proctoringConnection.apiKey)) {
|
if (StringUtils.isNotBlank(proctoringConnection.apiKey)) {
|
||||||
attributes.put(
|
attributes.put(
|
||||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_API_KEY,
|
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_API_KEY,
|
||||||
|
@ -262,6 +265,19 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
String.valueOf(additionalZoomRoomData.meeting_id),
|
String.valueOf(additionalZoomRoomData.meeting_id),
|
||||||
true);
|
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(
|
return new ProctoringRoomConnection(
|
||||||
ProctoringServerType.ZOOM,
|
ProctoringServerType.ZOOM,
|
||||||
null,
|
null,
|
||||||
|
@ -270,6 +286,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
roomName,
|
roomName,
|
||||||
subject,
|
subject,
|
||||||
jwt,
|
jwt,
|
||||||
|
sdkJWT,
|
||||||
credentials.accessToken,
|
credentials.accessToken,
|
||||||
credentials.clientId,
|
credentials.clientId,
|
||||||
String.valueOf(additionalZoomRoomData.meeting_id),
|
String.valueOf(additionalZoomRoomData.meeting_id),
|
||||||
|
@ -308,6 +325,19 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
.getConnectionData(connectionToken)
|
.getConnectionData(connectionToken)
|
||||||
.getOrThrow();
|
.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(
|
return new ProctoringRoomConnection(
|
||||||
ProctoringServerType.ZOOM,
|
ProctoringServerType.ZOOM,
|
||||||
connectionToken,
|
connectionToken,
|
||||||
|
@ -316,6 +346,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
roomName,
|
roomName,
|
||||||
subject,
|
subject,
|
||||||
jwt,
|
jwt,
|
||||||
|
sdkJWT,
|
||||||
credentials.accessToken,
|
credentials.accessToken,
|
||||||
credentials.clientId,
|
credentials.clientId,
|
||||||
String.valueOf(additionalZoomRoomData.meeting_id),
|
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(
|
private String createJWTForMeetingAccess(
|
||||||
final ClientCredentials credentials,
|
final ClientCredentials credentials,
|
||||||
final String meetingId,
|
final String meetingId,
|
||||||
|
|
|
@ -661,10 +661,14 @@ sebserver.exam.proctoring.form.url=Server URL
|
||||||
sebserver.exam.proctoring.form.url.tooltip=The proctoring 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=Collecting Room Size
|
||||||
sebserver.exam.proctoring.form.collectingRoomSize.tooltip=The size of proctor rooms to collect connecting SEB clients into.
|
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.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.secret.tooltip=The secret used to access the proctoring service
|
||||||
|
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 (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=Enabled Features
|
||||||
sebserver.exam.proctoring.form.features.TOWN_HALL=Town-Hall Room
|
sebserver.exam.proctoring.form.features.TOWN_HALL=Town-Hall Room
|
||||||
sebserver.exam.proctoring.form.features.ONE_TO_ONE=One to One Room
|
sebserver.exam.proctoring.form.features.ONE_TO_ONE=One to One Room
|
||||||
|
|
|
@ -37,6 +37,7 @@ public class JitsiWindowScriptResolverTest {
|
||||||
"ROOM",
|
"ROOM",
|
||||||
"SUBJECT",
|
"SUBJECT",
|
||||||
"ACCESS_TOKEN",
|
"ACCESS_TOKEN",
|
||||||
|
null,
|
||||||
"API_KEY",
|
"API_KEY",
|
||||||
"ROOM_KEY",
|
"ROOM_KEY",
|
||||||
"MEETING_ID",
|
"MEETING_ID",
|
||||||
|
@ -53,6 +54,7 @@ public class JitsiWindowScriptResolverTest {
|
||||||
"ROOM",
|
"ROOM",
|
||||||
"SUBJECT",
|
"SUBJECT",
|
||||||
"ACCESS_TOKEN",
|
"ACCESS_TOKEN",
|
||||||
|
null,
|
||||||
"API_KEY",
|
"API_KEY",
|
||||||
"ROOM_KEY",
|
"ROOM_KEY",
|
||||||
"MEETING_ID",
|
"MEETING_ID",
|
||||||
|
|
|
@ -37,6 +37,7 @@ public class ZoomWindowScriptResolverTest {
|
||||||
"ROOM",
|
"ROOM",
|
||||||
"SUBJECT",
|
"SUBJECT",
|
||||||
"ACCESS_TOKEN",
|
"ACCESS_TOKEN",
|
||||||
|
"SDK_TOKEN",
|
||||||
"API_KEY",
|
"API_KEY",
|
||||||
"ROOM_KEY",
|
"ROOM_KEY",
|
||||||
"MEETING_ID",
|
"MEETING_ID",
|
||||||
|
@ -53,6 +54,7 @@ public class ZoomWindowScriptResolverTest {
|
||||||
"ROOM",
|
"ROOM",
|
||||||
"SUBJECT",
|
"SUBJECT",
|
||||||
"ACCESS_TOKEN",
|
"ACCESS_TOKEN",
|
||||||
|
"SDK_TOKEN",
|
||||||
"API_KEY",
|
"API_KEY",
|
||||||
"ROOM_KEY",
|
"ROOM_KEY",
|
||||||
"MEETING_ID",
|
"MEETING_ID",
|
||||||
|
|
|
@ -63,7 +63,7 @@ public class ExamProctoringRoomServiceTest extends AdministrationAPIIntegrationT
|
||||||
2L,
|
2L,
|
||||||
new ProctoringServiceSettings(
|
new ProctoringServiceSettings(
|
||||||
2L, true, ProctoringServerType.JITSI_MEET, "http://jitsi.ch", 1, null, false,
|
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());
|
assertTrue(this.examAdminService.isProctoringEnabled(2L).get());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue