Merge branch 'development' of github.com:SafeExamBrowser/seb-server into SEBSLI-9

This commit is contained in:
Nadim Ritter 2024-02-29 16:56:28 +01:00
commit f3a34ab06f
39 changed files with 719 additions and 441 deletions

View file

@ -18,16 +18,16 @@ jobs:
steps:
-
name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
-
name: Set up JDK 8
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: '8'
distribution: 'adopt'
-
name: Cache Maven packages
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
@ -37,7 +37,7 @@ jobs:
run: mvn clean install -e -P let_reporting
-
name: Reporting
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
flags: unittests
name: SEB Server Build
@ -69,16 +69,16 @@ jobs:
echo ${{ env.TAG_NAME }}
-
name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
-
name: Set up JDK 17
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'adopt'
-
name: Cache Maven packages
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
@ -92,7 +92,7 @@ jobs:
name: Simplify package name
run: mv target/seb-server-${{ env.TAG_NAME }}-${{ env.SHA }}.jar target/seb-server.jar
-
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Package
path: target/seb-server.jar
@ -132,7 +132,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
@ -142,7 +142,7 @@ jobs:
uses: sigstore/cosign-installer@main
-
name: Download a single artifact
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: Package
-

View file

@ -1,3 +1,6 @@
Safe Exam Browser (SEB) Server
--------------------------------
Master:
.. image:: https://github.com/SafeExamBrowser/seb-server/actions/workflows/buildReporting.yml/badge.svg?branch=master
@ -32,7 +35,7 @@ What is Safe Exam Browser Server (SEB Server)?
While the interaction with SEB is well known in Learning Management Systems (LMS) like `Open edX <https://open.edx.org/>`_,
`Moodle <https://moodle.org/>`_ etc. the SEB Server is an entirely new component to set up secured online exams.
It interacts with the assessments system/LMS as well as with SEB on exam clients.It supports exam scenarios on student owned devices (BYOD)
It interacts with the assessments system/LMS as well as with SEB on exam clients. It supports exam scenarios on student owned devices (BYOD)
and on managed devices.
SEB Server is a modern webservice with a REST API and a GUI service on top of it. SEB Server is written in Java and uses Docker for installation and setup.
@ -62,7 +65,7 @@ New Features:
- Security: New Application Signature Key (ASK) integration within SEB Server exams and monitoring
- Security: Minimum SEB Client version tracking within SEB Server monitoring
- LMS Integration: Better Moodle integration with new `SEB Server Moodle Plugin <https://github.com/ethz-let/moodle-quizzaccess_sebserver>`_
- LMS Integration: Better Moodle integration with new `SEB Server Moodle Plugin <https://github.com/ethz-let/moodle-quizaccess_sebserver>`_
- Exam Maintenance: Added new SEB grouping functionality for Exam (and Exam Template) and Monitoring to be able to view/manage SEB Clients within defined groups (IP range, SEB client OS, ...)
- Exam Maintenance: Batch actions for archive and delete exams
- Exam Maintenance: Added SEB log export for finished and archived exams
@ -98,12 +101,20 @@ Bugfixes:
Docker-Image:
- Exact release version: docker pull anhefti/seb-server:v1.5.0 (sha256:21d62e24dd5cf697ab5f2b437dc458e6c7492ea294f77a424d39d05164d6c8cc)
- Stable minor version: docker pull anhefti/seb-server:v1.5-stable
Latest Version is 1.5.1 with Docker-Image:
- Exact release version: docker pull anhefti/seb-server:v1.5.1 (sha256:af860f5dd4d99db3e7acaa66d26c3ee72cf0ad08d8ca88febec6d4ecd160b9cf)
- Latest stable minor version with latest patches: docker pull anhefti/seb-server:v1.5-latest
SEB - SEB Server Compatibility
------------------------------
The table below shows available and upcoming SEB client versions that has SEB Server integration support and are compatible with particular
SEB Server version. There is an entry for each platform with a beta or testing release date and a official release date.
SEB Server versions. There is an entry for each platform with a beta or testing release date and an official release date.
**SEB Server Version 1.5.X**
@ -134,7 +145,61 @@ Getting started with SEB Server
For a complete SEB Server user guide please go to `SEB Server User Guide <https://seb-server.readthedocs.io/en/latest/#>`_
Project Background
------------------
The SEB Server is currently build and maintained by `ETH Zürich <https://ethz.ch/en.html>`_ and by the `Swiss MOOC Service <https://www.swissmooc.ch/>`_ that is founded by leading Swiss universities EPFL, ETH, SUPSI, USI and HES-SO. The Swiss MOOC Service was financially supported from 2018-2020 by the `Swissuniversities´ P5 program <https://www.swissuniversities.ch/themen/digitalisierung/p-5-wissenschaftliche-information>`_.
Contributing to SEB Server
------------------
We want to make contributing to this project as easy and transparent as possible, whether it's:
- Give us a star
- Reporting a bug
- Submitting a fix
- Proposing new features
- Becoming a SEB Alliance member
We use github to host code, to track issues and feature requests, as well as accept pull requests.
And we use `Github issues <https://github.com/SafeExamBrowser/seb-server/issues>`_ to track public bugs.
Report a bug by [opening a new issue]();
**Before enter a new bug-report, ensure the bug was not already reported**
Please fill and provide all the information suggested by the bug-report template
Great Bug Reports tend to have:
- A quick summary and/or background
- Steps to reproduce
- Be specific and give sample code if you can. Can also be Pseudocode.
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
**We Use Git-Flow for Code Contributions**
Pull requests are the best way to propose changes to the codebase. We use `Github Flow <https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow>`_. We actively welcome your pull requests:
1. Fork the repo and create your branch from `development`. The development branch always has the newest changes.
2. If you've added code that should be tested, add tests.
3. If you introduce new API also add clear documentation.
4. Ensure the test suite passes.
5. Make sure your code lints.
6. Issue that pull request!
**Use a Consistent Coding Style**
Have a close look to the existing code stile that is used within SEB Server and adapt to it as close as possible.
We reserve the right to adapt contributed code to the code style matching SEB Server code style before or after a pull request.
**Any contributions you make will be under the Mozilla Public License Version 2.0**
In short, when you submit code changes, your submissions are understood to be under the same `Mozilla Public License <https://github.com/SafeExamBrowser/seb-server?tab=MPL-2.0-1-ov-file>`_ that covers the project. Feel free to contact the maintainers if that's a concern.
**Becoming a SEB Alliance member**
The `SEB Alliance <https://www.safeexambrowser.org/alliance/members.html>`_ is the body which sustains ongoing funding of the Safe Exam Browser open source project to continue its maintenance, development and support activities. ETH Zurich provides the infrastructure for the management and the software engineering of the SEB project and appoints an alliance manager who will provide administrative support to the SEB Alliance, and ensure the day-to-day running of the SEB Alliance. ETH Zurich leads the Alliance and offers different contribution levels to parties interested in the evolution of the SEB open source project.
More information about `joining <https://www.safeexambrowser.org/alliance/join.html>`_ the Alliance is available in our `benefits <https://www.safeexambrowser.org/alliance/benefits.html>`_ and `documents <https://www.safeexambrowser.org/alliance/documents.html>`_ section.

