added encryption for SEB Exam config download
This commit is contained in:
parent
79725f9924
commit
a7097d8b81
23 changed files with 467 additions and 83 deletions
|
@ -6,7 +6,7 @@ ARG SEBSERVER_VERSION
|
|||
WORKDIR /demo
|
||||
RUN if [ "x${GIT_TAG}" = "x" ] ; \
|
||||
then git clone --depth 1 https://github.com/SafeExamBrowser/seb-server.git ; \
|
||||
else git clone -b "v$GIT_TAG" --depth 1 https://github.com/SafeExamBrowser/seb-server.git ; fi
|
||||
else git clone -b "$GIT_TAG" --depth 1 https://github.com/SafeExamBrowser/seb-server.git ; fi
|
||||
|
||||
FROM maven:3.5-jdk-8-alpine
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ services:
|
|||
container_name: seb-server-mariadb
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: somePW
|
||||
volumes:
|
||||
- seb-server-mariadb-data:/var/lib/mysql
|
||||
ports:
|
||||
- 3306:3306
|
||||
networks:
|
||||
|
@ -33,4 +35,7 @@ services:
|
|||
- "mariadb"
|
||||
|
||||
networks:
|
||||
ralph:
|
||||
ralph:
|
||||
|
||||
volumes:
|
||||
seb-server-mariadb-data:
|
|
@ -27,6 +27,7 @@ public final class API {
|
|||
|
||||
public static final String INSTITUTION_VAR_PATH_SEGMENT = "/{" + PARAM_INSTITUTION_ID + "}";
|
||||
public static final String MODEL_ID_VAR_PATH_SEGMENT = "/{" + PARAM_MODEL_ID + "}";
|
||||
public static final String PARENT_MODEL_ID_VAR_PATH_SEGMENT = "/{" + PARAM_PARENT_MODEL_ID + "}";
|
||||
|
||||
public static final String OAUTH_ENDPOINT = "/oauth";
|
||||
public static final String OAUTH_TOKEN_ENDPOINT = OAUTH_ENDPOINT + "/token"; // TODO to config properties?
|
||||
|
@ -100,6 +101,7 @@ public final class API {
|
|||
public static final String QUIZ_DISCOVERY_ENDPOINT = "/quiz";
|
||||
|
||||
public static final String EXAM_ADMINISTRATION_ENDPOINT = "/exam";
|
||||
public static final String EXAM_ADMINISTRATION_DOWNLOAD_CONFIG_PATH_SEGMENT = "/downloadConfig";
|
||||
|
||||
public static final String EXAM_INDICATOR_ENDPOINT = "/indicator";
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ import org.slf4j.LoggerFactory;
|
|||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.context.support.WebApplicationContextUtils;
|
||||
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.DownloadService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext;
|
||||
|
||||
|
|
|
@ -17,9 +17,12 @@ import java.util.function.Supplier;
|
|||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.tomcat.util.buf.StringUtils;
|
||||
import org.eclipse.rap.rwt.RWT;
|
||||
import org.eclipse.rap.rwt.client.service.UrlLauncher;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
@ -50,6 +53,8 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder;
|
|||
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.event.ActionEvent;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.download.SebExamConfigDownload;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeleteExamConfigMapping;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.DeleteIndicator;
|
||||
|
@ -73,9 +78,6 @@ public class ExamForm implements TemplateComposer {
|
|||
|
||||
private static final Logger log = LoggerFactory.getLogger(ExamForm.class);
|
||||
|
||||
private final PageService pageService;
|
||||
private final ResourceService resourceService;
|
||||
|
||||
private static final LocTextKey CONFIG_EMPTY_LIST_MESSAGE =
|
||||
new LocTextKey("sebserver.exam.configuration.list.empty");
|
||||
private static final LocTextKey INDICATOR_EMPTY_LIST_MESSAGE =
|
||||
|
@ -123,12 +125,21 @@ public class ExamForm implements TemplateComposer {
|
|||
private final static LocTextKey INDICATOR_EMPTY_SELECTION_TEXT_KEY =
|
||||
new LocTextKey("sebserver.exam.indicator.list.pleaseSelect");
|
||||
|
||||
private final PageService pageService;
|
||||
private final ResourceService resourceService;
|
||||
private final DownloadService downloadService;
|
||||
private final String downloadFileName;
|
||||
|
||||
protected ExamForm(
|
||||
final PageService pageService,
|
||||
final ResourceService resourceService) {
|
||||
final ResourceService resourceService,
|
||||
final DownloadService downloadService,
|
||||
@Value("${sebserver.gui.seb.exam.config.download.filename}") final String downloadFileName) {
|
||||
|
||||
this.pageService = pageService;
|
||||
this.resourceService = resourceService;
|
||||
this.downloadService = downloadService;
|
||||
this.downloadFileName = downloadFileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -305,19 +316,7 @@ public class ExamForm implements TemplateComposer {
|
|||
this.resourceService::localizedExamConfigStatusName))
|
||||
.withDefaultActionIf(
|
||||
() -> editable,
|
||||
t -> actionBuilder
|
||||
.newAction(ActionDefinition.EXAM_CONFIGURATION_EXAM_CONFIG_VIEW_PROP)
|
||||
.withSelectionSupplier(() -> {
|
||||
final ExamConfigurationMap selectedROWData = t.getSelectedROWData();
|
||||
final HashSet<EntityKey> result = new HashSet<>();
|
||||
if (selectedROWData != null) {
|
||||
result.add(new EntityKey(
|
||||
selectedROWData.configurationNodeId,
|
||||
EntityType.CONFIGURATION_NODE));
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.create())
|
||||
this::viewExamConfigPageAction)
|
||||
|
||||
.compose(pageContext.copyOf(content));
|
||||
|
||||
|
@ -356,6 +355,15 @@ public class ExamForm implements TemplateComposer {
|
|||
CONFIG_EMPTY_SELECTION_TEXT_KEY)
|
||||
.publishIf(() -> modifyGrant && configurationTable.hasAnyContent() && editable)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_CONFIGURATION_EXPORT)
|
||||
.withParentEntityKey(entityKey)
|
||||
.withSelect(
|
||||
getConfigSelection(configurationTable),
|
||||
this::downloadExamConfigAction,
|
||||
CONFIG_EMPTY_SELECTION_TEXT_KEY)
|
||||
.noEventPropagation()
|
||||
.publishIf(() -> userGrantCheck.r() && configurationTable.hasAnyContent())
|
||||
|
||||
.newAction(ActionDefinition.EXAM_CONFIGURATION_GET_CONFIG_KEY)
|
||||
.withSelect(
|
||||
getConfigSelection(configurationTable),
|
||||
|
@ -423,6 +431,41 @@ public class ExamForm implements TemplateComposer {
|
|||
}
|
||||
}
|
||||
|
||||
private PageAction viewExamConfigPageAction(final EntityTable<ExamConfigurationMap> table) {
|
||||
|
||||
final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(table.getPageContext()
|
||||
.clearEntityKeys()
|
||||
.removeAttribute(AttributeKeys.IMPORT_FROM_QUIZ_DATA));
|
||||
|
||||
return actionBuilder
|
||||
.newAction(ActionDefinition.EXAM_CONFIGURATION_EXAM_CONFIG_VIEW_PROP)
|
||||
.withSelectionSupplier(() -> {
|
||||
final ExamConfigurationMap selectedROWData = table.getSelectedROWData();
|
||||
final HashSet<EntityKey> result = new HashSet<>();
|
||||
if (selectedROWData != null) {
|
||||
result.add(new EntityKey(
|
||||
selectedROWData.configurationNodeId,
|
||||
EntityType.CONFIGURATION_NODE));
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.create();
|
||||
}
|
||||
|
||||
private PageAction downloadExamConfigAction(final PageAction action) {
|
||||
final UrlLauncher urlLauncher = RWT.getClient().getService(UrlLauncher.class);
|
||||
final EntityKey selection = action.getSingleSelection();
|
||||
if (selection != null) {
|
||||
final String downloadURL = this.downloadService.createDownloadURL(
|
||||
selection.modelId,
|
||||
action.pageContext().getParentEntityKey().modelId,
|
||||
SebExamConfigDownload.class,
|
||||
this.downloadFileName);
|
||||
urlLauncher.openURL(downloadURL);
|
||||
}
|
||||
return action;
|
||||
}
|
||||
|
||||
private Supplier<Set<EntityKey>> getConfigMappingSelection(
|
||||
final EntityTable<ExamConfigurationMap> configurationTable) {
|
||||
return () -> {
|
||||
|
|
|
@ -32,8 +32,8 @@ import ch.ethz.seb.sebserver.gui.service.page.PageContext;
|
|||
import ch.ethz.seb.sebserver.gui.service.page.PageService;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.impl.PageUtils;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.DownloadService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.SebClientConfigDownload;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.download.SebClientConfigDownload;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.clientconfig.ActivateClientConfig;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.clientconfig.DeactivateClientConfig;
|
||||
|
|
|
@ -39,8 +39,8 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService;
|
|||
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.DownloadService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.SebExamConfigDownload;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.download.DownloadService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.download.SebExamConfigPlaintextDownload;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ExportConfigKey;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.GetExamConfigNode;
|
||||
|
@ -188,7 +188,7 @@ public class SebExamConfigPropForm implements TemplateComposer {
|
|||
.withExec(action -> {
|
||||
final String downloadURL = this.downloadService.createDownloadURL(
|
||||
entityKey.modelId,
|
||||
SebExamConfigDownload.class,
|
||||
SebExamConfigPlaintextDownload.class,
|
||||
this.downloadFileName);
|
||||
urlLauncher.openURL(downloadURL);
|
||||
return action;
|
||||
|
|
|
@ -247,8 +247,13 @@ public enum ActionDefinition {
|
|||
ImageIcon.DELETE,
|
||||
PageStateDefinition.EXAM_VIEW,
|
||||
ActionCategory.EXAM_CONFIG_MAPPING_LIST),
|
||||
EXAM_CONFIGURATION_EXPORT(
|
||||
new LocTextKey("sebserver.exam.configuration.action.export-config"),
|
||||
ImageIcon.EXPORT,
|
||||
PageStateDefinition.EXAM_VIEW,
|
||||
ActionCategory.EXAM_CONFIG_MAPPING_LIST),
|
||||
EXAM_CONFIGURATION_GET_CONFIG_KEY(
|
||||
new LocTextKey("sebserver.examconfig.action.get-config-key"),
|
||||
new LocTextKey("sebserver.exam.configuration.action.get-config-key"),
|
||||
ImageIcon.SECURE,
|
||||
ActionCategory.EXAM_CONFIG_MAPPING_LIST),
|
||||
EXAM_CONFIGURATION_SAVE(
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ch.ethz.seb.sebserver.gui.service.remote;
|
||||
package ch.ethz.seb.sebserver.gui.service.remote.download;
|
||||
|
||||
import java.io.OutputStream;
|
||||
|
||||
|
@ -39,44 +39,34 @@ public abstract class AbstractDownloadServiceHandler implements DownloadServiceH
|
|||
|
||||
log.debug("download requested... trying to get needed parameter from request");
|
||||
|
||||
final String configId = request.getParameter(API.PARAM_MODEL_ID);
|
||||
if (StringUtils.isBlank(configId)) {
|
||||
final String modelId = request.getParameter(API.PARAM_MODEL_ID);
|
||||
if (StringUtils.isBlank(modelId)) {
|
||||
log.error(
|
||||
"Mandatory modelId parameter not found within HttpServletRequest. Download request is ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(
|
||||
"Found modelId: {} for {} download. Trying to request webservice...",
|
||||
configId,
|
||||
downloadFileName);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(
|
||||
"Found modelId: {} for {} download. Trying to request webservice...",
|
||||
modelId,
|
||||
downloadFileName);
|
||||
}
|
||||
|
||||
final String parentModelId = request.getParameter(API.PARAM_PARENT_MODEL_ID);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(
|
||||
"Found parentModelId: {} for {} download. Trying to request webservice...",
|
||||
modelId,
|
||||
downloadFileName);
|
||||
}
|
||||
|
||||
final String header =
|
||||
"attachment; filename=\"" + Utils.preventResponseSplittingAttack(downloadFileName) + "\"";
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, header);
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
|
||||
webserviceCall(configId, response.getOutputStream());
|
||||
|
||||
// final byte[] configFile = webserviceCall(configId);
|
||||
//
|
||||
// if (configFile == null) {
|
||||
// log.error("No or empty download received from webservice. Download request is ignored");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// log.debug("Sucessfully downloaded from webservice. File size: {}", configFile.length);
|
||||
//
|
||||
// response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
// response.setContentLength(configFile.length);
|
||||
//
|
||||
// final String header =
|
||||
// "attachment; filename=\"" + Utils.preventResponseSplittingAttack(downloadFileName) + "\"";
|
||||
// response.setHeader(HttpHeaders.CONTENT_DISPOSITION, header);
|
||||
//
|
||||
// log.debug("Write the download data to response output");
|
||||
//
|
||||
// response.getOutputStream().write(configFile);
|
||||
webserviceCall(modelId, parentModelId, response.getOutputStream());
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error(
|
||||
|
@ -85,6 +75,6 @@ public abstract class AbstractDownloadServiceHandler implements DownloadServiceH
|
|||
}
|
||||
}
|
||||
|
||||
protected abstract void webserviceCall(String configId, OutputStream downloadOut);
|
||||
protected abstract void webserviceCall(String modelId, String parentModelId, OutputStream downloadOut);
|
||||
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ch.ethz.seb.sebserver.gui.service.remote;
|
||||
package ch.ethz.seb.sebserver.gui.service.remote.download;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
@ -81,6 +81,15 @@ public class DownloadService implements ServiceHandler {
|
|||
final Class<? extends DownloadServiceHandler> handlerClass,
|
||||
final String downloadFileName) {
|
||||
|
||||
return createDownloadURL(modelId, null, handlerClass, downloadFileName);
|
||||
}
|
||||
|
||||
public String createDownloadURL(
|
||||
final String modelId,
|
||||
final String parentModelId,
|
||||
final Class<? extends DownloadServiceHandler> handlerClass,
|
||||
final String downloadFileName) {
|
||||
|
||||
final StringBuilder url = new StringBuilder()
|
||||
.append(RWT.getServiceManager()
|
||||
.getServiceHandlerUrl(DownloadService.DOWNLOAD_SERVICE_NAME))
|
||||
|
@ -96,6 +105,14 @@ public class DownloadService implements ServiceHandler {
|
|||
.append(DownloadService.DOWNLOAD_FILE_NAME)
|
||||
.append(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR)
|
||||
.append(downloadFileName);
|
||||
|
||||
if (StringUtils.isNotBlank(parentModelId)) {
|
||||
url.append(Constants.FORM_URL_ENCODED_SEPARATOR)
|
||||
.append(API.PARAM_PARENT_MODEL_ID)
|
||||
.append(Constants.FORM_URL_ENCODED_NAME_VALUE_SEPARATOR)
|
||||
.append(parentModelId);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ch.ethz.seb.sebserver.gui.service.remote;
|
||||
package ch.ethz.seb.sebserver.gui.service.remote.download;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
|
@ -6,7 +6,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ch.ethz.seb.sebserver.gui.service.remote;
|
||||
package ch.ethz.seb.sebserver.gui.service.remote.download;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -43,7 +43,7 @@ public class SebClientConfigDownload extends AbstractDownloadServiceHandler {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void webserviceCall(final String modelId, final OutputStream downloadOut) {
|
||||
protected void webserviceCall(final String modelId, final String parentModelId, final OutputStream downloadOut) {
|
||||
|
||||
final InputStream input = this.restService.getBuilder(ExportClientConfig.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, modelId)
|
|
@ -6,7 +6,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ch.ethz.seb.sebserver.gui.service.remote;
|
||||
package ch.ethz.seb.sebserver.gui.service.remote.download;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -21,7 +21,7 @@ import org.springframework.stereotype.Component;
|
|||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ExportPlainXML;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.ExportExamConfig;
|
||||
|
||||
@Lazy
|
||||
@Component
|
||||
|
@ -37,10 +37,11 @@ public class SebExamConfigDownload extends AbstractDownloadServiceHandler {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void webserviceCall(final String modelId, final OutputStream downloadOut) {
|
||||
protected void webserviceCall(final String modelId, final String parentModelId, final OutputStream downloadOut) {
|
||||
|
||||
final InputStream input = this.restService.getBuilder(ExportPlainXML.class)
|
||||
final InputStream input = this.restService.getBuilder(ExportExamConfig.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, modelId)
|
||||
.withURIVariable(API.PARAM_PARENT_MODEL_ID, parentModelId)
|
||||
.call()
|
||||
.getOrThrow();
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (c) 2019 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.gui.service.remote.download;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import org.apache.tomcat.util.http.fileupload.IOUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.seb.examconfig.ExportPlainXML;
|
||||
|
||||
@Lazy
|
||||
@Component
|
||||
@GuiProfile
|
||||
public class SebExamConfigPlaintextDownload extends AbstractDownloadServiceHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SebExamConfigPlaintextDownload.class);
|
||||
|
||||
private final RestService restService;
|
||||
|
||||
protected SebExamConfigPlaintextDownload(final RestService restService) {
|
||||
this.restService = restService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void webserviceCall(final String modelId, final String parentModelId, final OutputStream downloadOut) {
|
||||
|
||||
final InputStream input = this.restService.getBuilder(ExportPlainXML.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, modelId)
|
||||
.call()
|
||||
.getOrThrow();
|
||||
|
||||
try {
|
||||
IOUtils.copyLarge(input, downloadOut);
|
||||
} catch (final IOException e) {
|
||||
log.error(
|
||||
"Unexpected error while streaming incomming config data from web-service to output-stream of download response: ",
|
||||
e);
|
||||
} finally {
|
||||
try {
|
||||
downloadOut.flush();
|
||||
downloadOut.close();
|
||||
} catch (final IOException e) {
|
||||
log.error("Unexpected error while trying to close download output-stream");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) 2019 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.gui.service.remote.webservice.api.exam;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.AbstractExportCall;
|
||||
|
||||
@Lazy
|
||||
@Component
|
||||
@GuiProfile
|
||||
public class ExportExamConfig extends AbstractExportCall {
|
||||
|
||||
public ExportExamConfig() {
|
||||
super(new TypeKey<>(
|
||||
CallType.UNDEFINED,
|
||||
EntityType.EXAM,
|
||||
new TypeReference<InputStream>() {
|
||||
}),
|
||||
HttpMethod.GET,
|
||||
MediaType.APPLICATION_FORM_URLENCODED,
|
||||
API.EXAM_ADMINISTRATION_ENDPOINT
|
||||
+ API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.EXAM_ADMINISTRATION_DOWNLOAD_CONFIG_PATH_SEGMENT
|
||||
+ API.PARENT_MODEL_ID_VAR_PATH_SEGMENT);
|
||||
}
|
||||
|
||||
}
|
|
@ -189,6 +189,14 @@ public class EntityTable<ROW extends Entity> {
|
|||
this.sortOrder);
|
||||
}
|
||||
|
||||
public PageContext getPageContext() {
|
||||
if (this.pageContext == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.pageContext.copy();
|
||||
}
|
||||
|
||||
public boolean hasAnyContent() {
|
||||
return this.table.getItemCount() > 0;
|
||||
}
|
||||
|
|
|
@ -55,11 +55,6 @@ public class BatisConfig {
|
|||
factoryBean.setDataSource(dataSource);
|
||||
final SqlSessionFactory factory = factoryBean.getObject();
|
||||
|
||||
factory.getConfiguration()
|
||||
.addMappers("ch.ethz.seb.sebserver.webservice.datalayer.batis");
|
||||
factory.getConfiguration()
|
||||
.addMappers("ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper");
|
||||
|
||||
return factory;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,20 @@ public interface ExamConfigurationMapDAO extends
|
|||
EntityDAO<ExamConfigurationMap, ExamConfigurationMap>,
|
||||
BulkActionSupportDAO<ExamConfigurationMap> {
|
||||
|
||||
/** Get a specific ExamConfigurationMap by the mapping identifiers
|
||||
*
|
||||
* @param examId The Exam mapping identifier
|
||||
* @param configurationNodeId the ConfigurationNode mapping identifier
|
||||
* @return Result refer to the ExamConfigurationMap with specified mapping or to an exception if happened */
|
||||
public Result<ExamConfigurationMap> byMapping(Long examId, Long configurationNodeId);
|
||||
|
||||
/** Get the password cipher of a specific ExamConfigurationMap by the mapping identifiers
|
||||
*
|
||||
* @param examId The Exam mapping identifier
|
||||
* @param configurationNodeId the ConfigurationNode mapping identifier
|
||||
* @return Result refer to the password cipher of specified mapping or to an exception if happened */
|
||||
public Result<CharSequence> getConfigPasswortCipher(Long examId, Long configurationNodeId);
|
||||
|
||||
/** Get the ConfigurationNode identifier of the default Exam Configuration of
|
||||
* the Exam with specified identifier.
|
||||
*
|
||||
|
|
|
@ -126,6 +126,43 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
|
|||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Result<ExamConfigurationMap> byMapping(final Long examId, final Long configurationNodeId) {
|
||||
return Result.tryCatch(() -> this.examConfigurationMapRecordMapper
|
||||
.selectByExample()
|
||||
.where(
|
||||
ExamConfigurationMapRecordDynamicSqlSupport.examId,
|
||||
SqlBuilder.isEqualTo(examId))
|
||||
.and(
|
||||
ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId,
|
||||
SqlBuilder.isEqualTo(configurationNodeId))
|
||||
.build()
|
||||
.execute()
|
||||
.stream()
|
||||
.map(this::toDomainModel)
|
||||
.flatMap(DAOLoggingSupport::logAndSkipOnError)
|
||||
.collect(Utils.toSingleton()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Result<CharSequence> getConfigPasswortCipher(final Long examId, final Long configurationNodeId) {
|
||||
return Result.tryCatch(() -> this.examConfigurationMapRecordMapper
|
||||
.selectByExample()
|
||||
.where(
|
||||
ExamConfigurationMapRecordDynamicSqlSupport.examId,
|
||||
SqlBuilder.isEqualTo(examId))
|
||||
.and(
|
||||
ExamConfigurationMapRecordDynamicSqlSupport.configurationNodeId,
|
||||
SqlBuilder.isEqualTo(configurationNodeId))
|
||||
.build()
|
||||
.execute()
|
||||
.stream()
|
||||
.collect(Utils.toSingleton()))
|
||||
.map(ExamConfigurationMapRecord::getEncryptSecret);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Result<Long> getDefaultConfigurationForExam(final Long examId) {
|
||||
|
|
|
@ -61,7 +61,7 @@ public interface SebExamConfigService {
|
|||
* @param examId the exam identifier
|
||||
* @return The configuration node identifier (PK) */
|
||||
default Long exportForExam(final OutputStream out, final Long institutionId, final Long examId) {
|
||||
return exportForExam(out, institutionId, examId, null);
|
||||
return exportForExam(out, institutionId, examId, (String) null);
|
||||
}
|
||||
|
||||
/** Used to export the default SEB Exam Configuration for a given exam identifier.
|
||||
|
@ -75,7 +75,23 @@ public interface SebExamConfigService {
|
|||
* @return The configuration node identifier (PK) */
|
||||
Long exportForExam(OutputStream out, Long institutionId, Long examId, String userId);
|
||||
|
||||
/** TODO */
|
||||
/** Used to export the default SEB Exam Configuration for a given exam identifier.
|
||||
* either with encryption if defined or as plain text within the SEB Configuration format
|
||||
* as described here: https://www.safeexambrowser.org/developer/seb-file-format.html
|
||||
*
|
||||
* @param out The output stream to write the export data to
|
||||
* @param institutionId The identifier of the institution of the requesting user
|
||||
* @param examId the exam identifier that defines the mapping
|
||||
* @param configurationNodeId the configurationNodeId that defines the mapping
|
||||
* @return The configuration node identifier (PK) */
|
||||
Long exportForExam(OutputStream out, Long institutionId, Long examId, Long configurationNodeId);
|
||||
|
||||
/** Generates a Config-Key form the SEB exam configuration defined by configurationNodeId.
|
||||
* See https://www.safeexambrowser.org/developer/seb-config-key.html for more information about the Config-Key
|
||||
*
|
||||
* @param institutionId the institutional id
|
||||
* @param configurationNodeId the configurationNodeId
|
||||
* @return Result refer to the generated Config-Key or to an error if happened. */
|
||||
Result<String> generateConfigKey(Long institutionId, Long configurationNodeId);
|
||||
|
||||
}
|
||||
|
|
|
@ -33,11 +33,16 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues;
|
|||
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamConfigurationMapDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationFormat;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationValueValidator;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebConfigEncryptionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebConfigEncryptionService.Strategy;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebExamConfigService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ZipService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl.SebConfigEncryptionServiceImpl.EncryptionContext;
|
||||
|
||||
@Lazy
|
||||
@Service
|
||||
|
@ -50,17 +55,26 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
|
|||
private final ConfigurationAttributeDAO configurationAttributeDAO;
|
||||
private final ExamConfigurationMapDAO examConfigurationMapDAO;
|
||||
private final Collection<ConfigurationValueValidator> validators;
|
||||
private final ClientCredentialService clientCredentialService;
|
||||
private final ZipService zipService;
|
||||
private final SebConfigEncryptionService sebConfigEncryptionService;
|
||||
|
||||
protected SebExamConfigServiceImpl(
|
||||
final ExamConfigIO examConfigIO,
|
||||
final ConfigurationAttributeDAO configurationAttributeDAO,
|
||||
final ExamConfigurationMapDAO examConfigurationMapDAO,
|
||||
final Collection<ConfigurationValueValidator> validators) {
|
||||
final Collection<ConfigurationValueValidator> validators,
|
||||
final ClientCredentialService clientCredentialService,
|
||||
final ZipService zipService,
|
||||
final SebConfigEncryptionService sebConfigEncryptionService) {
|
||||
|
||||
this.examConfigIO = examConfigIO;
|
||||
this.configurationAttributeDAO = configurationAttributeDAO;
|
||||
this.examConfigurationMapDAO = examConfigurationMapDAO;
|
||||
this.validators = validators;
|
||||
this.clientCredentialService = clientCredentialService;
|
||||
this.zipService = zipService;
|
||||
this.sebConfigEncryptionService = sebConfigEncryptionService;
|
||||
|
||||
}
|
||||
|
||||
|
@ -114,7 +128,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
|
|||
final Long institutionId,
|
||||
final Long configurationNodeId) {
|
||||
|
||||
this.exportPlain(ConfigurationFormat.XML, out, institutionId, configurationNodeId);
|
||||
this.exportPlainOnly(ConfigurationFormat.XML, out, institutionId, configurationNodeId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -123,7 +137,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
|
|||
final Long institutionId,
|
||||
final Long configurationNodeId) {
|
||||
|
||||
this.exportPlain(ConfigurationFormat.JSON, out, institutionId, configurationNodeId);
|
||||
this.exportPlainOnly(ConfigurationFormat.JSON, out, institutionId, configurationNodeId);
|
||||
}
|
||||
|
||||
public Result<Long> getDefaultConfigurationIdForExam(final Long examId) {
|
||||
|
@ -147,9 +161,83 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
|
|||
: getUserConfigurationIdForExam(examId, userId)
|
||||
.getOrThrow();
|
||||
|
||||
// TODO add header, zip and encrypt if needed
|
||||
return exportForExam(out, institutionId, examId, configurationNodeId);
|
||||
}
|
||||
|
||||
this.exportPlainXML(out, institutionId, configurationNodeId);
|
||||
@Override
|
||||
public Long exportForExam(
|
||||
final OutputStream out,
|
||||
final Long institutionId,
|
||||
final Long examId,
|
||||
final Long configurationNodeId) {
|
||||
|
||||
final CharSequence passwordCipher = this.examConfigurationMapDAO
|
||||
.getConfigPasswortCipher(examId, configurationNodeId)
|
||||
.getOr(null);
|
||||
|
||||
if (StringUtils.isNotBlank(passwordCipher)) {
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("*** Seb exam configuration with password based encryption");
|
||||
}
|
||||
|
||||
final CharSequence encryptionPasswordPlaintext = this.clientCredentialService
|
||||
.decrypt(passwordCipher);
|
||||
|
||||
PipedOutputStream plainOut = null;
|
||||
PipedInputStream zipIn = null;
|
||||
|
||||
PipedOutputStream zipOut = null;
|
||||
PipedInputStream cryptIn = null;
|
||||
|
||||
PipedOutputStream cryptOut = null;
|
||||
PipedInputStream in = null;
|
||||
|
||||
try {
|
||||
|
||||
plainOut = new PipedOutputStream();
|
||||
zipIn = new PipedInputStream(plainOut);
|
||||
|
||||
zipOut = new PipedOutputStream();
|
||||
cryptIn = new PipedInputStream(zipOut);
|
||||
|
||||
cryptOut = new PipedOutputStream();
|
||||
in = new PipedInputStream(cryptOut);
|
||||
|
||||
// streaming...
|
||||
// export plain text
|
||||
this.examConfigIO.exportPlain(
|
||||
ConfigurationFormat.XML,
|
||||
plainOut,
|
||||
institutionId,
|
||||
configurationNodeId);
|
||||
// zip the plain text
|
||||
this.zipService.write(zipOut, zipIn);
|
||||
// encrypt the zipped plain text
|
||||
this.sebConfigEncryptionService.streamEncrypted(
|
||||
cryptOut,
|
||||
cryptIn,
|
||||
EncryptionContext.contextOf(
|
||||
Strategy.PASSWORD_PSWD,
|
||||
encryptionPasswordPlaintext));
|
||||
|
||||
// copy to output
|
||||
IOUtils.copyLarge(in, out);
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Error while zip and encrypt seb exam config stream: ", e);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(zipIn);
|
||||
IOUtils.closeQuietly(plainOut);
|
||||
IOUtils.closeQuietly(cryptIn);
|
||||
IOUtils.closeQuietly(zipOut);
|
||||
IOUtils.closeQuietly(in);
|
||||
IOUtils.closeQuietly(cryptOut);
|
||||
}
|
||||
} else {
|
||||
// just export in plain text XML format
|
||||
this.exportPlainXML(out, institutionId, configurationNodeId);
|
||||
}
|
||||
|
||||
return configurationNodeId;
|
||||
}
|
||||
|
@ -224,7 +312,7 @@ public class SebExamConfigServiceImpl implements SebExamConfigService {
|
|||
}
|
||||
}
|
||||
|
||||
private void exportPlain(
|
||||
private void exportPlainOnly(
|
||||
final ConfigurationFormat exportFormat,
|
||||
final OutputStream out,
|
||||
final Long institutionId,
|
||||
|
|
|
@ -8,17 +8,25 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.webservice.weblayer.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.mybatis.dynamic.sql.SqlTable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
@ -50,6 +58,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
|||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebExamConfigService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
|
||||
|
||||
@WebServiceProfile
|
||||
|
@ -57,9 +66,12 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe
|
|||
@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.EXAM_ADMINISTRATION_ENDPOINT)
|
||||
public class ExamAdministrationController extends ActivatableEntityController<Exam, Exam> {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ExamAdministrationController.class);
|
||||
|
||||
private final ExamDAO examDAO;
|
||||
private final UserDAO userDAO;
|
||||
private final LmsAPIService lmsAPIService;
|
||||
private final SebExamConfigService sebExamConfigService;
|
||||
|
||||
public ExamAdministrationController(
|
||||
final AuthorizationService authorization,
|
||||
|
@ -69,7 +81,8 @@ public class ExamAdministrationController extends ActivatableEntityController<Ex
|
|||
final BulkActionService bulkActionService,
|
||||
final BeanValidationService beanValidationService,
|
||||
final LmsAPIService lmsAPIService,
|
||||
final UserDAO userDAO) {
|
||||
final UserDAO userDAO,
|
||||
final SebExamConfigService sebExamConfigService) {
|
||||
|
||||
super(authorization,
|
||||
bulkActionService,
|
||||
|
@ -81,6 +94,7 @@ public class ExamAdministrationController extends ActivatableEntityController<Ex
|
|||
this.examDAO = examDAO;
|
||||
this.userDAO = userDAO;
|
||||
this.lmsAPIService = lmsAPIService;
|
||||
this.sebExamConfigService = sebExamConfigService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -133,6 +147,46 @@ public class ExamAdministrationController extends ActivatableEntityController<Ex
|
|||
}
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.EXAM_ADMINISTRATION_DOWNLOAD_CONFIG_PATH_SEGMENT
|
||||
+ API.PARENT_MODEL_ID_VAR_PATH_SEGMENT,
|
||||
method = RequestMethod.GET,
|
||||
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||
public void downloadPlainXMLConfig(
|
||||
@PathVariable final Long modelId,
|
||||
@PathVariable final Long parentModelId,
|
||||
@RequestParam(
|
||||
name = API.PARAM_INSTITUTION_ID,
|
||||
required = true,
|
||||
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
|
||||
final HttpServletResponse response) throws IOException {
|
||||
|
||||
this.entityDAO.byPK(modelId)
|
||||
.flatMap(this.authorization::checkRead)
|
||||
.flatMap(this.userActivityLogDAO::logExport);
|
||||
|
||||
final ServletOutputStream outputStream = response.getOutputStream();
|
||||
|
||||
try {
|
||||
|
||||
this.sebExamConfigService.exportForExam(
|
||||
outputStream,
|
||||
institutionId,
|
||||
parentModelId,
|
||||
modelId);
|
||||
|
||||
response.setStatus(HttpStatus.OK.value());
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Unexpected error while trying to downstream exam config: ", e);
|
||||
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
} finally {
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static Page<Exam> buildSortedExamPage(
|
||||
final Integer pageNumber,
|
||||
final Integer pageSize,
|
||||
|
|
|
@ -311,8 +311,8 @@ sebserver.exam.status.UP_COMING=Up Coming
|
|||
sebserver.exam.status.RUNNING=Running
|
||||
sebserver.exam.status.FINISHED=Finished
|
||||
|
||||
sebserver.exam.configuration.list.actions=SEB Configuration
|
||||
sebserver.exam.configuration.list.title=SEB Configuration
|
||||
sebserver.exam.configuration.list.actions=SEB Exam Configuration
|
||||
sebserver.exam.configuration.list.title=SEB Exam Configuration
|
||||
sebserver.exam.configuration.list.column.name=Name
|
||||
sebserver.exam.configuration.list.column.description=Description
|
||||
sebserver.exam.configuration.list.column.status=Status
|
||||
|
@ -320,10 +320,12 @@ sebserver.exam.configuration.list.empty=There is currently no SEB Configuration
|
|||
sebserver.exam.configuration.list.pleaseSelect=Please Select a SEB Configuration first
|
||||
sebserver.exam.configuration.action.noconfig.message=There is currently no SEB exam configuration to select.<br/>Please create one in SEB Configuration / Exam Configuration
|
||||
|
||||
sebserver.exam.configuration.action.list.new=Add
|
||||
sebserver.exam.configuration.action.list.modify=Edit
|
||||
sebserver.exam.configuration.action.list.delete=Delete
|
||||
sebserver.exam.configuration.action.save=Save
|
||||
sebserver.exam.configuration.action.list.new=Add Configuration
|
||||
sebserver.exam.configuration.action.list.modify=Edit Configuration
|
||||
sebserver.exam.configuration.action.list.delete=Delete Configuration
|
||||
sebserver.exam.configuration.action.save=Save Configuration
|
||||
sebserver.exam.configuration.action.export-config=Export Configuration
|
||||
sebserver.exam.configuration.action.get-config-key=Export Config-Key
|
||||
|
||||
sebserver.exam.configuration.form.title.new=Add SEB Configuration Mapping
|
||||
sebserver.exam.configuration.form.title=SEB Configuration Mapping
|
||||
|
|
Loading…
Reference in a new issue