From ad84e16ed977d767b392e88111ad45fef122ad31 Mon Sep 17 00:00:00 2001
From: felmer <franz-josef.elmer@id.ethz.ch>
Date: Thu, 26 Apr 2018 08:54:57 +0200
Subject: [PATCH] SSDM-6061: Audit log introduced. More tests more
 functionality, but not yet complete

---
 .../task/UserManagementMaintenanceTask.java   | 136 ++++---
 .../generic/server/task/UserManager.java      | 312 ++++++++++++----
 .../server/task/UserManagerReport.java        | 101 ++++++
 .../task/UserManagerExpectationsBuilder.java  |  88 ++++-
 .../systemtest/task/UserManagerTest.java      | 339 +++++++++++++++++-
 5 files changed, 836 insertions(+), 140 deletions(-)
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagerReport.java

diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagementMaintenanceTask.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagementMaintenanceTask.java
index d6352988338..494c616e734 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagementMaintenanceTask.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagementMaintenanceTask.java
@@ -27,6 +27,7 @@ import org.apache.log4j.Logger;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 
+import ch.ethz.sis.openbis.generic.server.asapi.v3.IApplicationServerInternalApi;
 import ch.systemsx.cisd.authentication.Principal;
 import ch.systemsx.cisd.authentication.ldap.LDAPAuthenticationService;
 import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
@@ -35,6 +36,8 @@ import ch.systemsx.cisd.common.logging.Log4jSimpleLogger;
 import ch.systemsx.cisd.common.logging.LogCategory;
 import ch.systemsx.cisd.common.logging.LogFactory;
 import ch.systemsx.cisd.common.maintenance.IMaintenanceTask;
+import ch.systemsx.cisd.common.utilities.ITimeAndWaitingProvider;
+import ch.systemsx.cisd.common.utilities.SystemTimeProvider;
 import ch.systemsx.cisd.openbis.generic.server.CommonServiceProvider;
 
 /**
@@ -42,18 +45,24 @@ import ch.systemsx.cisd.openbis.generic.server.CommonServiceProvider;
  */
 public class UserManagementMaintenanceTask implements IMaintenanceTask
 {
-    private static final String CONFIGURATION_FILE_PATH_PROPERTY = "configuration-file-path";
+    static final String CONFIGURATION_FILE_PATH_PROPERTY = "configuration-file-path";
 
-    private static final String DEFAULT_CONFIGURATION_FILE_PATH = "etc/user-management-maintenance-config.json";
+    static final String DEFAULT_CONFIGURATION_FILE_PATH = "etc/user-management-maintenance-config.json";
+
+    static final String AUDIT_LOG_FILE_PATH_PROPERTY = "audit-log-file-path";
+
+    static final String DEFAULT_AUDIT_LOG_FILE_PATH = "logs/user-management-audit_log.txt";
 
     private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
             UserManagementMaintenanceTask.class);
 
     private static final Logger notificationLog = LogFactory.getLogger(LogCategory.NOTIFY,
             UserManagementMaintenanceTask.class);
-    
+
     private File configurationFile;
-    
+
+    private File auditLogFile;
+
     private LDAPAuthenticationService ldapService;
 
     @Override
@@ -66,8 +75,12 @@ public class UserManagementMaintenanceTask implements IMaintenanceTask
             throw new ConfigurationFailureException("Configuration file '" + configurationFile.getAbsolutePath()
                     + "' doesn't exist or is a directory.");
         }
-        
-        ldapService = (LDAPAuthenticationService) CommonServiceProvider.getApplicationContext().getBean("ldap-authentication-service");
+        auditLogFile = new File(properties.getProperty(AUDIT_LOG_FILE_PATH_PROPERTY, DEFAULT_AUDIT_LOG_FILE_PATH));
+        if (auditLogFile.isDirectory())
+        {
+            throw new ConfigurationFailureException("Audit log file '" + auditLogFile.getAbsolutePath() + "' is a directory.");
+        }
+        ldapService = getLdapAuthenticationService();
         if (ldapService.isConfigured() == false)
         {
             throw new ConfigurationFailureException("There is no LDAP authentication service configured. "
@@ -75,7 +88,7 @@ public class UserManagementMaintenanceTask implements IMaintenanceTask
                     + "'ldap.security.principal.password' have to be specified in 'service.properties'.");
         }
         operationLog.info("Plugin '" + pluginName + "' initialized. Configuration file: " + configurationFile.getAbsolutePath());
-        
+
     }
 
     @Override
@@ -87,46 +100,20 @@ public class UserManagementMaintenanceTask implements IMaintenanceTask
             return;
         }
         Log4jSimpleLogger logger = new Log4jSimpleLogger(operationLog);
-        UserManager userManager = new UserManager(CommonServiceProvider.getApplicationServerApi(), config.getCommonSpaces(), logger);
+        UserManager userManager = new UserManager(ldapService, getService(),
+                config.getCommonSpaces(), logger, getTimeProvider());
         for (UserGroup group : config.getGroups())
         {
-            String key = group.getKey();
-            List<String> ldapGroupKeys = group.getLdapGroupKeys();
-            if (ldapGroupKeys == null || ldapGroupKeys.isEmpty())
+            if (addGroup(userManager, group) == false)
             {
-                operationLog.error("No ldapGroupKeys specified for group '" + key + "'. Task aborted.");
                 return;
             }
-            Map<String, Principal> principalsByUserId = new TreeMap<>();
-            for (String ldapGroupKey : ldapGroupKeys)
-            {
-                if (StringUtils.isBlank(ldapGroupKey))
-                {
-                    operationLog.error("Empty ldapGroupKey for group '" + key + "'. Task aborted.");
-                    return;
-                    
-                }
-                List<Principal> principals = ldapService.listPrincipalsByKeyValue("ou", ldapGroupKey);
-                if (principals.isEmpty())
-                {
-                    operationLog.error("No users found for ldapGroupKey '" + ldapGroupKey + "' for group '" + key + "'. Task aborted.");
-                    return;
-                }
-                for (Principal principal : principals)
-                {
-                    principalsByUserId.put(principal.getUserId(), principal);
-                }
-            }
-            userManager.addGroup(key, group, principalsByUserId);
-        }
-        String errorReport = userManager.manageUsers();
-        if (StringUtils.isNotBlank(errorReport))
-        {
-            notificationLog.error("User management failed for the following reasons:\n\n" + errorReport);
         }
+        UserManagerReport userManagerReport = userManager.manage();
+        handleReport(userManagerReport);
         operationLog.info("finished");
     }
-    
+
     private UserManagerConfig readGroupDefinitions()
     {
         if (configurationFile.isFile() == false)
@@ -137,7 +124,8 @@ public class UserManagementMaintenanceTask implements IMaintenanceTask
         String serializedConfig = FileUtilities.loadToString(configurationFile);
         try
         {
-            return deserialize(serializedConfig);
+            ObjectMapper mapper = new ObjectMapper();
+            return mapper.readValue(serializedConfig, UserManagerConfig.class);
         } catch (Exception e)
         {
             operationLog.error("Invalid content of configuration file '" + configurationFile.getAbsolutePath() + "': " + e, e);
@@ -145,9 +133,71 @@ public class UserManagementMaintenanceTask implements IMaintenanceTask
         }
     }
 
