diff --git a/pom.xml b/pom.xml index 6426509d..82f3e9d6 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ jar - 1.2.2 + 1.2.3 ${sebserver-version} ${sebserver-version} UTF-8 diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java index cc097985..8d79797b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java @@ -530,6 +530,10 @@ public final class Utils { return getMillisecondsNow() / 1000; } + public static long toSeconds(final long millis) { + return millis / 1000; + } + public static RGB toRGB(final String rgbString) { if (StringUtils.isNotBlank(rgbString)) { return new RGB( @@ -661,4 +665,5 @@ public final class Utils { return false; // Either timeout or unreachable or failed DNS lookup. } } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java index 3d056de1..9ffb7886 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java @@ -487,7 +487,7 @@ public class MonitoringProctoringService { 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 PageAction action = (PageAction) treeItem.getData(ActionPane.ACTION_EVENT_CALL_KEY); final Image image = active diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java index 9d69b307..63451f1e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java @@ -101,7 +101,7 @@ public class ProctoringGUIService { public boolean isCollectingRoomEnabled(final String roomName) { try { final Pair 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) { log.error("Failed to get actual collecting room size for room: {} cause: ", roomName, e.getMessage()); return false; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java index 6c326124..bc026a1f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/olat/OlatLmsRestTemplate.java @@ -9,9 +9,12 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.olat; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatus; @@ -66,12 +69,15 @@ public class OlatLmsRestTemplate extends RestTemplate { 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()); + final String authUrl = this.details.getAccessTokenUri(); + final Map credentials = new HashMap<>(); + credentials.put("username", this.details.getClientId()); + credentials.put("password", this.details.getClientSecret()); + final HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.set("content-type", "application/json"); + final HttpEntity> requestEntity = new HttpEntity<>(credentials, httpHeaders); try { - final ResponseEntity response = this.getForEntity(authUrl, String.class); + final ResponseEntity response = this.postForEntity(authUrl, requestEntity, String.class); final HttpHeaders responseHeaders = response.getHeaders(); log.debug("OLAT [authenticate] {} Headers: {}", response.getStatusCode(), responseHeaders); this.token = responseHeaders.getFirst("X-OLAT-TOKEN"); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java index f2430c4f..2e1106ee 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java @@ -133,6 +133,7 @@ class ExamSessionControlTask implements DisposableBean { .getOrThrow() .stream() .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))) .collect(Collectors.toMap(Exam::getId, Exam::getName)); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java index e4157f39..0118f4b9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java @@ -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.CircuitBreaker; 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.ProctoringServiceSettings; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType; @@ -92,6 +93,8 @@ public class ZoomProctoringService implements ExamProctoringService { "{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; private static final String ZOOM_API_ACCESS_TOKEN_PAYLOAD = "{\"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 SEB_API_NAME_INSTRUCTION_NAME_MAPPING = Utils.immutableMapOf(Arrays.asList( new Tuple<>( @@ -260,7 +263,7 @@ public class ZoomProctoringService implements ExamProctoringService { proctoringSettings.appSecret, remoteProctoringRoom.joinKey); - final String jwt = this.createJWTForMeetingAccess( + final String jwt = this.createSignatureForMeetingAccess( credentials, String.valueOf(additionalZoomRoomData.meeting_id), true); @@ -275,7 +278,7 @@ public class ZoomProctoringService implements ExamProctoringService { sdkJWT = this.createJWTForSDKAccess( sdkCredentials, - String.valueOf(additionalZoomRoomData.meeting_id)); + forExam(proctoringSettings)); } return new ProctoringRoomConnection( @@ -316,7 +319,7 @@ public class ZoomProctoringService implements ExamProctoringService { proctoringSettings.appSecret, remoteProctoringRoom.joinKey); - final String jwt = this.createJWTForMeetingAccess( + final String signature = this.createSignatureForMeetingAccess( credentials, String.valueOf(additionalZoomRoomData.meeting_id), false); @@ -335,7 +338,7 @@ public class ZoomProctoringService implements ExamProctoringService { sdkJWT = this.createJWTForSDKAccess( sdkCredentials, - String.valueOf(additionalZoomRoomData.meeting_id)); + forExam(proctoringSettings)); } return new ProctoringRoomConnection( @@ -345,7 +348,7 @@ public class ZoomProctoringService implements ExamProctoringService { additionalZoomRoomData.join_url, roomName, subject, - jwt, + signature, sdkJWT, credentials.accessToken, credentials.clientId, @@ -646,12 +649,57 @@ public class ZoomProctoringService implements ExamProctoringService { private String createJWTForSDKAccess( 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 String meetingId, 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 static final int LIZENSED_USER = 2;