package edu.ualberta.med.biobank.common.action.security; import java.util.HashSet; import java.util.Set; import javax.transaction.Synchronization; import org.hibernate.Transaction; import edu.ualberta.med.biobank.common.action.Action; import edu.ualberta.med.biobank.common.action.ActionContext; import edu.ualberta.med.biobank.common.action.exception.ActionException; import edu.ualberta.med.biobank.common.permission.Permission; import edu.ualberta.med.biobank.common.permission.security.UserManagerPermission; import edu.ualberta.med.biobank.model.Domain; import edu.ualberta.med.biobank.model.Group; import edu.ualberta.med.biobank.model.Membership; import edu.ualberta.med.biobank.model.PermissionEnum; import edu.ualberta.med.biobank.model.Role; import edu.ualberta.med.biobank.model.User; import edu.ualberta.med.biobank.model.util.IdUtil; import edu.ualberta.med.biobank.server.applicationservice.BiobankCSMSecurityUtil; import edu.ualberta.med.biobank.util.SetDiff; import edu.ualberta.med.biobank.util.SetDiff.Pair; import gov.nih.nci.system.applicationservice.ApplicationException; public class UserSaveAction implements Action<UserSaveOutput> { private static final long serialVersionUID = 1L; private static final Permission PERMISSION = new UserManagerPermission(); private final UserSaveInput input; public UserSaveAction(UserSaveInput input) { this.input = input; } @Override public boolean isAllowed(ActionContext context) throws ActionException { return PERMISSION.isAllowed(context); } @Override public UserSaveOutput run(ActionContext context) throws ActionException { User user = context.load(User.class, input.getUserId(), new User()); setProperties(context, user); setMemberships(context, user); setGroups(context, user); context.getSession().saveOrUpdate(user); return new UserSaveOutput(user.getId(), user.getCsmUserId()); } private void setProperties(ActionContext context, User user) { User executingUser = context.getUser(); // whether the manager _could_ have modified the user properties if (user.isFullyManageable(input.getContext().getManager()) // whether the manager (executing user) _still_ can && user.isFullyManageable(executingUser)) { User oldUserData = new User(); oldUserData.setId(user.getId()); oldUserData.setCsmUserId(user.getCsmUserId()); oldUserData.setLogin(user.getLogin()); oldUserData.setEmail(user.getEmail()); oldUserData.setFullName(user.getFullName()); oldUserData.setNeedPwdChange(user.getNeedPwdChange()); oldUserData.setRecvBulkEmails(user.getRecvBulkEmails()); user.setLogin(input.getLogin()); user.setEmail(input.getEmail()); user.setFullName(input.getFullName()); user.setNeedPwdChange(input.isNeedPwdChange()); user.setRecvBulkEmails(input.isRecvBulkEmails()); setCsmUserProperties(context, user, oldUserData); } } private void setCsmUserProperties(ActionContext context, User user, User oldUserData) { try { boolean newCsmUser = user.getCsmUserId() == null; String pw = input.getPassword(); if (newCsmUser) { Long csmUserId = BiobankCSMSecurityUtil.persistUser(user, pw); user.setCsmUserId(csmUserId); if (pw == null || pw.length() < 5) { throw new ActionException( "password for new user must be set and be at least 5 characters"); } Transaction tx = context.getSession().getTransaction(); tx.registerSynchronization(new DeleteCsmUserOnRollback(user)); } else { String login = oldUserData.getLogin(); String oldPw = BiobankCSMSecurityUtil.getUserPassword(login); BiobankCSMSecurityUtil.persistUser(user, pw); Transaction tx = context.getSession().getTransaction(); tx.registerSynchronization(new PersistCsmUserOnRollback( oldUserData, oldPw)); } } catch (ApplicationException e) { throw new ActionException(e); } } private Set<Membership> getManageableMemberships(ActionContext context, User user) { User manager = input.getContext().getManager(); User executor = context.getUser(); Set<Membership> managerMembs = user.getManageableMemberships(manager); Set<Membership> executorMembs = user.getManageableMemberships(executor); if (!managerMembs.containsAll(executorMembs)) { throw new ActionException( "No longer able to manage some submitted memberships. Please start over and try again."); } return managerMembs; } private void setMemberships(ActionContext context, User user) { Set<Membership> manageable = getManageableMemberships(context, user); SetDiff<Membership> diff = new SetDiff<Membership>( manageable, input.getMemberships()); handleMembershipDeletes(context, diff.getRemovals()); handleMembershipInserts(context, user, diff.getAdditions()); handleMembershipUpdates(context, diff.getIntersection()); mergeMembershipsOnDomain(context, user); for (Membership m : user.getMemberships()) { m.reducePermissions(); } } private void handleMembershipDeletes(ActionContext context, Set<Membership> removals) { for (Membership membership : removals) { checkFullyManageable(context, membership); membership.getPrincipal().getMemberships().remove(membership); context.getSession().delete(membership); } } private void handleMembershipInserts(ActionContext context, User user, Set<Membership> additions) { for (Membership membership : additions) { checkFullyManageable(context, membership); user.getMemberships().add(membership); membership.setPrincipal(user); } } private void handleMembershipUpdates(ActionContext context, Set<Pair<Membership>> conflicts) { User executingUser = context.getUser(); User manager = input.getContext().getManager(); // TODO: reload these Set<Role> contextRoles = input.getContext().getRoles(); Set<PermissionEnum> perms, oldPermissionScope, newPermissionScope; Set<Role> roles, oldRoleScope, newRoleScope; for (Pair<Membership> conflict : conflicts) { Membership oldM = conflict.getOld(); Membership newM = conflict.getNew(); // the permissions and roles the user would have intended to modify oldPermissionScope = oldM.getManageablePermissions(manager); oldRoleScope = oldM.getManageableRoles(manager, contextRoles); // the permissions and roles the executing user can currently modify newPermissionScope = newM.getManageablePermissions(executingUser); newRoleScope = newM.getManageableRoles(executingUser, contextRoles); // ensure the old (client?) scope can still be modified by the new // (server?) scope if (!newPermissionScope.containsAll(oldPermissionScope) || !newRoleScope.containsAll(oldRoleScope)) { // TODO: better exception // TODO: there is a bug here! got it when editting a Membership // on // testweirdkid throw new ActionException("reduced scope"); } Domain newD = newM.getDomain(); Domain oldD = oldM.getDomain(); oldD.getCenters().clear(); oldD.getCenters().addAll(newD.getCenters()); oldD.setAllCenters(newD.isAllCenters()); oldD.getStudies().clear(); oldD.getStudies().addAll(newD.getStudies()); oldD.setAllStudies(newD.isAllStudies()); // clear out the old scope and assign intended values oldM.getPermissions().removeAll(oldPermissionScope); oldM.getRoles().removeAll(oldRoleScope); perms = new HashSet<PermissionEnum>(); perms.addAll(newM.getPermissions()); perms.retainAll(oldPermissionScope); oldM.getPermissions().addAll(perms); roles = new HashSet<Role>(); roles.addAll(newM.getRoles()); roles.retainAll(newM.getRoles()); oldM.getRoles().addAll(roles); oldM.setUserManager(newM.isUserManager()); oldM.setEveryPermission(newM.isEveryPermission()); checkFullyManageable(context, oldM); } } private void mergeMembershipsOnDomain(ActionContext context, User user) { } private void checkFullyManageable(ActionContext context, Membership m) { User executingUser = context.getUser(); if (!m.isFullyManageable(executingUser)) { // TODO: better exception throw new ActionException( "you do not have permissions to make this change on this user"); } } private void setGroups(ActionContext context, User user) { User executingUser = context.getUser(); // TODO: reload these Set<Group> contextGroups = input.getContext().getGroups(); Set<Integer> contextGroupIds = IdUtil.getIds(contextGroups); if (!contextGroupIds.containsAll(input.getGroupIds())) { // TODO: better exception throw new ActionException("found groups out of context"); } // add or remove every Group in the context Set<Group> groups = context.load(Group.class, contextGroupIds); for (Group group : groups) { if (!group.isFullyManageable(executingUser)) { // TODO: throw exception throw new ActionException("modifying unmanageable group"); } if (input.getGroupIds().contains(group.getId())) { user.getGroups().add(group); group.getUsers().add(user); } else { user.getGroups().remove(group); group.getUsers().remove(user); } } } private static class DeleteCsmUserOnRollback extends EmptySynchronization { private final User user; private DeleteCsmUserOnRollback(User user) { this.user = user; } @Override public void afterCompletion(int status) { if (status == javax.transaction.Status.STATUS_ROLLEDBACK) { try { BiobankCSMSecurityUtil.deleteUser(user); } catch (ApplicationException e) { // TODO: what to do? } } } } private static class PersistCsmUserOnRollback extends EmptySynchronization { private final User user; private final String oldPassword; private PersistCsmUserOnRollback(User user, String oldPassword) { this.user = user; this.oldPassword = oldPassword; } @Override public void afterCompletion(int status) { if (status == javax.transaction.Status.STATUS_ROLLEDBACK) { try { BiobankCSMSecurityUtil.persistUser(user, oldPassword); } catch (ApplicationException e) { // TODO: what to do? } } } } private static class EmptySynchronization implements Synchronization { @Override public void afterCompletion(int status) { } @Override public void beforeCompletion() { } } }