-    private UserManagerConfig deserialize(String serializedConfig) throws Exception
+    private boolean addGroup(UserManager userManager, UserGroup group)
+    {
+        String key = group.getKey();
+        List<String> ldapGroupKeys = group.getLdapGroupKeys();
+        if (ldapGroupKeys == null || ldapGroupKeys.isEmpty())
+        {
+            operationLog.error("No ldapGroupKeys specified for group '" + key + "'. Task aborted.");
+            return false;
+        }
+        Map<String, Principal> principalsByUserId = new TreeMap<>();
+        for (String ldapGroupKey : ldapGroupKeys)
+        {
+            if (StringUtils.isBlank(ldapGroupKey))
+            {
+                operationLog.error("Empty ldapGroupKey for group '" + key + "'. Task aborted.");
+                return false;
+
+            }
+            List<Principal> principals = getUsersOfGroup(ldapGroupKey);
+            if (principals.isEmpty())
+            {
+                operationLog.error("No users found for ldapGroupKey '" + ldapGroupKey + "' for group '" + key + "'. Task aborted.");
+                return false;
+            }
+            for (Principal principal : principals)
+            {
+                principalsByUserId.put(principal.getUserId(), principal);
+            }
+        }
+        userManager.addGroup(group, principalsByUserId);
+        return true;
+    }
+
+    private void handleReport(UserManagerReport report)
     {
-        ObjectMapper mapper = new ObjectMapper();
-        return mapper.readValue(serializedConfig, UserManagerConfig.class);
+        String errorReport = report.getErrorReport();
+        if (StringUtils.isNotBlank(errorReport))
+        {
+            notificationLog.error("User management failed for the following reason(s):\n\n" + errorReport);
+        }
+        String auditLog = report.getAuditLog();
+        if (StringUtils.isNotBlank(auditLog))
+        {
+            FileUtilities.appendToFile(auditLogFile, auditLog, true);
+        }
     }
+
+    protected List<Principal> getUsersOfGroup(String ldapGroupKey)
+    {
+        return ldapService.listPrincipalsByKeyValue("ou", ldapGroupKey);
+    }
+
+    protected LDAPAuthenticationService getLdapAuthenticationService()
+    {
+        return (LDAPAuthenticationService) CommonServiceProvider.getApplicationContext().getBean("ldap-authentication-service");
+    }
+
+    protected IApplicationServerInternalApi getService()
+    {
+        return CommonServiceProvider.getApplicationServerApi();
+    }
+
+    protected ITimeAndWaitingProvider getTimeProvider()
+    {
+        return SystemTimeProvider.SYSTEM_TIME_PROVIDER;
+    }
+
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManager.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManager.java
index fbd1ddfbaaa..f591664a7fe 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManager.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManager.java
@@ -34,7 +34,6 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.create.Author
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.create.CreateAuthorizationGroupsOperation;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.fetchoptions.AuthorizationGroupFetchOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.id.AuthorizationGroupPermId;
-import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.id.IAuthorizationGroupId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.update.AuthorizationGroupUpdate;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.update.UpdateAuthorizationGroupsOperation;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.operation.IOperation;
@@ -45,6 +44,7 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.create.PersonCreation;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.fetchoptions.PersonFetchOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.id.IPersonId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.id.PersonPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.search.PersonSearchCriteria;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.update.PersonUpdate;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.update.UpdatePersonsOperation;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.roleassignment.Role;
@@ -57,35 +57,48 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.fetchoptions.SpaceFetchOpt
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.id.ISpaceId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.id.SpacePermId;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.IApplicationServerInternalApi;
+import ch.systemsx.cisd.authentication.IAuthenticationService;
 import ch.systemsx.cisd.authentication.Principal;
 import ch.systemsx.cisd.common.logging.ISimpleLogger;
 import ch.systemsx.cisd.common.logging.LogLevel;
+import ch.systemsx.cisd.common.utilities.ITimeProvider;
 
 /**
  * @author Franz-Josef Elmer
  */
 public class UserManager
 {
+    private final IAuthenticationService authenticationService;
+
     private final IApplicationServerInternalApi service;
 
     private final ISimpleLogger logger;
 
+    private final ITimeProvider timeProvider;
+
     private final Map<Role, List<String>> commonSpacesByRole;
 
     private final Map<String, UserInfo> userInfosByUserId = new TreeMap<>();
 
+    private final Map<String, Map<String, Principal>> usersByGroupCode = new TreeMap<>();
+
     private final List<String> groupCodes = new ArrayList<>();
 
-    public UserManager(IApplicationServerInternalApi service, Map<Role, List<String>> commonSpacesByRole, ISimpleLogger logger)
+    public UserManager(IAuthenticationService authenticationService, IApplicationServerInternalApi service,
+            Map<Role, List<String>> commonSpacesByRole, ISimpleLogger logger, ITimeProvider timeProvider)
     {
+        this.authenticationService = authenticationService;
         this.service = service;
         this.commonSpacesByRole = commonSpacesByRole;
         this.logger = logger;
+        this.timeProvider = timeProvider;
     }
 
-    public void addGroup(String key, UserGroup group, Map<String, Principal> principalsByUserId)
+    public void addGroup(UserGroup group, Map<String, Principal> principalsByUserId)
     {
-        groupCodes.add(key.toUpperCase());
+        String groupCode = group.getKey().toUpperCase();
+        usersByGroupCode.put(groupCode, principalsByUserId);
+        groupCodes.add(groupCode);
         Set<String> admins = asSet(group.getAdmins());
         for (Principal principal : principalsByUserId.values())
         {
@@ -96,70 +109,150 @@ public class UserManager
                 userInfo = new UserInfo(principal);
                 userInfosByUserId.put(userId, userInfo);
             }
-            userInfo.addGroupInfo(new GroupInfo(key, admins.contains(userId)));
+            userInfo.addGroupInfo(new GroupInfo(groupCode, admins.contains(userId)));
         }
-        logger.log(LogLevel.INFO, principalsByUserId.size() + " users for group " + key);
+        logger.log(LogLevel.INFO, principalsByUserId.size() + " users for group " + groupCode);
     }
 
-    public String manageUsers()
+    public UserManagerReport manage()
     {
-        String sessionToken = service.loginAsSystem();
-        Map<IPersonId, Person> users = getUsersWithRoleAssigments(sessionToken);
-        Map<IAuthorizationGroupId, AuthorizationGroup> authorizationGroups = getAuthorizationGroups(sessionToken);
-        StringBuilder builder = new StringBuilder();
+        UserManagerReport report = new UserManagerReport(timeProvider);
+        try
+        {
+            String sessionToken = service.loginAsSystem();
+
+            manageGroups(sessionToken, report);
+            manageUsers(sessionToken, report);
+
+            service.logout(sessionToken);
+        } catch (Throwable e)
+        {
+            report.addErrorMessage("Error: " + e.toString());
+            logger.log(LogLevel.ERROR, "", e);
+        }
+        return report;
+    }
+
+    private void manageGroups(String sessionToken, UserManagerReport report)
+    {
+        Map<String, Set<String>> usersByGroupCodes = getUsersByGroupCodes(sessionToken);
         for (String groupCode : groupCodes)
         {
             try
             {
-                manageGroup(sessionToken, groupCode, authorizationGroups);
+                Context context = new Context(sessionToken);
+                if (usersByGroupCodes.containsKey(groupCode))
+                {
+                    manageKnownGroup(context, groupCode, report);
+                } else
+                {
+                    manageNewGroup(context, groupCode, report);
+                }
+                context.executeOperations();
             } catch (Exception e)
             {
                 String message = String.format("Couldn't manage group '%s' because of the following error: %s",
                         groupCode, e);
-                builder.append(message).append("\n");
+                report.addErrorMessage(message);
                 logger.log(LogLevel.ERROR, message, e);
             }
         }
-        authorizationGroups = getAuthorizationGroups(sessionToken);
+    }
+
+    private void manageUsers(String sessionToken, UserManagerReport report)
+    {
+        revokeUsersUnkownByAuthenticationService(sessionToken, report);
+        Map<IPersonId, Person> users = getUsersWithRoleAssigments(sessionToken);
+        Map<String, Set<String>> currentUsersByGroupCodes = getUsersByGroupCodes(sessionToken);
+        Map<String, Set<String>> currentAdminUsersByGroupCodes = getAdminUsersByGroupCodes(sessionToken);
         for (UserInfo userInfo : userInfosByUserId.values())
         {
             try
             {
-                manageUser(sessionToken, userInfo, users, authorizationGroups);
+                manageUser(sessionToken, userInfo, users, currentUsersByGroupCodes, currentAdminUsersByGroupCodes, report);
             } catch (Exception e)
             {
                 String message = String.format("Couldn't manage user '%s' because of the following error: %s",
                         userInfo.getPrincipal().getUserId(), e);
-                builder.append(message).append("\n");
+                report.addErrorMessage(message);
+                logger.log(LogLevel.ERROR, message, e);
+            }
+        }
+
+        for (String groupCode : currentUsersByGroupCodes.keySet())
+        {
+            try
+            {
+                manageUsersRemovedFromGroup(sessionToken, groupCode, currentUsersByGroupCodes,
+                        currentAdminUsersByGroupCodes, report);
+            } catch (Exception e)
+            {
+                String message = String.format("Couldn't manage users removed from group %s because of the following error: %s",
+                        groupCode, e);
+                report.addErrorMessage(message);
                 logger.log(LogLevel.ERROR, message, e);
             }
         }
-        service.logout(sessionToken);
-        return builder.toString();
     }
 
