SEBSERV-497 new impl and prepare for role based features

This commit is contained in:
anhefti 2024-01-18 17:29:51 +01:00
parent aec6bd6c04
commit 7ffef0938f
24 changed files with 325 additions and 235 deletions

View file

@ -1,48 +0,0 @@
/*
* Copyright (c) 2023 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.gbl;
import ch.ethz.seb.sebserver.gbl.model.exam.CollectingStrategy;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
@Deprecated // we need another more flexible feature service that also take new User Role and Privileges into account
// SEBSERV-497
public interface FeatureService {
String FEATURE_SETTINGS_PREFIX = "sebserver.feature.";
enum ConfigurableFeature {
SCREEN_PROCTORING("seb.screenProctoring"),
INSTITUTION("admin.institution"),
REMOTE_PROCTORING("seb.remoteProctoring"),
TEST_LMS("lms.testLMS"),
EXAM_NO_LMS("exam.noLMS"),
LIGHT_SETUP("setup.light")
;
final String namespace;
ConfigurableFeature(final String namespace) {
this.namespace = namespace;
}
}
boolean isEnabled(ConfigurableFeature feature);
boolean isEnabled(LmsType LmsType);
boolean isEnabled(ProctoringServerType proctoringServerType);
boolean isEnabled(CollectingStrategy collectingRoomStrategy);
}

View file

@ -1,72 +0,0 @@
/*
* Copyright (c) 2023 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.gbl;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.model.exam.CollectingStrategy;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@Lazy
@Service
@WebServiceProfile
@Deprecated // we need another more flexible feature service that also take new User Role and Privileges into account
// SEBSERV-497
public class FeatureServiceImpl implements FeatureService {
private final Environment environment;
public FeatureServiceImpl(final Environment environment) {
this.environment = environment;
}
@Override
public boolean isEnabled(final LmsType LmsType) {
return this.environment.getProperty(toConfigName(
FEATURE_SETTINGS_PREFIX + LmsType.class.getSimpleName() + "."
+ LmsType.name()),
Boolean.class,
Boolean.TRUE);
}
@Override
public boolean isEnabled(final CollectingStrategy collectingRoomStrategy) {
return this.environment.getProperty(toConfigName(
FEATURE_SETTINGS_PREFIX + CollectingStrategy.class.getSimpleName() + "."
+ collectingRoomStrategy.name()),
Boolean.class,
Boolean.TRUE);
}
@Override
public boolean isEnabled(final ProctoringServerType proctoringServerType) {
return this.environment.getProperty(toConfigName(
FEATURE_SETTINGS_PREFIX + ProctoringServerType.class.getSimpleName() + "."
+ proctoringServerType.name()),
Boolean.class,
Boolean.TRUE);
}
@Override
public boolean isEnabled(final ConfigurableFeature feature) {
return this.environment.getProperty(toConfigName(
FEATURE_SETTINGS_PREFIX + feature.namespace + ".enabled"),
Boolean.class,
Boolean.FALSE);
}
private String toConfigName(final String key) {
return key.replaceAll("_", "-");
}
}

View file

@ -66,6 +66,8 @@ public final class API {
public static final String LOGIN_PATH_SEGMENT = "/loglogin";
public static final String LOGOUT_PATH_SEGMENT = "/loglogout";
public static final String FEATURES_PATH_SEGMENT = "/features";
public static final String INFO_ENDPOINT = "/info";
public static final String INFO_PARAM_INST_SUFFIX = "urlSuffix";
public static final String INFO_INST_PATH_SEGMENT = "/institution";

View file

@ -0,0 +1,71 @@
package ch.ethz.seb.sebserver.gbl.model.user;
import java.util.Map;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class UserFeatures {
public static final String ATTR_DEFAULT = "missingFeatureDefault";
public static final String ATTR_FEATURE_PRIVILEGES = "featurePrivileges";
public enum Feature {
INSTITUTION("admin.institution"),
SCREEN_PROCTORING("seb.screenProctoring"),
LIVE_PROCTORING("seb.liveProctoring"),
TEST_LMS("lms.type.MOCKUP"),
EXAM_NO_LMS("exam.noLMS"),
;
public final String featureName;
Feature(final String featureName) {
this.featureName = featureName;
}
}
@JsonProperty(Domain.USER.ATTR_ID)
public final String userId;
@JsonProperty(ATTR_DEFAULT)
public final Boolean missingFeatureDefault;
@JsonProperty(ATTR_FEATURE_PRIVILEGES)
public final Map<String, Boolean> featurePrivileges;
@JsonCreator
public UserFeatures(
@JsonProperty(Domain.USER.ATTR_ID) final String userId,
@JsonProperty(ATTR_DEFAULT) final Boolean missingFeatureDefault,
@JsonProperty(ATTR_FEATURE_PRIVILEGES) final Map<String, Boolean> featurePrivileges) {
this.userId = userId;
this.missingFeatureDefault = missingFeatureDefault;
this.featurePrivileges = Utils.immutableMapOf(featurePrivileges);
}
public String getUserId() {
return userId;
}
public Boolean getMissingFeatureDefault() {
return missingFeatureDefault;
}
public Map<String, Boolean> getFeaturePrivileges() {
return featurePrivileges;
}
public boolean isFeatureEnabled(final Feature feature) {
return featurePrivileges.getOrDefault(feature.featureName, missingFeatureDefault);
}
public boolean isFeatureEnabled(final String featureName) {
return featurePrivileges.getOrDefault(featureName, missingFeatureDefault);
}
}

View file

@ -8,9 +8,6 @@
package ch.ethz.seb.sebserver.gui;
import static ch.ethz.seb.sebserver.gbl.FeatureService.ConfigurableFeature.LIGHT_SETUP;
import ch.ethz.seb.sebserver.gbl.FeatureService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@ -35,10 +32,7 @@ public class GuiServiceInfo {
private final boolean distributedSetup;
private final boolean multilingualGUI;
public final FeatureService featureService;
public GuiServiceInfo(
final FeatureService featureService,
@Value("${sebserver.version:--}") final String sebServerVersion,
@Value("${server.address}") final String internalServer,
@Value("${server.port}") final String internalPort,
@ -50,7 +44,6 @@ public class GuiServiceInfo {
@Value("${sebserver.webservice.distributed:false}") final boolean distributedSetup,
@Value("${sebserver.gui.multilingual:false}") final boolean multilingualGUI) {
this.featureService = featureService;
if (StringUtils.isBlank(externalScheme)) {
throw new RuntimeException("Missing mandatory inital parameter sebserver.gui.http.external.servername");
}
@ -88,10 +81,6 @@ public class GuiServiceInfo {
this.multilingualGUI = multilingualGUI;
}
public boolean isLightSetup() {
return this.featureService.isEnabled(LIGHT_SETUP);
}
public String getExternalScheme() {
return this.externalScheme;
}

View file

@ -8,12 +8,11 @@
package ch.ethz.seb.sebserver.gui.content.exam;
import static ch.ethz.seb.sebserver.gbl.FeatureService.ConfigurableFeature.SCREEN_PROCTORING;
import static ch.ethz.seb.sebserver.gbl.model.user.UserFeatures.Feature.SCREEN_PROCTORING;
import java.util.*;
import java.util.function.Function;
import ch.ethz.seb.sebserver.gbl.FeatureService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.*;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
@ -124,7 +123,6 @@ public class ExamForm implements TemplateComposer {
private final ExamIndicatorsList examIndicatorsList;
private final ExamClientGroupList examClientGroupList;
private final ExamCreateClientConfigPopup examCreateClientConfigPopup;
private final FeatureService featureService;
protected ExamForm(
final PageService pageService,
@ -137,8 +135,7 @@ public class ExamForm implements TemplateComposer {
final ExamFormConfigs examFormConfigs,
final ExamIndicatorsList examIndicatorsList,
final ExamClientGroupList examClientGroupList,
final ExamCreateClientConfigPopup examCreateClientConfigPopup,
final FeatureService featureService) {
final ExamCreateClientConfigPopup examCreateClientConfigPopup) {
this.pageService = pageService;
this.resourceService = pageService.getResourceService();
@ -152,7 +149,6 @@ public class ExamForm implements TemplateComposer {
this.examIndicatorsList = examIndicatorsList;
this.examClientGroupList = examClientGroupList;
this.examCreateClientConfigPopup = examCreateClientConfigPopup;
this.featureService = featureService;
this.consistencyMessageMapping = new HashMap<>();
this.consistencyMessageMapping.put(
@ -255,7 +251,7 @@ public class ExamForm implements TemplateComposer {
.map(ProctoringServiceSettings::getEnableProctoring)
.getOr(false);
final boolean spsFeatureEnabled = this.featureService.isEnabled(SCREEN_PROCTORING);
final boolean spsFeatureEnabled = currentUser.isFeatureEnabled(SCREEN_PROCTORING);
final boolean screenProctoringEnabled = readonly && spsFeatureEnabled && this.restService
.getBuilder(GetScreenProctoringSettings.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)

View file

@ -8,14 +8,13 @@
package ch.ethz.seb.sebserver.gui.content.exam;
import static ch.ethz.seb.sebserver.gbl.FeatureService.ConfigurableFeature.EXAM_NO_LMS;
import static ch.ethz.seb.sebserver.gbl.model.user.UserFeatures.Feature.EXAM_NO_LMS;
import static ch.ethz.seb.sebserver.gui.service.page.PageContext.AttributeKeys.NEW_EXAM_NO_LMS;
import java.util.function.BiConsumer;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import ch.ethz.seb.sebserver.gbl.FeatureService;
import org.apache.commons.lang3.BooleanUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.widgets.Composite;
@ -149,7 +148,6 @@ public class ExamList implements TemplateComposer {
final CurrentUser currentUser = this.resourceService.getCurrentUser();
final RestService restService = this.resourceService.getRestService();
final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
final FeatureService featureService = this.pageService.getFeatureService();
// content page layout with title
final Composite content = widgetFactory.defaultPageLayout(
@ -286,7 +284,7 @@ public class ExamList implements TemplateComposer {
.newAction(ActionDefinition.EXAM_NEW)
.withAttribute(NEW_EXAM_NO_LMS, Constants.TRUE_STRING)
.publishIf(() -> userGrant.iw() && featureService.isEnabled(EXAM_NO_LMS))
.publishIf(() -> userGrant.iw() && currentUser.isFeatureEnabled(EXAM_NO_LMS))
;
}

View file

@ -8,8 +8,6 @@
package ch.ethz.seb.sebserver.gui.content.monitoring;
import static ch.ethz.seb.sebserver.gbl.FeatureService.ConfigurableFeature.SCREEN_PROCTORING;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -20,7 +18,6 @@ import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import ch.ethz.seb.sebserver.gbl.FeatureService;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.eclipse.swt.SWT;
@ -112,7 +109,6 @@ public class MonitoringRunningExam implements TemplateComposer {
private final MonitoringExamSearchPopup monitoringExamSearchPopup;
private final SEBSendLockPopup sebSendLockPopup;
private final MonitoringProctoringService monitoringProctoringService;
private final FeatureService featureService;
private final boolean distributedSetup;
private final long pollInterval;
@ -125,7 +121,6 @@ public class MonitoringRunningExam implements TemplateComposer {
final SEBSendLockPopup sebSendLockPopup,
final MonitoringProctoringService monitoringProctoringService,
final GuiServiceInfo guiServiceInfo,
final FeatureService featureService,
@Value("${sebserver.gui.webservice.poll-interval:2000}") final long pollInterval) {
this.serverPushService = serverPushService;
@ -139,7 +134,6 @@ public class MonitoringRunningExam implements TemplateComposer {
this.distributedSetup = guiServiceInfo.isDistributedSetup();
this.monitoringExamSearchPopup = monitoringExamSearchPopup;
this.sebSendLockPopup = sebSendLockPopup;
this.featureService = featureService;
}
@Override
@ -321,9 +315,7 @@ public class MonitoringRunningExam implements TemplateComposer {
final PageActionBuilder actionBuilder = this.pageService
.pageActionBuilder(pageContext.clearEntityKeys());
final boolean spsFeatureEnabled = this.featureService.isEnabled(SCREEN_PROCTORING);
final boolean proctoringEnabled = spsFeatureEnabled &&
proctoringSettings != null &&
final boolean proctoringEnabled = proctoringSettings != null &&
BooleanUtils.toBoolean(proctoringSettings.enableProctoring);
final boolean screenProctoringEnabled = screenProctoringSettings != null &&
BooleanUtils.toBoolean(screenProctoringSettings.enableScreenProctoring);

View file

@ -92,7 +92,8 @@ import ch.ethz.seb.sebserver.gui.service.session.MonitoringEntry;
@Service
@GuiProfile
/** Defines functionality to get resources or functions of resources to feed e.g. selection or
* combo-box content. */
* combo-box content.
* */
public class ResourceService {
private static final Logger log = LoggerFactory.getLogger(ResourceService.class);
@ -139,8 +140,6 @@ public class ResourceService {
public static final String EXAM_PROCTORING_FEATURES_PREFIX = "sebserver.exam.proctoring.form.features.";
public static final String VDI_TYPE_PREFIX = "sebserver.clientconfig.form.vditype.";
private static final String DISABLE_LMS_FLAG = "sebserver.gui.webservice.lms.disable.";
public static final EnumSet<AttributeType> ATTRIBUTE_TYPES_NOT_DISPLAYED = EnumSet.of(
AttributeType.LABEL,
AttributeType.COMPOSITE_TABLE,
@ -159,7 +158,6 @@ public class ResourceService {
private final I18nSupport i18nSupport;
private final RestService restService;
private final CurrentUser currentUser;
private final EnumSet<LmsType> disabledLmsTypes;
protected ResourceService(
final I18nSupport i18nSupport,
@ -170,13 +168,6 @@ public class ResourceService {
this.i18nSupport = i18nSupport;
this.restService = restService;
this.currentUser = currentUser;
this.disabledLmsTypes = EnumSet.noneOf(LmsType.class);
final List<LmsType> disabled = Arrays.asList(LmsType.values()).stream()
.filter(lmsType -> lmsType.features.isEmpty() ||
environment.getProperty(DISABLE_LMS_FLAG + lmsType.name(), Boolean.class, false) == true)
.collect(Collectors.toList());
this.disabledLmsTypes.addAll(disabled);
}
public I18nSupport getI18nSupport() {
@ -206,7 +197,7 @@ public class ResourceService {
public List<Tuple<String>> lmsTypeResources() {
return Arrays.stream(LmsType.values())
.filter(lmsType -> !this.disabledLmsTypes.contains(lmsType))
.filter(lmsType -> this.currentUser.isFeatureEnabled("lms.type." + lmsType.name()))
.map(lmsType -> new Tuple<>(
lmsType.name(),
this.i18nSupport.getText(LMSSETUP_TYPE_PREFIX + lmsType.name(), lmsType.name())))

View file

@ -17,7 +17,6 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import ch.ethz.seb.sebserver.gbl.FeatureService;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
@ -76,8 +75,6 @@ public interface PageService {
Cryptor getCryptor();
FeatureService getFeatureService();
/** Get the WidgetFactory service
*
* @return the WidgetFactory service */

View file

@ -20,7 +20,6 @@ import java.util.function.Supplier;
import javax.servlet.http.HttpSession;
import ch.ethz.seb.sebserver.gbl.FeatureService;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.swt.widgets.TreeItem;
import org.slf4j.Logger;
@ -89,7 +88,6 @@ public class PageServiceImpl implements PageService {
private final ResourceService resourceService;
private final CurrentUser currentUser;
private final ServerPushService serverPushService;
private final FeatureService featureService;
public PageServiceImpl(
final Cryptor cryptor,
@ -98,8 +96,7 @@ public class PageServiceImpl implements PageService {
final PolyglotPageService polyglotPageService,
final ResourceService resourceService,
final CurrentUser currentUser,
final ServerPushService serverPushService,
final FeatureService featureService) {
final ServerPushService serverPushService) {
this.cryptor = cryptor;
this.jsonMapper = jsonMapper;
@ -108,7 +105,6 @@ public class PageServiceImpl implements PageService {
this.resourceService = resourceService;
this.currentUser = currentUser;
this.serverPushService = serverPushService;
this.featureService = featureService;
}
@Override
@ -116,11 +112,6 @@ public class PageServiceImpl implements PageService {
return this.cryptor;
}
@Override
public FeatureService getFeatureService() {
return this.featureService;
}
@Override
public WidgetFactory getWidgetFactory() {
return this.widgetFactory;

View file

@ -0,0 +1,29 @@
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.useraccount;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.user.UserFeatures;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
import com.fasterxml.jackson.core.type.TypeReference;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
@Lazy
@Component
@GuiProfile
public class GetUserFeatures extends RestCall<UserFeatures> {
public GetUserFeatures() {
super(new TypeKey<>(
CallType.GET_SINGLE,
EntityType.USER,
new TypeReference<UserFeatures>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.USER_ACCOUNT_ENDPOINT + API.CURRENT_USER_PATH_SEGMENT + API.FEATURES_PATH_SEGMENT);
}
}

View file

@ -13,6 +13,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import ch.ethz.seb.sebserver.gbl.model.user.UserFeatures;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
@ -47,6 +48,8 @@ public class CurrentUser {
private final AuthorizationContextHolder authorizationContextHolder;
private SEBServerAuthorizationContext authContext = null;
private Map<RoleTypeKey, Privilege> privileges = null;
private UserFeatures features = null;
private final Map<String, String> attributes;
private final ProctoringGUIService proctoringGUIService;
@ -192,6 +195,7 @@ public class CurrentUser {
this.proctoringGUIService.clear();
this.privileges = null;
this.features = null;
if (isAvailable()) {
if (this.authContext.logout()) {
@ -272,6 +276,42 @@ public class CurrentUser {
}
}
public boolean isFeatureEnabled(final UserFeatures.Feature feature) {
loadFeatures();
return this.features != null && this.features.isFeatureEnabled(feature);
}
public boolean isFeatureEnabled(final String featureName) {
loadFeatures();
return this.features != null && this.features.isFeatureEnabled(featureName);
}
private void loadFeatures() {
if (this.features != null) {
return;
}
updateContext();
if (this.authContext != null) {
try {
final WebserviceURIService webserviceURIService =
this.authorizationContextHolder.getWebserviceURIService();
this.features = this.authContext.getRestTemplate()
.exchange(
webserviceURIService.getURIBuilder()
.path(API.USER_ACCOUNT_ENDPOINT + API.CURRENT_USER_PATH_SEGMENT + API.FEATURES_PATH_SEGMENT)
.toUriString(),
HttpMethod.GET,
HttpEntity.EMPTY,
UserFeatures.class)
.getBody();
} catch (final Exception e) {
log.error("Failed to load user feature privileges: ", e);
}
}
}
/** Wrapper can be used for base and institutional grant checks for a specified EntityType */
public class GrantCheck {
private final EntityType entityType;

View file

@ -8,20 +8,14 @@
package ch.ethz.seb.sebserver.webservice;
import static ch.ethz.seb.sebserver.gbl.FeatureService.ConfigurableFeature.LIGHT_SETUP;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.*;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.gbl.FeatureService;
import ch.ethz.seb.sebserver.gbl.model.user.UserFeatures;
import ch.ethz.seb.sebserver.gbl.model.exam.SPSAPIAccessData;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.FeatureService;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
@ -66,6 +60,7 @@ public class WebserviceInfo {
private final String discoveryEndpoint;
private final String contextPath;
private final boolean isLightSetup;
private final String serverURLPrefix;
private final boolean isDistributed;
private final String webserviceUUID;
@ -76,6 +71,7 @@ public class WebserviceInfo {
private final Set<String> activeProfiles;
private final WebserviceInfoDAO webserviceInfoDAO;
private final FeatureService featureService;
private boolean isMaster = false;
@Value("${sebserver.webservice.api.admin.accessTokenValiditySeconds:3600}")
@ -85,18 +81,16 @@ public class WebserviceInfo {
@Value("${sebserver.webservice.api.exam.accessTokenValiditySeconds:43200}")
private int examAPITokenValiditySeconds;
public final FeatureService featureService;
private final ScreenProctoringServiceBundle screenProctoringServiceBundle;
public WebserviceInfo(
final WebserviceInfoDAO webserviceInfoDAO,
final FeatureService featureService,
final Environment environment,
final Cryptor cryptor,
final FeatureService featureService) {
final Cryptor cryptor) {
this.featureService = featureService;
this.webserviceInfoDAO = webserviceInfoDAO;
this.featureService = featureService;
this.sebServerVersion = environment.getRequiredProperty(VERSION_KEY);
this.testProperty = environment.getProperty(WEB_SERVICE_TEST_PROPERTY, "NOT_AVAILABLE");
this.httpScheme = environment.getRequiredProperty(WEB_SERVICE_HTTP_SCHEME_KEY);
@ -135,6 +129,9 @@ public class WebserviceInfo {
}
this.serverURLPrefix = builder.toUriString();
this.isLightSetup = BooleanUtils.toBoolean(environment.getProperty(
"sebserver.webservice.light.setup",
Constants.FALSE_STRING));
this.isDistributed = BooleanUtils.toBoolean(environment.getProperty(
"sebserver.webservice.distributed",
Constants.FALSE_STRING));
@ -180,8 +177,11 @@ public class WebserviceInfo {
}
}
public boolean isLightSetup() {
return this.featureService.isEnabled(LIGHT_SETUP);
public Map<String, Boolean> configuredFeatures() {
return Arrays.stream(UserFeatures.Feature.values()).collect(Collectors.toMap(
f -> f.featureName,
featureService::isEnabledByConfig
));
}
public boolean isMaster() {
@ -276,7 +276,7 @@ public class WebserviceInfo {
/** Get the server URL prefix in the form of;
* [scheme{http|https}]://[server-address{DNS-name|IP}]:[port]
*
* <p>
* E.g.: https://seb.server.ch:8080
*
* @return the server URL prefix */
@ -284,6 +284,9 @@ public class WebserviceInfo {
return this.serverURLPrefix;
}
public boolean isLightSetup() {
return this.isLightSetup;
}
public boolean isDistributed() {
return this.isDistributed;
}

View file

@ -121,10 +121,11 @@ public class WebserviceInit implements ApplicationListener<ApplicationReadyEvent
this.environment.getProperty("sebserver.webservice.distributed.connectionUpdate", "2000"));
}
if (this.webserviceInfo.isLightSetup()) {
SEBServerInit.INIT_LOGGER.info("----> ");
SEBServerInit.INIT_LOGGER.info("----> SEB Server light setup enabled");
}
SEBServerInit.INIT_LOGGER.info("----> ");
SEBServerInit.INIT_LOGGER.info("----> Configured Features:");
this.webserviceInfo.configuredFeatures().entrySet().stream().forEach(entry -> {
SEBServerInit.INIT_LOGGER.info("----> {} --> {}", entry.getKey(), entry.getValue());
});
SEBServerInit.INIT_LOGGER.info("----> ");
SEBServerInit.INIT_LOGGER.info("----> Working with ping service: {}",

View file

@ -18,13 +18,14 @@ import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.authorization.Privilege;
import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType;
import ch.ethz.seb.sebserver.gbl.model.GrantEntity;
import ch.ethz.seb.sebserver.gbl.model.user.UserFeatures;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser;
/** A service to check authorization grants for a given user for entity-types and -instances
*
* <p>
* If there is one or more GrantEntity objects within an authenticated user-request, this service
* can be used check the authenticated user access grant within the object. Check if a given user
* has write, modify or even read rights on an entity instance or on an entity type. */
@ -40,17 +41,19 @@ public interface AuthorizationService {
* @return all registered Privileges */
Collection<Privilege> getAllPrivileges();
/** Check grant on privilege type for specified EntityType and for the given user and institution.
*
* <p>
* This makes a privilege grant check for every UserRole given. The first found successful grant
* will immediately return true
*
* <p>
* The privilege grant check function always checks first the base privilege with no institutional or owner grant.
* If user has a grant on base privileges this returns true without checking further institutional or owner grant
* If user has no base privilege grant the function checks further grants, first the institutional grant, where
* the institution id and the users institution id must match and further more the owner grant, where ownerId
* and the users id must match.
*
* <p>
* see Privilege.hasGrant for more information how the overall grant function works
*
* @param privilegeType The privilege type to check
@ -71,7 +74,7 @@ public interface AuthorizationService {
Set<UserRole> userRoles);
/** Check grant for a given privilege type and entity type for the current user.
*
* <p>
* NOTE: This only checks the base privilege grant because there is no Entity specific information
*
* @param privilegeType the privilege type to check
@ -109,11 +112,11 @@ public interface AuthorizationService {
/** Check base privilege grant and institutional privilege grant for a given privilege type
* on a given entity type.
*
* <p>
* If the question is similar like this:
* "Has the current user that belongs to institution A the right to create an entity of
* type X on institution B", then this is the answer, use:
*
* <p>
* hasPrivilege(PrivilegeType.WRITE, EntityType.X, B)
*
* @param privilegeType the privilege type to check

View file

@ -0,0 +1,21 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.authorization;
import java.util.Map;
import ch.ethz.seb.sebserver.gbl.model.user.UserFeatures;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.util.Result;
public interface FeatureService {
public static final String FEATURE_CONFIG_PREFIX = "sebserver.feature.";
/** Get all feature flags for current user.
*
* @return UserFeatures all feature flags for current user */
Result<UserFeatures> getCurrentUserFeatures();
Map<UserFeatures.Feature, Boolean> getUserRoleDefaults(UserRole role);
boolean isEnabledByConfig(final UserFeatures.Feature feature);
}

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.authorization;
import java.security.Principal;
import ch.ethz.seb.sebserver.gbl.model.user.UserFeatures;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.WebDataBinder;
@ -29,10 +30,10 @@ public interface UserService {
SEBServerUser getCurrentUser();
/** Extracts the internal SEBServerUser from a given Principal.
*
* <p>
* This is attended to apply some known strategies to extract the internal user from Principal. If there is no
* internal user found on the given Principal, a IllegalArgumentException is thrown.
*
* <p>
* If there is certainly a internal user within the given Principal but no strategy that finds it, this method can
* be extended with the needed strategy.
*
@ -54,7 +55,7 @@ public interface UserService {
/** Used to set authentication on different thread.
*
* @param authentication */
* @param authentication the Authentication context*/
void setAuthenticationIfAbsent(Authentication authentication);
}

View file

@ -0,0 +1,82 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl;
import java.util.*;
import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.gbl.model.user.UserFeatures;
import ch.ethz.seb.sebserver.gbl.model.user.UserFeatures.Feature;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.FeatureService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
@Lazy
@Service
@WebServiceProfile
public class FeatureServiceImpl implements FeatureService {
private final Environment environment;
private final UserService userService;
private final UserDAO userDAO;
public FeatureServiceImpl(
final Environment environment,
final UserService userService,
final UserDAO userDAO) {
this.environment = environment;
this.userService = userService;
this.userDAO = userDAO;
}
@Override
public Result<UserFeatures> getCurrentUserFeatures() {
return Result.tryCatch(() -> {
final String userId = userService.getCurrentUser().getUserInfo().uuid;
final EnumSet<Feature> userEnabledFeatures = getUserEnabledFeatures(userId);
final Map<String, Boolean> features = Arrays.stream(Feature.values()).collect(Collectors.toMap(
f -> f.featureName,
f -> isEnabledByConfig(f) && userEnabledFeatures.contains(f)
));
return new UserFeatures(userId, true, features);
});
}
@Override
public Map<Feature, Boolean> getUserRoleDefaults(final UserRole role) {
// TODO implement this when user role based features are available
return Collections.emptyMap();
}
public EnumSet<Feature> getUserEnabledFeatures(final String userId) {
// TODO implement this when user role based features are available
return EnumSet.allOf(Feature.class);
}
@Override
public boolean isEnabledByConfig(final Feature feature) {
final String configName = getConfigName(feature);
try {
return this.environment.getProperty(configName, Boolean.class, false)
|| this.environment.getProperty(configName + ".enabled", Boolean.class);
} catch (final Exception e) {
// NOTE: for now if there is not explicitly disabled from config, the feature is considered enabled
return true;
}
}
private String getConfigName(final Feature feature) {
return getConfigName(feature.featureName);
}
private String getConfigName(final String featureName) {
return FEATURE_CONFIG_PREFIX + featureName;
}
}

View file

@ -16,7 +16,9 @@ import java.util.List;
import javax.validation.Valid;
import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport;
import ch.ethz.seb.sebserver.gbl.model.user.*;
import ch.ethz.seb.sebserver.gbl.util.Pair;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.FeatureService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService;
import org.mybatis.dynamic.sql.SqlTable;
import org.springframework.beans.factory.annotation.Qualifier;
@ -38,12 +40,6 @@ import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.api.authorization.Privilege;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.user.PasswordChange;
import ch.ethz.seb.sebserver.gbl.model.user.UserAccount;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserLogActivityType;
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.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.UserRecordDynamicSqlSupport;
@ -66,6 +62,8 @@ public class UserAccountController extends ActivatableEntityController<UserInfo,
private final PasswordEncoder userPasswordEncoder;
private final ScreenProctoringService screenProctoringService;
private final FeatureService featureService;
public UserAccountController(
final UserDAO userDAO,
final AuthorizationService authorization,
@ -75,6 +73,7 @@ public class UserAccountController extends ActivatableEntityController<UserInfo,
final ApplicationEventPublisher applicationEventPublisher,
final BeanValidationService beanValidationService,
final ScreenProctoringService screenProctoringService,
final FeatureService featureService,
@Qualifier(WebSecurityConfig.USER_PASSWORD_ENCODER_BEAN_NAME) final PasswordEncoder userPasswordEncoder) {
super(authorization,
@ -87,6 +86,7 @@ public class UserAccountController extends ActivatableEntityController<UserInfo,
this.userDAO = userDAO;
this.userPasswordEncoder = userPasswordEncoder;
this.screenProctoringService = screenProctoringService;
this.featureService = featureService;
}
@RequestMapping(path = API.CURRENT_USER_PATH_SEGMENT, method = RequestMethod.GET)
@ -97,6 +97,14 @@ public class UserAccountController extends ActivatableEntityController<UserInfo,
.getUserInfo();
}
@RequestMapping(
path = API.CURRENT_USER_PATH_SEGMENT + API.FEATURES_PATH_SEGMENT,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public UserFeatures getCurrentUserFeatures() {
return this.featureService.getCurrentUserFeatures().getOrThrow();
}
@RequestMapping(path = API.LOGIN_PATH_SEGMENT, method = RequestMethod.POST)
public void logLogin() {
this.userActivityLogDAO.logLogin(this.authorization

View file

@ -68,7 +68,8 @@ springdoc.swagger-ui.oauth.clientSecret=${sebserver.password}
#springdoc.default-consumes-media-type=application/x-www-form-urlencoded
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

View file

@ -28,6 +28,3 @@ logging.level.com.zaxxer.hikari=INFO
sebserver.http.client.connect-timeout=15000
sebserver.http.client.connection-request-timeout=10000
sebserver.http.client.read-timeout=60000
# features
sebserver.feature.seb.screenProctoring.enabled=true

View file

@ -38,6 +38,7 @@ sebserver.webservice.api.admin.clientSecret=${sebserver.password}
sebserver.webservice.internalSecret=${sebserver.password}
### webservice setup configuration
sebserver.webservice.light.setup=false
sebserver.webservice.forceMaster=false
sebserver.webservice.distributed=false
sebserver.webservice.distributed.updateInterval=2000
@ -92,4 +93,17 @@ sebserver.webservice.api.exam.indicator.thresholds=[{"value":5000.0,"color":"ffc
sebserver.webservice.configtemplate.examconfig.default.name=__startDate__ __examName__
sebserver.webservice.configtemplate.examconfig.default.description=This has automatically been created from the exam template: __examTemplateName__ at: __currentDate__
# features
sebserver.feature.admin.institution.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}

View file

@ -62,20 +62,3 @@ security.require-ssl=false
# Disable this if a redirect is done by a reverse proxy for example
sebserver.ssl.redirect.enabled=false
sebserver.ssl.redirect.html.port=8080
# features
sebserver.feature.setup.light.enabled=false
sebserver.feature.admin.institution.enabled=true
sebserver.feature.seb.remoteProctoring.enabled=true
sebserver.feature.lms.testLMS.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.CollectingRoomStrategy.SEB-GROUP=false