SEBSERV-13 #general implementation of entity activation service

This commit is contained in:
anhefti 2018-12-19 12:08:53 +01:00
parent 70d66e6806
commit 41f9f25cd8
10 changed files with 373 additions and 156 deletions

View file

@ -8,8 +8,6 @@
package ch.ethz.seb.sebserver.gbl.model.user; package ch.ethz.seb.sebserver.gbl.model.user;
import javax.validation.constraints.NotNull;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonInclude.Include;
@ -95,16 +93,4 @@ public final class UserFilter {
+ this.email + ", active=" + this.active + ", locale=" + this.locale + "]"; + 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);
}
} }

View file

@ -13,11 +13,6 @@ import java.util.Collections;
import java.util.Locale; import java.util.Locale;
import java.util.Set; 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.apache.commons.lang3.BooleanUtils;
import org.joda.time.DateTimeZone; 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.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; 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;
import ch.ethz.seb.sebserver.gbl.model.Domain.USER_ROLE; import ch.ethz.seb.sebserver.gbl.model.Domain.USER_ROLE;
import ch.ethz.seb.sebserver.gbl.model.EntityType; 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. * to and from JSON within the Jackson library.
* *
* This domain model is immutable and thread-save */ * 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; private static final long serialVersionUID = 2526446136264377808L;
@ -45,45 +41,34 @@ public final class UserInfo implements GrantEntity, Serializable {
public final String uuid; public final String uuid;
/** The foreign key identifier to the institution where the User belongs to */ /** The foreign key identifier to the institution where the User belongs to */
@NotNull
@JsonProperty(USER.ATTR_INSTITUTION_ID) @JsonProperty(USER.ATTR_INSTITUTION_ID)
public final Long institutionId; public final Long institutionId;
/** Full name of the user */ /** Full name of the user */
@NotNull
@Size(min = 3, max = 255, message = "user:name:size:{min}:{max}:${validatedValue}")
@JsonProperty(USER.ATTR_NAME) @JsonProperty(USER.ATTR_NAME)
public final String name; public final String name;
/** The internal user name */ /** The internal user name */
@NotNull
@Size(min = 3, max = 255, message = "user:username:size:{min}:{max}:${validatedValue}")
@JsonProperty(USER.ATTR_USER_NAME) @JsonProperty(USER.ATTR_USER_NAME)
public final String userName; public final String userName;
/** E-mail address of the user */ /** E-mail address of the user */
@Email(message = "user:email:email:_:_:${validatedValue}")
@JsonProperty(USER.ATTR_EMAIL) @JsonProperty(USER.ATTR_EMAIL)
public final String email; public final String email;
/** Indicates whether this user is still active or not */ /** Indicates whether this user is still active or not */
@NotNull
@JsonProperty(USER.ATTR_ACTIVE) @JsonProperty(USER.ATTR_ACTIVE)
public final Boolean active; public final Boolean active;
/** The users locale */ /** The users locale */
@NotNull
@JsonProperty(USER.ATTR_LOCALE) @JsonProperty(USER.ATTR_LOCALE)
public final Locale locale; public final Locale locale;
/** The users time zone */ /** The users time zone */
@NotNull
@JsonProperty(USER.ATTR_TIMEZONE) @JsonProperty(USER.ATTR_TIMEZONE)
public final DateTimeZone timeZone; public final DateTimeZone timeZone;
/** The users roles in a unmodifiable set. Is never null */ /** The users roles in a unmodifiable set. Is never null */
@NotNull
@NotEmpty(message = "user:roles:notEmpty:_:_:_")
@JsonProperty(USER_ROLE.REFERENCE_NAME) @JsonProperty(USER_ROLE.REFERENCE_NAME)
public final Set<String> roles; public final Set<String> roles;
@ -155,6 +140,12 @@ public final class UserInfo implements GrantEntity, Serializable {
return this.active; return this.active;
} }
@JsonIgnore
@Override
public boolean isActive() {
return this.active;
}
public Locale getLocale() { public Locale getLocale() {
return this.locale; return this.locale;
} }

View file