-    private void manageGroup(String sessionToken, String groupCode, Map<IAuthorizationGroupId, AuthorizationGroup> knownAuthorizationGroups)
+    private void manageUsersRemovedFromGroup(String sessionToken, String groupCode, Map<String, Set<String>> currentUsersByGroupCodes,
+            Map<String, Set<String>> currentAdminUsersByGroupCodes, UserManagerReport report)
     {
+        Set<String> currentUsers = currentUsersByGroupCodes.get(groupCode);
+        Set<String> newUsers = usersByGroupCode.get(groupCode).keySet();
         Context context = new Context(sessionToken);
-        AuthorizationGroup knownGroup = knownAuthorizationGroups.get(new AuthorizationGroupPermId(groupCode));
-        if (knownGroup != null)
+        for (String currentUser : currentUsers)
         {
-            manageKnownGroup(context, knownGroup);
-        } else
-        {
-            manageNewGroup(context, groupCode);
+            if (newUsers.contains(currentUser) == false)
+            {
+                removePersonFromAuthorizationGroup(context, currentUsersByGroupCodes, groupCode, currentUser, report);
+                String adminGroupCode = createAdminGroupCode(groupCode);
+                removePersonFromAuthorizationGroup(context, currentAdminUsersByGroupCodes, adminGroupCode, currentUser, report);
+            }
         }
         context.executeOperations();
     }
 
-    private void manageNewGroup(Context context, String groupCode)
+    private void revokeUsersUnkownByAuthenticationService(String sessionToken, UserManagerReport report)
+    {
+        List<PersonUpdate> updates = new ArrayList<>();
+        List<Person> persons = service.searchPersons(sessionToken, new PersonSearchCriteria(), new PersonFetchOptions()).getObjects();
+        for (Person person : persons)
+        {
+            if (person.isActive())
+            {
+                try
+                {
+                    authenticationService.getPrincipal(person.getUserId());
+                } catch (IllegalArgumentException e)
+                {
+                    PersonUpdate update = new PersonUpdate();
+                    update.setUserId(person.getPermId());
+                    update.deactivate();
+                    updates.add(update);
+                    report.deactivateUser(person.getUserId());
+                }
+            }
+        }
+        if (updates.isEmpty() == false)
+        {
+            service.updatePersons(sessionToken, updates);
+        }
+    }
+
+    private void manageKnownGroup(Context context, String groupCode, UserManagerReport report)
+    {
+        // TODO Auto-generated method stub
+    }
+
+    private void manageNewGroup(Context context, String groupCode, UserManagerReport report)
     {
         String adminGroupCode = createAdminGroupCode(groupCode);
         assertAuthorizationGroupDoesNotExist(context, adminGroupCode);
         assertNoCommonSpaceExists(context, groupCode);
 
-        createAuthorizationGroup(context, groupCode);
-        createAuthorizationGroup(context, adminGroupCode);
+        createAuthorizationGroup(context, groupCode, report);
+        createAuthorizationGroup(context, adminGroupCode, report);
 
         for (Entry<Role, List<String>> entry : commonSpacesByRole.entrySet())
         {
@@ -167,8 +260,9 @@ public class UserManager
             for (String space : entry.getValue())
             {
                 ISpaceId spaceId = createSpace(context, createCommonSpaceCode(groupCode, space));
-                createRoleAssignment(context, new AuthorizationGroupPermId(groupCode), role, spaceId);
-                createRoleAssignment(context, new AuthorizationGroupPermId(adminGroupCode), Role.ADMIN, spaceId);
+                report.addSpace(spaceId);
+                createRoleAssignment(context, new AuthorizationGroupPermId(groupCode), role, spaceId, report);
+                createRoleAssignment(context, new AuthorizationGroupPermId(adminGroupCode), Role.ADMIN, spaceId, report);
             }
         }
     }
@@ -199,57 +293,104 @@ public class UserManager
         throw new IllegalStateException("The group '" + groupCode + "' has already the following spaces: " + existingSpaces);
     }
 
-    private void manageKnownGroup(Context context, AuthorizationGroup knownGroup)
-    {
-        // TODO Auto-generated method stub
-
-    }
-
     private void manageUser(String sessionToken, UserInfo userInfo, Map<IPersonId, Person> knownUsers,
-            Map<IAuthorizationGroupId, AuthorizationGroup> knownAuthorizationGroups)
+            Map<String, Set<String>> currentUsersByGroupCodes, Map<String, Set<String>> currentAdminUsersByGroupCodes,
+            UserManagerReport report)
     {
         Context context = new Context(sessionToken);
         Person knownUser = knownUsers.get(new PersonPermId(userInfo.getPrincipal().getUserId()));
         if (knownUser != null)
         {
-            manageKnownUser(context, userInfo, knownUser, knownAuthorizationGroups);
+            manageKnownUser(context, userInfo, knownUser, currentUsersByGroupCodes, currentAdminUsersByGroupCodes, report);
         } else
         {
-            manageNewUser(context, userInfo, knownAuthorizationGroups);
+            manageNewUser(context, userInfo, userInfo.getPrincipal().getUserId().toUpperCase(),
+                    currentUsersByGroupCodes, currentAdminUsersByGroupCodes, report);
         }
         context.executeOperations();
     }
 
     private void manageKnownUser(Context context, UserInfo userInfo, Person knownUser,
-            Map<IAuthorizationGroupId, AuthorizationGroup> knownAuthorizationGroups)
+            Map<String, Set<String>> currentUsersByGroupCodes, Map<String, Set<String>> currentAdminUsersByGroupCodes,
+            UserManagerReport report)
     {
-        System.out.println("UserManager.manageKnownUser() " + userInfo);
+        if (knownUser.isActive() == false)
+        {
+            int maxSequenceNumber = getMaxSequenceNumber(context, knownUser);
+            String homeSpaceCode = knownUser.getUserId() + "_" + (maxSequenceNumber + 1);
+            manageNewUser(context, userInfo, homeSpaceCode, currentUsersByGroupCodes, currentAdminUsersByGroupCodes, report);
+        } else
+        {
+            String userId = userInfo.principal.getUserId();
+            for (GroupInfo groupInfo : userInfo.getGroupInfosByGroupKey().values())
+            {
+                String groupCode = groupInfo.getKey();
+                addPersonToAuthorizationGroup(context, currentUsersByGroupCodes, groupCode, userId, report);
+                String adminGroupCode = createAdminGroupCode(groupCode);
+                if (groupInfo.isAdmin())
+                {
+                    addPersonToAuthorizationGroup(context, currentAdminUsersByGroupCodes, adminGroupCode, userId, report);
+                } else if (isCurrentAdminUser(currentAdminUsersByGroupCodes, adminGroupCode, userId))
+                {
+                    removePersonFromAuthorizationGroup(context, currentAdminUsersByGroupCodes, adminGroupCode, userId, report);
+                }
+            }
+        }
     }
 
-    private void manageNewUser(Context context, UserInfo userInfo,
-            Map<IAuthorizationGroupId, AuthorizationGroup> knownAuthorizationGroups)
+    private int getMaxSequenceNumber(Context context, Person knownUser)
     {
-        Principal principal = userInfo.getPrincipal();
-        String userId = principal.getUserId();
-        String spaceCode = userId.toUpperCase();
-        if (getSpaces(context.getSessionToken(), Arrays.asList(spaceCode)).isEmpty() == false)
+        PersonSearchCriteria searchCriteria = new PersonSearchCriteria();
+        searchCriteria.withUserId().thatStartsWith(knownUser.getUserId());
+        PersonFetchOptions fetchOptions = new PersonFetchOptions();
+        int maxSequenceNumber = 0;
+        for (Person person : service.searchPersons(context.getSessionToken(), searchCriteria, fetchOptions).getObjects())
         {
-            throw new IllegalStateException("There is already a space with code " + spaceCode + ".");
+            String[] splittedUserId = person.getUserId().split("_");
+            if (splittedUserId.length == 2)
+            {
+                try
+                {
+                    maxSequenceNumber = Math.max(maxSequenceNumber, Integer.parseInt(splittedUserId[1]));
+                } catch (NumberFormatException e)
+                {
+                    // silently ignored
+                }
+            }
         }
+        return maxSequenceNumber;
+    }
 
