SEBSERV-62 code cleanup, ClientConnectionController

This commit is contained in:
anhefti 2019-07-22 10:36:23 +02:00
parent 7e453edb00
commit ba49611c1c
12 changed files with 278 additions and 51 deletions

View file

@ -61,6 +61,8 @@ public final class API {
public static final String EXAM_MONITORING_ENDPOINT = "/monitoring";
public static final String SEB_CLIENT_EVENT_ENDPOINT = "/seb-client-event";
public static final String EXAM_INDICATOR_ENDPOINT = "/indicator";
public static final String SEB_CLIENT_CONFIG_ENDPOINT = "/client_configuration";
@ -108,6 +110,7 @@ public final class API {
public static final String EXAM_API_PARAM_EXAM_ID = "examId";
public static final String EXAM_API_SEB_CONNECTION_TOKEN = "SEBConnectionToken";
public static final String EXAM_API_SEB_CONNECTION_TOKEN_PATH = "/{" + EXAM_API_SEB_CONNECTION_TOKEN + "}";
public static final String EXAM_API_USER_SESSION_ID = "seb_user_session_id";

View file

@ -164,6 +164,13 @@ public final class Result<T> {
return this.error != null;
}
/** Indicates whether this Result refers to a value or not.
*
* @return true if this Result refers to a value (not null) and has no error */
public boolean hasValue() {
return this.value != null && this.error == null;
}
/** If a value is present, performs the given action with the value,
* otherwise performs the given empty-based action.
*

View file

@ -11,12 +11,14 @@ package ch.ethz.seb.sebserver.webservice;
import java.net.InetAddress;
import java.net.UnknownHostException;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@Lazy
@ -39,6 +41,8 @@ public class WebserviceInfo {
private final String serverURLPrefix;
private final boolean isDistributed;
public WebserviceInfo(final Environment environment) {
this.httpScheme = environment.getRequiredProperty(WEB_SERVICE_HTTP_SCHEME_KEY);
this.hostAddress = environment.getRequiredProperty(WEB_SERVICE_HOST_ADDRESS_KEY);
@ -53,6 +57,10 @@ public class WebserviceInfo {
: this.hostAddress)
.port(this.serverPort)
.toUriString();
this.isDistributed = BooleanUtils.toBoolean(environment.getProperty(
"sebserver.webservice.distributed",
Constants.FALSE_STRING));
}
public String getHttpScheme() {
@ -113,6 +121,10 @@ public class WebserviceInfo {
return this.serverURLPrefix;
}
public boolean isDistributed() {
return this.isDistributed;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();

View file

@ -230,4 +230,26 @@ public interface AuthorizationService {
return check(PrivilegeType.WRITE, grantEntity);
}
/** Checks if the current user has a specified role.
* If not a PermissionDeniedException is thrown for the given EntityType
*
* @param role The UserRole to check
* @param institution the institution identifier
* @param type EntityType for PermissionDeniedException
* @throws PermissionDeniedException if current user don't have the specified UserRole */
default void checkRole(final UserRole role, final Long institution, final EntityType type) {
final SEBServerUser currentUser = this
.getUserService()
.getCurrentUser();
if (!currentUser.institutionId().equals(institution) ||
!currentUser.getUserRoles().contains(role)) {
throw new PermissionDeniedException(
type,
PrivilegeType.READ,
currentUser.getUserInfo().uuid);
}
}
}

View file

@ -172,6 +172,18 @@ public class AuthorizationServiceImpl implements AuthorizationService {
.withInstitutionalPrivilege(PrivilegeType.READ)
.create();
// grants for SEB client connections
addPrivilege(EntityType.SEB_CLIENT_CONFIGURATION)
.forRole(UserRole.SEB_SERVER_ADMIN)
.withBasePrivilege(PrivilegeType.READ)
.forRole(UserRole.INSTITUTIONAL_ADMIN)
.withInstitutionalPrivilege(PrivilegeType.READ)
.andForRole(UserRole.EXAM_ADMIN)
.withInstitutionalPrivilege(PrivilegeType.READ)
.andForRole(UserRole.EXAM_SUPPORTER)
.withInstitutionalPrivilege(PrivilegeType.MODIFY)
.create();
// TODO other entities
// grants for user activity logs

View file

@ -66,6 +66,11 @@ public class DistributedServerPingHandler implements PingHandlingStrategy {
@Override
public void initForConnection(final Long connectionId, final String connectionToken) {
if (log.isDebugEnabled()) {
log.debug("Intitalize distributed ping handler for connection: {}", connectionId);
}
final ClientEventRecord clientEventRecord = new ClientEventRecord();
clientEventRecord.setConnectionId(connectionId);
clientEventRecord.setType(EventType.LAST_PING.id);

View file

@ -92,8 +92,9 @@ public class ExamSessionServiceImpl implements ExamSessionService {
public Result<Collection<Exam>> getRunningExamsForInstitution(final Long institutionId) {
return this.examDAO.allIdsOfInstituion(institutionId)
.map(col -> col.stream()
.map(examId -> this.examSessionCacheService.getRunningExam(examId))
.filter(exam -> exam != null)
.map(this::getRunningExam)
.filter(Result::hasValue)
.map(Result::get)
.collect(Collectors.toList()));
}
@ -186,17 +187,21 @@ public class ExamSessionServiceImpl implements ExamSessionService {
}
private void flushCache(final Exam exam) {
this.examSessionCacheService.evict(exam);
this.examSessionCacheService.evictDefaultSebConfig(exam.id);
this.clientConnectionDAO
.getConnectionTokens(exam.id)
.getOrElse(() -> Collections.emptyList())
.forEach(token -> {
// evict client connection
this.examSessionCacheService.evictClientConnection(token);
// evict also cached ping record
this.examSessionCacheService.evictPingRecord(token);
});
try {
this.examSessionCacheService.evict(exam);
this.examSessionCacheService.evictDefaultSebConfig(exam.id);
this.clientConnectionDAO
.getConnectionTokens(exam.id)
.getOrElse(() -> Collections.emptyList())
.forEach(token -> {
// evict client connection
this.examSessionCacheService.evictClientConnection(token);
// evict also cached ping record
this.examSessionCacheService.evictPingRecord(token);
});
} catch (Exception e) {
log.error("Unexpected error while trying to flush cache for exam: ", exam, e);
}
}
}

View file

@ -12,6 +12,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.PingHandlingStrategy;
@Lazy
@ -21,19 +22,24 @@ public class PingHandlingStrategyFactory {
private final SingleServerPingHandler singleServerPingHandler;
private final DistributedServerPingHandler distributedServerPingHandler;
private final WebserviceInfo webserviceInfo;
protected PingHandlingStrategyFactory(
final SingleServerPingHandler singleServerPingHandler,
final DistributedServerPingHandler distributedServerPingHandler) {
final DistributedServerPingHandler distributedServerPingHandler,
final WebserviceInfo webserviceInfo) {
this.singleServerPingHandler = singleServerPingHandler;
this.distributedServerPingHandler = distributedServerPingHandler;
this.webserviceInfo = webserviceInfo;
}
public PingHandlingStrategy get() {
// NOTE not returns always DistributedServerPingHandler for testing
// TODO: serve in case of distribution or single state
return this.distributedServerPingHandler;
if (this.webserviceInfo.isDistributed()) {
return this.distributedServerPingHandler;
} else {
return this.singleServerPingHandler;
}
}
}

View file

@ -0,0 +1,153 @@
/*
* 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.webservice.weblayer.api;
import java.util.Collection;
import javax.validation.Valid;
import org.mybatis.dynamic.sql.SqlTable;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.API.BulkActionType;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
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.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.ClientEventRecordDynamicSqlSupport;
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.PermissionDeniedException;
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.UserActivityLogDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
@WebServiceProfile
@RestController
@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.SEB_CLIENT_EVENT_ENDPOINT)
public class ClientEventController extends EntityController<ClientEvent, ClientEvent> {
private final ClientConnectionDAO clientConnectionDAO;
protected ClientEventController(
final AuthorizationService authorization,
final BulkActionService bulkActionService,
final ClientEventDAO entityDAO,
final UserActivityLogDAO userActivityLogDAO,
final PaginationService paginationService,
final BeanValidationService beanValidationService,
final ClientConnectionDAO clientConnectionDAO) {
super(authorization,
bulkActionService,
entityDAO,
userActivityLogDAO,
paginationService,
beanValidationService);
this.clientConnectionDAO = clientConnectionDAO;
}
@Override
public ClientEvent create(final MultiValueMap<String, String> allRequestParams, final Long institutionId) {
throw new UnsupportedOperationException();
}
@Override
public ClientEvent savePut(@Valid final ClientEvent modifyData) {
throw new UnsupportedOperationException();
}
@Override
public Collection<EntityKey> getDependencies(final String modelId, final BulkActionType bulkActionType) {
throw new UnsupportedOperationException();
}
@Override
protected ClientEvent createNew(final POSTMapper postParams) {
throw new UnsupportedOperationException();
}
@Override
protected SqlTable getSQLTableOfEntity() {
return ClientEventRecordDynamicSqlSupport.clientEventRecord;
}
@Override
protected void checkReadPrivilege(final Long institutionId) {
checkRead(institutionId);
}
@Override
protected Result<ClientEvent> checkReadAccess(final ClientEvent entity) {
return Result.tryCatch(() -> {
final ClientConnection clientConnection = this.clientConnectionDAO
.byPK(entity.connectionId)
.getOrThrow();
checkRead(clientConnection.institutionId);
return entity;
});
}
@Override
protected void checkModifyPrivilege(final Long institutionId) {
throw new PermissionDeniedException(
EntityType.CLIENT_EVENT,
PrivilegeType.MODIFY,
this.authorization.getUserService().getCurrentUser().uuid());
}
@Override
protected Result<ClientEvent> checkModifyAccess(final ClientEvent entity) {
throw new PermissionDeniedException(
EntityType.CLIENT_EVENT,
PrivilegeType.MODIFY,
this.authorization.getUserService().getCurrentUser().uuid());
}
@Override
protected Result<ClientEvent> checkWriteAccess(final ClientEvent entity) {
throw new PermissionDeniedException(
EntityType.CLIENT_EVENT,
PrivilegeType.WRITE,
this.authorization.getUserService().getCurrentUser().uuid());
}
@Override
protected Result<ClientEvent> checkCreateAccess(final ClientEvent entity) {
throw new PermissionDeniedException(
EntityType.CLIENT_EVENT,
PrivilegeType.WRITE,
this.authorization.getUserService().getCurrentUser().uuid());
}
private void checkRead(final Long institution) {
this.authorization.checkRole(
UserRole.EXAM_SUPPORTER,
institution,
EntityType.CLIENT_EVENT);
}
private void noModifyAccess() {
}
}

View file

@ -323,12 +323,6 @@ public abstract class EntityController<T extends Entity, M extends Entity> {
.getOrThrow();
}
protected void checkReadPrivilege() {
this.authorization.check(
PrivilegeType.READ,
getGrantEntityType());
}
protected void checkReadPrivilege(final Long institutionId) {
this.authorization.check(
PrivilegeType.READ,

View file

@ -33,9 +33,7 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
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.PermissionDeniedException;
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.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@ -70,10 +68,6 @@ public class ExamMonitoringController {
.addUsersInstitutionDefaultPropertySupport(binder);
}
// ******************
// * GET (getAll)
// ******************
/** Get a page of all currently running exams
*
* GET /{api}/{entity-type-endpoint-name}
@ -105,17 +99,10 @@ public class ExamMonitoringController {
@RequestParam(name = Page.ATTR_SORT, required = false) final String sort,
@RequestParam final MultiValueMap<String, String> allRequestParams) {
// check if user has EXAM_SUPPORTER privilege.
final SEBServerUser currentUser = this.authorization
.getUserService()
.getCurrentUser();
if (!currentUser.getUserRoles().contains(UserRole.EXAM_SUPPORTER)) {
throw new PermissionDeniedException(
EntityType.EXAM,
PrivilegeType.READ,
currentUser.getUserInfo().uuid);
}
this.authorization.checkRole(
UserRole.EXAM_SUPPORTER,
institutionId,
EntityType.EXAM);
final FilterMap filterMap = new FilterMap(allRequestParams);
@ -142,23 +129,43 @@ public class ExamMonitoringController {
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Collection<ClientConnectionData> getConnectionData(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long examId) {
// check if user has EXAM_SUPPORTER privilege.
final SEBServerUser currentUser = this.authorization
.getUserService()
.getCurrentUser();
if (!currentUser.getUserRoles().contains(UserRole.EXAM_SUPPORTER)) {
throw new PermissionDeniedException(
EntityType.EXAM,
PrivilegeType.READ,
currentUser.getUserInfo().uuid);
}
this.authorization.checkRole(
UserRole.EXAM_SUPPORTER,
institutionId,
EntityType.EXAM);
return this.examSessionService
.getConnectionData(examId)
.getOrThrow();
}
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT + API.EXAM_API_SEB_CONNECTION_TOKEN_PATH,
method = RequestMethod.GET,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ClientConnectionData getConnectionDataForSingleConnection(
@RequestParam(
name = API.PARAM_INSTITUTION_ID,
required = true,
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
@PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long examId,
@PathVariable(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken) {
this.authorization.checkRole(
UserRole.EXAM_SUPPORTER,
institutionId,
EntityType.EXAM);
return this.examSessionService
.getConnectionData(connectionToken)
.getOrThrow();
}
}

View file

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