added encryption for SEB Exam config download

This commit is contained in:
anhefti 2019-08-26 13:07:29 +02:00
parent 79725f9924
commit a7097d8b81
23 changed files with 467 additions and 83 deletions

View file

@ -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

View file

@ -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:

View file

@ -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";

View file

@ -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;

View file

@ -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 () -> {

View file

@ -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;

View file

@ -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;

View file

@ -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(

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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;

View file

@ -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)

View file

@ -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();

View file

@ -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");
}
}
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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.
*

View file

@ -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) {

View file

@ -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);
}

View file

@ -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,

View file

@ -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,

View file

@ -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