-        ISpaceId homeSpaceId = createSpace(context, spaceCode);
+    private boolean isCurrentAdminUser(Map<String, Set<String>> currentAdminUsersByGroupCodes, String groupCode, String userId)
+    {
+        Set<String> users = currentAdminUsersByGroupCodes.get(groupCode);
+        return users == null ? false : users.contains(userId);
+    }
+
+    private void manageNewUser(Context context, UserInfo userInfo, String homeSpaceCode,
+            Map<String, Set<String>> currentUsersByGroupCodes, Map<String, Set<String>> currentAdminUsersByGroupCodes,
+            UserManagerReport report)
+    {
+        String userId = userInfo.getPrincipal().getUserId();
+        if (getSpaces(context.getSessionToken(), Arrays.asList(homeSpaceCode)).isEmpty() == false)
+        {
+            throw new IllegalStateException("There is already a space with code " + homeSpaceCode + ".");
+        }
+
+        ISpaceId homeSpaceId = createSpace(context, homeSpaceCode);
         IPersonId personId = createPerson(context, userId);
         assignHomeSpace(context, homeSpaceId, personId);
+        report.addUser(userId, homeSpaceCode);
 
         for (GroupInfo groupInfo : userInfo.getGroupInfosByGroupKey().values())
         {
             String groupCode = groupInfo.getKey();
-            addPersonToAuthorizationGroup(context, new AuthorizationGroupPermId(groupCode), personId);
-            AuthorizationGroupPermId adminGroupId = new AuthorizationGroupPermId(createAdminGroupCode(groupCode));
-            createRoleAssignment(context, adminGroupId, Role.ADMIN, homeSpaceId);
+            addPersonToAuthorizationGroup(context, currentUsersByGroupCodes, groupCode, userId, report);
+            String adminGroupCode = createAdminGroupCode(groupCode);
+            createRoleAssignment(context, new AuthorizationGroupPermId(adminGroupCode), Role.ADMIN, homeSpaceId, report);
             if (groupInfo.isAdmin())
             {
-                addPersonToAuthorizationGroup(context, adminGroupId, personId);
+                addPersonToAuthorizationGroup(context, currentAdminUsersByGroupCodes, adminGroupCode, userId, report);
             }
         }
     }
@@ -267,12 +408,32 @@ public class UserManager
         context.add(roleCreation);
     }
 
-    private void addPersonToAuthorizationGroup(Context context, AuthorizationGroupPermId groupId, IPersonId personId)
+    private void addPersonToAuthorizationGroup(Context context, Map<String, Set<String>> currentUsersByGroupCodes,
+            String groupCode, String userId, UserManagerReport report)
+    {
+        Set<String> users = currentUsersByGroupCodes.get(groupCode);
+        if (users == null || users.contains(userId) == false)
+        {
+            AuthorizationGroupUpdate groupUpdate = new AuthorizationGroupUpdate();
+            groupUpdate.setAuthorizationGroupId(new AuthorizationGroupPermId(groupCode));
+            groupUpdate.getUserIds().add(new PersonPermId(userId));
+            context.add(groupUpdate);
+            report.addUserToGroup(groupCode, userId);
+        }
+    }
+
+    private void removePersonFromAuthorizationGroup(Context context, Map<String, Set<String>> currentUsersByGroupCodes,
+            String groupCode, String userId, UserManagerReport report)
     {
-        AuthorizationGroupUpdate groupUpdate = new AuthorizationGroupUpdate();
-        groupUpdate.setAuthorizationGroupId(groupId);
-        groupUpdate.getUserIds().add(personId);
-        context.add(groupUpdate);
+        Set<String> users = currentUsersByGroupCodes.get(groupCode);
+        if (users == null || users.contains(userId))
+        {
+            AuthorizationGroupUpdate groupUpdate = new AuthorizationGroupUpdate();
+            groupUpdate.setAuthorizationGroupId(new AuthorizationGroupPermId(groupCode));
+            groupUpdate.getUserIds().remove(new PersonPermId(userId));
+            context.add(groupUpdate);
+            report.removeUserFromGroup(groupCode, userId);
+        }
     }
 
     private ISpaceId createSpace(Context context, String spaceCode)
@@ -291,20 +452,23 @@ public class UserManager
         return new PersonPermId(userId);
     }
 
-    private void createAuthorizationGroup(Context context, String groupCode)
+    private void createAuthorizationGroup(Context context, String groupCode, UserManagerReport report)
     {
         AuthorizationGroupCreation creation = new AuthorizationGroupCreation();
         creation.setCode(groupCode);
         context.add(creation);
+        report.addGroup(groupCode);
     }
 
-    private void createRoleAssignment(Context context, AuthorizationGroupPermId groupId, Role role, ISpaceId spaceId)
+    private void createRoleAssignment(Context context, AuthorizationGroupPermId groupId, Role role, ISpaceId spaceId,
+            UserManagerReport report)
     {
         RoleAssignmentCreation roleCreation = new RoleAssignmentCreation();
         roleCreation.setAuthorizationGroupId(groupId);
         roleCreation.setRole(role);
         roleCreation.setSpaceId(spaceId);
         context.add(roleCreation);
+        report.assignRoleTo(groupId, role, spaceId);
     }
 
     private String createCommonSpaceCode(String groupCode, String spaceCode)
@@ -312,11 +476,16 @@ public class UserManager
         return groupCode + "_" + spaceCode;
     }
 
-    private String createAdminGroupCode(String groupCode)
+    public static String createAdminGroupCode(String groupCode)
     {
         return groupCode + "_ADMIN";
     }
 
+    private AuthorizationGroupPermId createAdminGroupId(String groupCode)
+    {
+        return new AuthorizationGroupPermId(createAdminGroupCode(groupCode));
+    }
+
     private Map<ISpaceId, Space> getSpaces(String sessionToken, Collection<String> spaceCodes)
     {
         SpaceFetchOptions fetchOptions = new SpaceFetchOptions();
@@ -333,13 +502,28 @@ public class UserManager
         return users;
     }
 
