From 908665ddcc03a23c0a6716c5bb13b0509381e5fa Mon Sep 17 00:00:00 2001 From: anhefti Date: Thu, 30 May 2024 16:33:58 +0200 Subject: [PATCH] SEBSERV-418 finished implementation for teacher account and login --- .../ch/ethz/seb/sebserver/gbl/Constants.java | 2 + .../gbl/model/user/TokenLoginInfo.java | 19 +- .../sebserver/gbl/model/user/UserRole.java | 2 +- .../sebserver/gui/GuiWebsecurityConfig.java | 1 + ...InstitutionalAuthenticationEntryPoint.java | 19 +- .../gui/content/activity/ActivitiesPane.java | 172 +++++++++--------- .../content/monitoring/FinishedExamList.java | 5 +- .../MonitoringClientConnection.java | 2 +- .../monitoring/MonitoringRunningExam.java | 2 +- .../monitoring/MonitoringRunningExamList.java | 5 +- .../OAuth2AuthorizationContextHolder.java | 43 +++-- .../webservice/auth/WebserviceURIService.java | 7 + .../authorization/TeacherAccountService.java | 52 +++++- .../impl/AuthorizationServiceImpl.java | 3 + .../impl/TeacherAccountServiceImpl.java | 66 +++---- .../webservice/servicelayer/dao/ExamDAO.java | 2 +- .../impl/FullLmsIntegrationServiceImpl.java | 2 +- .../moodle/MoodleRestTemplateFactoryImpl.java | 2 +- .../weblayer/api/APIExceptionHandler.java | 12 +- .../weblayer/api/AdminJWTAccess.java | 56 ++++++ .../api/ClientConnectionController.java | 3 +- .../weblayer/api/ClientEventController.java | 4 +- .../api/ExamMonitoringController.java | 6 +- .../api/LmsIntegrationController.java | 9 +- .../weblayer/api/RegisterUserController.java | 2 +- .../api/SEBClientConfigController.java | 11 +- .../oauth/AuthorizationServerConfig.java | 5 + 27 files changed, 336 insertions(+), 178 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/AdminJWTAccess.java diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java b/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java index de9dc9a8..e5d9b901 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java @@ -138,6 +138,8 @@ public final class Constants { public static final String XML_PLIST_INTEGER = "integer"; public static final String XML_PLIST_REAL = "real"; + public static final String OAUTH2_GRANT_TYPE = "grant_type"; + public static final String OAUTH2_USER_NAME = "username"; public static final String OAUTH2_GRANT_TYPE_PASSWORD = "password"; public static final String OAUTH2_CLIENT_SECRET = "client_secret"; public static final String OAUTH2_GRANT_TYPE_REFRESH_TOKEN = "refresh_token"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/TokenLoginInfo.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/TokenLoginInfo.java index 1be0526f..0701470a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/TokenLoginInfo.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/TokenLoginInfo.java @@ -8,26 +8,27 @@ package ch.ethz.seb.sebserver.gbl.model.user; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import org.springframework.security.oauth2.common.OAuth2AccessToken; public class TokenLoginInfo { - @JsonProperty + @JsonProperty("username") public final String username; - @JsonProperty + @JsonProperty("userUUID") public final String userUUID; - @JsonProperty - public final String redirect; - @JsonProperty + @JsonProperty("redirect") + public final EntityKey redirect; + @JsonProperty("login") public final OAuth2AccessToken login; @JsonCreator public TokenLoginInfo( - @JsonProperty final String username, - @JsonProperty final String userUUID, - @JsonProperty final String redirect, - @JsonProperty final OAuth2AccessToken login) { + @JsonProperty("username") final String username, + @JsonProperty("userUUID") final String userUUID, + @JsonProperty("redirect") final EntityKey redirect, + @JsonProperty("login") final OAuth2AccessToken login) { this.username = username; this.userUUID = userUUID; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserRole.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserRole.java index 99b6ccbf..2ddc44d7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserRole.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserRole.java @@ -50,7 +50,7 @@ public enum UserRole implements Entity, GrantedAuthority { public static List publicRolesForUser(final UserInfo user) { final EnumSet roles = user.getUserRoles(); if (roles.contains(SEB_SERVER_ADMIN)) { - return Arrays.asList(SEB_SERVER_ADMIN, INSTITUTIONAL_ADMIN, EXAM_ADMIN, EXAM_SUPPORTER); + return Arrays.asList(SEB_SERVER_ADMIN, INSTITUTIONAL_ADMIN, EXAM_ADMIN, EXAM_SUPPORTER, TEACHER); } else if (roles.contains(INSTITUTIONAL_ADMIN)) { return Arrays.asList(INSTITUTIONAL_ADMIN, EXAM_ADMIN, EXAM_SUPPORTER); } else if (roles.contains(EXAM_ADMIN)) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/GuiWebsecurityConfig.java b/src/main/java/ch/ethz/seb/sebserver/gui/GuiWebsecurityConfig.java index 7b36682c..1a441a70 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/GuiWebsecurityConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/GuiWebsecurityConfig.java @@ -91,6 +91,7 @@ public class GuiWebsecurityConfig extends WebSecurityConfigurerAdapter { .antMatchers(adminAPIEndpoint + API.INFO_ENDPOINT + API.LOGO_PATH_SEGMENT + "/**").permitAll() .antMatchers(adminAPIEndpoint + API.INFO_ENDPOINT + API.INFO_INST_PATH_SEGMENT + "/**").permitAll() .antMatchers(adminAPIEndpoint + API.REGISTER_ENDPOINT).permitAll() + .antMatchers(API.OAUTH_JWT_TOKEN_ENDPOINT + "/**").permitAll() .and() .antMatcher("/**") .authorizeRequests() diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/InstitutionalAuthenticationEntryPoint.java b/src/main/java/ch/ethz/seb/sebserver/gui/InstitutionalAuthenticationEntryPoint.java index 3f808d97..69f2feab 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/InstitutionalAuthenticationEntryPoint.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/InstitutionalAuthenticationEntryPoint.java @@ -21,6 +21,8 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext; import org.apache.commons.codec.binary.Base64InputStream; import org.apache.commons.lang3.StringUtils; import org.eclipse.rap.rwt.RWT; @@ -49,6 +51,8 @@ import ch.ethz.seb.sebserver.gbl.model.EntityName; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.WebserviceURIService; import ch.ethz.seb.sebserver.gui.widget.ImageUploadSelection; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; @Lazy @Component @@ -116,6 +120,20 @@ public final class InstitutionalAuthenticationEntryPoint implements Authenticati final HttpServletResponse response, final AuthenticationException authException) throws IOException, ServletException { + final String jwt = request.getParameter("jwt"); + if (StringUtils.isNotBlank(jwt)) { + final WebApplicationContext webApplicationContext = WebApplicationContextUtils + .getRequiredWebApplicationContext(request.getServletContext()); + final AuthorizationContextHolder authorizationContextHolder = webApplicationContext + .getBean(AuthorizationContextHolder.class); + final SEBServerAuthorizationContext authorizationContext = authorizationContextHolder + .getAuthorizationContext(request.getSession()); + if (authorizationContext.autoLogin(jwt)) { + forwardToEntryPoint(request, response, this.guiEntryPoint, true); + return; + } + } + final String institutionalEndpoint = extractInstitutionalEndpoint(request); if (StringUtils.isNotBlank(institutionalEndpoint)) { @@ -168,7 +186,6 @@ public final class InstitutionalAuthenticationEntryPoint implements Authenticati request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE); response.setStatus(HttpStatus.UNAUTHORIZED.value()); forwardToEntryPoint(request, response, this.guiEntryPoint, institutionalEndpoint == null); - } private void forwardToEntryPoint( diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/ActivitiesPane.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/ActivitiesPane.java index 57d617f9..d19f3ebc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/ActivitiesPane.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/ActivitiesPane.java @@ -93,103 +93,105 @@ public class ActivitiesPane implements TemplateComposer { final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(pageContext); + final boolean isTeacherOnly = this.currentUser.get().hasAnyRole(UserRole.TEACHER) && + !this.currentUser.get().hasAnyRole(UserRole.EXAM_SUPPORTER) && + !this.currentUser.get().hasAnyRole(UserRole.EXAM_ADMIN) ; + //-------------------------------------------------------------------------------------- // ---- SEB ADMIN ---------------------------------------------------------------------- final boolean isServerOrInstAdmin = this.currentUser.get() .hasAnyRole(UserRole.SEB_SERVER_ADMIN, UserRole.INSTITUTIONAL_ADMIN); - // SEB Server Administration - final TreeItem sebAdmin = this.widgetFactory.treeItemLocalized( - navigation, - ActivityDefinition.SEB_ADMINISTRATION.displayName); + if (isServerOrInstAdmin) { + // SEB Server Administration + final TreeItem sebAdmin = this.widgetFactory.treeItemLocalized( + navigation, + ActivityDefinition.SEB_ADMINISTRATION.displayName); - // Institution + // Institution - if (currentUser.isFeatureEnabled(UserFeatures.Feature.ADMIN_INSTITUTION)) { - // If current user has SEB Server Admin role, show the Institution list - if (userInfo.hasRole(UserRole.SEB_SERVER_ADMIN)) { - // institutions (list) as root - final TreeItem institutions = this.widgetFactory.treeItemLocalized( + if (currentUser.isFeatureEnabled(UserFeatures.Feature.ADMIN_INSTITUTION)) { + // If current user has SEB Server Admin role, show the Institution list + if (userInfo.hasRole(UserRole.SEB_SERVER_ADMIN)) { + // institutions (list) as root + final TreeItem institutions = this.widgetFactory.treeItemLocalized( + sebAdmin, + ActivityDefinition.INSTITUTION.displayName); + injectActivitySelection( + institutions, + actionBuilder + .newAction(ActionDefinition.INSTITUTION_VIEW_LIST) + .create()); + + } else if (userInfo.hasRole(UserRole.INSTITUTIONAL_ADMIN)) { + // otherwise show the form of the institution for current user + final TreeItem institutions = this.widgetFactory.treeItemLocalized( + sebAdmin, + ActivityDefinition.INSTITUTION.displayName); + injectActivitySelection( + institutions, + actionBuilder.newAction(ActionDefinition.INSTITUTION_VIEW_FORM) + .withEntityKey(userInfo.institutionId, EntityType.INSTITUTION) + .withAttribute(AttributeKeys.READ_ONLY, "true") + .create()); + } + } + + // User Account + // if current user has role seb-server admin or institutional-admin, show list + if (!pageService.isLightSetup() && currentUser.isFeatureEnabled(UserFeatures.Feature.ADMIN_USER_ADMINISTRATION)) { + + final TreeItem userAccounts = this.widgetFactory.treeItemLocalized( sebAdmin, - ActivityDefinition.INSTITUTION.displayName); + ActivityDefinition.USER_ACCOUNT.displayName); injectActivitySelection( - institutions, + userAccounts, actionBuilder - .newAction(ActionDefinition.INSTITUTION_VIEW_LIST) + .newAction(ActionDefinition.USER_ACCOUNT_VIEW_LIST) .create()); - - } else if (userInfo.hasRole(UserRole.INSTITUTIONAL_ADMIN)) { - // otherwise show the form of the institution for current user - final TreeItem institutions = this.widgetFactory.treeItemLocalized( + } else if (currentUser.isFeatureEnabled(UserFeatures.Feature.ADMIN_USER_ACCOUNT)) { + // otherwise show the user account form for current user + final TreeItem userAccounts = pageService.isLightSetup() || !currentUser.isFeatureEnabled(UserFeatures.Feature.ADMIN_USER_ADMINISTRATION) + ? this.widgetFactory.treeItemLocalized( sebAdmin, - ActivityDefinition.INSTITUTION.displayName); + ActivityDefinition.USER_ACCOUNT.displayName) + : this.widgetFactory.treeItemLocalized( + navigation, + ActivityDefinition.USER_ACCOUNT.displayName); injectActivitySelection( - institutions, - actionBuilder.newAction(ActionDefinition.INSTITUTION_VIEW_FORM) - .withEntityKey(userInfo.institutionId, EntityType.INSTITUTION) + userAccounts, + actionBuilder.newAction(ActionDefinition.USER_ACCOUNT_VIEW_FORM) + .withEntityKey(this.currentUser.get().getEntityKey()) .withAttribute(AttributeKeys.READ_ONLY, "true") .create()); } + + // User Activity Logs + final boolean viewUserActivityLogs = this.currentUser.hasInstitutionalPrivilege( + PrivilegeType.READ, + EntityType.USER_ACTIVITY_LOG) + && currentUser.isFeatureEnabled(UserFeatures.Feature.ADMIN_AUDIT_LOGS); + if (viewUserActivityLogs) { + final TreeItem activityLogs = this.widgetFactory.treeItemLocalized( + sebAdmin, + ActivityDefinition.USER_ACTIVITY_LOGS.displayName); + injectActivitySelection( + activityLogs, + actionBuilder + .newAction(ActionDefinition.LOGS_USER_ACTIVITY_LIST) + .create()); + } + + if (sebAdmin.getItemCount() > 0) { + sebAdmin.setExpanded(this.currentUser.get().hasAnyRole( + UserRole.SEB_SERVER_ADMIN, + UserRole.INSTITUTIONAL_ADMIN)); + } else { + sebAdmin.dispose(); + } } - // User Account - // if current user has role seb-server admin or institutional-admin, show list - if (isServerOrInstAdmin - && !pageService.isLightSetup() - && currentUser.isFeatureEnabled(UserFeatures.Feature.ADMIN_USER_ADMINISTRATION)) { - - final TreeItem userAccounts = this.widgetFactory.treeItemLocalized( - sebAdmin, - ActivityDefinition.USER_ACCOUNT.displayName); - injectActivitySelection( - userAccounts, - actionBuilder - .newAction(ActionDefinition.USER_ACCOUNT_VIEW_LIST) - .create()); - } else if (currentUser.isFeatureEnabled(UserFeatures.Feature.ADMIN_USER_ACCOUNT)) { - // otherwise show the user account form for current user - final TreeItem userAccounts = pageService.isLightSetup() || !currentUser.isFeatureEnabled(UserFeatures.Feature.ADMIN_USER_ADMINISTRATION) - ? this.widgetFactory.treeItemLocalized( - sebAdmin, - ActivityDefinition.USER_ACCOUNT.displayName) - : this.widgetFactory.treeItemLocalized( - navigation, - ActivityDefinition.USER_ACCOUNT.displayName); - injectActivitySelection( - userAccounts, - actionBuilder.newAction(ActionDefinition.USER_ACCOUNT_VIEW_FORM) - .withEntityKey(this.currentUser.get().getEntityKey()) - .withAttribute(AttributeKeys.READ_ONLY, "true") - .create()); - } - - // User Activity Logs - final boolean viewUserActivityLogs = this.currentUser.hasInstitutionalPrivilege( - PrivilegeType.READ, - EntityType.USER_ACTIVITY_LOG) - && currentUser.isFeatureEnabled(UserFeatures.Feature.ADMIN_AUDIT_LOGS); - if (viewUserActivityLogs) { - final TreeItem activityLogs = this.widgetFactory.treeItemLocalized( - sebAdmin, - ActivityDefinition.USER_ACTIVITY_LOGS.displayName); - injectActivitySelection( - activityLogs, - actionBuilder - .newAction(ActionDefinition.LOGS_USER_ACTIVITY_LIST) - .create()); - } - - if (sebAdmin.getItemCount() > 0) { - sebAdmin.setExpanded(this.currentUser.get().hasAnyRole( - UserRole.SEB_SERVER_ADMIN, - UserRole.INSTITUTIONAL_ADMIN)); - } else { - sebAdmin.dispose(); - } - - - // ---- SEB ADMIN ---------------------------------------------------------------------- //-------------------------------------------------------------------------------------- @@ -252,7 +254,7 @@ public class ActivitiesPane implements TemplateComposer { } // Certificate management - if (!isSupporterOnly && certificatesEnabled) { + if (certificatesEnabled) { final TreeItem examConfigTemplate = this.widgetFactory.treeItemLocalized( sebConfigs, ActivityDefinition.SEB_CERTIFICATE_MANAGEMENT.displayName); @@ -283,7 +285,7 @@ public class ActivitiesPane implements TemplateComposer { final boolean examTemplateEnabled = currentUser.isFeatureEnabled(UserFeatures.Feature.EXAM_TEMPLATE); final boolean anyExamAdminEnabled = lmsSetupEnabled || quizLookupEnabled || examEnabled || examTemplateEnabled; - if (anyExamAdminEnabled) { + if (anyExamAdminEnabled && !isTeacherOnly) { // Exam Administration final TreeItem examAdmin = this.widgetFactory.treeItemLocalized( navigation, @@ -369,7 +371,7 @@ public class ActivitiesPane implements TemplateComposer { ActivityDefinition.MONITORING.displayName); // Monitoring exams - if (isSupporter) { + if (isSupporter || isTeacherOnly) { if (monitoringEnabled) { final TreeItem monitoringExams = this.widgetFactory.treeItemLocalized( @@ -382,7 +384,7 @@ public class ActivitiesPane implements TemplateComposer { .create()); } - if (finishedEnabled) { + if (finishedEnabled && !isTeacherOnly) { final TreeItem finishedExams = this.widgetFactory.treeItemLocalized( monitoring, ActivityDefinition.FINISHED_EXAMS.displayName); @@ -395,7 +397,7 @@ public class ActivitiesPane implements TemplateComposer { } // SEB Client Logs - if (viewSEBClientLogs) { + if (viewSEBClientLogs && !isTeacherOnly) { final TreeItem sebLogs = (isSupporter) ? this.widgetFactory.treeItemLocalized( monitoring, @@ -414,7 +416,7 @@ public class ActivitiesPane implements TemplateComposer { monitoring.setExpanded( this.currentUser .get() - .hasAnyRole(UserRole.EXAM_SUPPORTER)); + .hasAnyRole(UserRole.EXAM_SUPPORTER, UserRole.TEACHER)); } else { monitoring.dispose(); } @@ -486,7 +488,7 @@ public class ActivitiesPane implements TemplateComposer { return findItemByActionDefinition( navigation.getItems(), ActivityDefinition.SEB_EXAM_CONFIG); - } else if (this.currentUser.get().hasAnyRole(UserRole.EXAM_SUPPORTER)) { + } else if (this.currentUser.get().hasAnyRole(UserRole.EXAM_SUPPORTER, UserRole.TEACHER)) { return findItemByActionDefinition( navigation.getItems(), ActivityDefinition.MONITORING_EXAMS); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/FinishedExamList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/FinishedExamList.java index e6d3cf66..e73aef9b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/FinishedExamList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/FinishedExamList.java @@ -95,6 +95,9 @@ public class FinishedExamList implements TemplateComposer { final RestService restService = this.resourceService.getRestService(); final I18nSupport i18nSupport = this.resourceService.getI18nSupport(); + boolean roleBasedAccess = currentUser.get() + .hasAnyRole(UserRole.EXAM_SUPPORTER); + // content page layout with title final Composite content = widgetFactory.defaultPageLayout( pageContext.getParent(), @@ -165,7 +168,7 @@ public class FinishedExamList implements TemplateComposer { table::getMultiSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY) - .publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER), false); + .publishIf(() -> roleBasedAccess, false); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringClientConnection.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringClientConnection.java index 3e275bc5..65c914a3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringClientConnection.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringClientConnection.java @@ -205,7 +205,7 @@ public class MonitoringClientConnection implements TemplateComposer { .onError(error -> pageContext.notifyLoadError(EntityType.EXAM, error)) .getOrThrow(); final UserInfo user = currentUser.get(); - final boolean supporting = user.hasRole(UserRole.EXAM_SUPPORTER) && + final boolean supporting = user.hasAnyRole(UserRole.EXAM_SUPPORTER, UserRole.TEACHER) && exam.supporter.contains(user.uuid); final BooleanSupplier isExamSupporter = () -> supporting || user.hasRole(UserRole.EXAM_ADMIN); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java index d1f07f1f..b5c7a658 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java @@ -156,7 +156,7 @@ public class MonitoringRunningExam implements TemplateComposer { final boolean quitEnabled = currentUser.isFeatureEnabled(MONITORING_RUNNING_EXAM_QUIT); final boolean lockscreenEnabled = currentUser.isFeatureEnabled(MONITORING_RUNNING_EXAM_QUIT); final boolean cancelEnabled = currentUser.isFeatureEnabled(MONITORING_RUNNING_EXAM_CANCEL_CON); - final boolean supporting = user.hasRole(UserRole.EXAM_SUPPORTER) && + final boolean supporting = user.hasAnyRole(UserRole.EXAM_SUPPORTER, UserRole.TEACHER) && exam.supporter.contains(user.uuid); final BooleanSupplier isExamSupporter = () -> supporting || user.hasRole(UserRole.EXAM_ADMIN); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExamList.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExamList.java index d828469d..3c396c65 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExamList.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExamList.java @@ -86,6 +86,9 @@ public class MonitoringRunningExamList implements TemplateComposer { final RestService restService = this.resourceService.getRestService(); final I18nSupport i18nSupport = this.resourceService.getI18nSupport(); + boolean hasRoleBasedAccess = currentUser.get().hasAnyRole( + UserRole.EXAM_SUPPORTER, UserRole.TEACHER); + // content page layout with title final Composite content = widgetFactory.defaultPageLayout( pageContext.getParent(), @@ -149,7 +152,7 @@ public class MonitoringRunningExamList implements TemplateComposer { table::getMultiSelection, PageAction::applySingleSelectionAsEntityKey, EMPTY_SELECTION_TEXT_KEY) - .publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER), false); + .publishIf(() -> hasRoleBasedAccess, false); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/OAuth2AuthorizationContextHolder.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/OAuth2AuthorizationContextHolder.java index 281e45ff..aa560b8b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/OAuth2AuthorizationContextHolder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/OAuth2AuthorizationContextHolder.java @@ -11,13 +11,11 @@ package ch.ethz.seb.sebserver.gui.service.remote.webservice.auth; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; import javax.servlet.http.HttpSession; import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.model.user.TokenLoginInfo; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -25,9 +23,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.converter.StringHttpMessageConverter; @@ -146,12 +142,14 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol private boolean valid = true; + private final ClientHttpRequestFactory clientHttpRequestFactory; private final ResourceOwnerPasswordResourceDetails resource; private final DisposableOAuth2RestTemplate restTemplate; private final String revokeTokenURI; private final String currentUserURI; private final String loginLogURI; private final String logoutLogURI; + private final String jwtTokenVerificationURI; private Result loggedInUser = null; @@ -161,6 +159,7 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol final WebserviceURIService webserviceURIService, final ClientHttpRequestFactory clientHttpRequestFactory) { + this.clientHttpRequestFactory = clientHttpRequestFactory; this.resource = new ResourceOwnerPasswordResourceDetails(); this.resource.setAccessTokenUri(webserviceURIService.getOAuthTokenURI()); this.resource.setClientId(guiClientId); @@ -179,6 +178,7 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol this.currentUserURI = webserviceURIService.getCurrentUserRequestURI(); this.loginLogURI = webserviceURIService.getLoginLogPostURI(); this.logoutLogURI = webserviceURIService.getLogoutLogPostURI(); + this.jwtTokenVerificationURI = webserviceURIService.getJWTTokenVerificationURI(); } @Override @@ -193,9 +193,6 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol return false; } - // TODO check if this is needed. If not remove it. - // This gets called many times for a page load - try { final ResponseEntity forEntity = this.restTemplate.getForEntity(this.currentUserURI, String.class); @@ -264,8 +261,32 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol @Override public boolean autoLogin(final String oneTimeToken) { - // TODO call auto-login API on Webservice to verify the oneTimeToken and - return false; + try { + // Create ad-hoc RestTemplate and call token verification + final RestTemplate verifyTemplate = new RestTemplate(this.clientHttpRequestFactory); + final HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.set("ONE_TIME_TOKEN_TO_VERIFY", oneTimeToken); + httpHeaders.setBasicAuth(resource.getClientId(), resource.getClientSecret()); + + final ResponseEntity response = verifyTemplate.exchange( + this.jwtTokenVerificationURI, + HttpMethod.POST, + new HttpEntity(null, httpHeaders), + TokenLoginInfo.class); + + if (response.getStatusCodeValue() != HttpStatus.OK.value()) { + log.warn("Autologin failed due to error response: {}", response); + return false; + } + + final TokenLoginInfo loginInfo = response.getBody(); + this.restTemplate.getOAuth2ClientContext().setAccessToken(loginInfo.login); + + return this.isLoggedIn(); + } catch (final Exception e) { + log.warn("Autologin failed due to unexpected error: {}", e.getMessage()); + return false; + } } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIService.java index d452d047..6aa4fbe9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIService.java @@ -83,4 +83,11 @@ public class WebserviceURIService { .path(API.USER_ACCOUNT_ENDPOINT + API.LOGOUT_PATH_SEGMENT) .toUriString(); } + + public String getJWTTokenVerificationURI() { + return UriComponentsBuilder.fromHttpUrl(this.webserviceServerAddress) + .path(this.contextPath) + .path(API.OAUTH_JWT_TOKEN_VERIFY_ENDPOINT) + .toUriString(); + } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/TeacherAccountService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/TeacherAccountService.java index 7b167a36..7fa302a4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/TeacherAccountService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/TeacherAccountService.java @@ -8,35 +8,75 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.authorization; -import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.user.TokenLoginInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService; +/** Service used to maintain Teacher Ad-Hoc Accounts */ public interface TeacherAccountService { + /** Creates an Ad-Hoc Teacher account for a given existing Exam. + * This also checks if such an account already exists and if so, + * it uses that and activates it if not already active + * + * @param exam The Exam instance + * @param adHocAccountData The account data for new Ad-Hoc account + * @return Result refer to the accounts UserInfo instance or to an error when happened. + */ + Result createNewTeacherAccountForExam( + Exam exam, + final FullLmsIntegrationService.AdHocAccountData adHocAccountData); + + /** Get the identifier for certain Teacher account for specified Exam. + * + * @param exam The Exam instance + * @param adHocAccountData the account data + * @return account identifier + */ default String getTeacherAccountIdentifier( final Exam exam, final FullLmsIntegrationService.AdHocAccountData adHocAccountData) { return getTeacherAccountIdentifier(exam.getModelId(), adHocAccountData.userId); } + /** Get the identifier for certain Teacher account for specified Exam. + * + * @param examId The Exam identifier + * @param userId the account id + * @return account identifier + */ String getTeacherAccountIdentifier(String examId, String userId); - Result createNewTeacherAccountForExam( - Exam exam, - final FullLmsIntegrationService.AdHocAccountData adHocAccountData); - + /** Deactivates a certain ad-hoc Teacher account + * Usually called when an exam is deleted. Checks if Teacher account for exam + * is not used by other active exams and if so, deactivates unused ad-hoc accounts + * + * @param exam The Exam for witch to deactivate all applied ad-hoc Teacher accounts + * if they are not used anymore. + * @return Result refer to the given exam or to an error when happened + */ Result deactivateTeacherAccountsForExam(Exam exam); + /** Get a One Time Access JWT Token for auto-login for a specific ad-hoc Teacher account. + * + * @param exam The involved Exam instance + * @param adHocAccountData the account data + * @param createIfNotExists Indicates if a ad-hoc Teacher account shall be created if there is none for given + * account data. + * @return Result refer to the One Time Access JWT Token or to an error when happened. + */ Result getOneTimeTokenForTeacherAccount( Exam exam, FullLmsIntegrationService.AdHocAccountData adHocAccountData, boolean createIfNotExists); + /** Used to verify a given One Time Access JWT Token. This must have the expected claims and must not be expired + * + * @param token The One Time Access JWT Token to verify access for + * @return Result refer to the login information for auto-login or to an error when happened or access is denied + */ Result verifyOneTimeTokenForTeacherAccount(String token); - } 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 4b18eef5..124f0fd5 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 @@ -123,6 +123,9 @@ public class AuthorizationServiceImpl implements AuthorizationService { .andForRole(UserRole.EXAM_SUPPORTER) .withInstitutionalPrivilege(PrivilegeType.ASSIGNED) .withOwnerPrivilege(PrivilegeType.MODIFY) + .andForRole(UserRole.TEACHER) + .withInstitutionalPrivilege(PrivilegeType.ASSIGNED) + .withOwnerPrivilege(PrivilegeType.READ) .create(); // grants for exam templates diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/TeacherAccountServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/TeacherAccountServiceImpl.java index 23de0e2e..a58832e9 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/TeacherAccountServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/impl/TeacherAccountServiceImpl.java @@ -13,6 +13,7 @@ import java.util.*; import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.user.TokenLoginInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; @@ -22,9 +23,7 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Cryptor; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Utils; -import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.TeacherAccountService; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService; @@ -42,6 +41,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.common.exceptions.UnauthorizedUserException; import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint; import org.springframework.stereotype.Service; @@ -56,13 +56,10 @@ public class TeacherAccountServiceImpl implements TeacherAccountService { private static final String USER_CLAIM = "usr"; private static final String EXAM_ID_CLAIM = "exam"; - private static final String EXAM_OTT_SUBJECT_PREFIX = "EXAM_OTT_SUBJECT_"; - private final UserDAO userDAO; private final ScreenProctoringService screenProctoringService; private final ExamDAO examDAO; private final Cryptor cryptor; - private final AdditionalAttributesDAO additionalAttributesDAO; final TokenEndpoint tokenEndpoint; private final AdminAPIClientDetails adminAPIClientDetails; @@ -71,7 +68,6 @@ public class TeacherAccountServiceImpl implements TeacherAccountService { final ScreenProctoringService screenProctoringService, final ExamDAO examDAO, final Cryptor cryptor, - final AdditionalAttributesDAO additionalAttributesDAO, final TokenEndpoint tokenEndpoint, final AdminAPIClientDetails adminAPIClientDetails) { @@ -79,7 +75,6 @@ public class TeacherAccountServiceImpl implements TeacherAccountService { this.screenProctoringService = screenProctoringService; this.examDAO = examDAO; this.cryptor = cryptor; - this.additionalAttributesDAO = additionalAttributesDAO; this.tokenEndpoint = tokenEndpoint; this.adminAPIClientDetails = adminAPIClientDetails; } @@ -90,7 +85,7 @@ public class TeacherAccountServiceImpl implements TeacherAccountService { throw new RuntimeException("examId and/or userId cannot be null"); } - return userId + Constants.UNDERLINE + examId; + return userId; } @Override @@ -164,7 +159,12 @@ public class TeacherAccountServiceImpl implements TeacherAccountService { public Result verifyOneTimeTokenForTeacherAccount(final String loginToken) { return Result.tryCatch(() -> { - final Claims claims = checkJWTValid(loginToken); + final Claims claims; + try { + claims = checkJWTValid(loginToken); + } catch (final Exception e) { + throw new UnauthorizedUserException("Invalid One Time JWT", e); + } final String userId = claims.get(USER_CLAIM, String.class); // check if requested user exists @@ -174,20 +174,24 @@ public class TeacherAccountServiceImpl implements TeacherAccountService { // login the user by getting access token final Map params = new HashMap<>(); - params.put("grant_type", "password"); - params.put("username", user.username); - params.put("password", user.uuid); - //final WebAuthenticationDetails details = new WebAuthenticationDetails("localhost", null); + params.put(Constants.OAUTH2_GRANT_TYPE, Constants.OAUTH2_GRANT_TYPE_PASSWORD); + params.put(Constants.OAUTH2_USER_NAME, user.username); + params.put(Constants.OAUTH2_GRANT_TYPE_PASSWORD, claims.get(SUBJECT_CLAIM_NAME, String.class)); final UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( - this.adminAPIClientDetails, // TODO are this the correct details? - null, + this.adminAPIClientDetails.getClientId(), + "N/A", Collections.emptyList()); final ResponseEntity accessToken = this.tokenEndpoint.postAccessToken(usernamePasswordAuthenticationToken, params); final OAuth2AccessToken token = accessToken.getBody(); - return new TokenLoginInfo(user.username, user.uuid, null, token); + final String examId = claims.get(EXAM_ID_CLAIM, String.class); + final EntityKey redirectTo = (StringUtils.isNotBlank(examId)) + ? new EntityKey(examId, EntityType.EXAM) + : null; + + return new TokenLoginInfo(user.username, user.uuid, redirectTo, token); }); } @@ -225,9 +229,8 @@ public class TeacherAccountServiceImpl implements TeacherAccountService { private String createOneTimeToken(final UserInfo account, final Long examId) { - // create a subject claim for this token only final String subjectClaim = UUID.randomUUID().toString(); - this.storeSubjectForExam(examId, account.uuid, subjectClaim); + userDAO.changePassword(account.uuid, subjectClaim); final Map claims = new HashMap<>(); claims.put(USER_CLAIM, account.uuid); @@ -276,38 +279,13 @@ public class TeacherAccountServiceImpl implements TeacherAccountService { final Long examPK = Long.parseLong(examId); // check subject - final String subjectClaim = getSubjectForExam(examPK, userId); - if (StringUtils.isBlank(subjectClaim)) { - throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.UNAUTHORIZED.of("Subject not found")); - } final String subject = claims.get(SUBJECT_CLAIM_NAME, String.class); - if (!subjectClaim.equals(subject)) { + if (StringUtils.isBlank(subject)) { throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.UNAUTHORIZED.of("Token subject mismatch")); } return claims; } - private void storeSubjectForExam(final Long examId, final String userId, final String subject) { - additionalAttributesDAO.saveAdditionalAttribute( - EntityType.EXAM, - examId, - EXAM_OTT_SUBJECT_PREFIX + userId, - subject) - .getOrThrow(); - } - - private void deleteSubjectForExam(final Long examId, final String userId) { - additionalAttributesDAO.delete(EntityType.EXAM, examId, EXAM_OTT_SUBJECT_PREFIX + userId); - } - - private String getSubjectForExam(final Long examId, final String userId) { - return additionalAttributesDAO - .getAdditionalAttribute(EntityType.EXAM, examId, EXAM_OTT_SUBJECT_PREFIX + userId) - .map(AdditionalAttributeRecord::getValue) - .onError(error -> log.warn("Failed to get OTT subject from exam: {}", error.getMessage())) - .getOrElse(null); - } - private UserInfo synchronizeSPSUserForExam(final UserInfo account, final Long examId) { if (this.screenProctoringService.isScreenProctoringEnabled(examId)) { this.screenProctoringService.synchronizeSPSUserForExam(examId); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java index b87503c3..0ccfc6e2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ExamDAO.java @@ -233,7 +233,7 @@ public interface ExamDAO extends ActivatableEntityDAO, BulkActionSup @CacheEvict( cacheNames = ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM, - key = "#examId") + key = "#exam.id") Result applySupporter(Exam exam, String userUUID); /** This is used by the internal update process to mark exams for which the LMS related data availability diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java index eec10228..7e754e1e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/FullLmsIntegrationServiceImpl.java @@ -552,7 +552,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService IOUtils.closeQuietly(out); } }) - .onError(error -> log.error("Failed to apply ConnectionConfiguration for exam: {} error: {}", exam, error.getMessage())) + .onError(error -> log.error("Failed to apply ConnectionConfiguration for exam: {} error: ", exam, error)) .getOr(exam); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactoryImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactoryImpl.java index 2c059db1..a4acf191 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactoryImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/lms/impl/moodle/MoodleRestTemplateFactoryImpl.java @@ -379,7 +379,7 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory multiPartAttributes.add("token", this.accessToken); return super.postForObject( - uploadEndpoint, + uri, multiPartAttributes, String.class); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/APIExceptionHandler.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/APIExceptionHandler.java index 4af3e306..bb1c1ed5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/APIExceptionHandler.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/APIExceptionHandler.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.concurrent.CompletionException; import java.util.stream.Collectors; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.NoResourceFoundException; import org.apache.catalina.connector.ClientAbortException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -61,7 +62,7 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler { ((OnlyMessageLogExceptionWrapper) ex).log(log); return new ResponseEntity<>(status); } else { - log.error("Unexpected generic error catched at the API endpoint: ", ex); + log.error("Unexpected generic error caught at the API endpoint: ", ex); } final List errors = Arrays.asList(APIMessage.ErrorMessage.GENERIC.of(ex.getMessage())); @@ -160,6 +161,15 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } + @ExceptionHandler(NoResourceFoundException.class) + public ResponseEntity handleNoResourceFoundException( + final NoResourceFoundException ex, + final WebRequest request) { + + return APIMessage.ErrorMessage.RESOURCE_NOT_FOUND + .createErrorResponse(ex.getMessage()); + } + @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity handleResourceNotFoundException( final ResourceNotFoundException ex, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/AdminJWTAccess.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/AdminJWTAccess.java new file mode 100644 index 00000000..e816147e --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/AdminJWTAccess.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019 ETH Zürich, IT Services + * + * 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 ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gbl.api.TooManyRequests; +import ch.ethz.seb.sebserver.gbl.model.user.TokenLoginInfo; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.TeacherAccountService; +import io.github.bucket4j.local.LocalBucket; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +@RestController +public class AdminJWTAccess { + + private final TeacherAccountService teacherAccountService; + private final LocalBucket requestRateLimitBucket; + + public AdminJWTAccess( + final TeacherAccountService teacherAccountService, + final RateLimitService rateLimitService) { + this.teacherAccountService = teacherAccountService; + this.requestRateLimitBucket = rateLimitService.createRequestLimitBucker(); + } + + @RequestMapping( + path = API.OAUTH_JWT_TOKEN_VERIFY_ENDPOINT, + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_VALUE) + public TokenLoginInfo verifyJWTToken(@RequestHeader(name = "ONE_TIME_TOKEN_TO_VERIFY") final String loginToken) { + + if (!this.requestRateLimitBucket.tryConsume(1)) { + throw new TooManyRequests(); + } + + final Result tokenLoginInfoResult = teacherAccountService + .verifyOneTimeTokenForTeacherAccount(loginToken); + + if (tokenLoginInfoResult.hasError()) { + throw new APIMessage.APIMessageException( + APIMessage.ErrorMessage.UNAUTHORIZED.of(tokenLoginInfoResult.getError())); + } + + return tokenLoginInfoResult.get(); + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientConnectionController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientConnectionController.java index b2cde1e5..155b427b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientConnectionController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ClientConnectionController.java @@ -192,8 +192,7 @@ public class ClientConnectionController extends ReadonlyEntityController> getAllData(final FilterMap filterMap) { 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 index 8872b386..aec59b4b 100644 --- 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 @@ -221,7 +221,9 @@ public class ClientEventController extends ReadonlyEntityController { 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 bc196f36..10f856dd 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 @@ -169,7 +169,7 @@ public class ExamMonitoringController { this.authorization.checkRole( institutionId, EntityType.EXAM, - UserRole.EXAM_SUPPORTER, + UserRole.EXAM_SUPPORTER, UserRole.TEACHER, UserRole.EXAM_ADMIN); final FilterMap filterMap = new FilterMap(allRequestParams, request.getQueryString()); @@ -230,7 +230,7 @@ public class ExamMonitoringController { this.authorization.checkRole( institutionId, EntityType.EXAM, - UserRole.EXAM_SUPPORTER, + UserRole.EXAM_SUPPORTER, UserRole.TEACHER, UserRole.EXAM_ADMIN); final FilterMap filterMap = new FilterMap(allRequestParams, request.getQueryString()); @@ -511,7 +511,7 @@ public class ExamMonitoringController { this.authorization.checkRole( institutionId, EntityType.EXAM, - UserRole.EXAM_SUPPORTER, + UserRole.EXAM_SUPPORTER, UserRole.TEACHER, UserRole.EXAM_ADMIN); // check exam running diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsIntegrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsIntegrationController.java index e32fc78b..0567c615 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsIntegrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsIntegrationController.java @@ -90,8 +90,8 @@ public class LmsIntegrationController { final EntityKey examID = fullLmsIntegrationService.deleteExam(lmsUUId, courseId, quizId) .onError(e -> log.error( - "Failed to delete exam: lmsId:{}, courseId: {}, quizId: {}", - lmsUUId, courseId, quizId, e)) + "Failed to delete exam: lmsId:{}, courseId: {}, quizId: {}, error: {}", + lmsUUId, courseId, quizId, e.getMessage())) .getOrThrow(); log.info("Auto delete of exam successful: {}", examID); @@ -141,8 +141,7 @@ public class LmsIntegrationController { @RequestMapping( path = API.LMS_FULL_INTEGRATION_LOGIN_TOKEN_ENDPOINT, method = RequestMethod.POST, - consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, - produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public FullLmsIntegrationService.TokenLoginResponse getOneTimeLoginToken( @RequestParam(name = API.LMS_FULL_INTEGRATION_LMS_UUID) final String lmsUUId, @RequestParam(name = API.LMS_FULL_INTEGRATION_COURSE_ID) final String courseId, @@ -166,7 +165,7 @@ public class LmsIntegrationController { final String token = this.fullLmsIntegrationService .getOneTimeLoginToken(lmsUUId, courseId, quizId, adHocAccountData) - .onError(error -> log.error("Failed to create ad-hoc account with one time login token: ", error)) + .onError(error -> log.error("Failed to create ad-hoc account with one time login token, error: {}", error.getMessage())) .getOrThrow(); return new FullLmsIntegrationService.TokenLoginResponse( diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/RegisterUserController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/RegisterUserController.java index a2aba794..54a7b571 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/RegisterUserController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/RegisterUserController.java @@ -68,7 +68,7 @@ public class RegisterUserController { this.userActivityLogDAO = userActivityLogDAO; this.userDAO = userDAO; this.beanValidationService = beanValidationService; - this. registeringEnabled = BooleanUtils.isTrue(features.get(UserFeatures.Feature.ADMIN_USER_ACCOUNT_SELF_REGISTERING.featureName)); + this.registeringEnabled = BooleanUtils.isTrue(features.get(UserFeatures.Feature.ADMIN_USER_ACCOUNT_SELF_REGISTERING.featureName)); this.autoActivation = BooleanUtils.isTrue(features.get(UserFeatures.Feature.ADMIN_USER_ACCOUNT_SELF_REGISTERING_AUTO_ACTIVATION.featureName)); this.requestRateLimitBucket = rateLimitService.createRequestLimitBucker(); this.createRateLimitBucket = rateLimitService.createCreationLimitBucker(); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SEBClientConfigController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SEBClientConfigController.java index d4566347..26783624 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SEBClientConfigController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SEBClientConfigController.java @@ -17,12 +17,14 @@ import java.util.Collection; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationChangeEvent; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.mybatis.dynamic.sql.SqlTable; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.scheduling.annotation.EnableAsync; @@ -62,6 +64,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe public class SEBClientConfigController extends ActivatableEntityController { private final ConnectionConfigurationService sebConnectionConfigurationService; + private final ApplicationEventPublisher applicationEventPublisher; public SEBClientConfigController( final SEBClientConfigDAO sebClientConfigDAO, @@ -70,7 +73,8 @@ public class SEBClientConfigController extends ActivatableEntityController