diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/Institution.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/Institution.java new file mode 100644 index 00000000..7f55a6c1 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/Institution.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gbl.model.institution; + +public class Institution { + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserActivityLog.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserActivityLog.java index cc075c51..d48ae52d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserActivityLog.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserActivityLog.java @@ -65,7 +65,7 @@ public class UserActivityLog implements Entity { return (this.id != null) ? String.valueOf(this.id) : null; } - public String getUserUUID() { + public String getUserUuid() { return this.userUUID; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserInfo.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserInfo.java index 5b2d5579..e2233956 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserInfo.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserInfo.java @@ -136,7 +136,7 @@ public final class UserInfo implements GrantEntity, Serializable { @JsonIgnore @Override public String getOwnerUUID() { - return null; + return this.uuid; } public String getName() { diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserMod.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserMod.java index b6fc1bc4..12585f34 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserMod.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserMod.java @@ -8,26 +8,66 @@ package ch.ethz.seb.sebserver.gbl.model.user; +import javax.validation.constraints.Size; + import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -public final class UserMod { +import ch.ethz.seb.sebserver.gbl.model.EntityType; +import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.GrantEntity; +public final class UserMod implements GrantEntity { + + public static final String ATTR_NAME_USER_INFO = "userInfo"; + public static final String ATTR_NAME_NEW_PASSWORD = "newPassword"; + public static final String ATTR_NAME_RETYPED_NEW_PASSWORD = "retypedNewPassword"; + + @JsonProperty(ATTR_NAME_USER_INFO) private final UserInfo userInfo; + + @Size(min = 8, max = 255, message = "userInfo:password:size:{min}:{max}:${validatedValue}") + @JsonProperty(ATTR_NAME_NEW_PASSWORD) private final String newPassword; + + @JsonProperty(ATTR_NAME_RETYPED_NEW_PASSWORD) private final String retypedNewPassword; @JsonCreator public UserMod( - @JsonProperty("userInfo") final UserInfo userInfo, - @JsonProperty("newPassword") final String newPassword, - @JsonProperty("retypedNewPassword") final String retypedNewPassword) { + @JsonProperty(ATTR_NAME_USER_INFO) final UserInfo userInfo, + @JsonProperty(ATTR_NAME_NEW_PASSWORD) final String newPassword, + @JsonProperty(ATTR_NAME_RETYPED_NEW_PASSWORD) final String retypedNewPassword) { this.userInfo = userInfo; this.newPassword = newPassword; this.retypedNewPassword = retypedNewPassword; } + @Override + @JsonIgnore + public String getId() { + return this.userInfo.getId(); + } + + @Override + @JsonIgnore + public EntityType entityType() { + return this.userInfo.entityType(); + } + + @Override + @JsonIgnore + public Long getInstitutionId() { + return this.userInfo.getInstitutionId(); + } + + @Override + @JsonIgnore + public String getOwnerUUID() { + return this.userInfo.getOwnerUUID(); + } + public UserInfo getUserInfo() { return this.userInfo; } @@ -81,5 +121,4 @@ public final class UserMod { public String toString() { return "UserMod [userInfo=" + this.userInfo + "]"; } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Tuple.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Tuple.java new file mode 100644 index 00000000..269f004f --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Tuple.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gbl.util; + +public class Tuple { + + public final T _1; + public final T _2; + + public Tuple(final T _1, final T _2) { + super(); + this._1 = _1; + this._2 = _2; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this._1 == null) ? 0 : this._1.hashCode()); + result = prime * result + ((this._2 == null) ? 0 : this._2.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + @SuppressWarnings("rawtypes") + final Tuple other = (Tuple) obj; + if (this._1 == null) { + if (other._1 != null) + return false; + } else if (!this._1.equals(other._1)) + return false; + if (this._2 == null) { + if (other._2 != null) + return false; + } else if (!this._2.equals(other._2)) + return false; + return true; + } + + @Override + public String toString() { + return "( " + this._1 + ", " + this._2 + ")"; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/AuthorizationGrantServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/AuthorizationGrantServiceImpl.java index 573796ee..f9299011 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/AuthorizationGrantServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/AuthorizationGrantServiceImpl.java @@ -85,7 +85,9 @@ public class AuthorizationGrantServiceImpl implements AuthorizationGrantService .andForRole(UserRole.INSTITUTIONAL_ADMIN) .withInstitutionalPrivilege(PrivilegeType.WRITE) .andForRole(UserRole.EXAM_ADMIN) + .withOwnerPrivilege(PrivilegeType.MODIFY) .andForRole(UserRole.EXAM_SUPPORTER) + .withOwnerPrivilege(PrivilegeType.MODIFY) .create(); // grants for user activity logs diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserActivityLogDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserActivityLogDAO.java index 076e950f..299caaa2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserActivityLogDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserActivityLogDAO.java @@ -28,6 +28,19 @@ public interface UserActivityLogDAO extends UserRelatedEntityDAO Result logUserActivity(ActivityType actionType, E entity, String message); + + /** Creates a user activity log entry for the current user. + * + * @param actionType the action type + * @param entity the Entity */ + Result logUserActivity(ActivityType actionType, E entity); + /** Creates a user activity log entry. * * @param user for specified SEBServerUser instance diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserActivityLogDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserActivityLogDAOImpl.java index 2c4965ba..7630d665 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserActivityLogDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserActivityLogDAOImpl.java @@ -62,6 +62,20 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { return EntityType.USER_ACTIVITY_LOG; } + @Override + public Result logUserActivity( + final ActivityType actionType, + final E entity, + final String message) { + + return logUserActivity(this.userService.getCurrentUser(), actionType, entity, message); + } + + @Override + public Result logUserActivity(final ActivityType actionType, final E entity) { + return logUserActivity(this.userService.getCurrentUser(), actionType, entity, null); + } + @Override @Transactional public Result logUserActivity( @@ -72,7 +86,7 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { try { - this.userLogRecordMapper.insert(new UserActivityLogRecord( + this.userLogRecordMapper.insertSelective(new UserActivityLogRecord( null, user.getUserInfo().uuid, System.currentTimeMillis(), diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserDaoImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserDaoImpl.java index d7f4b9d5..cc54c94f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserDaoImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserDaoImpl.java @@ -25,12 +25,15 @@ import org.apache.commons.lang3.BooleanUtils; import org.joda.time.DateTimeZone; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Lazy; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.interceptor.TransactionInterceptor; import org.springframework.util.CollectionUtils; +import ch.ethz.seb.sebserver.WebSecurityConfig; import ch.ethz.seb.sebserver.gbl.model.EntityType; import ch.ethz.seb.sebserver.gbl.model.user.UserFilter; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; @@ -57,13 +60,16 @@ public class UserDaoImpl implements UserDAO { private final UserRecordMapper userRecordMapper; private final RoleRecordMapper roleRecordMapper; + private final PasswordEncoder userPasswordEncoder; public UserDaoImpl( final UserRecordMapper userRecordMapper, - final RoleRecordMapper roleRecordMapper) { + final RoleRecordMapper roleRecordMapper, + @Qualifier(WebSecurityConfig.USER_PASSWORD_ENCODER_BEAN_NAME) final PasswordEncoder userPasswordEncoder) { this.userRecordMapper = userRecordMapper; this.roleRecordMapper = roleRecordMapper; + this.userPasswordEncoder = userPasswordEncoder; } @Override @@ -215,7 +221,7 @@ public class UserDaoImpl implements UserDAO { null, userInfo.name, userInfo.userName, - (changePWD) ? userMod.getNewPassword() : null, + (changePWD) ? this.userPasswordEncoder.encode(userMod.getNewPassword()) : null, userInfo.email, userInfo.locale.toLanguageTag(), userInfo.timeZone.getID(), @@ -230,9 +236,6 @@ public class UserDaoImpl implements UserDAO { private Result createNewUser(final SEBServerUser principal, final UserMod userMod) { final UserInfo userInfo = userMod.getUserInfo(); - if (userInfo.institutionId == null) { - return Result.ofError(new IllegalArgumentException("The users institution cannot be null")); - } if (!userMod.newPasswordMatch()) { return Result.ofError(new APIMessageException(ErrorMessage.PASSWORD_MISSMATCH)); @@ -244,7 +247,7 @@ public class UserDaoImpl implements UserDAO { UUID.randomUUID().toString(), userInfo.name, userInfo.userName, - userMod.getNewPassword(), + this.userPasswordEncoder.encode(userMod.getNewPassword()), userInfo.email, userInfo.locale.toLanguageTag(), userInfo.timeZone.getID(), @@ -345,5 +348,4 @@ public class UserDaoImpl implements UserDAO { userInfo, record.getPassword())); } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserAccountController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserAccountController.java index 59ecaf42..7b029c52 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserAccountController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserAccountController.java @@ -13,6 +13,7 @@ import java.util.Collection; import java.util.function.Predicate; import java.util.stream.Collectors; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -25,6 +26,7 @@ import ch.ethz.seb.sebserver.gbl.model.user.UserFilter; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserMod; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationGrantService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PrivilegeType; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.SEBServerUser; @@ -32,6 +34,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO.ActivityType; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO; +import ch.ethz.seb.sebserver.webservice.weblayer.oauth.RevokeTokenEndpoint; @WebServiceProfile @RestController @@ -42,17 +45,20 @@ public class UserAccountController { private final AuthorizationGrantService authorizationGrantService; private final UserService userService; private final UserActivityLogDAO userActivityLogDAO; + private final ApplicationEventPublisher applicationEventPublisher; public UserAccountController( final UserDAO userDao, final AuthorizationGrantService authorizationGrantService, final UserService userService, - final UserActivityLogDAO userActivityLogDAO) { + final UserActivityLogDAO userActivityLogDAO, + final ApplicationEventPublisher applicationEventPublisher) { this.userDao = userDao; this.authorizationGrantService = authorizationGrantService; this.userService = userService; this.userActivityLogDAO = userActivityLogDAO; + this.applicationEventPublisher = applicationEventPublisher; } @RequestMapping(method = RequestMethod.GET) @@ -123,7 +129,11 @@ public class UserAccountController { @RequestBody final UserMod userData, final Principal principal) { - return _saveUser(userData, principal, PrivilegeType.WRITE); + return _saveUser( + userData, + this.userService.extractFromPrincipal(principal), + PrivilegeType.WRITE) + .getOrThrow(); } @RequestMapping(value = "/save", method = RequestMethod.POST) @@ -131,31 +141,33 @@ public class UserAccountController { @RequestBody final UserMod userData, final Principal principal) { - return _saveUser(userData, principal, PrivilegeType.MODIFY); + return _saveUser( + userData, + this.userService.extractFromPrincipal(principal), + PrivilegeType.MODIFY) + .getOrThrow(); + } - private UserInfo _saveUser( + private Result _saveUser( final UserMod userData, - final Principal principal, - final PrivilegeType grantType) { + final SEBServerUser admin, + final PrivilegeType privilegeType) { - // fist check if current user has any privileges for this action - this.authorizationGrantService.checkHasAnyPrivilege( - EntityType.USER, - grantType); - - final SEBServerUser admin = this.userService.extractFromPrincipal(principal); final ActivityType actionType = (userData.getUserInfo().uuid == null) ? ActivityType.CREATE : ActivityType.MODIFY; - return this.userDao - .save(admin, userData) - .flatMap(userInfo -> this.userActivityLogDAO.logUserActivity( - admin, - actionType, - userInfo)) - .getOrThrow(); + return this.authorizationGrantService + .checkGrantOnEntity(userData, privilegeType) + .flatMap(data -> this.userDao.save(admin, data)) + .flatMap(userInfo -> this.userActivityLogDAO.logUserActivity(actionType, userInfo)) + .flatMap(userInfo -> { + // handle password change; revoke access tokens if password has changed + this.applicationEventPublisher.publishEvent( + new RevokeTokenEndpoint.RevokeTokenEvent(this, userInfo.userName)); + return Result.of(userInfo); + }); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/RevokeTokenEndpoint.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/RevokeTokenEndpoint.java new file mode 100644 index 00000000..82dade25 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/RevokeTokenEndpoint.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.weblayer.oauth; + +import java.util.Collection; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.event.EventListener; +import org.springframework.http.HttpStatus; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.provider.token.ConsumerTokenServices; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseStatus; + +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; + +@Controller +@WebServiceProfile +public class RevokeTokenEndpoint { + + private final ConsumerTokenServices tokenServices; + private final AdminAPIClientDetails adminAPIClientDetails; + private final TokenStore tokenStore; + + public RevokeTokenEndpoint( + final ConsumerTokenServices tokenServices, + final AdminAPIClientDetails adminAPIClientDetails, + final TokenStore tokenStore) { + + this.tokenServices = tokenServices; + this.adminAPIClientDetails = adminAPIClientDetails; + this.tokenStore = tokenStore; + } + + @RequestMapping(value = "/oauth/revoke-token", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.OK) + public void logout(final HttpServletRequest request) { + final String authHeader = request.getHeader("Authorization"); + if (authHeader != null) { + final String tokenId = authHeader.substring("Bearer".length() + 1); + this.tokenServices.revokeToken(tokenId); + } + } + + @EventListener(RevokeTokenEvent.class) + private void revokeAccessToken(final RevokeTokenEvent event) { + final String clientId = this.adminAPIClientDetails.getClientId(); + final Collection tokens = this.tokenStore + .findTokensByClientIdAndUserName(clientId, event.userName); + + if (tokens != null) { + for (final OAuth2AccessToken token : tokens) { + this.tokenStore.removeAccessToken(token); + } + } + } + + public static final class RevokeTokenEvent extends ApplicationEvent { + + private static final long serialVersionUID = 5776699085388043743L; + + public final String userName; + + public RevokeTokenEvent(final Object source, final String userName) { + super(source); + this.userName = userName; + } + + } + +} diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/UserAPITest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/UserAPITest.java index e94e3ea1..c1e11e04 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/UserAPITest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/UserAPITest.java @@ -9,8 +9,7 @@ package ch.ethz.seb.sebserver.webservice.integration.api; import static org.junit.Assert.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.util.Arrays; @@ -19,6 +18,8 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.NoSuchElementException; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.joda.time.DateTimeZone; import org.junit.Test; @@ -105,7 +106,7 @@ public class UserAPITest extends AdministrationAPIIntegrationTest { assertEquals( "{\"messageCode\":\"1001\"," + "\"systemMessage\":\"FORBIDDEN\"," - + "\"details\":\"No grant: READ_ONLY on type: USER entity institution: 1 entity owner: null for user: user3\"," + + "\"details\":\"No grant: READ_ONLY on type: USER entity institution: 1 entity owner: user1 for user: user3\"," + "\"attributes\":[]}", contentAsString); } @@ -146,8 +147,6 @@ public class UserAPITest extends AdministrationAPIIntegrationTest { assertNotNull(getUserInfo("admin", userInfos)); assertNotNull(getUserInfo("inst1Admin", userInfos)); assertNotNull(getUserInfo("examSupporter", userInfos)); - - // TODO more tests } @Test @@ -193,6 +192,15 @@ public class UserAPITest extends AdministrationAPIIntegrationTest { assertNotNull(getUserInfo("examSupporter", userInfos)); } + @Test + public void testOwnerGet() throws Exception { + final String examAdminToken1 = getExamAdmin1(); + this.mockMvc.perform(get(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/me") + .header("Authorization", "Bearer " + examAdminToken1)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + } + @Test public void createUserTest() throws Exception { final UserInfo userInfo = new UserInfo( @@ -247,6 +255,192 @@ public class UserAPITest extends AdministrationAPIIntegrationTest { assertEquals(createdUserGet.uuid, userActivityLog.entityId); } + @Test + public void modifyUserTest() throws Exception { + final String token = getSebAdminAccess(); + final UserInfo user = this.jsonMapper.readValue( + this.mockMvc.perform(get(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/user7") + .header("Authorization", "Bearer " + token)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), + new TypeReference() { + }); + + assertNotNull(user); + assertEquals("User", user.name); + assertEquals("user1", user.userName); + assertEquals("user@nomail.nomail", user.email); + assertEquals("[EXAM_SUPPORTER]", String.valueOf(user.roles)); + + // change userName, email and roles + final UserMod modifiedUser = new UserMod(new UserInfo( + user.getUuid(), + user.getInstitutionId(), + user.getName(), + "newUser1", + "newUser@nomail.nomail", + user.getActive(), + user.getLocale(), + user.getTimeZone(), + Stream.of(UserRole.EXAM_ADMIN.name(), UserRole.EXAM_SUPPORTER.name()).collect(Collectors.toSet())), + null, null); + final String modifiedUserJson = this.jsonMapper.writeValueAsString(modifiedUser); + + UserInfo modifiedUserResult = this.jsonMapper.readValue( + this.mockMvc.perform(post(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/save") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(modifiedUserJson)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), + new TypeReference() { + }); + + assertNotNull(modifiedUserResult); + assertEquals("User", modifiedUserResult.name); + assertEquals("newUser1", modifiedUserResult.userName); + assertEquals("newUser@nomail.nomail", modifiedUserResult.email); + assertEquals("[EXAM_ADMIN, EXAM_SUPPORTER]", String.valueOf(modifiedUserResult.roles)); + + // double check by getting the user by uuis + modifiedUserResult = this.jsonMapper.readValue( + this.mockMvc.perform(get(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/" + modifiedUserResult.uuid) + .header("Authorization", "Bearer " + token)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), + new TypeReference() { + }); + + assertNotNull(modifiedUserResult); + assertEquals("User", modifiedUserResult.name); + assertEquals("newUser1", modifiedUserResult.userName); + assertEquals("newUser@nomail.nomail", modifiedUserResult.email); + assertEquals("[EXAM_ADMIN, EXAM_SUPPORTER]", String.valueOf(modifiedUserResult.roles)); + } + + @Test + public void testOwnerModifyPossible() throws Exception { + final String examAdminToken1 = getExamAdmin1(); + final UserInfo examAdmin = this.jsonMapper.readValue( + this.mockMvc.perform(get(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/me") + .header("Authorization", "Bearer " + examAdminToken1)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), + new TypeReference() { + }); + + final UserMod modifiedUser = new UserMod(examAdmin, null, null); + final String modifiedUserJson = this.jsonMapper.writeValueAsString(modifiedUser); + + this.mockMvc.perform(post(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/save") + .header("Authorization", "Bearer " + examAdminToken1) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(modifiedUserJson)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + } + + @Test + public void institutionalAdminTryToCreateOrModifyUserForOtherInstituionNotPossible() throws Exception { + final UserInfo userInfo = new UserInfo( + null, 2L, "NewTestUser", "NewTestUser", + "", true, Locale.CANADA, DateTimeZone.UTC, + new HashSet<>(Arrays.asList(UserRole.EXAM_ADMIN.name()))); + final UserMod newUser = new UserMod(userInfo, "123", "123"); + final String newUserJson = this.jsonMapper.writeValueAsString(newUser); + + final String token = getAdminInstitution1Access(); + this.mockMvc.perform(put(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/create") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(newUserJson)) + .andExpect(status().isForbidden()) + .andReturn().getResponse().getContentAsString(); + + this.mockMvc.perform(post(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/save") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(newUserJson)) + .andExpect(status().isForbidden()) + .andReturn().getResponse().getContentAsString(); + } + + @Test + public void unauthorizedAdminTryToCreateUserNotPossible() throws Exception { + final UserInfo userInfo = new UserInfo( + null, 2L, "NewTestUser", "NewTestUser", + "", true, Locale.CANADA, DateTimeZone.UTC, + new HashSet<>(Arrays.asList(UserRole.EXAM_ADMIN.name()))); + final UserMod newUser = new UserMod(userInfo, "123", "123"); + final String newUserJson = this.jsonMapper.writeValueAsString(newUser); + + final String token = getExamAdmin1(); + this.mockMvc.perform(put(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/create") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(newUserJson)) + .andExpect(status().isForbidden()) + .andReturn().getResponse().getContentAsString(); + + this.mockMvc.perform(post(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/save") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(newUserJson)) + .andExpect(status().isForbidden()) + .andReturn().getResponse().getContentAsString(); + } + + @Test + public void modifyUserPassword() throws Exception { + final String examAdminToken1 = getExamAdmin1(); + assertNotNull(examAdminToken1); + + // a SEB Server Admin now changes the password of ExamAdmin1 + final String sebAdminToken = getSebAdminAccess(); + final UserInfo examAdmin1 = this.jsonMapper.readValue( + this.mockMvc.perform(get(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/user4") + .header("Authorization", "Bearer " + sebAdminToken)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), + new TypeReference() { + }); + + final UserMod modifiedUser = new UserMod( + UserInfo.of(examAdmin1), + "newPassword", + "newPassword"); + final String modifiedUserJson = this.jsonMapper.writeValueAsString(modifiedUser); + + this.mockMvc.perform(post(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/save") + .header("Authorization", "Bearer " + sebAdminToken) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(modifiedUserJson)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + // now it should not be possible to get a access token for ExamAdmin1 with the standard password + try { + getExamAdmin1(); + fail("AssertionError expected here"); + } catch (final AssertionError e) { + assertEquals("Status expected:<200> but was:<400>", e.getMessage()); + } + + // it should also not be possible to use an old token again after password change + this.mockMvc.perform(get(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/me") + .header("Authorization", "Bearer " + examAdminToken1)) + .andExpect(status().isUnauthorized()) + .andReturn().getResponse().getContentAsString(); + + // but it should be possible to get a new access token and request again + final String examAdminToken2 = obtainAccessToken("examAdmin1", "newPassword"); + this.mockMvc.perform(get(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/me") + .header("Authorization", "Bearer " + examAdminToken2)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + } + private UserInfo getUserInfo(final String name, final Collection infos) { return infos .stream()