-    private Map<IAuthorizationGroupId, AuthorizationGroup> getAuthorizationGroups(String sessionToken)
+    private Map<String, Set<String>> getUsersByGroupCodes(String sessionToken)
+    {
+        return getUsersByGroupCodes(sessionToken, groupCode -> new AuthorizationGroupPermId(groupCode));
+    }
+
+    private Map<String, Set<String>> getAdminUsersByGroupCodes(String sessionToken)
+    {
+        return getUsersByGroupCodes(sessionToken, groupCode -> createAdminGroupId(groupCode));
+    }
+
+    private Map<String, Set<String>> getUsersByGroupCodes(String sessionToken, Function<String, AuthorizationGroupPermId> mapper)
     {
-        Function<String, AuthorizationGroupPermId> mapper = key -> new AuthorizationGroupPermId(key);
         List<AuthorizationGroupPermId> groupPermIds = groupCodes.stream().map(mapper).collect(Collectors.toList());
         AuthorizationGroupFetchOptions fetchOptions = new AuthorizationGroupFetchOptions();
         fetchOptions.withUsers();
-        return service.getAuthorizationGroups(sessionToken, groupPermIds, fetchOptions);
+        Map<String, Set<String>> usersByGroupCodes = new TreeMap<>();
+        for (AuthorizationGroup group : service.getAuthorizationGroups(sessionToken, groupPermIds, fetchOptions).values())
+        {
+            Set<String> users = group.getUsers().stream().map(Person::getUserId).collect(Collectors.toSet());
+            usersByGroupCodes.put(group.getCode(), users);
+        }
+        return usersByGroupCodes;
     }
 
     private Set<String> asSet(List<String> users)
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagerReport.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagerReport.java
new file mode 100644
index 00000000000..89f57855411
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagerReport.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2018 ETH Zuerich, SIS
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.generic.server.task;
+
+import java.text.MessageFormat;
+import java.util.Date;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.id.AuthorizationGroupPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.roleassignment.Role;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.id.ISpaceId;
+import ch.systemsx.cisd.common.utilities.ITimeProvider;
+
+/**
+ * @author Franz-Josef Elmer
+ */
+public class UserManagerReport
+{
+    private StringBuilder errorReport = new StringBuilder();
+
+    private StringBuilder auditLog = new StringBuilder();
+
+    private ITimeProvider timeProvider;
+
+    public UserManagerReport(ITimeProvider timeProvider)
+    {
+        this.timeProvider = timeProvider;
+
+    }
+
+    public String getErrorReport()
+    {
+        return errorReport.toString();
+    }
+
+    public String getAuditLog()
+    {
+        return auditLog.toString();
+    }
+
+    void addErrorMessage(String message)
+    {
+        errorReport.append(message).append('\n');
+    }
+
+    void addGroup(String groupCode)
+    {
+        log("ADD-GROUP", groupCode);
+    }
+
+    void deactivateUser(String userId)
+    {
+        log("DEACTIVATE-USER", userId);
+    }
+
+    void addUser(String userId, String homeSpaceCode)
+    {
+        log("ADD-USER", userId + " (home space: " + homeSpaceCode + ")");
+    }
+
+    void addSpace(ISpaceId spaceId)
+    {
+        log("ADD-SPACE", spaceId);
+    }
+
+    void assignRoleTo(AuthorizationGroupPermId groupId, Role role, ISpaceId spaceId)
+    {
+        log("ASSIGN-ROLE-TO-GROUP", groupId + ", SPACE_" + role + " for " + spaceId);
+    }
+
+    void addUserToGroup(String groupCode, String userId)
+    {
+        log("ADD-USER-TO-GROUP", groupCode + ", " + userId);
+    }
+
+    void removeUserFromGroup(String groupCode, String userId)
+    {
+        log("REMOVE-USER-FROM-GROUP", groupCode + ", " + userId);
+    }
+
+    private void log(String action, Object details)
+    {
+        Date timeStamp = new Date(timeProvider.getTimeInMilliseconds());
+        MessageFormat messageFormat = new MessageFormat("{0,date,yyyy-MM-dd HH:mm:ss} [{1}] {2}\n");
+        auditLog.append(messageFormat.format(new Object[] { timeStamp, action, details }));
+    }
+
+}
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/task/UserManagerExpectationsBuilder.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/task/UserManagerExpectationsBuilder.java
index 04d963a1895..02ff5808bcf 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/task/UserManagerExpectationsBuilder.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/task/UserManagerExpectationsBuilder.java
@@ -32,6 +32,9 @@ import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.AuthorizationGroup;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.fetchoptions.AuthorizationGroupFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.authorizationgroup.id.AuthorizationGroupPermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.id.ObjectPermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.interfaces.ICodeHolder;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.Person;
@@ -45,6 +48,7 @@ import ch.ethz.sis.openbis.generic.server.asapi.v3.IApplicationServerInternalApi
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.IOperationContext;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.OperationContext;
 import ch.systemsx.cisd.authentication.Principal;
+import ch.systemsx.cisd.openbis.generic.server.task.UserManager;
 import ch.systemsx.cisd.openbis.generic.shared.IOpenBisSessionManager;
 import ch.systemsx.cisd.openbis.generic.shared.dto.Session;
 import ch.systemsx.cisd.openbis.generic.shared.dto.SpacePE;
@@ -65,8 +69,10 @@ class UserManagerExpectationsBuilder
 
     private Map<String, List<Principal>> usersByGroup = new TreeMap<>();
 
-    private Map<String, List<Principal>> disabledUsersByGroup = new TreeMap<>();
+    private List<Principal> unknownUsers = new ArrayList<>();
 
+    private Map<String, List<Principal>> disabledUsersByGroup = new TreeMap<>();
+    
     private Map<String, List<Principal>> normalUsersByGroup = new TreeMap<>();
 
     private Map<String, List<Principal>> adminUsersByGroup = new TreeMap<>();
@@ -80,6 +86,12 @@ class UserManagerExpectationsBuilder
         this.commonSpaces = commonSpaces;
     }
 
