diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index 6793bb4d..d409eb94 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -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"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java index e992ab36..27a1aeaa 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java @@ -164,6 +164,13 @@ public final class Result { 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. * diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInfo.java b/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInfo.java index 415217e7..776a9dfe 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInfo.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/WebserviceInfo.java @@ -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(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/AuthorizationService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/AuthorizationService.java index fdec0bff..68ad08bf 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/AuthorizationService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/AuthorizationService.java @@ -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); + } + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/AuthorizationServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/AuthorizationServiceImpl.java index b1a22ac6..c5792ec6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/AuthorizationServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/AuthorizationServiceImpl.java @@ -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 diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/DistributedServerPingHandler.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/DistributedServerPingHandler.java index d374239e..aca5fc7b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/DistributedServerPingHandler.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/DistributedServerPingHandler.java @@ -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); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java index b83851f1..0a124dfb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java @@ -92,8 +92,9 @@ public class ExamSessionServiceImpl implements ExamSessionService { public Result> 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); + } } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/PingHandlingStrategyFactory.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/PingHandlingStrategyFactory.java index 86b7ab89..b9aedf62 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/PingHandlingStrategyFactory.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/PingHandlingStrategyFactory.java @@ -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; + } } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientEventController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientEventController.java new file mode 100644 index 00000000..c9b1e1bf --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientEventController.java @@ -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 { + + 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 allRequestParams, final Long institutionId) { + throw new UnsupportedOperationException(); + } + + @Override + public ClientEvent savePut(@Valid final ClientEvent modifyData) { + throw new UnsupportedOperationException(); + } + + @Override + public Collection 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 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 checkModifyAccess(final ClientEvent entity) { + throw new PermissionDeniedException( + EntityType.CLIENT_EVENT, + PrivilegeType.MODIFY, + this.authorization.getUserService().getCurrentUser().uuid()); + } + + @Override + protected Result checkWriteAccess(final ClientEvent entity) { + throw new PermissionDeniedException( + EntityType.CLIENT_EVENT, + PrivilegeType.WRITE, + this.authorization.getUserService().getCurrentUser().uuid()); + } + + @Override + protected Result 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() { + + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/EntityController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/EntityController.java index adba27a0..e62191a6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/EntityController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/EntityController.java @@ -323,12 +323,6 @@ public abstract class EntityController { .getOrThrow(); } - protected void checkReadPrivilege() { - this.authorization.check( - PrivilegeType.READ, - getGrantEntityType()); - } - protected void checkReadPrivilege(final Long institutionId) { this.authorization.check( PrivilegeType.READ, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java index b5739b78..5fd8500c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java @@ -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 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 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(); + } + } diff --git a/src/main/resources/config/application-dev-ws.properties b/src/main/resources/config/application-dev-ws.properties index fd372390..10ca2005 100644 --- a/src/main/resources/config/application-dev-ws.properties +++ b/src/main/resources/config/application-dev-ws.properties @@ -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