Merge remote-tracking branch 'origin/rel-1.2.3'
This commit is contained in:
commit
c2e9df7761
7 changed files with 97 additions and 16 deletions
2
pom.xml
2
pom.xml
|
@ -18,7 +18,7 @@
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<sebserver-version>1.2.2</sebserver-version>
|
<sebserver-version>1.2.3</sebserver-version>
|
||||||
<build-version>${sebserver-version}</build-version>
|
<build-version>${sebserver-version}</build-version>
|
||||||
<revision>${sebserver-version}</revision>
|
<revision>${sebserver-version}</revision>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|
|
@ -530,6 +530,10 @@ public final class Utils {
|
||||||
return getMillisecondsNow() / 1000;
|
return getMillisecondsNow() / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static long toSeconds(final long millis) {
|
||||||
|
return millis / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
public static RGB toRGB(final String rgbString) {
|
public static RGB toRGB(final String rgbString) {
|
||||||
if (StringUtils.isNotBlank(rgbString)) {
|
if (StringUtils.isNotBlank(rgbString)) {
|
||||||
return new RGB(
|
return new RGB(
|
||||||
|
@ -661,4 +665,5 @@ public final class Utils {
|
||||||
return false; // Either timeout or unreachable or failed DNS lookup.
|
return false; // Either timeout or unreachable or failed DNS lookup.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -487,7 +487,7 @@ public class MonitoringProctoringService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final boolean active = room.roomSize > 0 && !room.isOpen;
|
final boolean active = room.roomSize > 0 /* && !room.isOpen SEBSERV-236 */;
|
||||||
final Display display = pageContext.getRoot().getDisplay();
|
final Display display = pageContext.getRoot().getDisplay();
|
||||||
final PageAction action = (PageAction) treeItem.getData(ActionPane.ACTION_EVENT_CALL_KEY);
|
final PageAction action = (PageAction) treeItem.getData(ActionPane.ACTION_EVENT_CALL_KEY);
|
||||||
final Image image = active
|
final Image image = active
|
||||||
|
|
|
@ -101,7 +101,7 @@ public class ProctoringGUIService {
|
||||||
public boolean isCollectingRoomEnabled(final String roomName) {
|
public boolean isCollectingRoomEnabled(final String roomName) {
|
||||||
try {
|
try {
|
||||||
final Pair<RemoteProctoringRoom, TreeItem> pair = this.collectingRoomsActionState.get(roomName);
|
final Pair<RemoteProctoringRoom, TreeItem> pair = this.collectingRoomsActionState.get(roomName);
|
||||||
return pair.a.roomSize > 0 && !pair.a.isOpen;
|
return pair.a.roomSize > 0 /* && !pair.a.isOpen SEBSERV-236 */;
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.error("Failed to get actual collecting room size for room: {} cause: ", roomName, e.getMessage());
|
log.error("Failed to get actual collecting room size for room: {} cause: ", roomName, e.getMessage());
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -9,9 +9,12 @@
|
||||||
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat;
|
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpRequest;
|
import org.springframework.http.HttpRequest;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
@ -66,12 +69,15 @@ public class OlatLmsRestTemplate extends RestTemplate {
|
||||||
private void authenticate() {
|
private void authenticate() {
|
||||||
// Authenticate with OLAT and store the received X-OLAT-TOKEN
|
// Authenticate with OLAT and store the received X-OLAT-TOKEN
|
||||||
this.token = "authenticating";
|
this.token = "authenticating";
|
||||||
final String authUrl = String.format("%s%s?password=%s",
|
final String authUrl = this.details.getAccessTokenUri();
|
||||||
this.details.getAccessTokenUri(),
|
final Map<String, String> credentials = new HashMap<>();
|
||||||
this.details.getClientId(),
|
credentials.put("username", this.details.getClientId());
|
||||||
this.details.getClientSecret());
|
credentials.put("password", this.details.getClientSecret());
|
||||||
|
final HttpHeaders httpHeaders = new HttpHeaders();
|
||||||
|
httpHeaders.set("content-type", "application/json");
|
||||||
|
final HttpEntity<Map<String,String>> requestEntity = new HttpEntity<>(credentials, httpHeaders);
|
||||||
try {
|
try {
|
||||||
final ResponseEntity<String> response = this.getForEntity(authUrl, String.class);
|
final ResponseEntity<String> response = this.postForEntity(authUrl, requestEntity, String.class);
|
||||||
final HttpHeaders responseHeaders = response.getHeaders();
|
final HttpHeaders responseHeaders = response.getHeaders();
|
||||||
log.debug("OLAT [authenticate] {} Headers: {}", response.getStatusCode(), responseHeaders);
|
log.debug("OLAT [authenticate] {} Headers: {}", response.getStatusCode(), responseHeaders);
|
||||||
this.token = responseHeaders.getFirst("X-OLAT-TOKEN");
|
this.token = responseHeaders.getFirst("X-OLAT-TOKEN");
|
||||||
|
|
|
@ -133,6 +133,7 @@ class ExamSessionControlTask implements DisposableBean {
|
||||||
.getOrThrow()
|
.getOrThrow()
|
||||||
.stream()
|
.stream()
|
||||||
.filter(exam -> exam.startTime.minus(this.examTimePrefix).isBefore(now))
|
.filter(exam -> exam.startTime.minus(this.examTimePrefix).isBefore(now))
|
||||||
|
.filter(exam -> exam.endTime != null && exam.endTime.plus(this.examTimeSuffix).isAfter(now))
|
||||||
.flatMap(exam -> Result.skipOnError(this.examUpdateHandler.setRunning(exam, updateId)))
|
.flatMap(exam -> Result.skipOnError(this.examUpdateHandler.setRunning(exam, updateId)))
|
||||||
.collect(Collectors.toMap(Exam::getId, Exam::getName));
|
.collect(Collectors.toMap(Exam::getId, Exam::getName));
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
|
||||||
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
|
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
|
||||||
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
|
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
|
||||||
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
|
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
|
||||||
|
@ -92,6 +93,8 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
"{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
|
"{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
|
||||||
private static final String ZOOM_API_ACCESS_TOKEN_PAYLOAD =
|
private static final String ZOOM_API_ACCESS_TOKEN_PAYLOAD =
|
||||||
"{\"iss\":\"%s\",\"exp\":%s}";
|
"{\"iss\":\"%s\",\"exp\":%s}";
|
||||||
|
private static final String ZOOM_SDK_ACCESS_TOKEN_PAYLOAD =
|
||||||
|
"{\"appKey\":\"%s\",\"iat\":%s,\"exp\":%s,\"tokenExp\":%s}";
|
||||||
|
|
||||||
private static final Map<String, String> SEB_API_NAME_INSTRUCTION_NAME_MAPPING = Utils.immutableMapOf(Arrays.asList(
|
private static final Map<String, String> SEB_API_NAME_INSTRUCTION_NAME_MAPPING = Utils.immutableMapOf(Arrays.asList(
|
||||||
new Tuple<>(
|
new Tuple<>(
|
||||||
|
@ -260,7 +263,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
proctoringSettings.appSecret,
|
proctoringSettings.appSecret,
|
||||||
remoteProctoringRoom.joinKey);
|
remoteProctoringRoom.joinKey);
|
||||||
|
|
||||||
final String jwt = this.createJWTForMeetingAccess(
|
final String jwt = this.createSignatureForMeetingAccess(
|
||||||
credentials,
|
credentials,
|
||||||
String.valueOf(additionalZoomRoomData.meeting_id),
|
String.valueOf(additionalZoomRoomData.meeting_id),
|
||||||
true);
|
true);
|
||||||
|
@ -275,7 +278,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
|
|
||||||
sdkJWT = this.createJWTForSDKAccess(
|
sdkJWT = this.createJWTForSDKAccess(
|
||||||
sdkCredentials,
|
sdkCredentials,
|
||||||
String.valueOf(additionalZoomRoomData.meeting_id));
|
forExam(proctoringSettings));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ProctoringRoomConnection(
|
return new ProctoringRoomConnection(
|
||||||
|
@ -316,7 +319,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
proctoringSettings.appSecret,
|
proctoringSettings.appSecret,
|
||||||
remoteProctoringRoom.joinKey);
|
remoteProctoringRoom.joinKey);
|
||||||
|
|
||||||
final String jwt = this.createJWTForMeetingAccess(
|
final String signature = this.createSignatureForMeetingAccess(
|
||||||
credentials,
|
credentials,
|
||||||
String.valueOf(additionalZoomRoomData.meeting_id),
|
String.valueOf(additionalZoomRoomData.meeting_id),
|
||||||
false);
|
false);
|
||||||
|
@ -335,7 +338,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
|
|
||||||
sdkJWT = this.createJWTForSDKAccess(
|
sdkJWT = this.createJWTForSDKAccess(
|
||||||
sdkCredentials,
|
sdkCredentials,
|
||||||
String.valueOf(additionalZoomRoomData.meeting_id));
|
forExam(proctoringSettings));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ProctoringRoomConnection(
|
return new ProctoringRoomConnection(
|
||||||
|
@ -345,7 +348,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
additionalZoomRoomData.join_url,
|
additionalZoomRoomData.join_url,
|
||||||
roomName,
|
roomName,
|
||||||
subject,
|
subject,
|
||||||
jwt,
|
signature,
|
||||||
sdkJWT,
|
sdkJWT,
|
||||||
credentials.accessToken,
|
credentials.accessToken,
|
||||||
credentials.clientId,
|
credentials.clientId,
|
||||||
|
@ -646,12 +649,57 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
|
|
||||||
private String createJWTForSDKAccess(
|
private String createJWTForSDKAccess(
|
||||||
final ClientCredentials sdkCredentials,
|
final ClientCredentials sdkCredentials,
|
||||||
final String meetingId) {
|
final Long expTime) {
|
||||||
|
|
||||||
return createJWTForMeetingAccess(sdkCredentials, meetingId, false);
|
try {
|
||||||
|
|
||||||
|
final CharSequence decryptedSecret = this.cryptor
|
||||||
|
.decrypt(sdkCredentials.secret)
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
final StringBuilder builder = new StringBuilder();
|
||||||
|
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
|
||||||
|
|
||||||
|
final String jwtHeaderPart = urlEncoder
|
||||||
|
.encodeToString(ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
// epoch time in seconds
|
||||||
|
final long secondsNow = Utils.getSecondsNow();
|
||||||
|
|
||||||
|
final String jwtPayload = String.format(
|
||||||
|
ZOOM_SDK_ACCESS_TOKEN_PAYLOAD
|
||||||
|
.replaceAll(" ", "")
|
||||||
|
.replaceAll("\n", ""),
|
||||||
|
sdkCredentials.clientIdAsString(),
|
||||||
|
secondsNow,
|
||||||
|
expTime,
|
||||||
|
expTime);
|
||||||
|
|
||||||
|
final String jwtPayloadPart = urlEncoder
|
||||||
|
.encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
final String message = jwtHeaderPart + "." + jwtPayloadPart;
|
||||||
|
|
||||||
|
final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG);
|
||||||
|
final SecretKeySpec secret_key = new SecretKeySpec(
|
||||||
|
Utils.toByteArray(decryptedSecret),
|
||||||
|
TOKEN_ENCODE_ALG);
|
||||||
|
|
||||||
|
sha256_HMAC.init(secret_key);
|
||||||
|
final String hash = urlEncoder
|
||||||
|
.encodeToString(sha256_HMAC.doFinal(Utils.toByteArray(message)));
|
||||||
|
|
||||||
|
builder.append(message)
|
||||||
|
.append(".")
|
||||||
|
.append(hash);
|
||||||
|
|
||||||
|
return builder.toString();
|
||||||
|
} catch (final Exception e) {
|
||||||
|
throw new RuntimeException("Failed to create JWT for Zoom API access: ", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String createJWTForMeetingAccess(
|
private String createSignatureForMeetingAccess(
|
||||||
final ClientCredentials credentials,
|
final ClientCredentials credentials,
|
||||||
final String meetingId,
|
final String meetingId,
|
||||||
final boolean host) {
|
final boolean host) {
|
||||||
|
@ -683,6 +731,27 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private long forExam(final ProctoringServiceSettings examProctoring) {
|
||||||
|
if (examProctoring.examId == null) {
|
||||||
|
throw new IllegalStateException("Missing exam identifier from ExamProctoring data");
|
||||||
|
}
|
||||||
|
|
||||||
|
long expTime = Utils.toSeconds(System.currentTimeMillis() + Constants.DAY_IN_MILLIS);
|
||||||
|
if (this.examSessionService.isExamRunning(examProctoring.examId)) {
|
||||||
|
final Exam exam = this.examSessionService.getRunningExam(examProctoring.examId)
|
||||||
|
.getOrThrow();
|
||||||
|
if (exam.endTime != null) {
|
||||||
|
expTime = Utils.toSeconds(exam.endTime.getMillis());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// refer to https://marketplace.zoom.us/docs/sdk/native-sdks/auth
|
||||||
|
// "exp": 0, //JWT expiration date (Min:1800 seconds greater than iat value, Max: 48 hours greater than iat value) in epoch format.
|
||||||
|
if (expTime - Utils.getSecondsNow() > Utils.toSeconds(2 * Constants.DAY_IN_MILLIS)) {
|
||||||
|
expTime = Utils.toSeconds(System.currentTimeMillis() + Constants.DAY_IN_MILLIS);
|
||||||
|
}
|
||||||
|
return expTime;
|
||||||
|
}
|
||||||
|
|
||||||
private final static class ZoomRestTemplate {
|
private final static class ZoomRestTemplate {
|
||||||
|
|
||||||
private static final int LIZENSED_USER = 2;
|
private static final int LIZENSED_USER = 2;
|
||||||
|
|
Loading…
Reference in a new issue