View file

@ -1,5 +1,5 @@
docutils<0.18
sphinx==5.3.0
sphinx_rtd_theme==2.0.0
readthedocs-sphinx-search==0.1.1
readthedocs-sphinx-search==0.3.2
urllib3==1.26.13

View file

@ -11,6 +11,11 @@ package ch.ethz.seb.sebserver.gbl.model.sebconfig;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.Collection;
import java.util.Set;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.validator.constraints.URL;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
@ -52,6 +57,7 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
public static final String ATTR_ENCRYPT_CERTIFICATE_ASYM = "cert_encryption_asym";
public static final String FILTER_ATTR_CREATION_DATE = "creation_date";
public static final String ATTR_EXAM_SELECTION = "exam_selection";
public enum ConfigPurpose {
START_EXAM,
@ -174,6 +180,8 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
@JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_LAST_UPDATE_USER)
public final String lastUpdateUser;
@JsonProperty(SEBClientConfig.ATTR_EXAM_SELECTION)
public final Set<Long> selectedExams;
@JsonCreator
public SEBClientConfig(
@ -204,7 +212,8 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
@JsonProperty(ATTR_ENCRYPT_CERTIFICATE_ASYM) final Boolean encryptCertificateAsym,
@JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_ACTIVE) final Boolean active,
@JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_LAST_UPDATE_TIME) final DateTime lastUpdateTime,
@JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_LAST_UPDATE_USER) final String lastUpdateUser) {
@JsonProperty(SEB_CLIENT_CONFIGURATION.ATTR_LAST_UPDATE_USER) final String lastUpdateUser,
@JsonProperty(SEBClientConfig.ATTR_EXAM_SELECTION) final Set<Long> selectedExams) {
this.id = id;
this.institutionId = institutionId;
@ -240,6 +249,7 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
this.active = active;
this.lastUpdateTime = lastUpdateTime;
this.lastUpdateUser = lastUpdateUser;
this.selectedExams = Utils.immutableSetOf(selectedExams);
}
public SEBClientConfig(final Long institutionId, final POSTMapper postParams) {
@ -281,6 +291,7 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
this.active = false;
this.lastUpdateTime = postParams.getDateTime(CONFIGURATION_NODE.ATTR_LAST_UPDATE_TIME);
this.lastUpdateUser = postParams.getString(CONFIGURATION_NODE.ATTR_LAST_UPDATE_USER);
this.selectedExams = Utils.immutableSetOf(Utils.getIdsFromString(postParams.getString(SEBClientConfig.ATTR_EXAM_SELECTION)));
}
@Override
@ -424,6 +435,10 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
return this.lastUpdateUser;
}
public Set<Long> getSelectedExams() {
return selectedExams;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
@ -503,7 +518,8 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
this.encryptCertificateAsym,
this.active,
this.lastUpdateTime,
this.lastUpdateUser);
this.lastUpdateUser,
this.selectedExams);
}
public static SEBClientConfig createNew(final Long institutionId, final long pingIterval) {
@ -533,6 +549,7 @@ public final class SEBClientConfig implements GrantEntity, Activatable {
false,
false,
null,
null,
null);
}

View file

