SEBSERV-65 SEBSERV-82 SEBSERV-83 bug-fixes

This commit is contained in:
anhefti 2019-08-22 09:26:46 +02:00
parent 2b3b44419c
commit dd826a3770
22 changed files with 246 additions and 69 deletions

View file

@ -8,11 +8,15 @@
package ch.ethz.seb.sebserver.gbl.api.authorization; package ch.ethz.seb.sebserver.gbl.api.authorization;
import java.util.Arrays;
import java.util.EnumSet; import java.util.EnumSet;
import org.apache.commons.lang3.StringUtils;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.user.UserAccount; 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.UserInfo;
@ -95,7 +99,8 @@ public final class Privilege {
* @param privilegeType the type of privilege to check (READ_ONLY, MODIFY, WRITE...) * @param privilegeType the type of privilege to check (READ_ONLY, MODIFY, WRITE...)
* @param institutionId the institution identifier of an Entity for the institutional grant check, * @param institutionId the institution identifier of an Entity for the institutional grant check,
* may be null in case the institutional grant check should be skipped * may be null in case the institutional grant check should be skipped
* @param ownerId the owner identifier of an Entity for ownership grant check, may be null in case * @param ownerId the owner identifier of an Entity for ownership grant check.
* This can be a single id or a comma-separated list of user ids and may be null in case
* the ownership grant check should be skipped * the ownership grant check should be skipped
* @return true if there is any grant within the given context or false on deny */ * @return true if there is any grant within the given context or false on deny */
public final boolean hasGrant( public final boolean hasGrant(
@ -111,7 +116,16 @@ public final class Privilege {
&& userInstitutionId.longValue() == institutionId && userInstitutionId.longValue() == institutionId
.longValue()) .longValue())
|| (this.hasOwnershipPrivilege(privilegeType) || (this.hasOwnershipPrivilege(privilegeType)
&& userId.equals(ownerId))); && isOwner(ownerId, userId)));
}
private boolean isOwner(final String ownerId, final String userId) {
if (StringUtils.isBlank(ownerId)) {
return false;
}
return Arrays.asList(StringUtils.split(ownerId, Constants.LIST_SEPARATOR))
.contains(userId);
} }
@Override @Override

View file

