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_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";

View file

@ -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;

View file

@ -50,7 +50,7 @@ public enum UserRole implements Entity, GrantedAuthority {
public static List<UserRole> publicRolesForUser(final UserInfo user) {
final EnumSet<UserRole> 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)) {

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

View file

@ -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(

View file

@ -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);

View file

@ -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);
}

View file

@ -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);

View file

@ -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);

View file

@ -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);
}

View file

@ -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<UserInfo> 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<String> 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<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

View file

@ -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();
}
}

View file

@ -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<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(
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<UserInfo> 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<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(
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<TokenLoginInfo> verifyOneTimeTokenForTeacherAccount(String token);
}

View file

@ -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

View file

@ -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<TokenLoginInfo> 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<String, String> 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<OAuth2AccessToken> 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<String, Object> 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);

View file

@ -233,7 +233,7 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
@CacheEvict(
cacheNames = ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM,
key = "#examId")
key = "#exam.id")
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

View file

@ -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);
}

View file

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

View file

@ -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<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);
}
@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)
public ResponseEntity<Object> handleResourceNotFoundException(
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(
institution,
EntityType.CLIENT_EVENT,
UserRole.EXAM_ADMIN,
UserRole.EXAM_SUPPORTER);
UserRole.EXAM_ADMIN, UserRole.EXAM_SUPPORTER, UserRole.TEACHER);
}
private Result<Collection<ClientConnectionData>> getAllData(final FilterMap filterMap) {

View file

@ -221,7 +221,9 @@ public class ClientEventController extends ReadonlyEntityController<ClientEvent,
.getUserService()
.getCurrentUser()
.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(() -> {

View file

@ -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

View file

@ -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(

View file

@ -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();

View file

@ -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<SEBClientConfig, SEBClientConfig> {
private final ConnectionConfigurationService sebConnectionConfigurationService;
private final ApplicationEventPublisher applicationEventPublisher;
public SEBClientConfigController(
final SEBClientConfigDAO sebClientConfigDAO,
@ -70,7 +73,8 @@ public class SEBClientConfigController extends ActivatableEntityController<SEBCl
final BulkActionService bulkActionService,
final PaginationService paginationService,
final BeanValidationService beanValidationService,
final ConnectionConfigurationService sebConnectionConfigurationService) {
final ConnectionConfigurationService sebConnectionConfigurationService,
final ApplicationEventPublisher applicationEventPublisher) {
super(authorization,
bulkActionService,
@ -80,6 +84,7 @@ public class SEBClientConfigController extends ActivatableEntityController<SEBCl
beanValidationService);
this.sebConnectionConfigurationService = sebConnectionConfigurationService;
this.applicationEventPublisher = applicationEventPublisher;
}
@RequestMapping(
@ -183,6 +188,10 @@ public class SEBClientConfigController extends ActivatableEntityController<SEBCl
if (entity.isActive()) {
// try to get access token for SEB client
this.sebConnectionConfigurationService.initialCheckAccess(entity);
// notify all
applicationEventPublisher.publishEvent(new ConnectionConfigurationChangeEvent(
entity.institutionId,
entity.id));
}
return super.notifySaved(entity);
}

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.weblayer.oauth;
import javax.servlet.http.HttpServletResponse;
import ch.ethz.seb.sebserver.gbl.api.API;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.http.MediaType;
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.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
@ -113,4 +116,6 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap
.tokenServices(defaultTokenServices);
}
}