SEBSERV-418 finished implementation for teacher account and login

This commit is contained in:
anhefti 2024-05-30 16:33:58 +02:00
parent 1d332fc579
commit 908665ddcc
27 changed files with 336 additions and 178 deletions

View file

@ -138,6 +138,8 @@ public final class Constants {
public static final String XML_PLIST_INTEGER = "integer"; public static final String XML_PLIST_INTEGER = "integer";
public static final String XML_PLIST_REAL = "real"; 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_GRANT_TYPE_PASSWORD = "password";
public static final String OAUTH2_CLIENT_SECRET = "client_secret"; public static final String OAUTH2_CLIENT_SECRET = "client_secret";
public static final String OAUTH2_GRANT_TYPE_REFRESH_TOKEN = "refresh_token"; public static final String OAUTH2_GRANT_TYPE_REFRESH_TOKEN = "refresh_token";

View file

@ -8,26 +8,27 @@
package ch.ethz.seb.sebserver.gbl.model.user; 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.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2AccessToken;
public class TokenLoginInfo { public class TokenLoginInfo {
@JsonProperty @JsonProperty("username")
public final String username; public final String username;
@JsonProperty @JsonProperty("userUUID")
public final String userUUID; public final String userUUID;
@JsonProperty @JsonProperty("redirect")
public final String redirect; public final EntityKey redirect;
@JsonProperty @JsonProperty("login")
public final OAuth2AccessToken login; public final OAuth2AccessToken login;
@JsonCreator @JsonCreator
public TokenLoginInfo( public TokenLoginInfo(
@JsonProperty final String username, @JsonProperty("username") final String username,
@JsonProperty final String userUUID, @JsonProperty("userUUID") final String userUUID,
@JsonProperty final String redirect, @JsonProperty("redirect") final EntityKey redirect,
@JsonProperty final OAuth2AccessToken login) { @JsonProperty("login") final OAuth2AccessToken login) {
this.username = username; this.username = username;
this.userUUID = userUUID; this.userUUID = userUUID;

View file

@ -50,7 +50,7 @@ public enum UserRole implements Entity, GrantedAuthority {
public static List<UserRole> publicRolesForUser(final UserInfo user) { public static List<UserRole> publicRolesForUser(final UserInfo user) {
final EnumSet<UserRole> roles = user.getUserRoles(); final EnumSet<UserRole> roles = user.getUserRoles();
if (roles.contains(SEB_SERVER_ADMIN)) { 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)) { } else if (roles.contains(INSTITUTIONAL_ADMIN)) {
return Arrays.asList(INSTITUTIONAL_ADMIN, EXAM_ADMIN, EXAM_SUPPORTER); return Arrays.asList(INSTITUTIONAL_ADMIN, EXAM_ADMIN, EXAM_SUPPORTER);
} else if (roles.contains(EXAM_ADMIN)) { } else if (roles.contains(EXAM_ADMIN)) {

View file

@ -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.LOGO_PATH_SEGMENT + "/**").permitAll()
.antMatchers(adminAPIEndpoint + API.INFO_ENDPOINT + API.INFO_INST_PATH_SEGMENT + "/**").permitAll() .antMatchers(adminAPIEndpoint + API.INFO_ENDPOINT + API.INFO_INST_PATH_SEGMENT + "/**").permitAll()
.antMatchers(adminAPIEndpoint + API.REGISTER_ENDPOINT).permitAll() .antMatchers(adminAPIEndpoint + API.REGISTER_ENDPOINT).permitAll()
.antMatchers(API.OAUTH_JWT_TOKEN_ENDPOINT + "/**").permitAll()
.and() .and()
.antMatcher("/**") .antMatcher("/**")
.authorizeRequests() .authorizeRequests()

View file

@ -21,6 +21,8 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; 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.codec.binary.Base64InputStream;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT; 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.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.WebserviceURIService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.WebserviceURIService;
import ch.ethz.seb.sebserver.gui.widget.ImageUploadSelection; import ch.ethz.seb.sebserver.gui.widget.ImageUploadSelection;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
@Lazy @Lazy
@Component @Component
@ -116,6 +120,20 @@ public final class InstitutionalAuthenticationEntryPoint implements Authenticati
final HttpServletResponse response, final HttpServletResponse response,
final AuthenticationException authException) throws IOException, ServletException { 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); final String institutionalEndpoint = extractInstitutionalEndpoint(request);
if (StringUtils.isNotBlank(institutionalEndpoint)) { if (StringUtils.isNotBlank(institutionalEndpoint)) {
@ -168,7 +186,6 @@ public final class InstitutionalAuthenticationEntryPoint implements Authenticati
request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE); request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE);
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setStatus(HttpStatus.UNAUTHORIZED.value());
forwardToEntryPoint(request, response, this.guiEntryPoint, institutionalEndpoint == null); forwardToEntryPoint(request, response, this.guiEntryPoint, institutionalEndpoint == null);
} }
private void forwardToEntryPoint( private void forwardToEntryPoint(

View file

@ -93,103 +93,105 @@ public class ActivitiesPane implements TemplateComposer {
final PageActionBuilder actionBuilder = this.pageService.pageActionBuilder(pageContext); 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 ---------------------------------------------------------------------- // ---- SEB ADMIN ----------------------------------------------------------------------
final boolean isServerOrInstAdmin = this.currentUser.get() final boolean isServerOrInstAdmin = this.currentUser.get()
.hasAnyRole(UserRole.SEB_SERVER_ADMIN, UserRole.INSTITUTIONAL_ADMIN); .hasAnyRole(UserRole.SEB_SERVER_ADMIN, UserRole.INSTITUTIONAL_ADMIN);
// SEB Server Administration if (isServerOrInstAdmin) {
final TreeItem sebAdmin = this.widgetFactory.treeItemLocalized( // SEB Server Administration
navigation, final TreeItem sebAdmin = this.widgetFactory.treeItemLocalized(
ActivityDefinition.SEB_ADMINISTRATION.displayName); navigation,
ActivityDefinition.SEB_ADMINISTRATION.displayName);
// Institution // Institution
if (currentUser.isFeatureEnabled(UserFeatures.Feature.ADMIN_INSTITUTION)) { if (currentUser.isFeatureEnabled(UserFeatures.Feature.ADMIN_INSTITUTION)) {
// If current user has SEB Server Admin role, show the Institution list // If current user has SEB Server Admin role, show the Institution list
if (userInfo.hasRole(UserRole.SEB_SERVER_ADMIN)) { if (userInfo.hasRole(UserRole.SEB_SERVER_ADMIN)) {
// institutions (list) as root // institutions (list) as root
final TreeItem institutions = this.widgetFactory.treeItemLocalized( 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, sebAdmin,
ActivityDefinition.INSTITUTION.displayName); ActivityDefinition.USER_ACCOUNT.displayName);
injectActivitySelection( injectActivitySelection(
institutions, userAccounts,
actionBuilder actionBuilder
.newAction(ActionDefinition.INSTITUTION_VIEW_LIST) .newAction(ActionDefinition.USER_ACCOUNT_VIEW_LIST)
.create()); .create());
} else if (currentUser.isFeatureEnabled(UserFeatures.Feature.ADMIN_USER_ACCOUNT)) {
} else if (userInfo.hasRole(UserRole.INSTITUTIONAL_ADMIN)) { // otherwise show the user account form for current user
// otherwise show the form of the institution for current user final TreeItem userAccounts = pageService.isLightSetup() || !currentUser.isFeatureEnabled(UserFeatures.Feature.ADMIN_USER_ADMINISTRATION)
final TreeItem institutions = this.widgetFactory.treeItemLocalized( ? this.widgetFactory.treeItemLocalized(
sebAdmin, sebAdmin,
ActivityDefinition.INSTITUTION.displayName); ActivityDefinition.USER_ACCOUNT.displayName)
: this.widgetFactory.treeItemLocalized(
navigation,
ActivityDefinition.USER_ACCOUNT.displayName);
injectActivitySelection( injectActivitySelection(
institutions, userAccounts,
actionBuilder.newAction(ActionDefinition.INSTITUTION_VIEW_FORM) actionBuilder.newAction(ActionDefinition.USER_ACCOUNT_VIEW_FORM)
.withEntityKey(userInfo.institutionId, EntityType.INSTITUTION) .withEntityKey(this.currentUser.get().getEntityKey())
.withAttribute(AttributeKeys.READ_ONLY, "true") .withAttribute(AttributeKeys.READ_ONLY, "true")
.create()); .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 ---------------------------------------------------------------------- // ---- SEB ADMIN ----------------------------------------------------------------------
//-------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------
@ -252,7 +254,7 @@ public class ActivitiesPane implements TemplateComposer {
} }
// Certificate management // Certificate management
if (!isSupporterOnly && certificatesEnabled) { if (certificatesEnabled) {
final TreeItem examConfigTemplate = this.widgetFactory.treeItemLocalized( final TreeItem examConfigTemplate = this.widgetFactory.treeItemLocalized(
sebConfigs, sebConfigs,
ActivityDefinition.SEB_CERTIFICATE_MANAGEMENT.displayName); 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 examTemplateEnabled = currentUser.isFeatureEnabled(UserFeatures.Feature.EXAM_TEMPLATE);
final boolean anyExamAdminEnabled = lmsSetupEnabled || quizLookupEnabled || examEnabled || examTemplateEnabled; final boolean anyExamAdminEnabled = lmsSetupEnabled || quizLookupEnabled || examEnabled || examTemplateEnabled;
if (anyExamAdminEnabled) { if (anyExamAdminEnabled && !isTeacherOnly) {
// Exam Administration // Exam Administration
final TreeItem examAdmin = this.widgetFactory.treeItemLocalized( final TreeItem examAdmin = this.widgetFactory.treeItemLocalized(
navigation, navigation,
@ -369,7 +371,7 @@ public class ActivitiesPane implements TemplateComposer {
ActivityDefinition.MONITORING.displayName); ActivityDefinition.MONITORING.displayName);
// Monitoring exams // Monitoring exams
if (isSupporter) { if (isSupporter || isTeacherOnly) {
if (monitoringEnabled) { if (monitoringEnabled) {
final TreeItem monitoringExams = this.widgetFactory.treeItemLocalized( final TreeItem monitoringExams = this.widgetFactory.treeItemLocalized(
@ -382,7 +384,7 @@ public class ActivitiesPane implements TemplateComposer {
.create()); .create());
} }
if (finishedEnabled) { if (finishedEnabled && !isTeacherOnly) {
final TreeItem finishedExams = this.widgetFactory.treeItemLocalized( final TreeItem finishedExams = this.widgetFactory.treeItemLocalized(
monitoring, monitoring,
ActivityDefinition.FINISHED_EXAMS.displayName); ActivityDefinition.FINISHED_EXAMS.displayName);
@ -395,7 +397,7 @@ public class ActivitiesPane implements TemplateComposer {
} }
// SEB Client Logs // SEB Client Logs
if (viewSEBClientLogs) { if (viewSEBClientLogs && !isTeacherOnly) {
final TreeItem sebLogs = (isSupporter) final TreeItem sebLogs = (isSupporter)
? this.widgetFactory.treeItemLocalized( ? this.widgetFactory.treeItemLocalized(
monitoring, monitoring,
@ -414,7 +416,7 @@ public class ActivitiesPane implements TemplateComposer {
monitoring.setExpanded( monitoring.setExpanded(
this.currentUser this.currentUser
.get() .get()
.hasAnyRole(UserRole.EXAM_SUPPORTER)); .hasAnyRole(UserRole.EXAM_SUPPORTER, UserRole.TEACHER));
} else { } else {
monitoring.dispose(); monitoring.dispose();
} }
@ -486,7 +488,7 @@ public class ActivitiesPane implements TemplateComposer {
return findItemByActionDefinition( return findItemByActionDefinition(
navigation.getItems(), navigation.getItems(),
ActivityDefinition.SEB_EXAM_CONFIG); 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( return findItemByActionDefinition(
navigation.getItems(), navigation.getItems(),
ActivityDefinition.MONITORING_EXAMS); ActivityDefinition.MONITORING_EXAMS);

View file

@ -95,6 +95,9 @@ public class FinishedExamList implements TemplateComposer {
final RestService restService = this.resourceService.getRestService(); final RestService restService = this.resourceService.getRestService();
final I18nSupport i18nSupport = this.resourceService.getI18nSupport(); final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
boolean roleBasedAccess = currentUser.get()
.hasAnyRole(UserRole.EXAM_SUPPORTER);
// content page layout with title // content page layout with title
final Composite content = widgetFactory.defaultPageLayout( final Composite content = widgetFactory.defaultPageLayout(
pageContext.getParent(), pageContext.getParent(),
@ -165,7 +168,7 @@ public class FinishedExamList implements TemplateComposer {
table::getMultiSelection, table::getMultiSelection,
PageAction::applySingleSelectionAsEntityKey, PageAction::applySingleSelectionAsEntityKey,
EMPTY_SELECTION_TEXT_KEY) EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER), false); .publishIf(() -> roleBasedAccess, false);
} }

View file

@ -205,7 +205,7 @@ public class MonitoringClientConnection implements TemplateComposer {
.onError(error -> pageContext.notifyLoadError(EntityType.EXAM, error)) .onError(error -> pageContext.notifyLoadError(EntityType.EXAM, error))
.getOrThrow(); .getOrThrow();
final UserInfo user = currentUser.get(); 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); exam.supporter.contains(user.uuid);
final BooleanSupplier isExamSupporter = () -> supporting || user.hasRole(UserRole.EXAM_ADMIN); final BooleanSupplier isExamSupporter = () -> supporting || user.hasRole(UserRole.EXAM_ADMIN);

View file

@ -156,7 +156,7 @@ public class MonitoringRunningExam implements TemplateComposer {
final boolean quitEnabled = currentUser.isFeatureEnabled(MONITORING_RUNNING_EXAM_QUIT); final boolean quitEnabled = currentUser.isFeatureEnabled(MONITORING_RUNNING_EXAM_QUIT);
final boolean lockscreenEnabled = 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 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); exam.supporter.contains(user.uuid);
final BooleanSupplier isExamSupporter = () -> supporting || user.hasRole(UserRole.EXAM_ADMIN); final BooleanSupplier isExamSupporter = () -> supporting || user.hasRole(UserRole.EXAM_ADMIN);

View file

@ -86,6 +86,9 @@ public class MonitoringRunningExamList implements TemplateComposer {
final RestService restService = this.resourceService.getRestService(); final RestService restService = this.resourceService.getRestService();
final I18nSupport i18nSupport = this.resourceService.getI18nSupport(); final I18nSupport i18nSupport = this.resourceService.getI18nSupport();
boolean hasRoleBasedAccess = currentUser.get().hasAnyRole(
UserRole.EXAM_SUPPORTER, UserRole.TEACHER);
// content page layout with title // content page layout with title
final Composite content = widgetFactory.defaultPageLayout( final Composite content = widgetFactory.defaultPageLayout(
pageContext.getParent(), pageContext.getParent(),
@ -149,7 +152,7 @@ public class MonitoringRunningExamList implements TemplateComposer {
table::getMultiSelection, table::getMultiSelection,
PageAction::applySingleSelectionAsEntityKey, PageAction::applySingleSelectionAsEntityKey,
EMPTY_SELECTION_TEXT_KEY) EMPTY_SELECTION_TEXT_KEY)
.publishIf(() -> currentUser.get().hasRole(UserRole.EXAM_SUPPORTER), false); .publishIf(() -> hasRoleBasedAccess, false);
} }

View file

@ -11,13 +11,11 @@ package ch.ethz.seb.sebserver.gui.service.remote.webservice.auth;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import ch.ethz.seb.sebserver.gbl.api.API; 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.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; 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.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod; import org.springframework.http.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter;
@ -146,12 +142,14 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol
private boolean valid = true; private boolean valid = true;
private final ClientHttpRequestFactory clientHttpRequestFactory;
private final ResourceOwnerPasswordResourceDetails resource; private final ResourceOwnerPasswordResourceDetails resource;
private final DisposableOAuth2RestTemplate restTemplate; private final DisposableOAuth2RestTemplate restTemplate;
private final String revokeTokenURI; private final String revokeTokenURI;
private final String currentUserURI; private final String currentUserURI;
private final String loginLogURI; private final String loginLogURI;
private final String logoutLogURI; private final String logoutLogURI;
private final String jwtTokenVerificationURI;
private Result<UserInfo> loggedInUser = null; private Result<UserInfo> loggedInUser = null;
@ -161,6 +159,7 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol
final WebserviceURIService webserviceURIService, final WebserviceURIService webserviceURIService,
final ClientHttpRequestFactory clientHttpRequestFactory) { final ClientHttpRequestFactory clientHttpRequestFactory) {
this.clientHttpRequestFactory = clientHttpRequestFactory;
this.resource = new ResourceOwnerPasswordResourceDetails(); this.resource = new ResourceOwnerPasswordResourceDetails();
this.resource.setAccessTokenUri(webserviceURIService.getOAuthTokenURI()); this.resource.setAccessTokenUri(webserviceURIService.getOAuthTokenURI());
this.resource.setClientId(guiClientId); this.resource.setClientId(guiClientId);
@ -179,6 +178,7 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol
this.currentUserURI = webserviceURIService.getCurrentUserRequestURI(); this.currentUserURI = webserviceURIService.getCurrentUserRequestURI();
this.loginLogURI = webserviceURIService.getLoginLogPostURI(); this.loginLogURI = webserviceURIService.getLoginLogPostURI();
this.logoutLogURI = webserviceURIService.getLogoutLogPostURI(); this.logoutLogURI = webserviceURIService.getLogoutLogPostURI();
this.jwtTokenVerificationURI = webserviceURIService.getJWTTokenVerificationURI();
} }
@Override @Override
@ -193,9 +193,6 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol
return false; return false;
} }
// TODO check if this is needed. If not remove it.
// This gets called many times for a page load
try { try {
final ResponseEntity<String> forEntity = final ResponseEntity<String> forEntity =
this.restTemplate.getForEntity(this.currentUserURI, String.class); this.restTemplate.getForEntity(this.currentUserURI, String.class);
@ -264,8 +261,32 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol
@Override @Override
public boolean autoLogin(final String oneTimeToken) { public boolean autoLogin(final String oneTimeToken) {
// TODO call auto-login API on Webservice to verify the oneTimeToken and try {
return false; // 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<TokenLoginInfo> response = verifyTemplate.exchange(
this.jwtTokenVerificationURI,
HttpMethod.POST,
new HttpEntity<TokenLoginInfo>(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 @Override

View file

@ -83,4 +83,11 @@ public class WebserviceURIService {
.path(API.USER_ACCOUNT_ENDPOINT + API.LOGOUT_PATH_SEGMENT) .path(API.USER_ACCOUNT_ENDPOINT + API.LOGOUT_PATH_SEGMENT)
.toUriString(); .toUriString();
} }
public String getJWTTokenVerificationURI() {
return UriComponentsBuilder.fromHttpUrl(this.webserviceServerAddress)
.path(this.contextPath)
.path(API.OAUTH_JWT_TOKEN_VERIFY_ENDPOINT)
.toUriString();
}
} }

View file

@ -8,35 +8,75 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.authorization; 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.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.user.TokenLoginInfo; import ch.ethz.seb.sebserver.gbl.model.user.TokenLoginInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService; import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
/** Service used to maintain Teacher Ad-Hoc Accounts */
public interface TeacherAccountService { 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<UserInfo> 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( default String getTeacherAccountIdentifier(
final Exam exam, final Exam exam,
final FullLmsIntegrationService.AdHocAccountData adHocAccountData) { final FullLmsIntegrationService.AdHocAccountData adHocAccountData) {
return getTeacherAccountIdentifier(exam.getModelId(), adHocAccountData.userId); 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); String getTeacherAccountIdentifier(String examId, String userId);
Result<UserInfo> createNewTeacherAccountForExam( /** Deactivates a certain ad-hoc Teacher account
Exam exam, * Usually called when an exam is deleted. Checks if Teacher account for exam
final FullLmsIntegrationService.AdHocAccountData adHocAccountData); * 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<Exam> deactivateTeacherAccountsForExam(Exam exam); Result<Exam> 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<String> getOneTimeTokenForTeacherAccount( Result<String> getOneTimeTokenForTeacherAccount(
Exam exam, Exam exam,
FullLmsIntegrationService.AdHocAccountData adHocAccountData, FullLmsIntegrationService.AdHocAccountData adHocAccountData,
boolean createIfNotExists); 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<TokenLoginInfo> verifyOneTimeTokenForTeacherAccount(String token); Result<TokenLoginInfo> verifyOneTimeTokenForTeacherAccount(String token);
} }

View file

@ -123,6 +123,9 @@ public class AuthorizationServiceImpl implements AuthorizationService {
.andForRole(UserRole.EXAM_SUPPORTER) .andForRole(UserRole.EXAM_SUPPORTER)
.withInstitutionalPrivilege(PrivilegeType.ASSIGNED) .withInstitutionalPrivilege(PrivilegeType.ASSIGNED)
.withOwnerPrivilege(PrivilegeType.MODIFY) .withOwnerPrivilege(PrivilegeType.MODIFY)
.andForRole(UserRole.TEACHER)
.withInstitutionalPrivilege(PrivilegeType.ASSIGNED)
.withOwnerPrivilege(PrivilegeType.READ)
.create(); .create();
// grants for exam templates // grants for exam templates

View file

@ -13,6 +13,7 @@ import java.util.*;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.api.APIMessage;
import ch.ethz.seb.sebserver.gbl.api.EntityType; 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.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.user.TokenLoginInfo; import ch.ethz.seb.sebserver.gbl.model.user.TokenLoginInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; 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.Cryptor;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.TeacherAccountService; 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.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService; 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.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken; 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.security.oauth2.provider.endpoint.TokenEndpoint;
import org.springframework.stereotype.Service; 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 USER_CLAIM = "usr";
private static final String EXAM_ID_CLAIM = "exam"; 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 UserDAO userDAO;
private final ScreenProctoringService screenProctoringService; private final ScreenProctoringService screenProctoringService;
private final ExamDAO examDAO; private final ExamDAO examDAO;
private final Cryptor cryptor; private final Cryptor cryptor;
private final AdditionalAttributesDAO additionalAttributesDAO;
final TokenEndpoint tokenEndpoint; final TokenEndpoint tokenEndpoint;
private final AdminAPIClientDetails adminAPIClientDetails; private final AdminAPIClientDetails adminAPIClientDetails;
@ -71,7 +68,6 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
final ScreenProctoringService screenProctoringService, final ScreenProctoringService screenProctoringService,
final ExamDAO examDAO, final ExamDAO examDAO,
final Cryptor cryptor, final Cryptor cryptor,
final AdditionalAttributesDAO additionalAttributesDAO,
final TokenEndpoint tokenEndpoint, final TokenEndpoint tokenEndpoint,
final AdminAPIClientDetails adminAPIClientDetails) { final AdminAPIClientDetails adminAPIClientDetails) {
@ -79,7 +75,6 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
this.screenProctoringService = screenProctoringService; this.screenProctoringService = screenProctoringService;
this.examDAO = examDAO; this.examDAO = examDAO;
this.cryptor = cryptor; this.cryptor = cryptor;
this.additionalAttributesDAO = additionalAttributesDAO;
this.tokenEndpoint = tokenEndpoint; this.tokenEndpoint = tokenEndpoint;
this.adminAPIClientDetails = adminAPIClientDetails; this.adminAPIClientDetails = adminAPIClientDetails;
} }
@ -90,7 +85,7 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
throw new RuntimeException("examId and/or userId cannot be null"); throw new RuntimeException("examId and/or userId cannot be null");
} }
return userId + Constants.UNDERLINE + examId; return userId;
} }
@Override @Override
@ -164,7 +159,12 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
public Result<TokenLoginInfo> verifyOneTimeTokenForTeacherAccount(final String loginToken) { public Result<TokenLoginInfo> verifyOneTimeTokenForTeacherAccount(final String loginToken) {
return Result.tryCatch(() -> { 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); final String userId = claims.get(USER_CLAIM, String.class);
// check if requested user exists // check if requested user exists
@ -174,20 +174,24 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
// login the user by getting access token // login the user by getting access token
final Map<String, String> params = new HashMap<>(); final Map<String, String> params = new HashMap<>();
params.put("grant_type", "password"); params.put(Constants.OAUTH2_GRANT_TYPE, Constants.OAUTH2_GRANT_TYPE_PASSWORD);
params.put("username", user.username); params.put(Constants.OAUTH2_USER_NAME, user.username);
params.put("password", user.uuid); params.put(Constants.OAUTH2_GRANT_TYPE_PASSWORD, claims.get(SUBJECT_CLAIM_NAME, String.class));
//final WebAuthenticationDetails details = new WebAuthenticationDetails("localhost", null);
final UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = final UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken( new UsernamePasswordAuthenticationToken(
this.adminAPIClientDetails, // TODO are this the correct details? this.adminAPIClientDetails.getClientId(),
null, "N/A",
Collections.emptyList()); Collections.emptyList());
final ResponseEntity<OAuth2AccessToken> accessToken = final ResponseEntity<OAuth2AccessToken> accessToken =
this.tokenEndpoint.postAccessToken(usernamePasswordAuthenticationToken, params); this.tokenEndpoint.postAccessToken(usernamePasswordAuthenticationToken, params);
final OAuth2AccessToken token = accessToken.getBody(); 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) { private String createOneTimeToken(final UserInfo account, final Long examId) {
// create a subject claim for this token only
final String subjectClaim = UUID.randomUUID().toString(); final String subjectClaim = UUID.randomUUID().toString();
this.storeSubjectForExam(examId, account.uuid, subjectClaim); userDAO.changePassword(account.uuid, subjectClaim);
final Map<String, Object> claims = new HashMap<>(); final Map<String, Object> claims = new HashMap<>();
claims.put(USER_CLAIM, account.uuid); claims.put(USER_CLAIM, account.uuid);
@ -276,38 +279,13 @@ public class TeacherAccountServiceImpl implements TeacherAccountService {
final Long examPK = Long.parseLong(examId); final Long examPK = Long.parseLong(examId);
// check subject // 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); 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")); throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.UNAUTHORIZED.of("Token subject mismatch"));
} }
return claims; 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) { private UserInfo synchronizeSPSUserForExam(final UserInfo account, final Long examId) {
if (this.screenProctoringService.isScreenProctoringEnabled(examId)) { if (this.screenProctoringService.isScreenProctoringEnabled(examId)) {
this.screenProctoringService.synchronizeSPSUserForExam(examId); this.screenProctoringService.synchronizeSPSUserForExam(examId);

View file

@ -233,7 +233,7 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
@CacheEvict( @CacheEvict(
cacheNames = ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM, cacheNames = ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM,
key = "#examId") key = "#exam.id")
Result<Exam> applySupporter(Exam exam, String userUUID); Result<Exam> applySupporter(Exam exam, String userUUID);
/** This is used by the internal update process to mark exams for which the LMS related data availability /** This is used by the internal update process to mark exams for which the LMS related data availability

View file

@ -552,7 +552,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
IOUtils.closeQuietly(out); 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); .getOr(exam);
} }

View file

@ -379,7 +379,7 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
multiPartAttributes.add("token", this.accessToken); multiPartAttributes.add("token", this.accessToken);
return super.postForObject( return super.postForObject(
uploadEndpoint, uri,
multiPartAttributes, multiPartAttributes,
String.class); String.class);
} }

View file

@ -14,6 +14,7 @@ import java.util.List;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.NoResourceFoundException;
import org.apache.catalina.connector.ClientAbortException; import org.apache.catalina.connector.ClientAbortException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -61,7 +62,7 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
((OnlyMessageLogExceptionWrapper) ex).log(log); ((OnlyMessageLogExceptionWrapper) ex).log(log);
return new ResponseEntity<>(status); return new ResponseEntity<>(status);
} else { } 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<APIMessage> errors = Arrays.asList(APIMessage.ErrorMessage.GENERIC.of(ex.getMessage())); final List<APIMessage> errors = Arrays.asList(APIMessage.ErrorMessage.GENERIC.of(ex.getMessage()));
@ -160,6 +161,15 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST); return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
} }
@ExceptionHandler(NoResourceFoundException.class)
public ResponseEntity<Object> handleNoResourceFoundException(
final NoResourceFoundException ex,
final WebRequest request) {
return APIMessage.ErrorMessage.RESOURCE_NOT_FOUND
.createErrorResponse(ex.getMessage());
}
@ExceptionHandler(ResourceNotFoundException.class) @ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<Object> handleResourceNotFoundException( public ResponseEntity<Object> handleResourceNotFoundException(
final ResourceNotFoundException ex, final ResourceNotFoundException ex,

View file

@ -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<TokenLoginInfo> tokenLoginInfoResult = teacherAccountService
.verifyOneTimeTokenForTeacherAccount(loginToken);
if (tokenLoginInfoResult.hasError()) {
throw new APIMessage.APIMessageException(
APIMessage.ErrorMessage.UNAUTHORIZED.of(tokenLoginInfoResult.getError()));
}
return tokenLoginInfoResult.get();
}
}

View file

@ -192,8 +192,7 @@ public class ClientConnectionController extends ReadonlyEntityController<ClientC
this.authorization.checkRole( this.authorization.checkRole(
institution, institution,
EntityType.CLIENT_EVENT, EntityType.CLIENT_EVENT,
UserRole.EXAM_ADMIN, UserRole.EXAM_ADMIN, UserRole.EXAM_SUPPORTER, UserRole.TEACHER);
UserRole.EXAM_SUPPORTER);
} }
private Result<Collection<ClientConnectionData>> getAllData(final FilterMap filterMap) { private Result<Collection<ClientConnectionData>> getAllData(final FilterMap filterMap) {

View file

@ -221,7 +221,9 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
.getUserService() .getUserService()
.getCurrentUser() .getCurrentUser()
.getUserRoles(); .getUserRoles();
final boolean isSupporterOnly = userRoles.size() == 1 && userRoles.contains(UserRole.EXAM_SUPPORTER); final boolean isSupporterOnly = userRoles.size() == 1 &&
(userRoles.contains(UserRole.EXAM_SUPPORTER) || userRoles.contains(UserRole.TEACHER));
return Result.tryCatch(() -> { return Result.tryCatch(() -> {

View file

@ -169,7 +169,7 @@ public class ExamMonitoringController {
this.authorization.checkRole( this.authorization.checkRole(
institutionId, institutionId,
EntityType.EXAM, EntityType.EXAM,
UserRole.EXAM_SUPPORTER, UserRole.EXAM_SUPPORTER, UserRole.TEACHER,
UserRole.EXAM_ADMIN); UserRole.EXAM_ADMIN);
final FilterMap filterMap = new FilterMap(allRequestParams, request.getQueryString()); final FilterMap filterMap = new FilterMap(allRequestParams, request.getQueryString());
@ -230,7 +230,7 @@ public class ExamMonitoringController {
this.authorization.checkRole( this.authorization.checkRole(
institutionId, institutionId,
EntityType.EXAM, EntityType.EXAM,
UserRole.EXAM_SUPPORTER, UserRole.EXAM_SUPPORTER, UserRole.TEACHER,
UserRole.EXAM_ADMIN); UserRole.EXAM_ADMIN);
final FilterMap filterMap = new FilterMap(allRequestParams, request.getQueryString()); final FilterMap filterMap = new FilterMap(allRequestParams, request.getQueryString());
@ -511,7 +511,7 @@ public class ExamMonitoringController {
this.authorization.checkRole( this.authorization.checkRole(
institutionId, institutionId,
EntityType.EXAM, EntityType.EXAM,
UserRole.EXAM_SUPPORTER, UserRole.EXAM_SUPPORTER, UserRole.TEACHER,
UserRole.EXAM_ADMIN); UserRole.EXAM_ADMIN);
// check exam running // check exam running

View file

@ -90,8 +90,8 @@ public class LmsIntegrationController {
final EntityKey examID = fullLmsIntegrationService.deleteExam(lmsUUId, courseId, quizId) final EntityKey examID = fullLmsIntegrationService.deleteExam(lmsUUId, courseId, quizId)
.onError(e -> log.error( .onError(e -> log.error(
"Failed to delete exam: lmsId:{}, courseId: {}, quizId: {}", "Failed to delete exam: lmsId:{}, courseId: {}, quizId: {}, error: {}",
lmsUUId, courseId, quizId, e)) lmsUUId, courseId, quizId, e.getMessage()))
.getOrThrow(); .getOrThrow();
log.info("Auto delete of exam successful: {}", examID); log.info("Auto delete of exam successful: {}", examID);
@ -141,8 +141,7 @@ public class LmsIntegrationController {
@RequestMapping( @RequestMapping(
path = API.LMS_FULL_INTEGRATION_LOGIN_TOKEN_ENDPOINT, path = API.LMS_FULL_INTEGRATION_LOGIN_TOKEN_ENDPOINT,
method = RequestMethod.POST, method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public FullLmsIntegrationService.TokenLoginResponse getOneTimeLoginToken( public FullLmsIntegrationService.TokenLoginResponse getOneTimeLoginToken(
@RequestParam(name = API.LMS_FULL_INTEGRATION_LMS_UUID) final String lmsUUId, @RequestParam(name = API.LMS_FULL_INTEGRATION_LMS_UUID) final String lmsUUId,
@RequestParam(name = API.LMS_FULL_INTEGRATION_COURSE_ID) final String courseId, @RequestParam(name = API.LMS_FULL_INTEGRATION_COURSE_ID) final String courseId,
@ -166,7 +165,7 @@ public class LmsIntegrationController {
final String token = this.fullLmsIntegrationService final String token = this.fullLmsIntegrationService
.getOneTimeLoginToken(lmsUUId, courseId, quizId, adHocAccountData) .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(); .getOrThrow();
return new FullLmsIntegrationService.TokenLoginResponse( return new FullLmsIntegrationService.TokenLoginResponse(

View file

@ -68,7 +68,7 @@ public class RegisterUserController {
this.userActivityLogDAO = userActivityLogDAO; this.userActivityLogDAO = userActivityLogDAO;
this.userDAO = userDAO; this.userDAO = userDAO;
this.beanValidationService = beanValidationService; 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.autoActivation = BooleanUtils.isTrue(features.get(UserFeatures.Feature.ADMIN_USER_ACCOUNT_SELF_REGISTERING_AUTO_ACTIVATION.featureName));
this.requestRateLimitBucket = rateLimitService.createRequestLimitBucker(); this.requestRateLimitBucket = rateLimitService.createRequestLimitBucker();
this.createRateLimitBucket = rateLimitService.createCreationLimitBucker(); this.createRateLimitBucket = rateLimitService.createCreationLimitBucker();

View file

@ -17,12 +17,14 @@ import java.util.Collection;
import javax.servlet.ServletOutputStream; import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConnectionConfigurationChangeEvent;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.joda.time.DateTimeZone; import org.joda.time.DateTimeZone;
import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.SqlTable;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableAsync;
@ -62,6 +64,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe
public class SEBClientConfigController extends ActivatableEntityController<SEBClientConfig, SEBClientConfig> { public class SEBClientConfigController extends ActivatableEntityController<SEBClientConfig, SEBClientConfig> {
private final ConnectionConfigurationService sebConnectionConfigurationService; private final ConnectionConfigurationService sebConnectionConfigurationService;
private final ApplicationEventPublisher applicationEventPublisher;
public SEBClientConfigController( public SEBClientConfigController(
final SEBClientConfigDAO sebClientConfigDAO, final SEBClientConfigDAO sebClientConfigDAO,
@ -70,7 +73,8 @@ public class SEBClientConfigController extends ActivatableEntityController<SEBCl
final BulkActionService bulkActionService, final BulkActionService bulkActionService,
final PaginationService paginationService, final PaginationService paginationService,
final BeanValidationService beanValidationService, final BeanValidationService beanValidationService,
final ConnectionConfigurationService sebConnectionConfigurationService) { final ConnectionConfigurationService sebConnectionConfigurationService,
final ApplicationEventPublisher applicationEventPublisher) {
super(authorization, super(authorization,
bulkActionService, bulkActionService,
@ -80,6 +84,7 @@ public class SEBClientConfigController extends ActivatableEntityController<SEBCl
beanValidationService); beanValidationService);
this.sebConnectionConfigurationService = sebConnectionConfigurationService; this.sebConnectionConfigurationService = sebConnectionConfigurationService;
this.applicationEventPublisher = applicationEventPublisher;
} }
@RequestMapping( @RequestMapping(
@ -183,6 +188,10 @@ public class SEBClientConfigController extends ActivatableEntityController<SEBCl
if (entity.isActive()) { if (entity.isActive()) {
// try to get access token for SEB client // try to get access token for SEB client
this.sebConnectionConfigurationService.initialCheckAccess(entity); this.sebConnectionConfigurationService.initialCheckAccess(entity);
// notify all
applicationEventPublisher.publishEvent(new ConnectionConfigurationChangeEvent(
entity.institutionId,
entity.id));
} }
return super.notifySaved(entity); return super.notifySaved(entity);
} }

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.weblayer.oauth;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import ch.ethz.seb.sebserver.gbl.api.API;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -19,6 +20,8 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
@ -113,4 +116,6 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap
.tokenServices(defaultTokenServices); .tokenServices(defaultTokenServices);
} }
} }