+    UserManagerExpectationsBuilder unknownUser(Principal user)
+    {
+        unknownUsers.add(user);
+        return this;
+    }
+    
     UserManagerExpectationsBuilder disabledUser(Principal user, String... groups)
     {
         return addUser(user, disabledUsersByGroup, groups);
@@ -120,6 +132,7 @@ class UserManagerExpectationsBuilder
     {
         String sessionToken = v3api.login(TEST_USER, PASSWORD);
         assertSpaces(sessionToken);
+        assertUnknownUsers(sessionToken);
         assertUsers(sessionToken);
         assertAuthorization(sessionToken);
         v3api.logout(sessionToken);
@@ -155,10 +168,49 @@ class UserManagerExpectationsBuilder
         List<String> expectedSpaces = extractedSortedPermIds(spaces);
         SpaceFetchOptions fetchOptions = new SpaceFetchOptions();
         List<String> actualSpaces = extractedSortedCodes(v3api.getSpaces(sessionToken, spaces, fetchOptions).values());
-        System.out.println("UserManagerTestExpectation: Spaces: " + expectedSpaces);
         assertEquals(actualSpaces.toString(), expectedSpaces.toString());
     }
 
+    private void assertUnknownUsers(String sessionToken)
+    {
+        Map<String, Set<String>> usersByGroupId = getAllUsersOfGroups(sessionToken);
+        assertEquals(usersByGroupId.isEmpty(), false);
+        List<PersonPermId> personIds = unknownUsers.stream().map(p -> new PersonPermId(p.getUserId())).collect(Collectors.toList());
+        PersonFetchOptions fetchOptions = new PersonFetchOptions();
+        fetchOptions.withRoleAssignments();
+        for (Person person : v3api.getPersons(sessionToken, personIds, fetchOptions).values())
+        {
+            String userId = person.getUserId();
+            assertEquals(person.isActive(), Boolean.FALSE, "Active flag of user " + userId);
+            assertEquals(person.getRoleAssignments().size(), 0, "Role assignments of user " + userId);
+            for (Entry<String, Set<String>> entry : usersByGroupId.entrySet())
+            {
+                if (entry.getValue().contains(userId))
+                {
+                    fail("User " + userId + " still in authorization group " + entry.getKey());
+                }
+            }
+        }
+    }
+
+    private Map<String, Set<String>> getAllUsersOfGroups(String sessionToken)
+    {
+        AuthorizationGroupFetchOptions groupFetchOptions = new AuthorizationGroupFetchOptions();
+        groupFetchOptions.withUsers();
+        List<AuthorizationGroupPermId> ids = new ArrayList<>();
+        for (String groupCode : usersByGroup.keySet())
+        {
+            ids.add(new AuthorizationGroupPermId(groupCode));
+            ids.add(new AuthorizationGroupPermId(UserManager.createAdminGroupCode(groupCode)));
+        }
+        Map<String, Set<String>> usersByGroupId = new TreeMap<>();
+        for (AuthorizationGroup group : v3api.getAuthorizationGroups(sessionToken, ids, groupFetchOptions).values())
+        {
+            usersByGroupId.put(group.getCode(), group.getUsers().stream().map(Person::getUserId).collect(Collectors.toSet()));
+        }
+        return usersByGroupId;
+    }
+    
     private void assertUsers(String sessionToken)
     {
         List<PersonPermId> allUsers = applyMapperToAllUsers(user -> new PersonPermId(user.getUserId()));
@@ -168,7 +220,6 @@ class UserManagerExpectationsBuilder
         Function<IPersonId, PersonPermId> mapper = id -> (PersonPermId) id;
         List<String> expectedUsers = extractedSortedPermIds(allUsers);
         List<String> actualUsers = extractedSortedPermIds(persons.keySet().stream().map(mapper).collect(Collectors.toSet()));
-        System.out.println("UserManagerTestExpectation: Users: " + expectedUsers);
         assertEquals(actualUsers.toString(), expectedUsers.toString());
         for (PersonPermId id : allUsers)
         {
@@ -192,28 +243,29 @@ class UserManagerExpectationsBuilder
     {
         for (Entry<String, List<Principal>> entry : disabledUsersByGroup.entrySet())
         {
-            String groupKey = entry.getKey();
+            String groupCode = entry.getKey();
             for (Principal user : entry.getValue())
             {
-                commonSpaces.get(Role.USER).forEach(expectForSpace(expectations, user, groupKey, Level.NON));
-                commonSpaces.get(Role.OBSERVER).forEach(expectForSpace(expectations, user, groupKey, Level.NON));
+                commonSpaces.get(Role.USER).forEach(expectForSpace(expectations, user, groupCode, Level.NON));
+                commonSpaces.get(Role.OBSERVER).forEach(expectForSpace(expectations, user, groupCode, Level.NON));
                 for (List<Principal> users2 : usersByGroup.values())
                 {
-                    users2.forEach(user2 -> expectations.expect(user, getOwenSpace(user2), Level.NON));
+                    users2.forEach(user2 -> expectations.expect(user, getOwenSpace(user2), 
+                            equals(user, user2) ? Level.SPACE_ADMIN : Level.NON));
                 }
             }
         }
     }
-
+    
     private void createExpectationsForNormalUsers(AuthorizationExpectations expectations)
     {
         for (Entry<String, List<Principal>> entry : normalUsersByGroup.entrySet())
         {
-            String groupKey = entry.getKey();
+            String groupCode = entry.getKey();
             for (Principal user : entry.getValue())
             {
-                commonSpaces.get(Role.USER).forEach(expectForSpace(expectations, user, groupKey, Level.SPACE_USER));
-                commonSpaces.get(Role.OBSERVER).forEach(expectForSpace(expectations, user, groupKey, Level.SPACE_OBSERVER));
+                commonSpaces.get(Role.USER).forEach(expectForSpace(expectations, user, groupCode, Level.SPACE_USER));
+                commonSpaces.get(Role.OBSERVER).forEach(expectForSpace(expectations, user, groupCode, Level.SPACE_OBSERVER));
                 for (List<Principal> users2 : usersByGroup.values())
                 {
                     users2.forEach(user2 -> expectations.expect(user, getOwenSpace(user2),
@@ -227,17 +279,17 @@ class UserManagerExpectationsBuilder
     {
         for (Entry<String, List<Principal>> entry : adminUsersByGroup.entrySet())
         {
-            String groupKey = entry.getKey();
+            String groupCode = entry.getKey();
             for (Principal user : entry.getValue())
             {
-                commonSpaces.get(Role.USER).forEach(expectForSpace(expectations, user, groupKey, Level.SPACE_ADMIN));
-                commonSpaces.get(Role.OBSERVER).forEach(expectForSpace(expectations, user, groupKey, Level.SPACE_ADMIN));
+                commonSpaces.get(Role.USER).forEach(expectForSpace(expectations, user, groupCode, Level.SPACE_ADMIN));
+                commonSpaces.get(Role.OBSERVER).forEach(expectForSpace(expectations, user, groupCode, Level.SPACE_ADMIN));
                 Set<Entry<String, List<Principal>>> entrySet = usersByGroup.entrySet();
                 for (Entry<String, List<Principal>> entry2 : entrySet)
                 {
-                    String group2Key = entry2.getKey();
+                    String groupCode2 = entry2.getKey();
                     List<Principal> users2 = entry2.getValue();
-                    if (groupKey.equals(group2Key))
+                    if (groupCode.equals(groupCode2))
                     {
                         users2.forEach(user2 -> expectations.expect(user, getOwenSpace(user2), Level.SPACE_ADMIN));
                     } else
@@ -249,9 +301,9 @@ class UserManagerExpectationsBuilder
         }
     }
 
-    private Consumer<String> expectForSpace(AuthorizationExpectations expectations, Principal user, String groupKey, Level level)
+    private Consumer<String> expectForSpace(AuthorizationExpectations expectations, Principal user, String groupCode, Level level)
     {
-        return space -> expectations.expect(user, createCommonSpaceCode(groupKey, space), level);
+        return space -> expectations.expect(user, createCommonSpaceCode(groupCode, space), level);
     }
 
     private static final class AuthorizationExpectations
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/task/UserManagerTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/task/UserManagerTest.java
index 3560b13297d..bcd42615584 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/task/UserManagerTest.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/task/UserManagerTest.java
@@ -22,7 +22,9 @@ import java.util.Arrays;
 import java.util.EnumMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.TreeMap;
+import java.util.stream.Collectors;
 
 import javax.annotation.Resource;
 
@@ -31,11 +33,14 @@ import org.testng.annotations.Test;
 
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.roleassignment.Role;
 import ch.ethz.sis.openbis.systemtest.asapi.v3.AbstractTest;
+import ch.systemsx.cisd.authentication.NullAuthenticationService;
 import ch.systemsx.cisd.authentication.Principal;
 import ch.systemsx.cisd.common.logging.MockLogger;
+import ch.systemsx.cisd.common.utilities.MockTimeProvider;
 import ch.systemsx.cisd.openbis.generic.server.ComponentNames;
 import ch.systemsx.cisd.openbis.generic.server.task.UserGroup;
 import ch.systemsx.cisd.openbis.generic.server.task.UserManager;
+import ch.systemsx.cisd.openbis.generic.server.task.UserManagerReport;
 import ch.systemsx.cisd.openbis.generic.shared.IOpenBisSessionManager;
 
 /**
@@ -69,17 +74,37 @@ public class UserManagerTest extends AbstractTest
         // Given
         MockLogger logger = new MockLogger();
         Map<Role, List<String>> commonSpaces = commonSpaces();
-        UserManager userManager = new UserManager(v3api, commonSpaces, logger);
+        UserManager userManager = createUserManager(commonSpaces, logger);
         Map<String, Principal> principals = principals(U3, U1, U2);
-        UserGroup group = new UserGroup();
-        group.setAdmins(Arrays.asList(U1.getUserId(), "blabla"));
-        userManager.addGroup("G1", group, principals);
+        UserGroup group = group("G1", U1.getUserId(), "blabla");
+        userManager.addGroup(group, principals);
 
         // When
-        String errorMessages = userManager.manageUsers();
+        UserManagerReport report = manage(userManager);
 
         // Then
-        assertEquals(errorMessages, "");
+        assertEquals(report.getErrorReport(), "");
+        assertEquals(report.getAuditLog(), "1970-01-01 01:00:00 [ADD-GROUP] G1\n"
+                + "1970-01-01 01:00:01 [ADD-GROUP] G1_ADMIN\n"
+                + "1970-01-01 01:00:02 [ADD-SPACE] G1_ALPHA\n"
+                + "1970-01-01 01:00:03 [ASSIGN-ROLE-TO-GROUP] G1, SPACE_USER for G1_ALPHA\n"
+                + "1970-01-01 01:00:04 [ASSIGN-ROLE-TO-GROUP] G1_ADMIN, SPACE_ADMIN for G1_ALPHA\n"
+                + "1970-01-01 01:00:05 [ADD-SPACE] G1_BETA\n"
+                + "1970-01-01 01:00:06 [ASSIGN-ROLE-TO-GROUP] G1, SPACE_USER for G1_BETA\n"
+                + "1970-01-01 01:00:07 [ASSIGN-ROLE-TO-GROUP] G1_ADMIN, SPACE_ADMIN for G1_BETA\n"
+                + "1970-01-01 01:00:08 [ADD-SPACE] G1_GAMMA\n"
+                + "1970-01-01 01:00:09 [ASSIGN-ROLE-TO-GROUP] G1, SPACE_OBSERVER for G1_GAMMA\n"
+                + "1970-01-01 01:00:10 [ASSIGN-ROLE-TO-GROUP] G1_ADMIN, SPACE_ADMIN for G1_GAMMA\n"
+                + "1970-01-01 01:00:11 [ADD-USER] u1 (home space: U1)\n"
+                + "1970-01-01 01:00:12 [ADD-USER-TO-GROUP] G1, u1\n"
+                + "1970-01-01 01:00:13 [ASSIGN-ROLE-TO-GROUP] G1_ADMIN, SPACE_ADMIN for U1\n"
+                + "1970-01-01 01:00:14 [ADD-USER-TO-GROUP] G1_ADMIN, u1\n"
+                + "1970-01-01 01:00:15 [ADD-USER] u2 (home space: U2)\n"
+                + "1970-01-01 01:00:16 [ADD-USER-TO-GROUP] G1, u2\n"
+                + "1970-01-01 01:00:17 [ASSIGN-ROLE-TO-GROUP] G1_ADMIN, SPACE_ADMIN for U2\n"
+                + "1970-01-01 01:00:18 [ADD-USER] u3 (home space: U3)\n"
+                + "1970-01-01 01:00:19 [ADD-USER-TO-GROUP] G1, u3\n"
+                + "1970-01-01 01:00:20 [ASSIGN-ROLE-TO-GROUP] G1_ADMIN, SPACE_ADMIN for U3\n");
         UserManagerExpectationsBuilder builder = createBuilder(commonSpaces);
         builder.adminUser(U1, "G1");
         builder.user(U2, "G1");
@@ -93,20 +118,27 @@ public class UserManagerTest extends AbstractTest
         // Given
         MockLogger logger = new MockLogger();
         Map<Role, List<String>> commonSpaces = commonSpaces();
-        UserManager userManager = new UserManager(v3api, commonSpaces, logger);
-        UserGroup group = new UserGroup();
-        group.setAdmins(Arrays.asList(U1.getUserId(), "blabla"));
-        userManager.addGroup("G2", group, principals(U1));
-        assertEquals(userManager.manageUsers(), "");
+        UserManager userManager = createUserManager(commonSpaces, logger);
+        UserGroup group = group("G2", U1.getUserId(), "blabla");
+        userManager.addGroup(group, principals(U1));
+        assertEquals(manage(userManager).getErrorReport(), "");
+        createBuilder(commonSpaces).adminUser(U1, "G2").assertExpectations();
 
-        userManager = new UserManager(v3api, commonSpaces, logger);
-        userManager.addGroup("G2", group, principals(U2, U3));
+        userManager = createUserManager(commonSpaces, logger);
+        group.setAdmins(Arrays.asList(U1.getUserId()));
+        userManager.addGroup(group, principals(U1, U2, U3));
 
         // When
-        String errorMessages = userManager.manageUsers();
+        UserManagerReport report = manage(userManager);
 
         // Then
-        assertEquals(errorMessages, "");
+        assertEquals(report.getErrorReport(), "");
+        assertEquals(report.getAuditLog(), "1970-01-01 01:00:00 [ADD-USER] u2 (home space: U2)\n"
+                + "1970-01-01 01:00:01 [ADD-USER-TO-GROUP] G2, u2\n"
+                + "1970-01-01 01:00:02 [ASSIGN-ROLE-TO-GROUP] G2_ADMIN, SPACE_ADMIN for U2\n"
+                + "1970-01-01 01:00:03 [ADD-USER] u3 (home space: U3)\n"
+                + "1970-01-01 01:00:04 [ADD-USER-TO-GROUP] G2, u3\n"
+                + "1970-01-01 01:00:05 [ASSIGN-ROLE-TO-GROUP] G2_ADMIN, SPACE_ADMIN for U3\n");
         UserManagerExpectationsBuilder builder = createBuilder(commonSpaces);
         builder.adminUser(U1, "G2");
         builder.user(U2, "G2");
@@ -114,11 +146,281 @@ public class UserManagerTest extends AbstractTest
         builder.assertExpectations();
     }
 
+    @Test
+    public void testRemoveNormalUserFromAGroup()
+    {
+        // Given
+        MockLogger logger = new MockLogger();
+        Map<Role, List<String>> commonSpaces = commonSpaces();
+        UserManager userManager = createUserManager(commonSpaces, logger);
+        UserGroup group = group("G2", U1.getUserId(), "blabla");
+        userManager.addGroup(group, principals(U1, U2, U3));
+        assertEquals(manage(userManager).getErrorReport(), "");
+        createBuilder(commonSpaces).adminUser(U1, "G2").user(U2, "G2").user(U3, "G2").assertExpectations();
+
+        userManager = createUserManager(commonSpaces, logger);
+        userManager.addGroup(group, principals(U1, U3));
+
+        // When
+        UserManagerReport report = manage(userManager);
+
+        // Then
+        assertEquals(report.getErrorReport(), "");
+        assertEquals(report.getAuditLog(), "1970-01-01 01:00:00 [REMOVE-USER-FROM-GROUP] G2, u2\n");
+        UserManagerExpectationsBuilder builder = createBuilder(commonSpaces);
+        builder.adminUser(U1, "G2");
+        builder.disabledUser(U2, "G2");
+        builder.user(U3, "G2");
+        builder.assertExpectations();
+    }
+
+    @Test
+    public void testChangeNormalUserToAdmin()
+    {
+        // Given
+        MockLogger logger = new MockLogger();
+        Map<Role, List<String>> commonSpaces = commonSpaces();
+        UserManager userManager = createUserManager(commonSpaces, logger);
+        UserGroup group = group("G2");
+        userManager.addGroup(group, principals(U1, U2, U3));
+        assertEquals(manage(userManager).getErrorReport(), "");
+        createBuilder(commonSpaces).user(U1, "G2").user(U2, "G2").user(U3, "G2").assertExpectations();
+
+        userManager = createUserManager(commonSpaces, logger);
+        group = group("G2", U1.getUserId());
+        userManager.addGroup(group, principals(U1, U2, U3));
+
+        // When
+        UserManagerReport report = manage(userManager);
+
+        // Then
+        assertEquals(report.getErrorReport(), "");
+        assertEquals(report.getAuditLog(), "1970-01-01 01:00:00 [ADD-USER-TO-GROUP] G2_ADMIN, u1\n");
+        UserManagerExpectationsBuilder builder = createBuilder(commonSpaces);
+        builder.adminUser(U1, "G2");
+        builder.user(U2, "G2");
+        builder.user(U3, "G2");
+        builder.assertExpectations();
+    }
+
+    @Test
+    public void testChangeAdminUserToNormalAdmin()
+    {
+        // Given
+        MockLogger logger = new MockLogger();
+        Map<Role, List<String>> commonSpaces = commonSpaces();
+        UserManager userManager = createUserManager(commonSpaces, logger);
+        UserGroup group = group("G2", U1.getUserId());
+        userManager.addGroup(group, principals(U1, U2, U3));
+        assertEquals(manage(userManager).getErrorReport(), "");
+        createBuilder(commonSpaces).adminUser(U1, "G2").user(U2, "G2").user(U3, "G2").assertExpectations();
+
+        userManager = createUserManager(commonSpaces, logger);
+        group = group("G2");
+        userManager.addGroup(group, principals(U1, U2, U3));
+
+        // When
+        UserManagerReport report = manage(userManager);
+
+        // Then
+        assertEquals(report.getErrorReport(), "");
+        assertEquals(report.getAuditLog(), "1970-01-01 01:00:00 [REMOVE-USER-FROM-GROUP] G2_ADMIN, u1\n");
+        UserManagerExpectationsBuilder builder = createBuilder(commonSpaces);
+        builder.user(U1, "G2");
+        builder.user(U2, "G2");
+        builder.user(U3, "G2");
+        builder.assertExpectations();
+    }
+
+    @Test
+    public void testRemoveNormalUserFromAGroupAndAddItAgain()
+    {
+        // Given
+        MockLogger logger = new MockLogger();
+        Map<Role, List<String>> commonSpaces = commonSpaces();
+        UserManager userManager = createUserManager(commonSpaces, logger);
+        UserGroup group = group("G2", U1.getUserId(), "blabla");
+        userManager.addGroup(group, principals(U1, U2, U3));
+        assertEquals(manage(userManager).getErrorReport(), "");
+
+        userManager = createUserManager(commonSpaces, logger);
+        userManager.addGroup(group, principals(U1, U3));
+        assertEquals(manage(userManager).getErrorReport(), "");
+        createBuilder(commonSpaces).adminUser(U1, "G2").disabledUser(U2, "G2").user(U3, "G2").assertExpectations();
+
+        userManager = createUserManager(commonSpaces, logger);
+        userManager.addGroup(group, principals(U1, U2, U3));
+
+        // When
+        UserManagerReport report = manage(userManager);
+
+        // Then
+        assertEquals(report.getErrorReport(), "");
+        assertEquals(report.getAuditLog(), "1970-01-01 01:00:00 [ADD-USER-TO-GROUP] G2, u2\n");
+        UserManagerExpectationsBuilder builder = createBuilder(commonSpaces);
+        builder.adminUser(U1, "G2");
+        builder.user(U2, "G2");
+        builder.user(U3, "G2");
+        builder.assertExpectations();
+    }
+
+    @Test
+    public void testRemoveAdminUserFromAGroup()
+    {
+        // Given
+        MockLogger logger = new MockLogger();
+        Map<Role, List<String>> commonSpaces = commonSpaces();
+        UserManager userManager = createUserManager(commonSpaces, logger);
+        UserGroup group = group("G2", U1.getUserId(), "blabla");
+        userManager.addGroup(group, principals(U1, U2, U3));
+        assertEquals(manage(userManager).getErrorReport(), "");
+        createBuilder(commonSpaces).adminUser(U1, "G2").user(U2, "G2").user(U3, "G2").assertExpectations();
+
+        userManager = createUserManager(commonSpaces, logger);
+        userManager.addGroup(group, principals(U2, U3));
+
+        // When
+        UserManagerReport report = manage(userManager);
+
+        // Then
+        assertEquals(report.getErrorReport(), "");
+        assertEquals(report.getAuditLog(), "1970-01-01 01:00:00 [REMOVE-USER-FROM-GROUP] G2, u1\n"
+                + "1970-01-01 01:00:01 [REMOVE-USER-FROM-GROUP] G2_ADMIN, u1\n");
+        UserManagerExpectationsBuilder builder = createBuilder(commonSpaces);
+        builder.disabledUser(U1, "G2");
+        builder.user(U2, "G2");
+        builder.user(U3, "G2");
+        builder.assertExpectations();
+    }
+
+    @Test
+    public void testRemoveAdminUserFromAGroupAndAddItAgain()
+    {
+        // Given
+        MockLogger logger = new MockLogger();
+        Map<Role, List<String>> commonSpaces = commonSpaces();
+        UserManager userManager = createUserManager(commonSpaces, logger);
+        UserGroup group = group("G2", U1.getUserId(), "blabla");
+        userManager.addGroup(group, principals(U1, U2, U3));
+        assertEquals(manage(userManager).getErrorReport(), "");
+
+        userManager = createUserManager(commonSpaces, logger);
+        userManager.addGroup(group, principals(U2, U3));
+        assertEquals(manage(userManager).getErrorReport(), "");
+        createBuilder(commonSpaces).disabledUser(U1, "G2").user(U2, "G2").user(U3, "G2").assertExpectations();
+
+        userManager = createUserManager(commonSpaces, logger);
+        userManager.addGroup(group, principals(U1, U2, U3));
+
+        // When
+        UserManagerReport report = manage(userManager);
+
+        // Then
+        assertEquals(report.getErrorReport(), "");
+        assertEquals(report.getAuditLog(), "1970-01-01 01:00:00 [ADD-USER-TO-GROUP] G2, u1\n"
+                + "1970-01-01 01:00:01 [ADD-USER-TO-GROUP] G2_ADMIN, u1\n");
+        UserManagerExpectationsBuilder builder = createBuilder(commonSpaces);
+        builder.adminUser(U1, "G2");
+        builder.user(U2, "G2");
+        builder.user(U3, "G2");
+        builder.assertExpectations();
+    }
+
+    @Test
+    public void testUserFromAGroupHasLefted()
+    {
+        // Given
+        MockLogger logger = new MockLogger();
+        Map<Role, List<String>> commonSpaces = commonSpaces();
+        UserManager userManager = createUserManager(commonSpaces, logger);
+        UserGroup group = group("G2", U1.getUserId(), "blabla");
+        userManager.addGroup(group, principals(U1, U2, U3));
+        assertEquals(manage(userManager).getErrorReport(), "");
+        createBuilder(commonSpaces).adminUser(U1, "G2").user(U2, "G2").user(U3, "G2").assertExpectations();
+
+        userManager = createUserManager(commonSpaces, logger, U2);
+
+        // When
+        UserManagerReport report = manage(userManager);
+
+        // Then
+        assertEquals(report.getErrorReport(), "");
+        assertEquals(report.getAuditLog(), "1970-01-01 01:00:00 [DEACTIVATE-USER] u2\n");
+        UserManagerExpectationsBuilder builder = createBuilder(commonSpaces);
+        builder.adminUser(U1, "G2");
+        builder.unknownUser(U2);
+        builder.user(U3, "G2");
+        builder.assertExpectations();
+    }
+
+    // @Test
+    public void testReuseSameUserId()
+    {
+        // Given
+        MockLogger logger = new MockLogger();
+        Map<Role, List<String>> commonSpaces = commonSpaces();
+        UserManager userManager = createUserManager(commonSpaces, logger);
+        UserGroup group = group("G2", U1.getUserId(), "blabla");
+        userManager.addGroup(group, principals(U1, U2, U3));
+        assertEquals(manage(userManager), "");
+        createBuilder(commonSpaces).adminUser(U1, "G2").user(U2, "G2").user(U3, "G2").assertExpectations();
+
+        userManager = createUserManager(commonSpaces, logger, U2);
+        userManager.addGroup(group, principals(U1, U3));
+        assertEquals(manage(userManager), "");
+        createBuilder(commonSpaces).adminUser(U1, "G2").unknownUser(U2).user(U3, "G2").assertExpectations();
+
+        userManager = createUserManager(commonSpaces, logger);
+        userManager.addGroup(group, principals(U1, U2, U3));
+
+        // When
+        UserManagerReport report = manage(userManager);
+
+        // Then
+        assertEquals(report.getErrorReport(), "");
+        UserManagerExpectationsBuilder builder = createBuilder(commonSpaces);
+        builder.adminUser(U1, "G2");
+        builder.user(cloneFor(U2, "u2_2"), "G2");
+        builder.user(U3, "G2");
+        builder.assertExpectations();
+    }
+
+    private Principal cloneFor(Principal user, String userId)
+    {
+        return new Principal(userId, user.getFirstName(), user.getLastName(), user.getEmail());
+    }
+
+    private UserManager createUserManager(Map<Role, List<String>> commonSpaces, MockLogger logger,
+            Principal... usersUnknownByAuthenticationService)
+    {
+        Set<String> unknownUsers = Arrays.asList(usersUnknownByAuthenticationService).stream().map(Principal::getUserId).collect(Collectors.toSet());
+        NullAuthenticationService authenticationService = new NullAuthenticationService()
+            {
+                @Override
+                public Principal getPrincipal(String user) throws IllegalArgumentException
+                {
+                    if (unknownUsers.contains(user))
+                    {
+                        throw new IllegalArgumentException("Unknown user " + user);
+                    }
+                    return new Principal(user, "John", "Doe", "jd@abc.de");
+                }
+            };
+        return new UserManager(authenticationService, v3api, commonSpaces, logger, new MockTimeProvider(0, 1000));
+    }
+
     private UserManagerExpectationsBuilder createBuilder(Map<Role, List<String>> commonSpaces)
     {
         return new UserManagerExpectationsBuilder(v3api, testService, sessionManager, commonSpaces);
     }
 
+    private UserManagerReport manage(UserManager userManager)
+    {
+        UserManagerReport errorReport = userManager.manage();
+        daoFactory.getSessionFactory().getCurrentSession().flush();
+        return errorReport;
+    }
+
     private Map<String, Principal> principals(Principal... principals)
     {
         Map<String, Principal> map = new TreeMap<>();
@@ -129,4 +431,11 @@ public class UserManagerTest extends AbstractTest
         return map;
     }
 
+    private UserGroup group(String groupKey, String... admins)
+    {
+        UserGroup userGroup = new UserGroup();
+        userGroup.setKey(groupKey);
+        userGroup.setAdmins(Arrays.asList(admins));
+        return userGroup;
+    }
 }
-- 
GitLab