@ -17,8 +17,12 @@ public class UserFeatures {
public enum Feature {
ADMIN_INSTITUTION("admin.institution"),
ADMIN_USER_ADMINISTRATION("admin.user.administration"),
ADMIN_USER_ACCOUNT("admin.user.account"),
ADMIN_USER_ACCOUNT_SELF_REGISTERING("admin.user.account.self.registering"),
ADMIN_USER_ACCOUNT_SELF_REGISTERING_AUTO_ACTIVATION("admin.user.account.self.registering.autoactivation"),
ADMIN_AUDIT_LOGS("admin.auditlogs"),
CONFIG_CONNECTION_CONFIGURATION("config.connection.configuration"),

View file

@ -935,4 +935,22 @@ public final class Utils {
return "Basic " + base64Creds;
}
public static Set<Long> getIdsFromString(final String idsString) {
if (StringUtils.isBlank(idsString)) {
return Collections.emptySet();
}
return Arrays
.stream(StringUtils.split(idsString, Constants.LIST_SEPARATOR_CHAR))
.map(s -> {
try {
return Long.valueOf(s);
} catch (final Exception e) {
log.warn("Failed to parse String: {} to Long", s);
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
}

View file

@ -59,14 +59,16 @@ public class LoginPage implements TemplateComposer {
public LoginPage(
final PageService pageService,
final DefaultRegisterPage defaultRegisterPage,
@Value("${sebserver.gui.registering:false}") final Boolean registeringEnabled) {
@Value("${sebserver.gui.registering:false}") final boolean guiRegEnabled,
@Value("${sebserver.gui.self-registering:false}") final boolean guiRegEnabledOld,
@Value("${sebserver.feature.admin.user.account.self.registering:true}") final boolean webRegEnabled) {
this.pageService = pageService;
this.authorizationContextHolder = pageService.getAuthorizationContextHolder();
this.widgetFactory = pageService.getWidgetFactory();
this.i18nSupport = pageService.getI18nSupport();
this.defaultRegisterPage = defaultRegisterPage;
this.registeringEnabled = BooleanUtils.toBoolean(registeringEnabled);
this.registeringEnabled = webRegEnabled && (guiRegEnabled || guiRegEnabledOld);
}
@Override

View file

@ -85,6 +85,9 @@ public class RegisterPage implements TemplateComposer {
static final LocTextKey MESSAGE_SUCCESS_TEXT =
new LocTextKey("sebserver.login.register.success");
static final LocTextKey MESSAGE_SUCCESS_ACTIVATION_TEXT =
new LocTextKey("sebserver.login.register.success.activate");
private final PageService pageService;
private final ResourceService resourceService;
private final WidgetFactory widgetFactory;
@ -247,7 +250,9 @@ public class RegisterPage implements TemplateComposer {
}
pageContext.forwardToLoginPage();
pageContext.publishPageMessage(MESSAGE_SUCCESS_TILE, MESSAGE_SUCCESS_TEXT);
pageContext.publishPageMessage(
MESSAGE_SUCCESS_TILE,
(result.get().active) ? MESSAGE_SUCCESS_TEXT : MESSAGE_SUCCESS_ACTIVATION_TEXT);
});

View file

@ -134,6 +134,9 @@ public class SEBClientConfigForm implements TemplateComposer {
private static final LocTextKey FORM_CONFIRM_ENCRYPT_SECRET_TEXT_KEY =
new LocTextKey("sebserver.clientconfig.form.encryptSecret.confirm");
private static final LocTextKey FORM_EXAM_SELECTION_TEXT_KEY =
new LocTextKey("sebserver.clientconfig.form.examselection");
private static final LocTextKey DELETE_CONFIRM =
new LocTextKey("sebserver.clientconfig.action.delete.confirm");
private static final LocTextKey DELETE_SUCCESS =
@ -409,20 +412,28 @@ public class SEBClientConfigForm implements TemplateComposer {
.mandatory(!isReadonly))
.withDefaultSpanEmptyCell(3)
.addField(FormBuilder.multiComboSelection(
SEBClientConfig.ATTR_EXAM_SELECTION,
FORM_EXAM_SELECTION_TEXT_KEY,
StringUtils.join(clientConfig.selectedExams, Constants.LIST_SEPARATOR),
() -> pageService.getResourceService().getExamLogSelectionResources())
.withInputSpan(5))
.withDefaultSpanEmptyCell(1);
// VDI
.withDefaultSpanInput(2)
.addFieldIf(
() -> false, // TODO skipped for version 1.2 --> 1.3 or 1.4
() -> FormBuilder.singleSelection(
SEBClientConfig.ATTR_VDI_TYPE,
VDI_TYPE_TEXT_KEY,
clientConfig.vdiType != null
? clientConfig.vdiType.name()
: SEBClientConfig.VDIType.NO.name(),
() -> this.pageService.getResourceService().vdiTypeResources())
.mandatory(!isReadonly))
.withDefaultSpanEmptyCell(3);
// .withDefaultSpanInput(2)
// .addFieldIf(
// () -> false, // TODO skipped for version 1.2 --> 1.3 or 1.4
// () -> FormBuilder.singleSelection(
// SEBClientConfig.ATTR_VDI_TYPE,
// VDI_TYPE_TEXT_KEY,
// clientConfig.vdiType != null
// ? clientConfig.vdiType.name()
// : SEBClientConfig.VDIType.NO.name(),
// () -> this.pageService.getResourceService().vdiTypeResources())
// .mandatory(!isReadonly))
// .withDefaultSpanEmptyCell(3);
// VDI Attributes
@ -460,7 +471,10 @@ public class SEBClientConfigForm implements TemplateComposer {
FALLBACK_TEXT_KEY,
clientConfig.fallback != null
? clientConfig.fallback.toString()
: Constants.FALSE_STRING));
: Constants.FALSE_STRING))
.withDefaultSpanEmptyCell(3)
;
// Fallback Attributes

View file

@ -137,6 +137,7 @@ public class ExamFormConfigs implements TemplateComposer {
.withSelectionListener(this.pageService.getSelectionPublisher(
pageContext,
ActionDefinition.EXAM_CONFIGURATION_MODIFY_FROM_LIST,
ActionDefinition.EXAM_CONFIGURATION_EXAM_CONFIG_VIEW_PROP,
ActionDefinition.EXAM_CONFIGURATION_DELETE_FROM_LIST,
ActionDefinition.EXAM_CONFIGURATION_EXPORT,
@ -144,7 +145,7 @@ public class ExamFormConfigs implements TemplateComposer {
.compose(pageContext.copyOf(content));
final EntityKey configMapKey = (configurationTable.hasAnyContent())
final EntityKey configKey = (configurationTable.hasAnyContent())
? new EntityKey(
configurationTable.getFirstRowData().configurationNodeId,
EntityType.CONFIGURATION_NODE)
@ -164,9 +165,18 @@ public class ExamFormConfigs implements TemplateComposer {
.newAction(ActionDefinition.EXAM_CONFIGURATION_EXAM_CONFIG_VIEW_PROP)
.withParentEntityKey(entityKey)
.withEntityKey(configMapKey)
.withEntityKey(configKey)
.publishIf(() -> examConfigEnabled && readGrant && configurationTable.hasAnyContent(), false)
.newAction(ActionDefinition.EXAM_CONFIGURATION_MODIFY_FROM_LIST)
.withParentEntityKey(entityKey)
.withSelect(
getConfigMappingSelection(configurationTable),
this.examToConfigBindingForm.bindFunction(),
CONFIG_EMPTY_SELECTION_TEXT_KEY)
.noEventPropagation()
.publishIf(() -> examConfigEnabled && editable && configurationTable.hasAnyContent(), false)
.newAction(ActionDefinition.EXAM_CONFIGURATION_DELETE_FROM_LIST)
.withEntityKey(entityKey)
.withSelect(
@ -179,7 +189,7 @@ public class ExamFormConfigs implements TemplateComposer {
}
return null;
})
.publishIf(() -> examConfigEnabled && editable && configurationTable.hasAnyContent() && editable, false)
.publishIf(() -> examConfigEnabled && editable && configurationTable.hasAnyContent(), false)
.newAction(ActionDefinition.EXAM_CONFIGURATION_GET_CONFIG_KEY)
.withSelect(

View file

@ -82,7 +82,7 @@ public class ExamToConfigBindingForm {
return action -> {
final PageContext pageContext = action.pageContext();
final EntityKey entityKey = pageContext.getEntityKey();
final EntityKey entityKey = action.getSingleSelection();
final boolean isNew = entityKey == null;
if (isNew) {
@ -103,11 +103,12 @@ public class ExamToConfigBindingForm {
final BindFormContext bindFormContext = new BindFormContext(
this.pageService,
action.pageContext());
action.pageContext()
.withEntityKey(entityKey));
final Predicate<FormHandle<ExamConfigurationMap>> doBind = formHandle -> doCreate(
this.pageService,
pageContext,
bindFormContext.pageContext,
formHandle);
// the default page layout
@ -181,14 +182,13 @@ public class ExamToConfigBindingForm {
final EntityKey parentEntityKey = this.pageContext.getParentEntityKey();
final boolean isNew = entityKey == null;
final Exam exam = (isNew)
? restService
final Exam exam = restService
.getBuilder(GetExam.class)
.withURIVariable(API.PARAM_MODEL_ID, parentEntityKey.modelId)
.call()
.onError(error -> this.pageContext.notifyLoadError(EntityType.EXAM, error))
.getOrThrow()
: null;
.getOrThrow();
// get data or create new. Handle error if happen
final ExamConfigurationMap examConfigurationMap = (isNew)
@ -217,8 +217,13 @@ public class ExamToConfigBindingForm {
.putStaticValue(
Domain.EXAM_CONFIGURATION_MAP.ATTR_EXAM_ID,
String.valueOf(examConfigurationMap.examId))
.putStaticValueIf(
() -> !isNew,
Domain.EXAM_CONFIGURATION_MAP.ATTR_CONFIGURATION_NODE_ID,
String.valueOf(examConfigurationMap.configurationNodeId))
.addField(FormBuilder.singleSelection(
.addFieldIf( () -> isNew,
() -> FormBuilder.singleSelection(
Domain.EXAM_CONFIGURATION_MAP.ATTR_CONFIGURATION_NODE_ID,
CONFIG_MAPPING_NAME_TEXT_KEY,
String.valueOf(examConfigurationMap.configurationNodeId),
@ -226,6 +231,13 @@ public class ExamToConfigBindingForm {
.withSelectionListener(form -> updateFormValuesFromConfigSelection(form, resourceService))
.mandatory())
.addFieldIf( () -> !isNew,
() -> FormBuilder.text(
Domain.EXAM_CONFIGURATION_MAP.ATTR_CONFIGURATION_NODE_ID,
CONFIG_MAPPING_NAME_TEXT_KEY,
examConfigurationMap.configName)
.readonly(true))
.addField(FormBuilder.text(
Domain.CONFIGURATION_NODE.ATTR_DESCRIPTION,
FORM_DESCRIPTION_TEXT_KEY,

View file

@ -111,6 +111,13 @@ public class ProctoringSettingsPopup {
private final static LocTextKey RESET_SUCCESS_KEY =
new LocTextKey("sebserver.exam.proctoring.form.resetOk");
private final static LocTextKey DEPRECATION_MESSAGE =
new LocTextKey("sebserver.exam.proctoring.deprecation.message");
private final static LocTextKey DEPRECATION_NOTE =
new LocTextKey("sebserver.exam.proctoring.deprecation.title");
Function<PageAction, PageAction> settingsFunction(final PageService pageService, final boolean modifyGrant) {
return action -> {
@ -125,7 +132,7 @@ public class ProctoringSettingsPopup {
action.pageContext().getParent().getShell(),
pageService.getWidgetFactory())
.setDialogWidth(860)
.setDialogHeight(600);
.setDialogHeight(650);
final ResetButtonHandler resetButtonHandler = new ResetButtonHandler();
if (modifyGrant) {
@ -326,6 +333,10 @@ public class ProctoringSettingsPopup {
final RestService restService = this.pageService.getRestService();
final EntityKey entityKey = this.pageContext.getEntityKey();
final Composite warningPanel = this.pageService.getWidgetFactory().createWarningPanel(parent);
this.pageService.getWidgetFactory().labelLocalized(warningPanel, WidgetFactory.CustomVariant.TITLE_LABEL, DEPRECATION_NOTE);
this.pageService.getWidgetFactory().labelLocalized(warningPanel, WidgetFactory.CustomVariant.SUBTITLE, DEPRECATION_MESSAGE, null);
final Composite content = this.pageService
.getWidgetFactory()
.createPopupScrollComposite(parent);

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.gui.form;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
@ -122,6 +123,7 @@ public final class SelectionFieldBuilder extends FieldBuilder<String> {
this.itemsSupplier.get()
.stream()
.filter(tuple -> keys.contains(tuple._1))
.sorted((t1, t2) -> String.CASE_INSENSITIVE_ORDER.compare(t1._2, t2._2))
.map(tuple -> tuple._1)
.forEach(v -> buildReadonlyLabel(builder, composite, v, 1));
}

View file

@ -8,22 +8,17 @@
package ch.ethz.seb.sebserver.gui.widget;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.*;
import java.util.List;
import java.util.Optional;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.widgets.DropDown;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -33,13 +28,11 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService;
public final class MultiSelectionCombo extends Composite implements Selection {
public static final LocTextKey DESELECT_TOOLTIP = new LocTextKey( "sebserver.form.multiselect.deselect.tooltip" );
private static final Logger log = LoggerFactory.getLogger(MultiSelectionCombo.class);
private static final long serialVersionUID = -7787134114963647332L;
private final WidgetFactory widgetFactory;
private final List<Control> selectionControls = new ArrayList<>();
private final List<Tuple<String>> valueMapping = new ArrayList<>();
private final List<Tuple<String>> availableValues = new ArrayList<>();
private final List<Tuple<String>> selectedValues = new ArrayList<>();
@ -50,7 +43,10 @@ public final class MultiSelectionCombo extends Composite implements Selection {
private final Composite updateAnchor;
private final String testKey;
private final Table selectionTable;
private Listener listener = null;
private WidgetFactory widgetFactory;
MultiSelectionCombo(
final Composite parent,
@ -78,7 +74,7 @@ public final class MultiSelectionCombo extends Composite implements Selection {
this.textInput.addListener(SWT.FocusIn, event -> openDropDown());
this.textInput.addListener(SWT.Modify, event -> openDropDown());
this.textInput.addListener(SWT.MouseUp, event -> openDropDown());
this.dropDown.addListener(SWT.Selection, event -> {
this.dropDown.addListener(SWT.DefaultSelection, event -> {
final int selectionIndex = this.dropDown.getSelectionIndex();
if (selectionIndex >= 0) {
final String selectedItem = this.dropDown.getItems()[selectionIndex];
@ -86,21 +82,29 @@ public final class MultiSelectionCombo extends Composite implements Selection {
}
});
selectionTable = widgetFactory.tableLocalized(this, SWT.NONE);
final GridLayout tableLayout = new GridLayout(1, true);
selectionTable.setLayout(tableLayout);
final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, false);
selectionTable.setLayoutData(gridData);
//selectionTable.setToolTipText();
selectionTable.addListener(SWT.MouseDoubleClick, this::removeComboSelection);
selectionTable.addListener(SWT.Selection, event -> {
selectionTable.setSelection(-1);
});
selectionTable.setHeaderVisible(false);
selectionTable.setLinesVisible(true);
this.updateAnchor = updateAnchor;
}
private void openDropDown() {
final String text = this.textInput.getText();
if (text == null) {
this.dropDown.setVisible(false);
@Override
public void setToolTipText(final String tooltipText) {
if (tooltipText == null) {
super.setToolTipText(widgetFactory.getI18nSupport().getText(DESELECT_TOOLTIP));
return;
}
this.dropDown.setItems(this.availableValues
.stream()
.filter(it -> it._2 != null && it._2.startsWith(text))
.map(t -> t._2).toArray(String[]::new));
this.dropDown.setSelectionIndex(0);
this.dropDown.setVisible(true);
super.setToolTipText(tooltipText + "\n\n" + widgetFactory.getI18nSupport().getText(DESELECT_TOOLTIP));
}
@Override
@ -157,9 +161,6 @@ public final class MultiSelectionCombo extends Composite implements Selection {
@Override
public void clear() {
this.selectedValues.clear();
this.selectionControls
.forEach(Control::dispose);
this.selectionControls.clear();
this.availableValues.clear();
this.availableValues.addAll(this.valueMapping);
}
@ -170,42 +171,39 @@ public final class MultiSelectionCombo extends Composite implements Selection {
}
this.selectedValues.add(item);
final Label label = this.widgetFactory.label(this, item._2);
label.setData(OPTION_VALUE, item._2);
final GridData textCell = new GridData(SWT.LEFT, SWT.CENTER, true, true);
label.setLayoutData(textCell);
label.addListener(SWT.MouseDoubleClick, this::removeComboSelection);
this.selectionControls.add(label);
WidgetFactory.setARIALabel(label, item._2);
WidgetFactory.setTestId(label, (this.testKey != null) ? this.testKey + "_" + item._1 : item._1);
sortSelectedTable();
this.availableValues.remove(item);
PageService.updateScrolledComposite(this);
this.updateAnchor.layout(true, true);
}
private void sortSelectedTable() {
selectionTable.removeAll();
selectedValues.sort((t1, t2) -> String.CASE_INSENSITIVE_ORDER.compare(t1._2, t2._2));
selectedValues.stream().forEach(t -> {
final TableItem tItem = new TableItem(selectionTable, SWT.NONE);
tItem.setText(0, t._2);
tItem.setData("tuple", t);
WidgetFactory.setARIALabel(tItem, t._2);
WidgetFactory.setTestId(tItem, (this.testKey != null) ? this.testKey + "_" + t._1 : t._1);
});
}
private void removeComboSelection(final Event event) {
if (event.widget == null) {
return;
}
final String selectionKey = (String) event.widget.getData(OPTION_VALUE);
final Optional<Control> findFirst = this.selectionControls.stream()
.filter(t -> selectionKey.equals(t.getData(OPTION_VALUE)))
.findFirst();
if (!findFirst.isPresent()) {
return;
}
final Control control = findFirst.get();
final int indexOf = this.selectionControls.indexOf(control);
this.selectionControls.remove(control);
control.dispose();
final Tuple<String> value = this.selectedValues.remove(indexOf);
final TableItem item = selectionTable.getItem(new Point(event.x, event.y));
@SuppressWarnings("unchecked")
final Tuple<String> value = (Tuple<String>) item.getData("tuple");
this.selectedValues.remove(value);
this.availableValues.add(value);
sortSelectedTable();
PageService.updateScrolledComposite(this);
this.updateAnchor.layout(true, true);
if (this.listener != null) {
@ -238,4 +236,18 @@ public final class MultiSelectionCombo extends Composite implements Selection {
return findFirst.orElse(null);
}
private void openDropDown() {
final String text = this.textInput.getText();
if (text == null) {
this.dropDown.setVisible(false);
return;
}
this.dropDown.setItems(this.availableValues
.stream()
.filter(it -> it._2 != null && it._2.startsWith(text))
.map(t -> t._2).toArray(String[]::new));
this.dropDown.setSelectionIndex(0);
this.dropDown.setVisible(true);
}
}

View file

@ -12,10 +12,12 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@ -44,15 +46,18 @@ class AdminUserInitializer {
private final boolean initializeAdmin;
private final String adminName;
private final String orgName;
private final Environment environment;
public AdminUserInitializer(
final UserDAO userDAO,
final InstitutionDAO institutionDAO,
final Environment environment,
@Qualifier(WebSecurityConfig.USER_PASSWORD_ENCODER_BEAN_NAME) final PasswordEncoder passwordEncoder,
@Value("${sebserver.init.adminaccount.gen-on-init:false}") final boolean initializeAdmin,
@Value("${sebserver.init.adminaccount.username:seb-server-admin}") final String adminName,
@Value("${sebserver.init.organisation.name:[SET_ORGANIZATION_NAME]}") final String orgName) {
this.environment = environment;
this.userDAO = userDAO;
this.institutionDAO = institutionDAO;
this.passwordEncoder = passwordEncoder;
@ -89,7 +94,11 @@ class AdminUserInitializer {
}
}
} else {
final CharSequence generateAdminPassword = this.generateAdminPassword();
final String initPWD = environment.getProperty("sebserver.init.adminaccount.init.pwd", "");
final CharSequence generateAdminPassword = StringUtils.isNotBlank(initPWD)
? initPWD :
this.generateAdminPassword();
Long institutionId = this.institutionDAO.allMatching(new FilterMap())
.getOrElse(Collections::emptyList)
.stream()

View file

@ -156,21 +156,21 @@ public class WebserviceInfo {
}
final boolean spsEnabled = BooleanUtils.toBoolean(environment.getProperty(
"sebserver.feature.seb.screenProctoring.enabled",
"sebserver.feature.exam.seb.screenProctoring.enabled",
Constants.FALSE_STRING));
final boolean spsBundled = BooleanUtils.toBoolean(environment.getProperty(
"sebserver.feature.seb.screenProctoring.bundled",
"sebserver.feature.exam.seb.screenProctoring.bundled",
Constants.FALSE_STRING));
if (spsEnabled && spsBundled) {
this.screenProctoringServiceBundle = new ScreenProctoringServiceBundle(
environment.getProperty("sebserver.feature.seb.screenProctoring.bundled.url"),
environment.getProperty("sebserver.feature.seb.screenProctoring.bundled.clientId"),
environment.getProperty("sebserver.feature.exam.seb.screenProctoring.bundled.url"),
environment.getProperty("sebserver.feature.exam.seb.screenProctoring.bundled.clientId"),
cryptor.encrypt(
environment.getProperty("sebserver.feature.seb.screenProctoring.bundled.clientPassword"))
environment.getProperty("sebserver.feature.exam.seb.screenProctoring.bundled.clientPassword"))
.getOrThrow(),
environment.getProperty("sebserver.feature.seb.screenProctoring.bundled.sebserveraccount.username"),
environment.getProperty("sebserver.feature.exam.seb.screenProctoring.bundled.sebserveraccount.username"),
cryptor.encrypt(environment
.getProperty("sebserver.feature.seb.screenProctoring.bundled.sebserveraccount.password"))
.getProperty("sebserver.feature.exam.seb.screenProctoring.bundled.sebserveraccount.password"))
.getOrThrow());
} else {
this.screenProctoringServiceBundle = new ScreenProctoringServiceBundle();
@ -178,10 +178,10 @@ public class WebserviceInfo {
}
public Map<String, Boolean> configuredFeatures() {
return Arrays.stream(UserFeatures.Feature.values()).collect(Collectors.toMap(
return new TreeMap<>( Arrays.stream(UserFeatures.Feature.values()).collect(Collectors.toMap(
f -> f.featureName,
featureService::isEnabledByConfig
));
)));
}
public boolean isMaster() {

View file

@ -76,6 +76,7 @@ public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent
SEBServerInit.INIT_LOGGER.info("----> ");
SEBServerInit.INIT_LOGGER.info("----> Register Webservice: {}", this.webserviceInfo.getWebserviceUUID());
try {
if (this.webserviceInfoDAO.isInitialized()) {
this.registerWebservice();
@ -93,6 +94,9 @@ public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent
this.registerWebservice();
}
} catch (final Exception e) {
SEBServerInit.INIT_LOGGER.error("Failed to apply data import and migration --> ", e);
}
SEBServerInit.INIT_LOGGER.info("----> ");
SEBServerInit.INIT_LOGGER.info("----> Initialize Services...");

View file

@ -216,7 +216,9 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
*
* @param examId the Exam identifier */
@CacheEvict(
cacheNames = ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM,
cacheNames = {
ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM,
ExamSessionCacheService.CACHE_NAME_SEB_CONFIG_EXAM },
key = "#examId")
void markUpdate(Long examId);

View file

@ -8,6 +8,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl;
import static ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamConfigurationMapRecordDynamicSqlSupport.*;
import static org.mybatis.dynamic.sql.SqlBuilder.*;
import java.util.ArrayList;
@ -23,6 +24,7 @@ import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.mybatis.dynamic.sql.SqlBuilder;
import org.mybatis.dynamic.sql.update.UpdateDSL;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@ -260,15 +262,17 @@ public class ExamConfigurationMapDAOImpl implements ExamConfigurationMapDAO {
public Result<ExamConfigurationMap> save(final ExamConfigurationMap data) {
return Result.tryCatch(() -> {
final ExamConfigurationMapRecord newRecord = new ExamConfigurationMapRecord(
data.id,
null,
null,
null,
getEncryptionPassword(data),
data.clientGroupId);
final String p = (StringUtils.isNotBlank(data.encryptSecret))
? getEncryptionPassword(data)
: null;
UpdateDSL.updateWithMapper(examConfigurationMapRecordMapper::update, examConfigurationMapRecord)
.set(encryptSecret).equalTo(p )
.set(clientGroupId).equalToWhenPresent(data.clientGroupId)
.where(id, isEqualTo(data.id))
.build()
.execute();
this.examConfigurationMapRecordMapper.updateByPrimaryKeySelective(newRecord);
return this.examConfigurationMapRecordMapper.selectByPrimaryKey(data.id);
})
.flatMap(this::toDomainModel)

View file

@ -489,7 +489,11 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
additionalAttributes.containsKey(SEBClientConfig.ATTR_ENCRYPT_CERTIFICATE_ASYM),
BooleanUtils.toBooleanObject(record.getActive()),
Utils.toDateTimeUTC(record.getLastUpdateTime()),
record.getLastUpdateUser()));
record.getLastUpdateUser(),
Utils.getIdsFromString(
additionalAttributes.containsKey(SEBClientConfig.ATTR_EXAM_SELECTION)
? additionalAttributes.get(SEBClientConfig.ATTR_EXAM_SELECTION).getValue()
: null)));
}
private String getEncryptionPassword(final SEBClientConfig sebClientConfig) {
@ -685,6 +689,21 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
configId,
SEBClientConfig.ATTR_ENCRYPT_CERTIFICATE_ASYM);
}
final Set<Long> selectedExams = sebClientConfig.getSelectedExams();
if (selectedExams != null && !selectedExams.isEmpty()) {
final String ids = StringUtils.join(selectedExams, Constants.LIST_SEPARATOR);
this.additionalAttributesDAO.saveAdditionalAttribute(
EntityType.SEB_CLIENT_CONFIGURATION,
configId,
SEBClientConfig.ATTR_EXAM_SELECTION,
ids);
} else {
this.additionalAttributesDAO.delete(
EntityType.SEB_CLIENT_CONFIGURATION,
configId,
SEBClientConfig.ATTR_EXAM_SELECTION);
}
}
private Long disposeSEBClientConfig(final Long pk) {

View file

@ -57,6 +57,7 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
.execute();
return true;
} catch (final Exception e) {
log.warn("DB Context not initialized yet: {}", e.getMessage());
return false;
}
}

View file

@ -63,5 +63,5 @@ public interface ClientConfigService {
boolean checkAccess(SEBClientConfig config);
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
void initalCheckAccess(SEBClientConfig config);
void initialCheckAccess(SEBClientConfig config);
}

View file

@ -504,7 +504,7 @@ public class ClientConfigServiceImpl implements ClientConfigService {
}
@Override
public void initalCheckAccess(final SEBClientConfig config) {
public void initialCheckAccess(final SEBClientConfig config) {
checkAccess(config);
}

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
import java.io.OutputStream;
import java.security.Principal;
import java.util.Collection;
import java.util.Set;
import java.util.function.Predicate;
@ -130,9 +131,10 @@ public interface ExamSessionService {
/** Gets all currently running Exams for a particular Institution.
*
* @param institutionId the Institution identifier
* @param examSelectionFilter Exam selection filter from SEB connection configuration
* @return Result referencing the list of all currently running Exams of the institution or to an error if
* happened. */
Result<Collection<Exam>> getRunningExamsForInstitution(Long institutionId);
Result<Collection<Exam>> getRunningExams(Long institutionId, Predicate<Long> examSelectionFilter);
/** Gets all currently running Exams for a particular FilterMap.
*

View file

@ -13,7 +13,6 @@ import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -251,9 +250,10 @@ public class ExamSessionServiceImpl implements ExamSessionService {
}
@Override
public Result<Collection<Exam>> getRunningExamsForInstitution(final Long institutionId) {
public Result<Collection<Exam>> getRunningExams(final Long institutionId, final Predicate<Long> examSelectionFilter) {
return this.examDAO.allIdsOfRunning(institutionId)
.map(col -> col.stream()
.filter(examSelectionFilter)
.map(this::getRunningExam)
.filter(Result::hasValue)
.map(Result::get)

View file

@ -11,17 +11,17 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.SEBClientConfig;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -132,7 +132,10 @@ public class ExamAPI_V1_Controller {
// Crate list of running exams
final List<RunningExamInfo> result;
if (examId == null) {
result = this.examSessionService.getRunningExamsForInstitution(institutionId)
result = this.examSessionService.getRunningExams(
institutionId,
getExamSelectionPredicate(principal.getName()))
.getOrThrow()
.stream()
.map(this::createRunningExamInfo)
@ -161,17 +164,7 @@ public class ExamAPI_V1_Controller {
this.executor);
}
private boolean checkConsistency(final RunningExamInfo info) {
if (StringUtils.isNotBlank(info.name) &&
StringUtils.isNotBlank(info.url) &&
StringUtils.isNotBlank(info.examId)) {
return true;
}
log.warn("Invalid running exam detected. Filter out exam : {}", info);
return false;
}
@RequestMapping(
path = API.EXAM_API_HANDSHAKE_ENDPOINT,
@ -420,4 +413,31 @@ public class ExamAPI_V1_Controller {
.onSuccess(bek -> response.setHeader(API.EXAM_API_EXAM_ALT_BEK, bek));
}
private Predicate<Long> getExamSelectionPredicate(final String clientName) {
return this.sebClientConfigDAO
.byClientName(clientName)
.map(this::getExamSelectionPredicate)
.onError(error -> log.warn("Failed to get SEB connection configuration by name: {}", clientName))
.getOr(Utils.truePredicate());
}
private Predicate<Long> getExamSelectionPredicate(final SEBClientConfig config) {
if (config == null || config.selectedExams.isEmpty()) {
return Utils.truePredicate();
}
return config.getSelectedExams()::contains;
}
private boolean checkConsistency(final RunningExamInfo info) {
if (StringUtils.isNotBlank(info.name) &&
StringUtils.isNotBlank(info.url) &&
StringUtils.isNotBlank(info.examId)) {
return true;
}
log.warn("Invalid running exam detected. Filter out exam : {}", info);
return false;
}
}

View file

@ -707,6 +707,9 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
examSessionService.hasActiveSEBClientConnections(exam.id)) {
final Exam oldExam = this.examDAO.byPK(exam.id).getOrThrow();
final CharSequence pwd = cryptor.decrypt(oldExam.quitPassword).getOr(oldExam.quitPassword);
if (StringUtils.isBlank(pwd) && StringUtils.isBlank(exam.quitPassword)) {
return exam;
}
if (!Objects.equals(pwd, exam.quitPassword)) {
throw new APIMessageException(APIMessage.fieldValidationError(
new FieldError(

View file

@ -13,6 +13,8 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.mybatis.dynamic.sql.SqlTable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.FieldError;
@ -57,6 +59,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe
@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.EXAM_CONFIGURATION_MAP_ENDPOINT)
public class ExamConfigurationMappingController extends EntityController<ExamConfigurationMap, ExamConfigurationMap> {
private static final Logger log = LoggerFactory.getLogger(ExamConfigurationMappingController.class);
private final ExamDAO examDao;
private final ConfigurationNodeDAO configurationNodeDAO;
private final ExamConfigUpdateService examConfigUpdateService;
@ -133,6 +137,18 @@ public class ExamConfigurationMappingController extends EntityController<ExamCon
.map(this::checkNoActiveClientConnections);
}
@Override
protected Result<ExamConfigurationMap> validForSave(final ExamConfigurationMap entity) {
return super.validForSave(entity)
.map(this::checkPasswordMatch);
}
@Override
protected Result<ExamConfigurationMap> notifySaved(final ExamConfigurationMap entity) {
examDao.markUpdate(entity.examId);
return super.notifySaved(entity);
}
@Override
@RequestMapping(
method = RequestMethod.POST,
@ -154,7 +170,6 @@ public class ExamConfigurationMappingController extends EntityController<ExamCon
final ExamConfigurationMap requestModel = this.createNew(postMap);
return this.checkCreateAccess(requestModel)
.flatMap(this::validForCreate)
.map(this::checkPasswordMatch)
.flatMap(entity -> this.examConfigUpdateService.processExamConfigurationMappingChange(
entity,
this.entityDAO::createNew))

View file

@ -10,10 +10,15 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import ch.ethz.seb.sebserver.gbl.model.user.*;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.MultiValueMap;
@ -30,10 +35,6 @@ import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.api.TooManyRequests;
import ch.ethz.seb.sebserver.gbl.model.Domain.USER_ROLE;
import ch.ethz.seb.sebserver.gbl.model.user.PasswordChange;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserMod;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.InstitutionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
@ -51,6 +52,8 @@ public class RegisterUserController {
private final BeanValidationService beanValidationService;
private final LocalBucket requestRateLimitBucket;
private final LocalBucket createRateLimitBucket;
private final boolean registeringEnabled;
private final boolean autoActivation;
protected RegisterUserController(
final InstitutionDAO institutionDAO,
@ -58,12 +61,15 @@ public class RegisterUserController {
final UserDAO userDAO,
final BeanValidationService beanValidationService,
final RateLimitService rateLimitService,
final WebserviceInfo webserviceInfo,
@Qualifier(WebSecurityConfig.USER_PASSWORD_ENCODER_BEAN_NAME) final PasswordEncoder userPasswordEncoder) {
final Map<String, Boolean> features = webserviceInfo.configuredFeatures();
this.userActivityLogDAO = userActivityLogDAO;
this.userDAO = userDAO;
this.beanValidationService = beanValidationService;
this. registeringEnabled = BooleanUtils.isTrue(features.get(UserFeatures.Feature.ADMIN_USER_ACCOUNT_SELF_REGISTERING.featureName));
this.autoActivation = BooleanUtils.isTrue(features.get(UserFeatures.Feature.ADMIN_USER_ACCOUNT_SELF_REGISTERING_AUTO_ACTIVATION.featureName));
this.requestRateLimitBucket = rateLimitService.createRequestLimitBucker();
this.createRateLimitBucket = rateLimitService.createCreationLimitBucker();
}
@ -76,6 +82,10 @@ public class RegisterUserController {
@RequestParam final MultiValueMap<String, String> allRequestParams,
final HttpServletRequest request) {
if (!registeringEnabled) {
throw new RuntimeException("Registering is not enabled from backend!");
}
if (!this.requestRateLimitBucket.tryConsume(1)) {
throw new TooManyRequests();
}
@ -107,7 +117,7 @@ public class RegisterUserController {
return userAccount;
})
.flatMap(this.userDAO::createNew)
.flatMap(account -> this.userDAO.setActive(account, true))
.flatMap(account -> this.userDAO.setActive(account, autoActivation))
.flatMap(this.userActivityLogDAO::logRegisterAccount)
.flatMap(account -> this.userDAO.byModelId(account.getModelId()))
.getOrThrow();

View file

@ -182,7 +182,7 @@ public class SEBClientConfigController extends ActivatableEntityController<SEBCl
protected Result<SEBClientConfig> notifySaved(final SEBClientConfig entity) {
if (entity.isActive()) {
// try to get access token for SEB client
this.sebClientConfigService.initalCheckAccess(entity);
this.sebClientConfigService.initialCheckAccess(entity);
}
return super.notifySaved(entity);
}

View file

@ -10,12 +10,16 @@ package ch.ethz.seb.sebserver.webservice.weblayer.oauth;
import javax.annotation.PostConstruct;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.webservice.weblayer.WebServiceUserDetails;
@Lazy
@Component
@WebServiceProfile
public class PreAuthProvider extends PreAuthenticatedAuthenticationProvider {
private final WebServiceUserDetails webServiceUserDetails;

View file

@ -72,11 +72,11 @@ springdoc.swagger-ui.oauth.clientSecret=${sebserver.password}
springdoc.paths-to-exclude=/exam-api,/exam-api/discovery,/sebserver/error,/sebserver/check,/oauth,/exam-api/v1/*
# features
sebserver.feature.seb.screenProctoring.enabled=true
sebserver.feature.seb.screenProctoring.bundled=true
sebserver.feature.seb.screenProctoring.bundled.url=http://localhost:8090
sebserver.feature.seb.screenProctoring.bundled.clientId=sebserverClient
sebserver.feature.seb.screenProctoring.bundled.sebserveraccount.username=SEBServerAPIAccount
sebserver.feature.exam.seb.screenProctoring.enabled=true
sebserver.feature.exam.seb.screenProctoring.bundled=true
sebserver.feature.exam.seb.screenProctoring.bundled.url=http://localhost:8090
sebserver.feature.exam.seb.screenProctoring.bundled.clientId=sebserverClient
sebserver.feature.exam.seb.screenProctoring.bundled.sebserveraccount.username=SEBServerAPIAccount
#sebserver.feature.admin.user.administration.enabled=false
#sebserver.feature.admin.user.account.enabled=false

View file

@ -95,15 +95,18 @@ sebserver.webservice.configtemplate.examconfig.default.description=This has auto
# features
sebserver.feature.admin.institution.enabled=true
sebserver.feature.admin.user.account.self.registering.enabled=true
sebserver.feature.admin.user.account.self.registering.autoactivation.enabled=true
sebserver.feature.seb.liveProctoring.enabled=true
sebserver.feature.lms.type.MOCKUP.enabled=true
sebserver.feature.exam.noLMS.enabled=true
sebserver.feature.seb.screenProctoring.enabled=false
sebserver.feature.seb.screenProctoring.bundled=true
sebserver.feature.seb.screenProctoring.bundled.url=sps-service:8090
sebserver.feature.seb.screenProctoring.bundled.clientId=sebserverClient
sebserver.feature.seb.screenProctoring.bundled.clientPassword=${sps.sebserver.client.secret}
sebserver.feature.seb.screenProctoring.bundled.sebserveraccount.username=SEBServerAPIAccount
sebserver.feature.seb.screenProctoring.bundled.sebserveraccount.password=${sps.sebserver.password}
sebserver.feature.exam.seb.screenProctoring.enabled=false
sebserver.feature.exam.seb.screenProctoring.bundled=true
sebserver.feature.exam.seb.screenProctoring.bundled.url=sps-service:8090
sebserver.feature.exam.seb.screenProctoring.bundled.clientId=sebserverClient
sebserver.feature.exam.seb.screenProctoring.bundled.clientPassword=${sps.sebserver.client.secret}
sebserver.feature.exam.seb.screenProctoring.bundled.sebserveraccount.username=SEBServerAPIAccount
sebserver.feature.exam.seb.screenProctoring.bundled.sebserveraccount.password=${sps.sebserver.password}

View file

@ -129,6 +129,7 @@ sebserver.form.mandatory.label={0} mandatory
sebserver.form.confirm.label=confirm {0}
sebserver.form.tablefilter.label={0} table-column filter
sebserver.table.column.sort.default.tooltip=Click on the column header to sort the table within this column
sebserver.form.multiselect.deselect.tooltip=Please use double-click to deselect selected items.
sebserver.dialog.confirm.deactivation=Note that there are {0} other entities that belong to this entity.<br/>Those will also be deactivated by deactivating this entity.<br/><br/>Are you sure to deactivate this entity?
sebserver.dialog.confirm.deactivation.noDependencies=Are you sure you want to deactivate?
@ -162,6 +163,7 @@ sebserver.login.register=Register
sebserver.login.register.form.title=Create an Account
sebserver.login.register.do=Create Account
sebserver.login.register.success=New account successfully created.<br/> Please log in with your username and password.
sebserver.login.register.success.activate=New account successfully created.<br/> Please contact your system administrator for account activation.
################################
@ -677,7 +679,7 @@ sebserver.exam.configuration.list.pleaseSelect=At first please select an exam co
sebserver.exam.configuration.action.noconfig.message=There is currently no exam configuration to select.<br/>Please create one in Exam Configurations
sebserver.exam.configuration.action.list.new=Add Exam Configuration
sebserver.exam.configuration.action.list.modify=Edit Exam Configuration
sebserver.exam.configuration.action.list.modify=Edit Encryption Password
sebserver.exam.configuration.action.list.view=View Exam Configuration
sebserver.exam.configuration.action.list.delete=Remove Exam Configuration
sebserver.exam.configuration.action.save=Save Exam Configuration
@ -828,6 +830,8 @@ sebserver.exam.proctoring.form.collect.strategy=Collecting Room Strategy
sebserver.exam.proctoring.form.collect.strategy.tooltip=This specifies the strategy how connecting SEB clients are collected into proctoring rooms
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.deprecation.title=Deprecation Note:
sebserver.exam.proctoring.deprecation.message=The SEB Server live proctoring integration with Zoom and Jitsi Meet has been deprecated since this version of SEB Server.<br/>This means live proctoring is still available for dedicated SEB versions, but not actively maintained and supported any more.
sebserver.exam.proctoring.form.appkey.jitsi=App Key
sebserver.exam.proctoring.form.appkey.jitsi.tooltip=The application key of the proctoring service server
@ -1033,6 +1037,8 @@ sebserver.clientconfig.form.certificate=Encrypt with Certificate
sebserver.clientconfig.form.certificate.tooltip=Choose identity certificate to be used for encrypting the connection configuration
sebserver.clientconfig.form.type.async=Use asymmetric-only encryption
sebserver.clientconfig.form.type.async.tooltip=Use old asymmetric-only encryption (for SEB < 2.2)
sebserver.clientconfig.form.examselection=Exams
sebserver.clientconfig.form.examselection.tooltip=List of Exams selected to work with this Connection Configuration.
sebserver.clientconfig.form.credentials.title=Client Credentials of Connection Configuration
sebserver.clientconfig.form.credentials.info=A SEB client that loads this connection configuration<br/>uses the following credentials to securely connect to the SEB Server.

View file

@ -138,7 +138,8 @@ public class ModelObjectJSONGenerator {
false,
true,
DateTime.now(),
"user123");
"user123",
null);
System.out.println(domainObject.getClass().getSimpleName() + ":");
System.out.println(writerWithDefaultPrettyPrinter.writeValueAsString(domainObject));

View file

@ -128,6 +128,7 @@ public class ClientConfigTest extends GuiIntegrationTest {
false,
null,
null,
null,
null))
.call();
@ -161,6 +162,7 @@ public class ClientConfigTest extends GuiIntegrationTest {
false,
null,
null,
null,
null))
.call()
.getOrThrow();

View file

@ -12,6 +12,7 @@ import static org.junit.Assert.*;
import java.util.Collection;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.Order;
@ -61,7 +62,7 @@ public class ExamProctoringRoomServiceTest extends AdministrationAPIIntegrationT
@Order(1)
public void test01_checkExamRunning() {
final Result<Collection<Exam>> runningExamsForInstitution =
this.examSessionService.getRunningExamsForInstitution(1L);
this.examSessionService.getRunningExams(1L, Utils.truePredicate());
assertFalse(runningExamsForInstitution.hasError());
final Collection<Exam> collection = runningExamsForInstitution.get();
assertFalse(collection.isEmpty());

View file

@ -48,4 +48,4 @@ sebserver.webservice.api.exam.indicator.color=b4b4b4
sebserver.webservice.api.exam.indicator.thresholds=[{"value":5000.0,"color":"22b14c"},{"value":10000.0,"color":"ff7e00"},{"value":15000.0,"color":"ed1c24"}]
sebserver.webservice.master.delay.threshold=1000
sebserver.feature.seb.screenProctoring.bundled=false
sebserver.feature.exam.seb.screenProctoring.bundled=false