@ -8,23 +8,70 @@
package ch.ethz.seb.sebserver.gbl.model.user; 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 javax.validation.constraints.Size;
import org.joda.time.DateTimeZone;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; 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.gbl.model.EntityType;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.GrantEntity; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.GrantEntity;
public final class UserMod implements 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_NEW_PASSWORD = "newPassword";
public static final String ATTR_NAME_RETYPED_NEW_PASSWORD = "retypedNewPassword"; public static final String ATTR_NAME_RETYPED_NEW_PASSWORD = "retypedNewPassword";
@JsonProperty(ATTR_NAME_USER_INFO) public final String uuid;
private final UserInfo userInfo;
/** 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<String> roles;
@Size(min = 8, max = 255, message = "user:password:size:{min}:{max}:${validatedValue}") @Size(min = 8, max = 255, message = "user:password:size:{min}:{max}:${validatedValue}")
@JsonProperty(ATTR_NAME_NEW_PASSWORD) @JsonProperty(ATTR_NAME_NEW_PASSWORD)
@ -35,47 +82,97 @@ public final class UserMod implements GrantEntity {
@JsonCreator @JsonCreator
public UserMod( 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_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<String> roles) {
this.userInfo = userInfo; this.uuid = uuid;
this.institutionId = institutionId;
this.newPassword = newPassword; this.newPassword = newPassword;
this.retypedNewPassword = retypedNewPassword; 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 @Override
@JsonIgnore @JsonIgnore
public String getId() { public String getId() {
return this.userInfo.getId(); return this.uuid;
} }
@Override @Override
@JsonIgnore @JsonIgnore
public EntityType entityType() { public EntityType entityType() {
return this.userInfo.entityType(); return EntityType.USER;
} }
@Override @Override
@JsonIgnore @JsonIgnore
public Long getInstitutionId() { public Long getInstitutionId() {
return this.userInfo.getInstitutionId(); return this.institutionId;
} }
@Override @Override
@JsonIgnore @JsonIgnore
public String getOwnerUUID() { public String getOwnerUUID() {
return this.userInfo.getOwnerUUID(); return this.uuid;
}
public UserInfo getUserInfo() {
return this.userInfo;
} }
public String getNewPassword() { public String getNewPassword() {
return this.newPassword; 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<String> getRoles() {
return this.roles;
}
public String getRetypedNewPassword() { public String getRetypedNewPassword() {
return this.retypedNewPassword; return this.retypedNewPassword;
} }
@ -88,37 +185,13 @@ public final class UserMod implements GrantEntity {
return passwordChangeRequest() && this.newPassword.equals(this.retypedNewPassword); 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 @Override
public String toString() { 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 + "]";
} }
} }

View file

@ -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<ActivatableEntityDAO<?>> activatableEntityDAOs;
public EntityActivationService(final Collection<ActivatableEntityDAO<?>> 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);
}
}
}
}

View file

@ -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, /** 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 * save and modify user related data within UserMod and get internal user principal data
* within SEBServerUser. */ * within SEBServerUser. */
public interface UserDAO extends EntityDAO<UserInfo> { public interface UserDAO extends EntityDAO<UserInfo>, ActivatableEntityDAO<UserInfo> {
/** Use this to get UserInfo by database identifier /** Use this to get UserInfo by database identifier
* *
@ -48,11 +48,6 @@ public interface UserDAO extends EntityDAO<UserInfo> {
* @return a Result of SEBServerUser for specified username. Or an exception result on error case */ * @return a Result of SEBServerUser for specified username. Or an exception result on error case */
Result<SEBServerUser> sebServerUserByUsername(String username); Result<SEBServerUser> 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<Collection<UserInfo>> allActive();
/** Use this to get a Collection of UserInfo that matches a given predicate. /** 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 * NOTE: This first gets all UserRecord from database, for each creates new UserInfo

View file

@ -13,12 +13,34 @@ import java.util.Collection;
import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
/** Interface for a DAo handling an Entity with relations to an user (account)
*
*
* @param <T> the concrete type of the Entity */
public interface UserRelatedEntityDAO<T extends Entity> extends EntityDAO<T> { public interface UserRelatedEntityDAO<T extends Entity> extends EntityDAO<T> {
Result<Collection<T>> 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<Collection<T>> getAllForUser(String userUuid);
Result<Integer> 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<Integer> overwriteUserReferences(final String userUuid, boolean deactivate);
Result<Integer> 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<Integer> deleteUserEnities(final String userUuid);
} }

View file

@ -116,8 +116,8 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Collection<UserActivityLog>> getAllForUser(final String userId) { public Result<Collection<UserActivityLog>> getAllForUser(final String userUuid) {
return all(userId, null, null, model -> true); return all(userUuid, null, null, model -> true);
} }
@Override @Override
@ -182,11 +182,11 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Integer> deleteUserReferences(final String userId) { public Result<Integer> overwriteUserReferences(final String userUuid, final boolean deactivate) {
try { try {
final List<UserActivityLogRecord> records = this.userLogRecordMapper.selectByExample() final List<UserActivityLogRecord> records = this.userLogRecordMapper.selectByExample()
.where(UserActivityLogRecordDynamicSqlSupport.userUuid, SqlBuilder.isEqualTo(userId)) .where(UserActivityLogRecordDynamicSqlSupport.userUuid, SqlBuilder.isEqualTo(userUuid))
.build() .build()
.execute(); .execute();
@ -196,19 +196,19 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO {
records records
.stream() .stream()
.forEach(this::overrrideUser); .forEach(this::overwriteUser);
return Result.of(records.size()); return Result.of(records.size());
} catch (final Throwable t) { } catch (final Throwable t) {
log.error( log.error(
"Unexpected error while trying to delete all user references form activity logs for user with id: {}", "Unexpected error while trying to delete all user references form activity logs for user with id: {}",
userId); userUuid);
return Result.ofError(t); return Result.ofError(t);
} }
} }
private void overrrideUser(final UserActivityLogRecord record) { private void overwriteUser(final UserActivityLogRecord record) {
final UserActivityLogRecord selective = new UserActivityLogRecord( final UserActivityLogRecord selective = new UserActivityLogRecord(
record.getId(), record.getId(),
this.userService.getAnonymousUser().getUsername(), this.userService.getAnonymousUser().getUsername(),
@ -218,16 +218,16 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO {
} }
@Override @Override
public Result<Integer> deleteUserEnities(final String userId) { public Result<Integer> deleteUserEnities(final String userUuid) {
try { try {
return Result.of(this.userLogRecordMapper.deleteByExample() return Result.of(this.userLogRecordMapper.deleteByExample()
.where(UserActivityLogRecordDynamicSqlSupport.userUuid, SqlBuilder.isEqualToWhenPresent(userId)) .where(UserActivityLogRecordDynamicSqlSupport.userUuid, SqlBuilder.isEqualToWhenPresent(userUuid))
.build() .build()
.execute()); .execute());
} catch (final Throwable t) { } 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); return Result.ofError(t);
} }
} }

View file

@ -34,10 +34,10 @@ 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.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.APIMessageException;
import ch.ethz.seb.sebserver.gbl.model.APIMessage.ErrorMessage; 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.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;
@ -110,7 +110,7 @@ public class UserDaoImpl implements UserDAO {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Collection<UserInfo>> allActive() { public Result<Collection<UserInfo>> allActive() {
return all(UserFilter.ofActive()); return all(new UserFilter(null, null, null, null, true, null));
} }
@Override @Override
@ -168,8 +168,7 @@ public class UserDaoImpl implements UserDAO {
try { try {
final UserInfo userInfo = userMod.getUserInfo(); return (userMod.uuid != null)
return (userInfo.uuid != null)
? updateUser(userMod) ? updateUser(userMod)
: createNewUser(userMod); : createNewUser(userMod);
@ -189,6 +188,63 @@ public class UserDaoImpl implements UserDAO {
return Result.ofError(new RuntimeException("TODO")); return Result.ofError(new RuntimeException("TODO"));
} }
@Override
@Transactional
public Result<UserInfo> 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<Collection<UserInfo>> fromRecords( private Result<Collection<UserInfo>> fromRecords(
final List<UserRecord> records, final List<UserRecord> records,
final Predicate<UserInfo> predicate) { final Predicate<UserInfo> predicate) {
@ -204,13 +260,8 @@ public class UserDaoImpl implements UserDAO {
} }
private Result<UserInfo> updateUser(final UserMod userMod) { private Result<UserInfo> updateUser(final UserMod userMod) {
final UserInfo userInfo = userMod.getUserInfo(); return recordByUUID(userMod.uuid)
return recordByUUID(userInfo.uuid)
.flatMap(record -> { .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(); final boolean changePWD = userMod.passwordChangeRequest();
if (changePWD && !userMod.newPasswordMatch()) { if (changePWD && !userMod.newPasswordMatch()) {
return Result.ofError(new APIMessageException(ErrorMessage.PASSWORD_MISSMATCH)); return Result.ofError(new APIMessageException(ErrorMessage.PASSWORD_MISSMATCH));
@ -220,23 +271,22 @@ public class UserDaoImpl implements UserDAO {
record.getId(), record.getId(),
null, null,
null, null,
userInfo.name, userMod.name,
userInfo.userName, userMod.userName,
(changePWD) ? this.userPasswordEncoder.encode(userMod.getNewPassword()) : null, (changePWD) ? this.userPasswordEncoder.encode(userMod.getNewPassword()) : null,
userInfo.email, userMod.email,
userInfo.locale.toLanguageTag(), userMod.locale.toLanguageTag(),
userInfo.timeZone.getID(), userMod.timeZone.getID(),
BooleanUtils.toIntegerObject(userInfo.active)); null);
this.userRecordMapper.updateByPrimaryKeySelective(newRecord); this.userRecordMapper.updateByPrimaryKeySelective(newRecord);
updateRolesForUser(record.getId(), userInfo.roles); updateRolesForUser(record.getId(), userMod.roles);
return byId(record.getId()); return byId(record.getId());
}); });
} }
private Result<UserInfo> createNewUser(final UserMod userMod) { private Result<UserInfo> createNewUser(final UserMod userMod) {
final UserInfo userInfo = userMod.getUserInfo();
if (!userMod.newPasswordMatch()) { if (!userMod.newPasswordMatch()) {
return Result.ofError(new APIMessageException(ErrorMessage.PASSWORD_MISSMATCH)); return Result.ofError(new APIMessageException(ErrorMessage.PASSWORD_MISSMATCH));
@ -244,19 +294,19 @@ public class UserDaoImpl implements UserDAO {
final UserRecord newRecord = new UserRecord( final UserRecord newRecord = new UserRecord(
null, null,
userInfo.institutionId, userMod.institutionId,
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
userInfo.name, userMod.name,
userInfo.userName, userMod.userName,
this.userPasswordEncoder.encode(userMod.getNewPassword()), this.userPasswordEncoder.encode(userMod.getNewPassword()),
userInfo.email, userMod.email,
userInfo.locale.toLanguageTag(), userMod.locale.toLanguageTag(),
userInfo.timeZone.getID(), userMod.timeZone.getID(),
BooleanUtils.toIntegerObject(userInfo.active)); BooleanUtils.toInteger(false));
this.userRecordMapper.insert(newRecord); this.userRecordMapper.insert(newRecord);
final Long newUserId = newRecord.getId(); final Long newUserId = newRecord.getId();
insertRolesForUser(newUserId, userInfo.roles); insertRolesForUser(newUserId, userMod.roles);
return byId(newUserId); return byId(newUserId);
} }
@ -349,4 +399,5 @@ public class UserDaoImpl implements UserDAO {
userInfo, userInfo,
record.getPassword())); record.getPassword()));
} }
} }

View file

@ -8,7 +8,6 @@
package ch.ethz.seb.sebserver.webservice.weblayer.api; package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.security.Principal;
import java.util.Collection; import java.util.Collection;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -16,11 +15,11 @@ import java.util.stream.Collectors;
import javax.validation.Valid; import javax.validation.Valid;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
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;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import ch.ethz.seb.sebserver.gbl.model.EntityType; 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.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.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.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.UserService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
@ -64,19 +64,30 @@ public class UserAccountController {
@RequestMapping(method = RequestMethod.GET) @RequestMapping(method = RequestMethod.GET)
public Collection<UserInfo> getAll( public Collection<UserInfo> getAll(
//@RequestParam(required = false) final UserFilter filter, @RequestParam(required = false) final Long institutionId,
@RequestBody(required = false) final UserFilter userFilter, @RequestParam(required = false) final Boolean active,
final Principal principal) { @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 // fist check if current user has any privileges for this action
this.authorizationGrantService.checkHasAnyPrivilege( this.authorizationGrantService.checkHasAnyPrivilege(
EntityType.USER, EntityType.USER,
PrivilegeType.READ_ONLY); 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( if (this.authorizationGrantService.hasBasePrivilege(
EntityType.USER, EntityType.USER,
PrivilegeType.READ_ONLY, PrivilegeType.READ_ONLY)) {
principal)) {
return (userFilter != null) return (userFilter != null)
? this.userDao.all(userFilter).getOrThrow() ? this.userDao.all(userFilter).getOrThrow()
@ -86,8 +97,7 @@ public class UserAccountController {
final Predicate<UserInfo> grantFilter = this.authorizationGrantService.getGrantFilter( final Predicate<UserInfo> grantFilter = this.authorizationGrantService.getGrantFilter(
EntityType.USER, EntityType.USER,
PrivilegeType.READ_ONLY, PrivilegeType.READ_ONLY);
principal);
if (userFilter == null) { if (userFilter == null) {
@ -108,14 +118,14 @@ public class UserAccountController {
} }
@RequestMapping(value = "/me", method = RequestMethod.GET) @RequestMapping(value = "/me", method = RequestMethod.GET)
public UserInfo loggedInUser(final Authentication auth) { public UserInfo loggedInUser() {
return this.userService return this.userService
.getCurrentUser() .getCurrentUser()
.getUserInfo(); .getUserInfo();
} }
@RequestMapping(value = "/{userUUID}", method = RequestMethod.GET) @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 return this.userDao
.byUuid(userUUID) .byUuid(userUUID)
.flatMap(userInfo -> this.authorizationGrantService.checkGrantOnEntity( .flatMap(userInfo -> this.authorizationGrantService.checkGrantOnEntity(
@ -126,27 +136,42 @@ public class UserAccountController {
} }
@RequestMapping(value = "/create", method = RequestMethod.PUT) @RequestMapping(value = "/create", method = RequestMethod.PUT)
public UserInfo createUser( public UserInfo createUser(@Valid @RequestBody final UserMod userData) {
@Valid @RequestBody final UserMod userData,
final Principal principal) {
return _saveUser(userData, PrivilegeType.WRITE) return _saveUser(userData, PrivilegeType.WRITE)
.getOrThrow(); .getOrThrow();
} }
@RequestMapping(value = "/save", method = RequestMethod.POST) @RequestMapping(value = "/save", method = RequestMethod.POST)
public UserInfo saveUser( public UserInfo saveUser(@Valid @RequestBody final UserMod userData) {
@Valid @RequestBody final UserMod userData,
final Principal principal) {
return _saveUser(userData, PrivilegeType.MODIFY) return _saveUser(userData, PrivilegeType.MODIFY)
.getOrThrow(); .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<UserInfo> _saveUser(final UserMod userData, final PrivilegeType privilegeType) { private Result<UserInfo> _saveUser(final UserMod userData, final PrivilegeType privilegeType) {
final ActivityType actionType = (userData.getUserInfo().uuid == null) final ActivityType actionType = (userData.uuid == null)
? ActivityType.CREATE ? ActivityType.CREATE
: ActivityType.MODIFY; : ActivityType.MODIFY;

View file

@ -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.APIMessage;
import ch.ethz.seb.sebserver.gbl.model.user.UserActivityLog; 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.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.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
@ -152,15 +151,10 @@ public class UserAPITest extends AdministrationAPIIntegrationTest {
@Test @Test
public void getAllUserInfoWithSearchInactive() throws Exception { public void getAllUserInfoWithSearchInactive() throws Exception {
final UserFilter filter = UserFilter.ofInactive();
final String filterJson = this.jsonMapper.writeValueAsString(filter);
final String token = getSebAdminAccess(); final String token = getSebAdminAccess();
final List<UserInfo> userInfos = this.jsonMapper.readValue( final List<UserInfo> userInfos = this.jsonMapper.readValue(
this.mockMvc.perform(get(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT) this.mockMvc.perform(get(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "?active=false")
.header("Authorization", "Bearer " + token) .header("Authorization", "Bearer " + token))
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(filterJson))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andReturn().getResponse().getContentAsString(), .andReturn().getResponse().getContentAsString(),
new TypeReference<List<UserInfo>>() { new TypeReference<List<UserInfo>>() {
@ -173,15 +167,10 @@ public class UserAPITest extends AdministrationAPIIntegrationTest {
@Test @Test
public void getAllUserInfoWithSearchUsernameLike() throws Exception { 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 String token = getSebAdminAccess();
final List<UserInfo> userInfos = this.jsonMapper.readValue( final List<UserInfo> userInfos = this.jsonMapper.readValue(
this.mockMvc.perform(get(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT) this.mockMvc.perform(get(this.endpoint + RestAPI.ENDPOINT_USER_ACCOUNT + "?userName=exam")
.header("Authorization", "Bearer " + token) .header("Authorization", "Bearer " + token))
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(filterJson))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andReturn().getResponse().getContentAsString(), .andReturn().getResponse().getContentAsString(),
new TypeReference<List<UserInfo>>() { new TypeReference<List<UserInfo>>() {
@ -236,6 +225,7 @@ public class UserAPITest extends AdministrationAPIIntegrationTest {
assertNotNull(createdUserGet); assertNotNull(createdUserGet);
assertEquals(createdUser, createdUserGet); assertEquals(createdUser, createdUserGet);
assertFalse(createdUserGet.isActive());
// check user activity log for newly created user // check user activity log for newly created user
final List<UserActivityLog> logs = this.jsonMapper.readValue( final List<UserActivityLog> logs = this.jsonMapper.readValue(
@ -497,6 +487,50 @@ public class UserAPITest extends AdministrationAPIIntegrationTest {
assertEquals("1300", messages.get(0).messageCode); 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<UserInfo>() {
});
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<UserInfo>() {
});
assertNotNull(deactivatedUser);
assertTrue(deactivatedUser.isActive());
}
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()