Implement SDKToken for Zoom proctoring with SEB client for iOS and MacOS

This commit is contained in:
anhefti 2021-08-05 09:37:14 +02:00
parent 9a9ad6e840
commit 9d27dbfbcf
12 changed files with 143 additions and 10 deletions

View file

@ -23,6 +23,7 @@ public class ProctoringRoomConnection {
public static final String ATTR_ROOM_NAME = "roomName";
public static final String ATTR_SUBJECT = "subject";
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_USER_NAME = "userName";
public static final String ATTR_ROOM_KEY = "roomKey";
@ -50,6 +51,9 @@ public class ProctoringRoomConnection {
@JsonProperty(ATTR_ACCESS_TOKEN)
public final CharSequence accessToken;
@JsonProperty(ATTR_SDK_TOKEN)
public final CharSequence sdkToken;
@JsonProperty(ATTR_ROOM_KEY)
public final CharSequence roomKey;
@ -71,6 +75,7 @@ public class ProctoringRoomConnection {
@JsonProperty(ATTR_ROOM_NAME) final String roomName,
@JsonProperty(ATTR_SUBJECT) final String subject,
@JsonProperty(ATTR_ACCESS_TOKEN) final CharSequence accessToken,
@JsonProperty(ATTR_SDK_TOKEN) final CharSequence sdkToken,
@JsonProperty(ATTR_ROOM_KEY) final CharSequence roomKey,
@JsonProperty(ATTR_API_KEY) final CharSequence apiKey,
@JsonProperty(ATTR_MEETING_ID) final String meetingId,
@ -83,6 +88,7 @@ public class ProctoringRoomConnection {
this.roomName = roomName;
this.subject = subject;
this.accessToken = accessToken;
this.sdkToken = sdkToken;
this.roomKey = roomKey;
this.apiKey = apiKey;
this.meetingId = meetingId;
@ -105,6 +111,10 @@ public class ProctoringRoomConnection {
return this.accessToken;
}
public CharSequence getSdkToken() {
return this.sdkToken;
}
public CharSequence getRoomKey() {
return this.roomKey;
}

View file

@ -43,6 +43,8 @@ public class ProctoringServiceSettings implements Entity {
public static final String ATTR_SERVER_URL = "serverURL";
public static final String ATTR_APP_KEY = "appKey";
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_ENABLED_FEATURES = "enabledFeatures";
public static final String ATTR_COLLECT_ALL_ROOM_NAME = "collectAllRoomName";
@ -67,6 +69,12 @@ public class ProctoringServiceSettings implements Entity {
@JsonProperty(ATTR_APP_SECRET)
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)
public final Integer collectingRoomSize;
@ -86,7 +94,9 @@ public class ProctoringServiceSettings implements Entity {
@JsonProperty(ATTR_ENABLED_FEATURES) final EnumSet<ProctoringFeature> enabledFeatures,
@JsonProperty(ATTR_SERVICE_IN_USE) final Boolean serviceInUse,
@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.enableProctoring = BooleanUtils.isTrue(enableProctoring);
@ -97,6 +107,8 @@ public class ProctoringServiceSettings implements Entity {
this.serviceInUse = serviceInUse;
this.appKey = appKey;
this.appSecret = appSecret;
this.sdkKey = sdkKey;
this.sdkSecret = sdkSecret;
}
@ -147,6 +159,14 @@ public class ProctoringServiceSettings implements Entity {
return this.appSecret;
}
public String getSdkKey() {
return this.sdkKey;
}
public CharSequence getSdkSecret() {
return this.sdkSecret;
}
public Boolean getServiceInUse() {
return this.serviceInUse;
}
@ -193,7 +213,11 @@ public class ProctoringServiceSettings implements Entity {
builder.append(", appKey=");
builder.append(this.appKey);
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(this.collectingRoomSize);
builder.append(", enabledFeatures=");

View file

@ -54,6 +54,7 @@ public final class ClientInstruction {
public static final String ZOOM_USER_NAME = "zoomUserName";
public static final String ZOOM_API_KEY = "zoomAPIKey";
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_RECEIVE_AUDIO = "zoomReceiveAudio";
public static final String ZOOM_RECEIVE_VIDEO = "zoomReceiveVideo";

View file

@ -72,6 +72,11 @@ public class ExamProctoringSettings {
new LocTextKey("sebserver.exam.proctoring.form.appkey");
private final static LocTextKey SEB_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 =
new LocTextKey("sebserver.exam.proctoring.form.features");
@ -155,7 +160,9 @@ public class ExamProctoringSettings {
featureFlags,
false,
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) {
log.error("Unexpected error while trying to get settings from form: ", e);
@ -225,6 +232,8 @@ public class ExamProctoringSettings {
.copyOf(content)
.clearEntityKeys();
final boolean isZoom = proctoringSettings.serverType == ProctoringServerType.ZOOM;
final FormHandle<ProctoringServiceSettings> formHandle = this.pageService.formBuilder(
formContext)
.withDefaultSpanInput(5)
@ -249,7 +258,8 @@ public class ExamProctoringSettings {
ProctoringServiceSettings.ATTR_SERVER_TYPE,
SEB_PROCTORING_FORM_TYPE,
proctoringSettings.serverType.name(),
resourceService::examProctoringTypeResources))
resourceService::examProctoringTypeResources)
.withSelectionListener(this::serviceSelection))
.addField(FormBuilder.text(
ProctoringServiceSettings.ATTR_SERVER_URL,
@ -269,6 +279,21 @@ public class ExamProctoringSettings {
? String.valueOf(proctoringSettings.appSecret)
: 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)
.addField(FormBuilder.text(
ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE,
@ -294,6 +319,14 @@ public class ExamProctoringSettings {
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);
}
}
}

View file

@ -213,7 +213,9 @@ public class ExamAdminServiceImpl implements ExamAdminService {
getEnabledFeatures(mapping),
this.remoteProctoringRoomDAO.isServiceInUse(examId).getOr(true),
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()
.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(
EntityType.EXAM,
examId,

View file

@ -378,6 +378,7 @@ public class JitsiProctoringService implements ExamProctoringService {
null,
null,
null,
null,
clientName);
});
}

View file

@ -214,6 +214,9 @@ public class ZoomProctoringService implements ExamProctoringService {
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_TOKEN,
String.valueOf(proctoringConnection.accessToken));
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_SDK_TOKEN,
String.valueOf(proctoringConnection.sdkToken));
if (StringUtils.isNotBlank(proctoringConnection.apiKey)) {
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_API_KEY,
@ -262,6 +265,19 @@ public class ZoomProctoringService implements ExamProctoringService {
String.valueOf(additionalZoomRoomData.meeting_id),
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(
ProctoringServerType.ZOOM,
null,
@ -270,6 +286,7 @@ public class ZoomProctoringService implements ExamProctoringService {
roomName,
subject,
jwt,
sdkJWT,
credentials.accessToken,
credentials.clientId,
String.valueOf(additionalZoomRoomData.meeting_id),
@ -308,6 +325,19 @@ public class ZoomProctoringService implements ExamProctoringService {
.getConnectionData(connectionToken)
.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(
ProctoringServerType.ZOOM,
connectionToken,
@ -316,6 +346,7 @@ public class ZoomProctoringService implements ExamProctoringService {
roomName,
subject,
jwt,
sdkJWT,
credentials.accessToken,
credentials.clientId,
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(
final ClientCredentials credentials,
final String meetingId,

View file

@ -1,11 +1,11 @@
server.address=localhost
server.port=8090
server.port=8080
sebserver.gui.http.external.scheme=http
sebserver.gui.entrypoint=/gui
sebserver.gui.webservice.protocol=http
sebserver.gui.webservice.address=localhost
sebserver.gui.webservice.port=8090
sebserver.gui.webservice.port=8080
sebserver.gui.webservice.apipath=/admin-api/v1
# defines the polling interval that is used to poll the webservice for client connection data on a monitored exam page
sebserver.gui.webservice.poll-interval=1000

View file

@ -658,10 +658,14 @@ sebserver.exam.proctoring.form.url=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.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.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.sdkkey=SDK Key (Zoom)
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 (Zoom)
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.TOWN_HALL=Town-Hall Room
sebserver.exam.proctoring.form.features.ONE_TO_ONE=One to One Room

View file

@ -37,6 +37,7 @@ public class JitsiWindowScriptResolverTest {
"ROOM",
"SUBJECT",
"ACCESS_TOKEN",
null,
"API_KEY",
"ROOM_KEY",
"MEETING_ID",
@ -53,6 +54,7 @@ public class JitsiWindowScriptResolverTest {
"ROOM",
"SUBJECT",
"ACCESS_TOKEN",
null,
"API_KEY",
"ROOM_KEY",
"MEETING_ID",

View file

@ -37,6 +37,7 @@ public class ZoomWindowScriptResolverTest {
"ROOM",
"SUBJECT",
"ACCESS_TOKEN",
"SDK_TOKEN",
"API_KEY",
"ROOM_KEY",
"MEETING_ID",
@ -53,6 +54,7 @@ public class ZoomWindowScriptResolverTest {
"ROOM",
"SUBJECT",
"ACCESS_TOKEN",
"SDK_TOKEN",
"API_KEY",
"ROOM_KEY",
"MEETING_ID",

View file

@ -63,7 +63,7 @@ public class ExamProctoringRoomServiceTest extends AdministrationAPIIntegrationT
2L,
new ProctoringServiceSettings(
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());
}