@ -8,11 +8,13 @@
package ch.ethz.seb.sebserver.gbl.model.exam; package ch.ethz.seb.sebserver.gbl.model.exam;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.joda.time.DateTimeZone; import org.joda.time.DateTimeZone;
@ -205,9 +207,25 @@ public final class Exam implements GrantEntity, Activatable {
return this.institutionId; return this.institutionId;
} }
public boolean isOwner(final String userId) {
if (StringUtils.isBlank(userId)) {
return false;
}
if (userId.equals(this.owner)) {
return true;
}
return this.supporter.contains(userId);
}
@Override @Override
public String getOwnerId() { public String getOwnerId() {
return this.owner; final ArrayList<String> owners = new ArrayList<>(this.supporter);
if (!StringUtils.isBlank(this.owner)) {
owners.add(this.owner);
}
return StringUtils.join(owners, Constants.LIST_SEPARATOR);
} }
public Long getLmsSetupId() { public Long getLmsSetupId() {
@ -261,7 +279,7 @@ public final class Exam implements GrantEntity, Activatable {
return ExamStatus.UP_COMING; return ExamStatus.UP_COMING;
} }
// expand time frame // expanded time frame
final DateTime expStartTime = this.startTime.minusHours(EXAM_RUN_TIME_EXPAND_HOURS); final DateTime expStartTime = this.startTime.minusHours(EXAM_RUN_TIME_EXPAND_HOURS);
final DateTime expEndTime = (this.endTime != null) final DateTime expEndTime = (this.endTime != null)
? this.endTime.plusHours(EXAM_RUN_TIME_EXPAND_HOURS) : null; ? this.endTime.plusHours(EXAM_RUN_TIME_EXPAND_HOURS) : null;

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.gbl.model.user; package ch.ethz.seb.sebserver.gbl.model.user;
import java.io.Serializable; import java.io.Serializable;
import java.util.Arrays;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
@ -22,6 +23,7 @@ import javax.validation.constraints.Size;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTimeZone; import org.joda.time.DateTimeZone;
import org.springframework.util.CollectionUtils;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
@ -201,6 +203,13 @@ public final class UserInfo implements UserAccount, Activatable, Serializable {
return this.roles.contains(userRole.name()); return this.roles.contains(userRole.name());
} }
public boolean hasAnyRole(final UserRole... userRole) {
if (userRole == null) {
return false;
}
return CollectionUtils.containsAny(getUserRoles(), Arrays.asList(userRole));
}
@JsonIgnore @JsonIgnore
@Override @Override
public EntityKey getEntityKey() { public EntityKey getEntityKey() {

View file

@ -33,6 +33,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap; import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition; import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
@ -171,7 +172,9 @@ public class ExamForm implements TemplateComposer {
final EntityGrantCheck userGrantCheck = currentUser.entityGrantCheck(exam); final EntityGrantCheck userGrantCheck = currentUser.entityGrantCheck(exam);
final boolean modifyGrant = userGrantCheck.m(); final boolean modifyGrant = userGrantCheck.m();
final ExamStatus examStatus = exam.getStatus(); final ExamStatus examStatus = exam.getStatus();
final boolean editable = examStatus == ExamStatus.UP_COMING; final boolean editable = examStatus == ExamStatus.UP_COMING ||
examStatus == ExamStatus.RUNNING &&
currentUser.get().hasRole(UserRole.EXAM_ADMIN);
// The Exam form // The Exam form
final FormHandle<Exam> formHandle = this.pageService.formBuilder( final FormHandle<Exam> formHandle = this.pageService.formBuilder(
@ -299,14 +302,26 @@ public class ExamForm implements TemplateComposer {
this.resourceService::localizedExamConfigStatusName)) this.resourceService::localizedExamConfigStatusName))
.withDefaultActionIf( .withDefaultActionIf(
() -> editable, () -> editable,
() -> actionBuilder t -> actionBuilder
.newAction(ActionDefinition.EXAM_CONFIGURATION_MODIFY_FROM_LIST) .newAction(ActionDefinition.EXAM_CONFIGURATION_EXAM_CONFIG_VIEW_PROP)
.withSelectionSupplier(() -> {
final ExamConfigurationMap selectedROWData = t.getSelectedROWData();
final HashSet<EntityKey> result = new HashSet<>();
if (selectedROWData != null) {
result.add(new EntityKey(
selectedROWData.configurationNodeId,
EntityType.CONFIGURATION_NODE));
}
return result;
})
.create()) .create())
.compose(pageContext.copyOf(content)); .compose(pageContext.copyOf(content));
final EntityKey configMapKey = (configurationTable.hasAnyContent()) final EntityKey configMapKey = (configurationTable.hasAnyContent())
? configurationTable.getFirstRowData().getEntityKey() ? new EntityKey(
configurationTable.getFirstRowData().configurationNodeId,
EntityType.CONFIGURATION_NODE)
: null; : null;
actionBuilder actionBuilder
@ -315,7 +330,7 @@ public class ExamForm implements TemplateComposer {
.withParentEntityKey(entityKey) .withParentEntityKey(entityKey)
.publishIf(() -> modifyGrant && editable && !configurationTable.hasAnyContent()) .publishIf(() -> modifyGrant && editable && !configurationTable.hasAnyContent())
.newAction(ActionDefinition.EXAM_CONFIGURATION_MODIFY_FROM_LIST) .newAction(ActionDefinition.EXAM_CONFIGURATION_EXAM_CONFIG_VIEW_PROP)
.withParentEntityKey(entityKey) .withParentEntityKey(entityKey)
.withEntityKey(configMapKey) .withEntityKey(configMapKey)
.publishIf(() -> modifyGrant && editable && configurationTable.hasAnyContent()) .publishIf(() -> modifyGrant && editable && configurationTable.hasAnyContent())

View file

@ -141,7 +141,7 @@ public class SebClientLogs implements TemplateComposer {
this.examFilter = new TableFilterAttribute( this.examFilter = new TableFilterAttribute(
CriteriaType.SINGLE_SELECTION, CriteriaType.SINGLE_SELECTION,
ExtendedClientEvent.FILTER_ATTRIBUTE_EXAM, ExtendedClientEvent.FILTER_ATTRIBUTE_EXAM,
this.resourceService::getExamResources); this.resourceService::getExamLogSelectionResources);
this.clientSessionFilter = new TableFilterAttribute( this.clientSessionFilter = new TableFilterAttribute(
CriteriaType.TEXT, CriteriaType.TEXT,

View file

@ -109,7 +109,7 @@ public enum ActionDefinition {
ActionCategory.FORM), ActionCategory.FORM),
USER_ACCOUNT_CHANGE_PASSOWRD( USER_ACCOUNT_CHANGE_PASSOWRD(
new LocTextKey("sebserver.useraccount.action.change.password"), new LocTextKey("sebserver.useraccount.action.change.password"),
ImageIcon.EDIT, ImageIcon.SECURE,
PageStateDefinition.USER_ACCOUNT_PASSWORD_CHANGE, PageStateDefinition.USER_ACCOUNT_PASSWORD_CHANGE,
ActionCategory.FORM), ActionCategory.FORM),
USER_ACCOUNT_CHANGE_PASSOWRD_SAVE( USER_ACCOUNT_CHANGE_PASSOWRD_SAVE(
@ -237,6 +237,11 @@ public enum ActionDefinition {
ImageIcon.EDIT, ImageIcon.EDIT,
PageStateDefinition.EXAM_CONFIG_MAP_EDIT, PageStateDefinition.EXAM_CONFIG_MAP_EDIT,
ActionCategory.EXAM_CONFIG_MAPPING_LIST), ActionCategory.EXAM_CONFIG_MAPPING_LIST),
EXAM_CONFIGURATION_EXAM_CONFIG_VIEW_PROP(
new LocTextKey("sebserver.examconfig.action.view"),
ImageIcon.SHOW,
PageStateDefinition.SEB_EXAM_CONFIG_VIEW,
ActionCategory.EXAM_CONFIG_MAPPING_LIST),
EXAM_CONFIGURATION_DELETE_FROM_LIST( EXAM_CONFIGURATION_DELETE_FROM_LIST(
new LocTextKey("sebserver.exam.configuration.action.list.delete"), new LocTextKey("sebserver.exam.configuration.action.list.delete"),
ImageIcon.DELETE, ImageIcon.DELETE,

View file

@ -109,8 +109,7 @@ public class ActivitiesPane implements TemplateComposer {
// User Account // User Account
// if current user has role seb-server admin or institutional-admin, show list // if current user has role seb-server admin or institutional-admin, show list
if (this.currentUser.get().hasRole(UserRole.SEB_SERVER_ADMIN) || if (this.currentUser.get().hasAnyRole(UserRole.SEB_SERVER_ADMIN, UserRole.INSTITUTIONAL_ADMIN)) {
this.currentUser.get().hasRole(UserRole.INSTITUTIONAL_ADMIN)) {
final TreeItem userAccounts = this.widgetFactory.treeItemLocalized( final TreeItem userAccounts = this.widgetFactory.treeItemLocalized(
navigation, navigation,
@ -146,10 +145,10 @@ public class ActivitiesPane implements TemplateComposer {
} }
// Exam (Quiz Discovery) // Exam (Quiz Discovery)
if (this.currentUser.hasInstitutionalPrivilege(PrivilegeType.READ, EntityType.EXAM)) { if (this.currentUser.get().hasAnyRole(UserRole.EXAM_SUPPORTER, UserRole.EXAM_ADMIN) ||
this.currentUser.hasInstitutionalPrivilege(PrivilegeType.READ, EntityType.EXAM)) {
// Quiz Discovery // Quiz Discovery
// TODO discussion if this should be visible on Activity Pane or just over the Exam activity and Import action
final TreeItem quizDiscovery = this.widgetFactory.treeItemLocalized( final TreeItem quizDiscovery = this.widgetFactory.treeItemLocalized(
navigation, navigation,
ActivityDefinition.QUIZ_DISCOVERY.displayName); ActivityDefinition.QUIZ_DISCOVERY.displayName);
@ -220,7 +219,7 @@ public class ActivitiesPane implements TemplateComposer {
} }
// Monitoring exams // Monitoring exams
if (this.currentUser.get().hasRole(UserRole.EXAM_SUPPORTER)) { if (this.currentUser.get().hasAnyRole(UserRole.EXAM_SUPPORTER)) {
final TreeItem clientConfig = this.widgetFactory.treeItemLocalized( final TreeItem clientConfig = this.widgetFactory.treeItemLocalized(
navigation, navigation,
ActivityDefinition.MONITORING_EXAMS.displayName); ActivityDefinition.MONITORING_EXAMS.displayName);
@ -237,7 +236,8 @@ public class ActivitiesPane implements TemplateComposer {
EntityType.USER_ACTIVITY_LOG); EntityType.USER_ACTIVITY_LOG);
final boolean viewSebClientLogs = this.currentUser.hasInstitutionalPrivilege( final boolean viewSebClientLogs = this.currentUser.hasInstitutionalPrivilege(
PrivilegeType.READ, PrivilegeType.READ,
EntityType.EXAM); EntityType.EXAM) ||
this.currentUser.get().hasRole(UserRole.EXAM_SUPPORTER);
TreeItem logRoot = null; TreeItem logRoot = null;
if (viewUserActivityLogs && viewSebClientLogs) { if (viewUserActivityLogs && viewSebClientLogs) {

View file

@ -30,6 +30,7 @@ import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.model.EntityName; import ch.ethz.seb.sebserver.gbl.model.EntityName;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType; import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType;
import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap; import ch.ethz.seb.sebserver.gbl.model.exam.ExamConfigurationMap;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType;
@ -52,6 +53,7 @@ import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMappingNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamConfigMappingNames;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamNames;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExams;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution.GetInstitutionNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution.GetInstitutionNames;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetupNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.lmssetup.GetLmsSetupNames;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.useraccount.GetUserAccountNames; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.useraccount.GetUserAccountNames;
@ -419,6 +421,20 @@ public class ResourceService {
.getText(ResourceService.EXAM_TYPE_PREFIX + exam.type.name()); .getText(ResourceService.EXAM_TYPE_PREFIX + exam.type.name());
} }
public List<Tuple<String>> getExamLogSelectionResources() {
final UserInfo userInfo = this.currentUser.get();
return this.restService.getBuilder(GetExams.class)
.withQueryParam(Entity.FILTER_ATTR_INSTITUTION, String.valueOf(userInfo.getInstitutionId()))
.call()
.getOr(Collections.emptyList())
.stream()
.filter(exam -> exam != null
&& (exam.getStatus() == ExamStatus.RUNNING || exam.getStatus() == ExamStatus.FINISHED))
.map(exam -> new Tuple<>(exam.getModelId(), exam.name))
.sorted(RESOURCE_COMPARATOR)
.collect(Collectors.toList());
}
public List<Tuple<String>> getExamResources() { public List<Tuple<String>> getExamResources() {
final UserInfo userInfo = this.currentUser.get(); final UserInfo userInfo = this.currentUser.get();
return this.restService.getBuilder(GetExamNames.class) return this.restService.getBuilder(GetExamNames.class)

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam;
import java.util.List;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.PageToListCallAdapter;
@Lazy
@Component
@GuiProfile
public class GetExams extends PageToListCallAdapter<Exam> {
public GetExams() {
super(
GetExamPage.class,
EntityType.EXAM,
new TypeReference<List<Exam>>() {
},
API.EXAM_ADMINISTRATION_ENDPOINT);
}
}

View file

@ -101,6 +101,17 @@ public class TableBuilder<ROW extends Entity> {
return this; return this;
} }
public TableBuilder<ROW> withDefaultActionIf(
final BooleanSupplier condition,
final Function<EntityTable<ROW>, PageAction> defaultActionFunction) {
if (condition.getAsBoolean()) {
return withDefaultAction(defaultActionFunction);
}
return this;
}
public TableBuilder<ROW> withDefaultAction(final Function<EntityTable<ROW>, PageAction> defaultActionFunction) { public TableBuilder<ROW> withDefaultAction(final Function<EntityTable<ROW>, PageAction> defaultActionFunction) {
this.defaultActionFunction = defaultActionFunction; this.defaultActionFunction = defaultActionFunction;
return this; return this;

View file

@ -177,14 +177,31 @@ public interface AuthorizationService {
* @param entityType the type of the entity to check the given privilege type on * @param entityType the type of the entity to check the given privilege type on
* @param institutionId the institution identifier for institutional privilege grant check */ * @param institutionId the institution identifier for institutional privilege grant check */
default void check(final PrivilegeType privilegeType, final EntityType entityType, final Long institutionId) { default void check(final PrivilegeType privilegeType, final EntityType entityType, final Long institutionId) {
if (!hasGrant(privilegeType, entityType, institutionId)) { // check institutional grant
throw new PermissionDeniedException( if (hasGrant(privilegeType, entityType, institutionId)) {
entityType, return;
privilegeType,
getUserService().getCurrentUser().getUserInfo());
} }
// if there is no institutional grant the user may have owner based grant on the specified realm
if (hasOwnerPrivilege(privilegeType, entityType, institutionId)) {
return;
}
throw new PermissionDeniedException(
entityType,
privilegeType,
getUserService().getCurrentUser().getUserInfo());
} }
/** Indicates if the current user has an owner privilege for this give entity type and institution
*
* @param privilegeType the privilege type to check
* @param entityType entityType the type of the entity to check ownership privilege
* @param institutionId the institution identifier for institutional privilege grant check
* @return true if the current user has owner privilege */
boolean hasOwnerPrivilege(final PrivilegeType privilegeType, final EntityType entityType, Long institutionId);
/** Check grant by using corresponding hasGrant(XY) method and throws PermissionDeniedException /** Check grant by using corresponding hasGrant(XY) method and throws PermissionDeniedException
* on deny or return the given grantEntity within a Result on successful grant. * on deny or return the given grantEntity within a Result on successful grant.
* This is useful to use with a Result based functional chain. * This is useful to use with a Result based functional chain.

View file

@ -111,7 +111,7 @@ public class AuthorizationServiceImpl implements AuthorizationService {
.andForRole(UserRole.EXAM_ADMIN) .andForRole(UserRole.EXAM_ADMIN)
.withInstitutionalPrivilege(PrivilegeType.WRITE) .withInstitutionalPrivilege(PrivilegeType.WRITE)
.andForRole(UserRole.EXAM_SUPPORTER) .andForRole(UserRole.EXAM_SUPPORTER)
.withInstitutionalPrivilege(PrivilegeType.READ) .withOwnerPrivilege(PrivilegeType.MODIFY)
.create(); .create();
// grants for configuration node // grants for configuration node
@ -181,7 +181,7 @@ public class AuthorizationServiceImpl implements AuthorizationService {
.andForRole(UserRole.EXAM_ADMIN) .andForRole(UserRole.EXAM_ADMIN)
.withInstitutionalPrivilege(PrivilegeType.READ) .withInstitutionalPrivilege(PrivilegeType.READ)
.andForRole(UserRole.EXAM_SUPPORTER) .andForRole(UserRole.EXAM_SUPPORTER)
.withInstitutionalPrivilege(PrivilegeType.MODIFY) .withOwnerPrivilege(PrivilegeType.READ)
.create(); .create();
// grants for user activity logs // grants for user activity logs
@ -217,6 +217,27 @@ public class AuthorizationServiceImpl implements AuthorizationService {
.isPresent(); .isPresent();
} }
@Override
public boolean hasOwnerPrivilege(
final PrivilegeType privilegeType,
final EntityType entityType,
final Long institutionId) {
final SEBServerUser currentUser = this.getUserService().getCurrentUser();
if (!currentUser.institutionId().equals(institutionId)) {
return false;
}
return currentUser.getUserRoles()
.stream()
.map(role -> new RoleTypeKey(entityType, role))
.map(key -> this.privileges.get(key))
.filter(priv -> (priv != null) && priv.hasOwnershipPrivilege(privilegeType))
.findFirst()
.isPresent();
}
private PrivilegeBuilder addPrivilege(final EntityType entityType) { private PrivilegeBuilder addPrivilege(final EntityType entityType) {
return new PrivilegeBuilder(entityType); return new PrivilegeBuilder(entityType);
} }

View file

@ -10,12 +10,24 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import java.util.Collection; import java.util.Collection;
import org.springframework.cache.annotation.CacheEvict;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.ExamSessionCacheService;
/** Concrete EntityDAO interface of Exam entities */ /** Concrete EntityDAO interface of Exam entities */
public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSupportDAO<Exam> { public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSupportDAO<Exam> {
Result<Collection<Long>> allIdsOfInstituion(Long institutionId); Result<Collection<Long>> allIdsOfInstituion(Long institutionId);
@Override
@CacheEvict(
cacheNames = ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM,
key = "#exam.id")
Result<Exam> save(Exam exam);
Result<Exam> byClientConnection(Long connectionId);
} }

View file

@ -38,6 +38,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ExamRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ExamRecord;
@ -54,13 +55,16 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
public class ExamDAOImpl implements ExamDAO { public class ExamDAOImpl implements ExamDAO {
private final ExamRecordMapper examRecordMapper; private final ExamRecordMapper examRecordMapper;
private final ClientConnectionRecordMapper clientConnectionRecordMapper;
private final LmsAPIService lmsAPIService; private final LmsAPIService lmsAPIService;
public ExamDAOImpl( public ExamDAOImpl(
final ExamRecordMapper examRecordMapper, final ExamRecordMapper examRecordMapper,
final ClientConnectionRecordMapper clientConnectionRecordMapper,
final LmsAPIService lmsAPIService) { final LmsAPIService lmsAPIService) {
this.examRecordMapper = examRecordMapper; this.examRecordMapper = examRecordMapper;
this.clientConnectionRecordMapper = clientConnectionRecordMapper;
this.lmsAPIService = lmsAPIService; this.lmsAPIService = lmsAPIService;
} }
@ -76,6 +80,17 @@ public class ExamDAOImpl implements ExamDAO {
.flatMap(this::toDomainModel); .flatMap(this::toDomainModel);
} }
@Override
@Transactional(readOnly = true)
public Result<Exam> byClientConnection(final Long connectionId) {
return Result.tryCatch(() -> {
return this.clientConnectionRecordMapper.selectByPrimaryKey(connectionId);
})
.flatMap(ccRecord -> recordById(ccRecord.getExamId()))
.flatMap(this::toDomainModel)
.onError(TransactionHandler::rollback);
}
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Collection<Exam>> all(final Long institutionId, final Boolean active) { public Result<Collection<Exam>> all(final Long institutionId, final Boolean active) {

View file

@ -195,8 +195,7 @@ public class LmsAPIServiceImpl implements LmsAPIService {
case MOCKUP: case MOCKUP:
return new MockupLmsAPITemplate( return new MockupLmsAPITemplate(
lmsSetup, lmsSetup,
credentials, credentials);
this.clientCredentialService);
case OPEN_EDX: case OPEN_EDX:
return new OpenEdxLmsAPITemplate( return new OpenEdxLmsAPITemplate(
this.asyncService, this.asyncService,

View file

@ -23,7 +23,6 @@ import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService;
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials; import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
@ -33,18 +32,15 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
private static final Logger log = LoggerFactory.getLogger(MockupLmsAPITemplate.class); private static final Logger log = LoggerFactory.getLogger(MockupLmsAPITemplate.class);
private final ClientCredentialService clientCredentialService;
private final LmsSetup lmsSetup; private final LmsSetup lmsSetup;
private final ClientCredentials credentials; private final ClientCredentials credentials;
private final Collection<QuizData> mockups; private final Collection<QuizData> mockups;
MockupLmsAPITemplate( MockupLmsAPITemplate(
final LmsSetup lmsSetup, final LmsSetup lmsSetup,
final ClientCredentials credentials, final ClientCredentials credentials) {
final ClientCredentialService clientCredentialService) {
this.lmsSetup = lmsSetup; this.lmsSetup = lmsSetup;
this.clientCredentialService = clientCredentialService;
this.credentials = credentials; this.credentials = credentials;
final Long lmsSetupId = lmsSetup.id; final Long lmsSetupId = lmsSetup.id;
@ -68,7 +64,7 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
"2018-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/")); "2018-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData( this.mockups.add(new QuizData(
"quiz6", institutionId, lmsSetupId, lmsType, "Demo Quiz 6", "Demo Quit Mockup", "quiz6", institutionId, lmsSetupId, lmsType, "Demo Quiz 6", "Demo Quit Mockup",
"2018-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/")); "2019-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/"));
this.mockups.add(new QuizData( this.mockups.add(new QuizData(
"quiz7", institutionId, lmsSetupId, lmsType, "Demo Quiz 7", "Demo Quit Mockup", "quiz7", institutionId, lmsSetupId, lmsType, "Demo Quiz 7", "Demo Quit Mockup",
"2018-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/")); "2018-01-01T09:00:00Z", "2021-01-01T09:00:00Z", "http://lms.mockup.com/api/"));

View file

@ -41,5 +41,4 @@ public class PingHandlingStrategyFactory {
return this.singleServerPingHandler; return this.singleServerPingHandler;
} }
} }
} }

View file

@ -23,19 +23,20 @@ import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType;
import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType; import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.GrantEntity;
import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent; import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; 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.ClientEventRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PermissionDeniedException;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService; import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService; import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
@ -45,7 +46,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe
@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.SEB_CLIENT_EVENT_ENDPOINT) @RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.SEB_CLIENT_EVENT_ENDPOINT)
public class ClientEventController extends ReadonlyEntityController<ClientEvent, ClientEvent> { public class ClientEventController extends ReadonlyEntityController<ClientEvent, ClientEvent> {
private final ClientConnectionDAO clientConnectionDAO; private final ExamDAO examDAO;
private final ClientEventDAO clientEventDAO; private final ClientEventDAO clientEventDAO;
protected ClientEventController( protected ClientEventController(
@ -55,7 +56,7 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
final UserActivityLogDAO userActivityLogDAO, final UserActivityLogDAO userActivityLogDAO,
final PaginationService paginationService, final PaginationService paginationService,
final BeanValidationService beanValidationService, final BeanValidationService beanValidationService,
final ClientConnectionDAO clientConnectionDAO) { final ExamDAO examDAO) {
super(authorization, super(authorization,
bulkActionService, bulkActionService,
@ -64,7 +65,7 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
paginationService, paginationService,
beanValidationService); beanValidationService);
this.clientConnectionDAO = clientConnectionDAO; this.examDAO = examDAO;
this.clientEventDAO = entityDAO; this.clientEventDAO = entityDAO;
} }
@ -83,7 +84,7 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
@RequestParam(name = Page.ATTR_SORT, required = false) final String sort, @RequestParam(name = Page.ATTR_SORT, required = false) final String sort,
@RequestParam final MultiValueMap<String, String> allRequestParams) { @RequestParam final MultiValueMap<String, String> allRequestParams) {
// at least current user must have read access for specified entity type within its own institution // at least current user must have base read access for specified entity type within its own institution
checkReadPrivilege(institutionId); checkReadPrivilege(institutionId);
final FilterMap filterMap = new FilterMap(allRequestParams); final FilterMap filterMap = new FilterMap(allRequestParams);
@ -113,34 +114,22 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
return ClientEventRecordDynamicSqlSupport.clientEventRecord; return ClientEventRecordDynamicSqlSupport.clientEventRecord;
} }
@Override
protected GrantEntity toGrantEntity(final ClientEvent entity) {
return this.examDAO
.byClientConnection(entity.connectionId)
.getOrThrow();
}
@Override @Override
protected void checkReadPrivilege(final Long institutionId) { protected void checkReadPrivilege(final Long institutionId) {
checkRead(institutionId); final SEBServerUser currentUser = this.authorization.getUserService().getCurrentUser();
} if (currentUser.institutionId().longValue() != institutionId.longValue()) {
throw new PermissionDeniedException(
@Override EntityType.CLIENT_EVENT,
protected Result<ClientEvent> checkReadAccess(final ClientEvent entity) { PrivilegeType.READ,
return Result.tryCatch(() -> { currentUser.getUserInfo());
}
final ClientConnection clientConnection = this.clientConnectionDAO
.byPK(entity.connectionId)
.getOrThrow();
this.authorization.checkRead(clientConnection);
return entity;
});
}
@Override
protected boolean hasReadAccess(final ClientEvent entity) {
return true;
}
private void checkRead(final Long institutionId) {
this.authorization.check(
PrivilegeType.READ,
EntityType.CLIENT_CONNECTION,
institutionId);
} }
} }

View file

@ -122,7 +122,7 @@ public class ExamAdministrationController extends ActivatableEntityController<Ex
final List<Exam> exams = new ArrayList<>( final List<Exam> exams = new ArrayList<>(
this.examDAO this.examDAO
.allMatching(new FilterMap(allRequestParams)) .allMatching(new FilterMap(allRequestParams), this::hasReadAccess)
.getOrThrow()); .getOrThrow());
return buildSortedExamPage( return buildSortedExamPage(

View file

@ -198,7 +198,9 @@ public class ExamMonitoringController {
if (exam == null) { if (exam == null) {
return false; return false;
} }
return exam.institutionId.equals(institution) && this.authorization.hasReadGrant(exam);
final String userId = this.authorization.getUserService().getCurrentUser().getUserInfo().uuid;
return exam.institutionId.equals(institution) && exam.isOwner(userId);
} }
} }

View file

@ -18,6 +18,7 @@ spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.platform=demo spring.datasource.platform=demo
# webservice configuration # webservice configuration
sebserver.webservice.distributed=false
sebserver.webservice.http.scheme=http sebserver.webservice.http.scheme=http
sebserver.webservice.http.server.name=ralph.ethz.ch sebserver.webservice.http.server.name=ralph.ethz.ch
sebserver.webservice.http.redirect.gui=${sebserver.gui.entrypoint} sebserver.webservice.http.redirect.gui=${sebserver.gui.entrypoint}

View file

@ -13,7 +13,7 @@ spring.datasource.platform=dev
spring.datasource.hikari.max-lifetime=600000 spring.datasource.hikari.max-lifetime=600000
# webservice configuration # webservice configuration
sebserver.webservice.distributed=true sebserver.webservice.distributed=false
sebserver.webservice.http.scheme=http sebserver.webservice.http.scheme=http
sebserver.webservice.http.server.name=${server.address} sebserver.webservice.http.server.name=${server.address}