diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserFilter.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserFilter.java index 992d102b..371a5734 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserFilter.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserFilter.java @@ -8,8 +8,6 @@ package ch.ethz.seb.sebserver.gbl.model.user; -import javax.validation.constraints.NotNull; - import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; @@ -95,16 +93,4 @@ public final class UserFilter { + this.email + ", active=" + this.active + ", locale=" + this.locale + "]"; } - public static UserFilter ofActive() { - return new UserFilter(null, null, null, null, true, null); - } - - public static UserFilter ofInactive() { - return new UserFilter(null, null, null, null, false, null); - } - - public static UserFilter ofInstitution(@NotNull final Long institutionId) { - return new UserFilter(institutionId, null, null, null, true, null); - } - } 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 c5312834..7267cef3 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 @@ -13,11 +13,6 @@ import java.util.Collections; import java.util.Locale; import java.util.Set; -import javax.validation.constraints.Email; -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - import org.apache.commons.lang3.BooleanUtils; import org.joda.time.DateTimeZone; @@ -25,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import ch.ethz.seb.sebserver.gbl.model.Activatable; import ch.ethz.seb.sebserver.gbl.model.Domain.USER; import ch.ethz.seb.sebserver.gbl.model.Domain.USER_ROLE; import ch.ethz.seb.sebserver.gbl.model.EntityType; @@ -36,7 +32,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.GrantEntity; * to and from JSON within the Jackson library. * * This domain model is immutable and thread-save */ -public final class UserInfo implements GrantEntity, Serializable { +public final class UserInfo implements GrantEntity, Activatable, Serializable { private static final long serialVersionUID = 2526446136264377808L; @@ -45,45 +41,34 @@ public final class UserInfo implements GrantEntity, Serializable { public final String uuid; /** The foreign key identifier to the institution where the User belongs to */ - @NotNull @JsonProperty(USER.ATTR_INSTITUTION_ID) public final Long institutionId; /** Full name of the user */ - @NotNull - @Size(min = 3, max = 255, message = "user:name:size:{min}:{max}:${validatedValue}") @JsonProperty(USER.ATTR_NAME) public final String name; /** The internal user name */ - @NotNull - @Size(min = 3, max = 255, message = "user:username:size:{min}:{max}:${validatedValue}") @JsonProperty(USER.ATTR_USER_NAME) public final String userName; /** E-mail address of the user */ - @Email(message = "user:email:email:_:_:${validatedValue}") @JsonProperty(USER.ATTR_EMAIL) public final String email; /** Indicates whether this user is still active or not */ - @NotNull @JsonProperty(USER.ATTR_ACTIVE) public final Boolean active; /** The users locale */ - @NotNull @JsonProperty(USER.ATTR_LOCALE) public final Locale locale; /** The users time zone */ - @NotNull @JsonProperty(USER.ATTR_TIMEZONE) public final DateTimeZone timeZone; /** The users roles in a unmodifiable set. Is never null */ - @NotNull - @NotEmpty(message = "user:roles:notEmpty:_:_:_") @JsonProperty(USER_ROLE.REFERENCE_NAME) public final Set roles; @@ -155,6 +140,12 @@ public final class UserInfo implements GrantEntity, Serializable { return this.active; } + @JsonIgnore + @Override + public boolean isActive() { + return this.active; + } + public Locale getLocale() { return this.locale; } 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 25b657f0..5ddb1a16 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,23 +8,70 @@ package ch.ethz.seb.sebserver.gbl.model.user; +import java.util.Collections; +import java.util.Locale; +import java.util.Set; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; +import org.joda.time.DateTimeZone; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import ch.ethz.seb.sebserver.gbl.model.Domain.USER; +import ch.ethz.seb.sebserver.gbl.model.Domain.USER_ROLE; 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; + public final String uuid; + + /** The foreign key identifier to the institution where the User belongs to */ + @NotNull + @JsonProperty(USER.ATTR_INSTITUTION_ID) + public final Long institutionId; + + /** Full name of the user */ + @NotNull + @Size(min = 3, max = 255, message = "user:name:size:{min}:{max}:${validatedValue}") + @JsonProperty(USER.ATTR_NAME) + public final String name; + + /** The internal user name */ + @NotNull + @Size(min = 3, max = 255, message = "user:username:size:{min}:{max}:${validatedValue}") + @JsonProperty(USER.ATTR_USER_NAME) + public final String userName; + + /** E-mail address of the user */ + @Email(message = "user:email:email:_:_:${validatedValue}") + @JsonProperty(USER.ATTR_EMAIL) + public final String email; + + /** The users locale */ + @NotNull + @JsonProperty(USER.ATTR_LOCALE) + public final Locale locale; + + /** The users time zone */ + @NotNull + @JsonProperty(USER.ATTR_TIMEZONE) + public final DateTimeZone timeZone; + + /** The users roles in a unmodifiable set. Is never null */ + @NotNull + @NotEmpty(message = "user:roles:notEmpty:_:_:_") + @JsonProperty(USER_ROLE.REFERENCE_NAME) + public final Set roles; @Size(min = 8, max = 255, message = "user:password:size:{min}:{max}:${validatedValue}") @JsonProperty(ATTR_NAME_NEW_PASSWORD) @@ -35,47 +82,97 @@ public final class UserMod implements GrantEntity { @JsonCreator public UserMod( - @JsonProperty(ATTR_NAME_USER_INFO) final UserInfo userInfo, + @JsonProperty(USER.ATTR_UUID) final String uuid, + @JsonProperty(USER.ATTR_INSTITUTION_ID) final Long institutionId, + @JsonProperty(USER.ATTR_NAME) final String name, + @JsonProperty(USER.ATTR_USER_NAME) final String userName, @JsonProperty(ATTR_NAME_NEW_PASSWORD) final String newPassword, - @JsonProperty(ATTR_NAME_RETYPED_NEW_PASSWORD) final String retypedNewPassword) { + @JsonProperty(ATTR_NAME_RETYPED_NEW_PASSWORD) final String retypedNewPassword, + @JsonProperty(USER.ATTR_EMAIL) final String email, + @JsonProperty(USER.ATTR_ACTIVE) final Boolean active, + @JsonProperty(USER.ATTR_LOCALE) final Locale locale, + @JsonProperty(USER.ATTR_TIMEZONE) final DateTimeZone timeZone, + @JsonProperty(USER_ROLE.REFERENCE_NAME) final Set roles) { - this.userInfo = userInfo; + this.uuid = uuid; + this.institutionId = institutionId; this.newPassword = newPassword; this.retypedNewPassword = retypedNewPassword; + this.name = name; + this.userName = userName; + this.email = email; + this.locale = locale; + this.timeZone = timeZone; + this.roles = (roles != null) + ? Collections.unmodifiableSet(roles) + : Collections.emptySet(); + } + + public UserMod(final UserInfo userInfo, final String newPassword, final String retypedNewPassword) { + this.uuid = userInfo.uuid; + this.institutionId = userInfo.institutionId; + this.newPassword = newPassword; + this.retypedNewPassword = retypedNewPassword; + this.name = userInfo.name; + this.userName = userInfo.userName; + this.email = userInfo.email; + this.locale = userInfo.locale; + this.timeZone = userInfo.timeZone; + this.roles = userInfo.roles; } @Override @JsonIgnore public String getId() { - return this.userInfo.getId(); + return this.uuid; } @Override @JsonIgnore public EntityType entityType() { - return this.userInfo.entityType(); + return EntityType.USER; } @Override @JsonIgnore public Long getInstitutionId() { - return this.userInfo.getInstitutionId(); + return this.institutionId; } @Override @JsonIgnore public String getOwnerUUID() { - return this.userInfo.getOwnerUUID(); - } - - public UserInfo getUserInfo() { - return this.userInfo; + return this.uuid; } public String getNewPassword() { return this.newPassword; } + public String getName() { + return this.name; + } + + public String getUserName() { + return this.userName; + } + + public String getEmail() { + return this.email; + } + + public Locale getLocale() { + return this.locale; + } + + public DateTimeZone getTimeZone() { + return this.timeZone; + } + + public Set getRoles() { + return this.roles; + } + public String getRetypedNewPassword() { return this.retypedNewPassword; } @@ -88,37 +185,13 @@ public final class UserMod implements GrantEntity { return passwordChangeRequest() && this.newPassword.equals(this.retypedNewPassword); } - public boolean createNew() { - return this.userInfo.uuid == null; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((this.userInfo == null) ? 0 : this.userInfo.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; - final UserMod other = (UserMod) obj; - if (this.userInfo == null) { - if (other.userInfo != null) - return false; - } else if (!this.userInfo.equals(other.userInfo)) - return false; - return true; - } - @Override public String toString() { - return "UserMod [userInfo=" + this.userInfo + "]"; + return "UserMod [uuid=" + this.uuid + ", institutionId=" + this.institutionId + ", name=" + this.name + + ", userName=" + + this.userName + ", email=" + this.email + ", locale=" + this.locale + ", timeZone=" + this.timeZone + + ", roles=" + this.roles + + ", newPassword=" + this.newPassword + ", retypedNewPassword=" + this.retypedNewPassword + "]"; } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/activation/EntityActivationService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/activation/EntityActivationService.java new file mode 100644 index 00000000..1863ff2f --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/activation/EntityActivationService.java @@ -0,0 +1,40 @@ +/* + * 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.servicelayer.activation; + +import java.util.Collection; + +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ActivatableEntityDAO; + +@Service +@WebServiceProfile +public class EntityActivationService { + + private final Collection> activatableEntityDAOs; + + public EntityActivationService(final Collection> activatableEntityDAOs) { + this.activatableEntityDAOs = activatableEntityDAOs; + } + + @EventListener(EntityActivationEvent.class) + public void notifyActivationEvent(final EntityActivationEvent event) { + for (final ActivatableEntityDAO dao : this.activatableEntityDAOs) { + if (event.activated) { + dao.notifyActivation(event.entity); + } else { + dao.notifyDeactivation(event.entity); + } + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserDAO.java index c43b48a7..4d7e144c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserDAO.java @@ -21,7 +21,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.SEBServerUser /** The Data Access Object for all User related data like get user data within UserInfo, * save and modify user related data within UserMod and get internal user principal data * within SEBServerUser. */ -public interface UserDAO extends EntityDAO { +public interface UserDAO extends EntityDAO, ActivatableEntityDAO { /** Use this to get UserInfo by database identifier * @@ -48,11 +48,6 @@ public interface UserDAO extends EntityDAO { * @return a Result of SEBServerUser for specified username. Or an exception result on error case */ Result sebServerUserByUsername(String username); - /** Use this to get a Collection of UserInfo for all active users. - * - * @return a Result of Collection of UserInfo for all active users. Or an exception result on error case */ - Result> allActive(); - /** Use this to get a Collection of UserInfo that matches a given predicate. * * NOTE: This first gets all UserRecord from database, for each creates new UserInfo diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserRelatedEntityDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserRelatedEntityDAO.java index 59b036f7..e97175bb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserRelatedEntityDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserRelatedEntityDAO.java @@ -13,12 +13,34 @@ import java.util.Collection; import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.util.Result; +/** Interface for a DAo handling an Entity with relations to an user (account) + * + * + * @param the concrete type of the Entity */ public interface UserRelatedEntityDAO extends EntityDAO { - Result> getAllForUser(String userId); + /** Get all Entity instances that has a relation to the user-account + * of a given user identity (UUID) + * + * @param userUuid the users identity + * @return A Result of all Entity instances that has a relation to the user-account + * of a given user identity (UUID) or a Result with error if happen */ + Result> getAllForUser(String userUuid); - Result deleteUserReferences(final String userId); + /** Overwrite all user-references for entities that belongs to the user with the given identity + * to refer to an internal anonymous user-account + * + * @param userUuid the users identity + * @param deactivate indicates if the effected entities should also be deactivated if possible + * @return A Result with the number of overwrite Entity instances or with an error if happen */ + Result overwriteUserReferences(final String userUuid, boolean deactivate); - Result deleteUserEnities(final String userId); + /** Delete all user-references for entities that belongs to the user with the given identity + * + * NOTE: This processes a hard-delete. All effected data will be lost. + * + * @param userUuid the users identity + * @return A Result with the number of deleted Entity instances or with an error if happen */ + Result deleteUserEnities(final String userUuid); } 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 7630d665..1b07081a 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 @@ -116,8 +116,8 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { @Override @Transactional(readOnly = true) - public Result> getAllForUser(final String userId) { - return all(userId, null, null, model -> true); + public Result> getAllForUser(final String userUuid) { + return all(userUuid, null, null, model -> true); } @Override @@ -182,11 +182,11 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { @Override @Transactional(readOnly = true) - public Result deleteUserReferences(final String userId) { + public Result overwriteUserReferences(final String userUuid, final boolean deactivate) { try { final List records = this.userLogRecordMapper.selectByExample() - .where(UserActivityLogRecordDynamicSqlSupport.userUuid, SqlBuilder.isEqualTo(userId)) + .where(UserActivityLogRecordDynamicSqlSupport.userUuid, SqlBuilder.isEqualTo(userUuid)) .build() .execute(); @@ -196,19 +196,19 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { records .stream() - .forEach(this::overrrideUser); + .forEach(this::overwriteUser); return Result.of(records.size()); } catch (final Throwable t) { log.error( "Unexpected error while trying to delete all user references form activity logs for user with id: {}", - userId); + userUuid); return Result.ofError(t); } } - private void overrrideUser(final UserActivityLogRecord record) { + private void overwriteUser(final UserActivityLogRecord record) { final UserActivityLogRecord selective = new UserActivityLogRecord( record.getId(), this.userService.getAnonymousUser().getUsername(), @@ -218,16 +218,16 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { } @Override - public Result deleteUserEnities(final String userId) { + public Result deleteUserEnities(final String userUuid) { try { return Result.of(this.userLogRecordMapper.deleteByExample() - .where(UserActivityLogRecordDynamicSqlSupport.userUuid, SqlBuilder.isEqualToWhenPresent(userId)) + .where(UserActivityLogRecordDynamicSqlSupport.userUuid, SqlBuilder.isEqualToWhenPresent(userUuid)) .build() .execute()); } catch (final Throwable t) { - log.error("Unexpected error while trying to delete all activity logs for user with id: {}", userId); + log.error("Unexpected error while trying to delete all activity logs for user with id: {}", userUuid); return Result.ofError(t); } } 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 dbe4eb2b..ee0baf5c 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 @@ -34,10 +34,10 @@ import org.springframework.transaction.interceptor.TransactionInterceptor; import org.springframework.util.CollectionUtils; import ch.ethz.seb.sebserver.WebSecurityConfig; -import ch.ethz.seb.sebserver.gbl.model.Entity; -import ch.ethz.seb.sebserver.gbl.model.EntityType; import ch.ethz.seb.sebserver.gbl.model.APIMessage.APIMessageException; import ch.ethz.seb.sebserver.gbl.model.APIMessage.ErrorMessage; +import ch.ethz.seb.sebserver.gbl.model.Entity; +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; import ch.ethz.seb.sebserver.gbl.model.user.UserMod; @@ -110,7 +110,7 @@ public class UserDaoImpl implements UserDAO { @Override @Transactional(readOnly = true) public Result> allActive() { - return all(UserFilter.ofActive()); + return all(new UserFilter(null, null, null, null, true, null)); } @Override @@ -168,8 +168,7 @@ public class UserDaoImpl implements UserDAO { try { - final UserInfo userInfo = userMod.getUserInfo(); - return (userInfo.uuid != null) + return (userMod.uuid != null) ? updateUser(userMod) : createNewUser(userMod); @@ -189,6 +188,63 @@ public class UserDaoImpl implements UserDAO { return Result.ofError(new RuntimeException("TODO")); } + @Override + @Transactional + public Result setActive(final String entityId, final boolean active) { + try { + + this.userRecordMapper.updateByExampleSelective( + new UserRecord( + null, null, null, null, null, null, null, null, null, + BooleanUtils.toIntegerObject(active))) + .where(UserRecordDynamicSqlSupport.uuid, isEqualTo(entityId)) + .build() + .execute(); + + return byUuid(entityId); + + } catch (final Exception e) { + log.error("unexpected error: ", e); + return Result.ofError(e); + } + } + + @Override + public void notifyActivation(final Entity source) { + // If an Institution has been deactivated, all its user accounts gets also be deactivated + if (source.entityType() == EntityType.INSTITUTION) { + setAllActiveForInstitution(Long.parseLong(source.getId()), true); + } + } + + @Override + public void notifyDeactivation(final Entity source) { + // If an Institution has been deactivated, all its user accounts gets also be deactivated + if (source.entityType() == EntityType.INSTITUTION) { + setAllActiveForInstitution(Long.parseLong(source.getId()), false); + } + } + + private void setAllActiveForInstitution(final Long institutionId, final boolean active) { + try { + + final UserRecord record = new UserRecord( + null, null, null, null, null, null, null, null, null, + BooleanUtils.toIntegerObject(active)); + + this.userRecordMapper.updateByExampleSelective(record) + .where(UserRecordDynamicSqlSupport.institutionId, isEqualTo(institutionId)) + .build() + .execute(); + + } catch (final Exception e) { + log.error("Unexpected error while trying to set all active: {} for institution: {}", + active, + institutionId, + e); + } + } + private Result> fromRecords( final List records, final Predicate predicate) { @@ -204,13 +260,8 @@ public class UserDaoImpl implements UserDAO { } private Result updateUser(final UserMod userMod) { - final UserInfo userInfo = userMod.getUserInfo(); - return recordByUUID(userInfo.uuid) + return recordByUUID(userMod.uuid) .flatMap(record -> { - if (record.getInstitutionId().longValue() != userInfo.institutionId.longValue()) { - return Result.ofError(new IllegalArgumentException("The users institution cannot be changed")); - } - final boolean changePWD = userMod.passwordChangeRequest(); if (changePWD && !userMod.newPasswordMatch()) { return Result.ofError(new APIMessageException(ErrorMessage.PASSWORD_MISSMATCH)); @@ -220,23 +271,22 @@ public class UserDaoImpl implements UserDAO { record.getId(), null, null, - userInfo.name, - userInfo.userName, + userMod.name, + userMod.userName, (changePWD) ? this.userPasswordEncoder.encode(userMod.getNewPassword()) : null, - userInfo.email, - userInfo.locale.toLanguageTag(), - userInfo.timeZone.getID(), - BooleanUtils.toIntegerObject(userInfo.active)); + userMod.email, + userMod.locale.toLanguageTag(), + userMod.timeZone.getID(), + null); this.userRecordMapper.updateByPrimaryKeySelective(newRecord); - updateRolesForUser(record.getId(), userInfo.roles); + updateRolesForUser(record.getId(), userMod.roles); return byId(record.getId()); }); } private Result createNewUser(final UserMod userMod) { - final UserInfo userInfo = userMod.getUserInfo(); if (!userMod.newPasswordMatch()) { return Result.ofError(new APIMessageException(ErrorMessage.PASSWORD_MISSMATCH)); @@ -244,19 +294,19 @@ public class UserDaoImpl implements UserDAO { final UserRecord newRecord = new UserRecord( null, - userInfo.institutionId, + userMod.institutionId, UUID.randomUUID().toString(), - userInfo.name, - userInfo.userName, + userMod.name, + userMod.userName, this.userPasswordEncoder.encode(userMod.getNewPassword()), - userInfo.email, - userInfo.locale.toLanguageTag(), - userInfo.timeZone.getID(), - BooleanUtils.toIntegerObject(userInfo.active)); + userMod.email, + userMod.locale.toLanguageTag(), + userMod.timeZone.getID(), + BooleanUtils.toInteger(false)); this.userRecordMapper.insert(newRecord); final Long newUserId = newRecord.getId(); - insertRolesForUser(newUserId, userInfo.roles); + insertRolesForUser(newUserId, userMod.roles); return byId(newUserId); } @@ -349,4 +399,5 @@ 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 bab96393..6069faa2 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 @@ -8,7 +8,6 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api; -import java.security.Principal; import java.util.Collection; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -16,11 +15,11 @@ import java.util.stream.Collectors; import javax.validation.Valid; 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; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import ch.ethz.seb.sebserver.gbl.model.EntityType; @@ -29,6 +28,7 @@ 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.activation.EntityActivationEvent; 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.UserService; @@ -64,19 +64,30 @@ public class UserAccountController { @RequestMapping(method = RequestMethod.GET) public Collection getAll( - //@RequestParam(required = false) final UserFilter filter, - @RequestBody(required = false) final UserFilter userFilter, - final Principal principal) { + @RequestParam(required = false) final Long institutionId, + @RequestParam(required = false) final Boolean active, + @RequestParam(required = false) final String name, + @RequestParam(required = false) final String userName, + @RequestParam(required = false) final String email, + @RequestParam(required = false) final String locale) { // fist check if current user has any privileges for this action this.authorizationGrantService.checkHasAnyPrivilege( EntityType.USER, PrivilegeType.READ_ONLY); + final UserFilter userFilter = ((institutionId != null) || + (active != null) || + (name != null) || + (userName != null) || + (email != null) || + (locale != null)) + ? new UserFilter(institutionId, name, userName, email, active, locale) + : null; + if (this.authorizationGrantService.hasBasePrivilege( EntityType.USER, - PrivilegeType.READ_ONLY, - principal)) { + PrivilegeType.READ_ONLY)) { return (userFilter != null) ? this.userDao.all(userFilter).getOrThrow() @@ -86,8 +97,7 @@ public class UserAccountController { final Predicate grantFilter = this.authorizationGrantService.getGrantFilter( EntityType.USER, - PrivilegeType.READ_ONLY, - principal); + PrivilegeType.READ_ONLY); if (userFilter == null) { @@ -108,14 +118,14 @@ public class UserAccountController { } @RequestMapping(value = "/me", method = RequestMethod.GET) - public UserInfo loggedInUser(final Authentication auth) { + public UserInfo loggedInUser() { return this.userService .getCurrentUser() .getUserInfo(); } @RequestMapping(value = "/{userUUID}", method = RequestMethod.GET) - public UserInfo accountInfo(@PathVariable final String userUUID, final Principal principal) { + public UserInfo accountInfo(@PathVariable final String userUUID) { return this.userDao .byUuid(userUUID) .flatMap(userInfo -> this.authorizationGrantService.checkGrantOnEntity( @@ -126,27 +136,42 @@ public class UserAccountController { } @RequestMapping(value = "/create", method = RequestMethod.PUT) - public UserInfo createUser( - @Valid @RequestBody final UserMod userData, - final Principal principal) { - + public UserInfo createUser(@Valid @RequestBody final UserMod userData) { return _saveUser(userData, PrivilegeType.WRITE) .getOrThrow(); } @RequestMapping(value = "/save", method = RequestMethod.POST) - public UserInfo saveUser( - @Valid @RequestBody final UserMod userData, - final Principal principal) { - + public UserInfo saveUser(@Valid @RequestBody final UserMod userData) { return _saveUser(userData, PrivilegeType.MODIFY) .getOrThrow(); } + @RequestMapping(value = "/{userUUID}/activate", method = RequestMethod.POST) + public UserInfo activateUser(@PathVariable final String userUUID) { + return setActivity(userUUID, true); + } + + @RequestMapping(value = "/{userUUID}/deactivate", method = RequestMethod.POST) + public UserInfo deactivateUser(@PathVariable final String userUUID) { + return setActivity(userUUID, false); + } + + private UserInfo setActivity(final String userUUID, final boolean activity) { + return this.userDao.byUuid(userUUID) + .flatMap(userInfo -> this.authorizationGrantService.checkGrantOnEntity(userInfo, PrivilegeType.WRITE)) + .flatMap(userInfo -> this.userDao.setActive(userInfo.uuid, activity)) + .map(userInfo -> { + this.applicationEventPublisher.publishEvent(new EntityActivationEvent(userInfo, activity)); + return userInfo; + }) + .getOrThrow(); + } + private Result _saveUser(final UserMod userData, final PrivilegeType privilegeType) { - final ActivityType actionType = (userData.getUserInfo().uuid == null) + final ActivityType actionType = (userData.uuid == null) ? ActivityType.CREATE : ActivityType.MODIFY; 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 82dee79b..f3a2c49f 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 @@ -30,7 +30,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import ch.ethz.seb.sebserver.gbl.model.APIMessage; import ch.ethz.seb.sebserver.gbl.model.user.UserActivityLog; -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.model.user.UserRole; @@ -152,15 +151,10 @@ public class UserAPITest extends AdministrationAPIIntegrationTest { @Test public void getAllUserInfoWithSearchInactive() throws Exception { - final UserFilter filter = UserFilter.ofInactive(); - final String filterJson = this.jsonMapper.writeValueAsString(filter); - final String token = getSebAdminAccess(); final List userInfos = this.jsonMapper.readValue( - this.mockMvc.perform(get(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT) - .header("Authorization", "Bearer " + token) - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(filterJson)) + this.mockMvc.perform(get(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "?active=false") + .header("Authorization", "Bearer " + token)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -173,15 +167,10 @@ public class UserAPITest extends AdministrationAPIIntegrationTest { @Test public void getAllUserInfoWithSearchUsernameLike() throws Exception { - final UserFilter filter = new UserFilter(null, null, "exam", null, null, null); - final String filterJson = this.jsonMapper.writeValueAsString(filter); - final String token = getSebAdminAccess(); final List userInfos = this.jsonMapper.readValue( - this.mockMvc.perform(get(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT) - .header("Authorization", "Bearer " + token) - .contentType(MediaType.APPLICATION_JSON_UTF8) - .content(filterJson)) + this.mockMvc.perform(get(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "?userName=exam") + .header("Authorization", "Bearer " + token)) .andExpect(status().isOk()) .andReturn().getResponse().getContentAsString(), new TypeReference>() { @@ -236,6 +225,7 @@ public class UserAPITest extends AdministrationAPIIntegrationTest { assertNotNull(createdUserGet); assertEquals(createdUser, createdUserGet); + assertFalse(createdUserGet.isActive()); // check user activity log for newly created user final List logs = this.jsonMapper.readValue( @@ -497,6 +487,50 @@ public class UserAPITest extends AdministrationAPIIntegrationTest { assertEquals("1300", messages.get(0).messageCode); } + @Test + public void deactivateUserAccount() throws Exception { + // only a SEB Administrator or an Institutional administrator should be able to deactivate a user-account + final String examAdminToken = getExamAdmin1(); + this.mockMvc.perform(post(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/user4/deactivate") + .header("Authorization", "Bearer " + examAdminToken)) + .andExpect(status().isForbidden()); + + // With SEB Administrator it should work + final String sebAdminToken = getSebAdminAccess(); + final UserInfo deactivatedUser = this.jsonMapper.readValue( + this.mockMvc.perform(post(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/user4/deactivate") + .header("Authorization", "Bearer " + sebAdminToken)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), + new TypeReference() { + }); + + assertNotNull(deactivatedUser); + assertFalse(deactivatedUser.isActive()); + } + + @Test + public void activateUserAccount() throws Exception { + // only a SEB Administrator or an Institutional administrator should be able to deactivate a user-account + final String examAdminToken = getExamAdmin1(); + this.mockMvc.perform(post(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/user6/activate") + .header("Authorization", "Bearer " + examAdminToken)) + .andExpect(status().isForbidden()); + + // With SEB Administrator it should work + final String sebAdminToken = getSebAdminAccess(); + final UserInfo deactivatedUser = this.jsonMapper.readValue( + this.mockMvc.perform(post(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "/user6/activate") + .header("Authorization", "Bearer " + sebAdminToken)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), + new TypeReference() { + }); + + assertNotNull(deactivatedUser); + assertTrue(deactivatedUser.isActive()); + } + private UserInfo getUserInfo(final String name, final Collection infos) { return infos .stream()