SEBSERV-8 #more tests and fixes
This commit is contained in:
parent
b7a6bd831b
commit
ad9865bfa3
12 changed files with 469 additions and 39 deletions
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
|
@ -65,7 +65,7 @@ public class UserActivityLog implements Entity {
|
||||||
return (this.id != null) ? String.valueOf(this.id) : null;
|
return (this.id != null) ? String.valueOf(this.id) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUserUUID() {
|
public String getUserUuid() {
|
||||||
return this.userUUID;
|
return this.userUUID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -136,7 +136,7 @@ public final class UserInfo implements GrantEntity, Serializable {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
@Override
|
@Override
|
||||||
public String getOwnerUUID() {
|
public String getOwnerUUID() {
|
||||||
return null;
|
return this.uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
|
|
|
@ -8,26 +8,66 @@
|
||||||
|
|
||||||
package ch.ethz.seb.sebserver.gbl.model.user;
|
package ch.ethz.seb.sebserver.gbl.model.user;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
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;
|
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;
|
private final String newPassword;
|
||||||
|
|
||||||
|
@JsonProperty(ATTR_NAME_RETYPED_NEW_PASSWORD)
|
||||||
private final String retypedNewPassword;
|
private final String retypedNewPassword;
|
||||||
|
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
public UserMod(
|
public UserMod(
|
||||||
@JsonProperty("userInfo") final UserInfo userInfo,
|
@JsonProperty(ATTR_NAME_USER_INFO) final UserInfo userInfo,
|
||||||
@JsonProperty("newPassword") final String newPassword,
|
@JsonProperty(ATTR_NAME_NEW_PASSWORD) final String newPassword,
|
||||||
@JsonProperty("retypedNewPassword") final String retypedNewPassword) {
|
@JsonProperty(ATTR_NAME_RETYPED_NEW_PASSWORD) final String retypedNewPassword) {
|
||||||
|
|
||||||
this.userInfo = userInfo;
|
this.userInfo = userInfo;
|
||||||
this.newPassword = newPassword;
|
this.newPassword = newPassword;
|
||||||
this.retypedNewPassword = retypedNewPassword;
|
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() {
|
public UserInfo getUserInfo() {
|
||||||
return this.userInfo;
|
return this.userInfo;
|
||||||
}
|
}
|
||||||
|
@ -81,5 +121,4 @@ public final class UserMod {
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "UserMod [userInfo=" + this.userInfo + "]";
|
return "UserMod [userInfo=" + this.userInfo + "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
59
src/main/java/ch/ethz/seb/sebserver/gbl/util/Tuple.java
Normal file
59
src/main/java/ch/ethz/seb/sebserver/gbl/util/Tuple.java
Normal file
|
@ -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<T> {
|
||||||
|
|
||||||
|
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 + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -85,7 +85,9 @@ public class AuthorizationGrantServiceImpl implements AuthorizationGrantService
|
||||||
.andForRole(UserRole.INSTITUTIONAL_ADMIN)
|
.andForRole(UserRole.INSTITUTIONAL_ADMIN)
|
||||||
.withInstitutionalPrivilege(PrivilegeType.WRITE)
|
.withInstitutionalPrivilege(PrivilegeType.WRITE)
|
||||||
.andForRole(UserRole.EXAM_ADMIN)
|
.andForRole(UserRole.EXAM_ADMIN)
|
||||||
|
.withOwnerPrivilege(PrivilegeType.MODIFY)
|
||||||
.andForRole(UserRole.EXAM_SUPPORTER)
|
.andForRole(UserRole.EXAM_SUPPORTER)
|
||||||
|
.withOwnerPrivilege(PrivilegeType.MODIFY)
|
||||||
.create();
|
.create();
|
||||||
|
|
||||||
// grants for user activity logs
|
// grants for user activity logs
|
||||||
|
|
|
@ -28,6 +28,19 @@ public interface UserActivityLogDAO extends UserRelatedEntityDAO<UserActivityLog
|
||||||
DELETE
|
DELETE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates a user activity log entry for the current user.
|
||||||
|
*
|
||||||
|
* @param actionType the action type
|
||||||
|
* @param entity the Entity
|
||||||
|
* @param message an optional message */
|
||||||
|
<E extends Entity> Result<E> 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 */
|
||||||
|
<E extends Entity> Result<E> logUserActivity(ActivityType actionType, E entity);
|
||||||
|
|
||||||
/** Creates a user activity log entry.
|
/** Creates a user activity log entry.
|
||||||
*
|
*
|
||||||
* @param user for specified SEBServerUser instance
|
* @param user for specified SEBServerUser instance
|
||||||
|
|
|
@ -62,6 +62,20 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO {
|
||||||
return EntityType.USER_ACTIVITY_LOG;
|
return EntityType.USER_ACTIVITY_LOG;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Entity> Result<E> logUserActivity(
|
||||||
|
final ActivityType actionType,
|
||||||
|
final E entity,
|
||||||
|
final String message) {
|
||||||
|
|
||||||
|
return logUserActivity(this.userService.getCurrentUser(), actionType, entity, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Entity> Result<E> logUserActivity(final ActivityType actionType, final E entity) {
|
||||||
|
return logUserActivity(this.userService.getCurrentUser(), actionType, entity, null);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public <E extends Entity> Result<E> logUserActivity(
|
public <E extends Entity> Result<E> logUserActivity(
|
||||||
|
@ -72,7 +86,7 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
this.userLogRecordMapper.insert(new UserActivityLogRecord(
|
this.userLogRecordMapper.insertSelective(new UserActivityLogRecord(
|
||||||
null,
|
null,
|
||||||
user.getUserInfo().uuid,
|
user.getUserInfo().uuid,
|
||||||
System.currentTimeMillis(),
|
System.currentTimeMillis(),
|
||||||
|
|
|
@ -25,12 +25,15 @@ import org.apache.commons.lang3.BooleanUtils;
|
||||||
import org.joda.time.DateTimeZone;
|
import org.joda.time.DateTimeZone;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
||||||
import org.springframework.util.CollectionUtils;
|
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.EntityType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.UserFilter;
|
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.UserInfo;
|
||||||
|
@ -57,13 +60,16 @@ public class UserDaoImpl implements UserDAO {
|
||||||
|
|
||||||
private final UserRecordMapper userRecordMapper;
|
private final UserRecordMapper userRecordMapper;
|
||||||
private final RoleRecordMapper roleRecordMapper;
|
private final RoleRecordMapper roleRecordMapper;
|
||||||
|
private final PasswordEncoder userPasswordEncoder;
|
||||||
|
|
||||||
public UserDaoImpl(
|
public UserDaoImpl(
|
||||||
final UserRecordMapper userRecordMapper,
|
final UserRecordMapper userRecordMapper,
|
||||||
final RoleRecordMapper roleRecordMapper) {
|
final RoleRecordMapper roleRecordMapper,
|
||||||
|
@Qualifier(WebSecurityConfig.USER_PASSWORD_ENCODER_BEAN_NAME) final PasswordEncoder userPasswordEncoder) {
|
||||||
|
|
||||||
this.userRecordMapper = userRecordMapper;
|
this.userRecordMapper = userRecordMapper;
|
||||||
this.roleRecordMapper = roleRecordMapper;
|
this.roleRecordMapper = roleRecordMapper;
|
||||||
|
this.userPasswordEncoder = userPasswordEncoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -215,7 +221,7 @@ public class UserDaoImpl implements UserDAO {
|
||||||
null,
|
null,
|
||||||
userInfo.name,
|
userInfo.name,
|
||||||
userInfo.userName,
|
userInfo.userName,
|
||||||
(changePWD) ? userMod.getNewPassword() : null,
|
(changePWD) ? this.userPasswordEncoder.encode(userMod.getNewPassword()) : null,
|
||||||
userInfo.email,
|
userInfo.email,
|
||||||
userInfo.locale.toLanguageTag(),
|
userInfo.locale.toLanguageTag(),
|
||||||
userInfo.timeZone.getID(),
|
userInfo.timeZone.getID(),
|
||||||
|
@ -230,9 +236,6 @@ public class UserDaoImpl implements UserDAO {
|
||||||
|
|
||||||
private Result<UserInfo> createNewUser(final SEBServerUser principal, final UserMod userMod) {
|
private Result<UserInfo> createNewUser(final SEBServerUser principal, final UserMod userMod) {
|
||||||
final UserInfo userInfo = userMod.getUserInfo();
|
final UserInfo userInfo = userMod.getUserInfo();
|
||||||
if (userInfo.institutionId == null) {
|
|
||||||
return Result.ofError(new IllegalArgumentException("The users institution cannot be null"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!userMod.newPasswordMatch()) {
|
if (!userMod.newPasswordMatch()) {
|
||||||
return Result.ofError(new APIMessageException(ErrorMessage.PASSWORD_MISSMATCH));
|
return Result.ofError(new APIMessageException(ErrorMessage.PASSWORD_MISSMATCH));
|
||||||
|
@ -244,7 +247,7 @@ public class UserDaoImpl implements UserDAO {
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
userInfo.name,
|
userInfo.name,
|
||||||
userInfo.userName,
|
userInfo.userName,
|
||||||
userMod.getNewPassword(),
|
this.userPasswordEncoder.encode(userMod.getNewPassword()),
|
||||||
userInfo.email,
|
userInfo.email,
|
||||||
userInfo.locale.toLanguageTag(),
|
userInfo.locale.toLanguageTag(),
|
||||||
userInfo.timeZone.getID(),
|
userInfo.timeZone.getID(),
|
||||||
|
@ -345,5 +348,4 @@ public class UserDaoImpl implements UserDAO {
|
||||||
userInfo,
|
userInfo,
|
||||||
record.getPassword()));
|
record.getPassword()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import java.util.Collection;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
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.UserInfo;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.UserMod;
|
import ch.ethz.seb.sebserver.gbl.model.user.UserMod;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
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.AuthorizationGrantService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PrivilegeType;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PrivilegeType;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.SEBServerUser;
|
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;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO.ActivityType;
|
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.servicelayer.dao.UserDAO;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.weblayer.oauth.RevokeTokenEndpoint;
|
||||||
|
|
||||||
@WebServiceProfile
|
@WebServiceProfile
|
||||||
@RestController
|
@RestController
|
||||||
|
@ -42,17 +45,20 @@ public class UserAccountController {
|
||||||
private final AuthorizationGrantService authorizationGrantService;
|
private final AuthorizationGrantService authorizationGrantService;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final UserActivityLogDAO userActivityLogDAO;
|
private final UserActivityLogDAO userActivityLogDAO;
|
||||||
|
private final ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
public UserAccountController(
|
public UserAccountController(
|
||||||
final UserDAO userDao,
|
final UserDAO userDao,
|
||||||
final AuthorizationGrantService authorizationGrantService,
|
final AuthorizationGrantService authorizationGrantService,
|
||||||
final UserService userService,
|
final UserService userService,
|
||||||
final UserActivityLogDAO userActivityLogDAO) {
|
final UserActivityLogDAO userActivityLogDAO,
|
||||||
|
final ApplicationEventPublisher applicationEventPublisher) {
|
||||||
|
|
||||||
this.userDao = userDao;
|
this.userDao = userDao;
|
||||||
this.authorizationGrantService = authorizationGrantService;
|
this.authorizationGrantService = authorizationGrantService;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.userActivityLogDAO = userActivityLogDAO;
|
this.userActivityLogDAO = userActivityLogDAO;
|
||||||
|
this.applicationEventPublisher = applicationEventPublisher;
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(method = RequestMethod.GET)
|
@RequestMapping(method = RequestMethod.GET)
|
||||||
|
@ -123,7 +129,11 @@ public class UserAccountController {
|
||||||
@RequestBody final UserMod userData,
|
@RequestBody final UserMod userData,
|
||||||
final Principal principal) {
|
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)
|
@RequestMapping(value = "/save", method = RequestMethod.POST)
|
||||||
|
@ -131,31 +141,33 @@ public class UserAccountController {
|
||||||
@RequestBody final UserMod userData,
|
@RequestBody final UserMod userData,
|
||||||
final Principal principal) {
|
final Principal principal) {
|
||||||
|
|
||||||
return _saveUser(userData, principal, PrivilegeType.MODIFY);
|
return _saveUser(
|
||||||
|
userData,
|
||||||
|
this.userService.extractFromPrincipal(principal),
|
||||||
|
PrivilegeType.MODIFY)
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private UserInfo _saveUser(
|
private Result<UserInfo> _saveUser(
|
||||||
final UserMod userData,
|
final UserMod userData,
|
||||||
final Principal principal,
|
final SEBServerUser admin,
|
||||||
final PrivilegeType grantType) {
|
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)
|
final ActivityType actionType = (userData.getUserInfo().uuid == null)
|
||||||
? ActivityType.CREATE
|
? ActivityType.CREATE
|
||||||
: ActivityType.MODIFY;
|
: ActivityType.MODIFY;
|
||||||
|
|
||||||
return this.userDao
|
return this.authorizationGrantService
|
||||||
.save(admin, userData)
|
.checkGrantOnEntity(userData, privilegeType)
|
||||||
.flatMap(userInfo -> this.userActivityLogDAO.logUserActivity(
|
.flatMap(data -> this.userDao.save(admin, data))
|
||||||
admin,
|
.flatMap(userInfo -> this.userActivityLogDAO.logUserActivity(actionType, userInfo))
|
||||||
actionType,
|
.flatMap(userInfo -> {
|
||||||
userInfo))
|
// handle password change; revoke access tokens if password has changed
|
||||||
.getOrThrow();
|
this.applicationEventPublisher.publishEvent(
|
||||||
|
new RevokeTokenEndpoint.RevokeTokenEvent(this, userInfo.userName));
|
||||||
|
return Result.of(userInfo);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<OAuth2AccessToken> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -9,8 +9,7 @@
|
||||||
package ch.ethz.seb.sebserver.webservice.integration.api;
|
package ch.ethz.seb.sebserver.webservice.integration.api;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -19,6 +18,8 @@ import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.joda.time.DateTimeZone;
|
import org.joda.time.DateTimeZone;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
@ -105,7 +106,7 @@ public class UserAPITest extends AdministrationAPIIntegrationTest {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"{\"messageCode\":\"1001\","
|
"{\"messageCode\":\"1001\","
|
||||||
+ "\"systemMessage\":\"FORBIDDEN\","
|
+ "\"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\":[]}",
|
+ "\"attributes\":[]}",
|
||||||
contentAsString);
|
contentAsString);
|
||||||
}
|
}
|
||||||
|
@ -146,8 +147,6 @@ public class UserAPITest extends AdministrationAPIIntegrationTest {
|
||||||
assertNotNull(getUserInfo("admin", userInfos));
|
assertNotNull(getUserInfo("admin", userInfos));
|
||||||
assertNotNull(getUserInfo("inst1Admin", userInfos));
|
assertNotNull(getUserInfo("inst1Admin", userInfos));
|
||||||
assertNotNull(getUserInfo("examSupporter", userInfos));
|
assertNotNull(getUserInfo("examSupporter", userInfos));
|
||||||
|
|
||||||
// TODO more tests
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -193,6 +192,15 @@ public class UserAPITest extends AdministrationAPIIntegrationTest {
|
||||||
assertNotNull(getUserInfo("examSupporter", userInfos));
|
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
|
@Test
|
||||||
public void createUserTest() throws Exception {
|
public void createUserTest() throws Exception {
|
||||||
final UserInfo userInfo = new UserInfo(
|
final UserInfo userInfo = new UserInfo(
|
||||||
|
@ -247,6 +255,192 @@ public class UserAPITest extends AdministrationAPIIntegrationTest {
|
||||||
assertEquals(createdUserGet.uuid, userActivityLog.entityId);
|
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<UserInfo>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
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<UserInfo>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
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<UserInfo>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
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<UserInfo>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
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<UserInfo>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
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<UserInfo> infos) {
|
private UserInfo getUserInfo(final String name, final Collection<UserInfo> infos) {
|
||||||
return infos
|
return infos
|
||||||
.stream()
|
.stream()
|
||||||
|
|
Loading…
Reference in a new issue