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 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 EXAM_INDICATOR_ENDPOINT = "/indicator";
public static final String SEB_CLIENT_CONFIG_ENDPOINT = "/client_configuration"; 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_PARAM_EXAM_ID = "examId";
public static final String EXAM_API_SEB_CONNECTION_TOKEN = "SEBConnectionToken"; 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"; 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; 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, /** If a value is present, performs the given action with the value,
* otherwise performs the given empty-based action. * 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.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
@Lazy @Lazy
@ -39,6 +41,8 @@ public class WebserviceInfo {
private final String serverURLPrefix; private final String serverURLPrefix;
private final boolean isDistributed;
public WebserviceInfo(final Environment environment) { public WebserviceInfo(final Environment environment) {
this.httpScheme = environment.getRequiredProperty(WEB_SERVICE_HTTP_SCHEME_KEY); this.httpScheme = environment.getRequiredProperty(WEB_SERVICE_HTTP_SCHEME_KEY);
this.hostAddress = environment.getRequiredProperty(WEB_SERVICE_HOST_ADDRESS_KEY); this.hostAddress = environment.getRequiredProperty(WEB_SERVICE_HOST_ADDRESS_KEY);
@ -53,6 +57,10 @@ public class WebserviceInfo {
: this.hostAddress) : this.hostAddress)
.port(this.serverPort) .port(this.serverPort)
.toUriString(); .toUriString();
this.isDistributed = BooleanUtils.toBoolean(environment.getProperty(
"sebserver.webservice.distributed",
Constants.FALSE_STRING));
} }
public String getHttpScheme() { public String getHttpScheme() {
@ -113,6 +121,10 @@ public class WebserviceInfo {
return this.serverURLPrefix; return this.serverURLPrefix;
} }
public boolean isDistributed() {
return this.isDistributed;
}
@Override @Override
public String toString() { public String toString() {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();

View file

@ -230,4 +230,26 @@ public interface AuthorizationService {
return check(PrivilegeType.WRITE, grantEntity); 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) .withInstitutionalPrivilege(PrivilegeType.READ)
.create(); .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 // TODO other entities
// grants for user activity logs // grants for user activity logs

View file

@ -66,6 +66,11 @@ public class DistributedServerPingHandler implements PingHandlingStrategy {
@Override @Override
public void initForConnection(final Long connectionId, final String connectionToken) { 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(); final ClientEventRecord clientEventRecord = new ClientEventRecord();
clientEventRecord.setConnectionId(connectionId); clientEventRecord.setConnectionId(connectionId);
clientEventRecord.setType(EventType.LAST_PING.id); 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) { public Result<Collection<Exam>> getRunningExamsForInstitution(final Long institutionId) {
return this.examDAO.allIdsOfInstituion(institutionId) return this.examDAO.allIdsOfInstituion(institutionId)
.map(col -> col.stream() .map(col -> col.stream()
.map(examId -> this.examSessionCacheService.getRunningExam(examId)) .map(this::getRunningExam)
.filter(exam -> exam != null) .filter(Result::hasValue)
.map(Result::get)
.collect(Collectors.toList())); .collect(Collectors.toList()));
} }
@ -186,17 +187,21 @@ public class ExamSessionServiceImpl implements ExamSessionService {
} }
private void flushCache(final Exam exam) { private void flushCache(final Exam exam) {
this.examSessionCacheService.evict(exam); try {
this.examSessionCacheService.evictDefaultSebConfig(exam.id); this.examSessionCacheService.evict(exam);
this.clientConnectionDAO this.examSessionCacheService.evictDefaultSebConfig(exam.id);
.getConnectionTokens(exam.id) this.clientConnectionDAO
.getOrElse(() -> Collections.emptyList()) .getConnectionTokens(exam.id)
.forEach(token -> { .getOrElse(() -> Collections.emptyList())
// evict client connection .forEach(token -> {
this.examSessionCacheService.evictClientConnection(token); // evict client connection
// evict also cached ping record this.examSessionCacheService.evictClientConnection(token);
this.examSessionCacheService.evictPingRecord(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 org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.PingHandlingStrategy; import ch.ethz.seb.sebserver.webservice.servicelayer.session.PingHandlingStrategy;
@Lazy @Lazy
@ -21,19 +22,24 @@ public class PingHandlingStrategyFactory {
private final SingleServerPingHandler singleServerPingHandler; private final SingleServerPingHandler singleServerPingHandler;
private final DistributedServerPingHandler distributedServerPingHandler; private final DistributedServerPingHandler distributedServerPingHandler;
private final WebserviceInfo webserviceInfo;
protected PingHandlingStrategyFactory( protected PingHandlingStrategyFactory(
final SingleServerPingHandler singleServerPingHandler, final SingleServerPingHandler singleServerPingHandler,
final DistributedServerPingHandler distributedServerPingHandler) { final DistributedServerPingHandler distributedServerPingHandler,
final WebserviceInfo webserviceInfo) {
this.singleServerPingHandler = singleServerPingHandler; this.singleServerPingHandler = singleServerPingHandler;
this.distributedServerPingHandler = distributedServerPingHandler; this.distributedServerPingHandler = distributedServerPingHandler;
this.webserviceInfo = webserviceInfo;
} }
public PingHandlingStrategy get() { public PingHandlingStrategy get() {
// NOTE not returns always DistributedServerPingHandler for testing if (this.webserviceInfo.isDistributed()) {
// TODO: serve in case of distribution or single state return this.distributedServerPingHandler;
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(); .getOrThrow();
} }
protected void checkReadPrivilege() {
this.authorization.check(
PrivilegeType.READ,
getGrantEntityType());
}
protected void checkReadPrivilege(final Long institutionId) { protected void checkReadPrivilege(final Long institutionId) {
this.authorization.check( this.authorization.check(
PrivilegeType.READ, 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.gbl.util.Utils;
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.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@ -70,10 +68,6 @@ public class ExamMonitoringController {
.addUsersInstitutionDefaultPropertySupport(binder); .addUsersInstitutionDefaultPropertySupport(binder);
} }
// ******************
// * GET (getAll)
// ******************
/** Get a page of all currently running exams /** Get a page of all currently running exams
* *
* GET /{api}/{entity-type-endpoint-name} * GET /{api}/{entity-type-endpoint-name}
@ -105,17 +99,10 @@ public class ExamMonitoringController {
@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) {
// check if user has EXAM_SUPPORTER privilege. this.authorization.checkRole(
final SEBServerUser currentUser = this.authorization UserRole.EXAM_SUPPORTER,
.getUserService() institutionId,
.getCurrentUser(); EntityType.EXAM);
if (!currentUser.getUserRoles().contains(UserRole.EXAM_SUPPORTER)) {
throw new PermissionDeniedException(
EntityType.EXAM,
PrivilegeType.READ,
currentUser.getUserInfo().uuid);
}
final FilterMap filterMap = new FilterMap(allRequestParams); final FilterMap filterMap = new FilterMap(allRequestParams);
@ -142,23 +129,43 @@ public class ExamMonitoringController {
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE) produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Collection<ClientConnectionData> getConnectionData( 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) { @PathVariable(name = API.PARAM_MODEL_ID, required = true) final Long examId) {
// check if user has EXAM_SUPPORTER privilege. this.authorization.checkRole(
final SEBServerUser currentUser = this.authorization UserRole.EXAM_SUPPORTER,
.getUserService() institutionId,
.getCurrentUser(); EntityType.EXAM);
if (!currentUser.getUserRoles().contains(UserRole.EXAM_SUPPORTER)) {
throw new PermissionDeniedException(
EntityType.EXAM,
PrivilegeType.READ,
currentUser.getUserInfo().uuid);
}
return this.examSessionService return this.examSessionService
.getConnectionData(examId) .getConnectionData(examId)
.getOrThrow(); .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 spring.datasource.hikari.max-lifetime=600000
# webservice configuration # webservice configuration
sebserver.webservice.distributed=true
sebserver.webservice.http.scheme=http sebserver.webservice.http.scheme=http
sebserver.webservice.http.server.name=${server.address} sebserver.webservice.http.server.name=${server.address}
sebserver.webservice.http.redirect.gui=/gui sebserver.webservice.http.redirect.gui=/gui