diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPDirectoryConfiguration.java b/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPDirectoryConfiguration.java
index 13729994ed398288ac3019a62ce70c8fa05a9e62..80b63a21b3d283a62ab5ccc66809f8e019f66005 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPDirectoryConfiguration.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPDirectoryConfiguration.java
@@ -432,7 +432,10 @@ public final class LDAPDirectoryConfiguration
      */
     public void setServerUrl(String ldapUrl)
     {
-        this.serverUrl = ldapUrl;
+        if (isResolved(ldapUrl))
+        {
+            this.serverUrl = ldapUrl;
+        }
     }
 
     private static boolean isResolved(String name)
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/Group.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserGroup.java
similarity index 79%
rename from openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/Group.java
rename to openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserGroup.java
index db1fe91cf09dea6233ac680df34b6779da3e2bc1..7dc251453a73ff0c5e2917d6a20654d30277ad77 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/Group.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserGroup.java
@@ -18,34 +18,43 @@ package ch.systemsx.cisd.openbis.generic.server.task;
 
 import java.util.List;
 
-class Group
+public class UserGroup
 {
     private String name;
+    
+    private String key;
 
     private List<String> ldapGroupKeys;
 
     private List<String> admins;
 
-    private List<String> usersBlackList;
-
     public String getName()
     {
         return name;
     }
 
-    public List<String> getAdmins()
+    public String getKey()
     {
-        return admins;
+        return key;
     }
 
-    public List<String> getLdapGroupKeys()
+    public void setKey(String key)
     {
-        return ldapGroupKeys;
+        this.key = key;
     }
 
-    public List<String> getUsersBlackList()
+    public List<String> getAdmins()
     {
-        return usersBlackList;
+        return admins;
     }
 
+    public void setAdmins(List<String> admins)
+    {
+        this.admins = admins;
+    }
+    
+    public List<String> getLdapGroupKeys()
+    {
+        return ldapGroupKeys;
+    }
 }
\ No newline at end of file
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 42b697b343c84f13e1d0604c754fcf19cd58c2c1..d63529883383e616c9d5bfb24cd8e144e6777923 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
@@ -19,14 +19,12 @@ package ch.systemsx.cisd.openbis.generic.server.task;
 import java.io.File;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Properties;
 import java.util.TreeMap;
 
 import org.apache.commons.lang.StringUtils;
 import org.apache.log4j.Logger;
 
-import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 import ch.systemsx.cisd.authentication.Principal;
@@ -51,8 +49,11 @@ public class UserManagementMaintenanceTask implements IMaintenanceTask
     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 LDAPAuthenticationService ldapService;
 
     @Override
@@ -65,7 +66,14 @@ 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");
+        if (ldapService.isConfigured() == false)
+        {
+            throw new ConfigurationFailureException("There is no LDAP authentication service configured. "
+                    + "At least 'ldap.server.url', 'ldap.security.principal.distinguished.name', "
+                    + "'ldap.security.principal.password' have to be specified in 'service.properties'.");
+        }
         operationLog.info("Plugin '" + pluginName + "' initialized. Configuration file: " + configurationFile.getAbsolutePath());
         
     }
@@ -73,17 +81,16 @@ public class UserManagementMaintenanceTask implements IMaintenanceTask
     @Override
     public void execute()
     {
-        Map<String, Group> groups = readGroupDefinitions();
-        if (groups == null)
+        UserManagerConfig config = readGroupDefinitions();
+        if (config == null || config.getGroups() == null)
         {
             return;
         }
         Log4jSimpleLogger logger = new Log4jSimpleLogger(operationLog);
-        UserManager userManager = new UserManager(CommonServiceProvider.getApplicationServerApi(), logger);
-        for (Entry<String, Group> entry : groups.entrySet())
+        UserManager userManager = new UserManager(CommonServiceProvider.getApplicationServerApi(), config.getCommonSpaces(), logger);
+        for (UserGroup group : config.getGroups())
         {
-            String key = entry.getKey();
-            Group group = entry.getValue();
+            String key = group.getKey();
             List<String> ldapGroupKeys = group.getLdapGroupKeys();
             if (ldapGroupKeys == null || ldapGroupKeys.isEmpty())
             {
@@ -112,11 +119,15 @@ public class UserManagementMaintenanceTask implements IMaintenanceTask
             }
             userManager.addGroup(key, group, principalsByUserId);
         }
-        userManager.manageUsers();
+        String errorReport = userManager.manageUsers();
+        if (StringUtils.isNotBlank(errorReport))
+        {
+            notificationLog.error("User management failed for the following reasons:\n\n" + errorReport);
+        }
         operationLog.info("finished");
     }
     
-    private Map<String, Group> readGroupDefinitions()
+    private UserManagerConfig readGroupDefinitions()
     {
         if (configurationFile.isFile() == false)
         {
@@ -134,10 +145,9 @@ public class UserManagementMaintenanceTask implements IMaintenanceTask
         }
     }
 
-    private Map<String, Group> deserialize(String serializedConfig) throws Exception
+    private UserManagerConfig deserialize(String serializedConfig) throws Exception
     {
         ObjectMapper mapper = new ObjectMapper();
-        return mapper.readValue(serializedConfig, new TypeReference<Map<String, Group>>(){});
+        return mapper.readValue(serializedConfig, UserManagerConfig.class);
     }
-    
 }
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 12effa23e0a731ec0140264e0f36beb0c51fb1c0..fbd1ddfbaaa79fe1853b8a1e2cd4d6b2953c3775 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
@@ -17,9 +17,12 @@
 package ch.systemsx.cisd.openbis.generic.server.task;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeSet;
@@ -27,13 +30,32 @@ 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.create.AuthorizationGroupCreation;
+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;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.operation.SynchronousOperationExecutionOptions;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.Person;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.create.CreatePersonsOperation;
+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.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;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.roleassignment.create.CreateRoleAssignmentsOperation;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.roleassignment.create.RoleAssignmentCreation;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.Space;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.create.CreateSpacesOperation;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.create.SpaceCreation;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.fetchoptions.SpaceFetchOptions;
+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.Principal;
 import ch.systemsx.cisd.common.logging.ISimpleLogger;
@@ -42,27 +64,29 @@ import ch.systemsx.cisd.common.logging.LogLevel;
 /**
  * @author Franz-Josef Elmer
  */
-class UserManager
+public class UserManager
 {
     private final IApplicationServerInternalApi service;
 
     private final ISimpleLogger logger;
-    
+
+    private final Map<Role, List<String>> commonSpacesByRole;
+
     private final Map<String, UserInfo> userInfosByUserId = new TreeMap<>();
-    
+
     private final List<String> groupCodes = new ArrayList<>();
 
-    UserManager(IApplicationServerInternalApi service, ISimpleLogger logger)
+    public UserManager(IApplicationServerInternalApi service, Map<Role, List<String>> commonSpacesByRole, ISimpleLogger logger)
     {
         this.service = service;
+        this.commonSpacesByRole = commonSpacesByRole;
         this.logger = logger;
     }
 
-    public void addGroup(String key, Group group, Map<String, Principal> principalsByUserId)
+    public void addGroup(String key, UserGroup group, Map<String, Principal> principalsByUserId)
     {
-        groupCodes.add(key);
+        groupCodes.add(key.toUpperCase());
         Set<String> admins = asSet(group.getAdmins());
-        Set<String> blackListedUsers = asSet(group.getUsersBlackList());
         for (Principal principal : principalsByUserId.values())
         {
             String userId = principal.getUserId();
@@ -72,27 +96,231 @@ class UserManager
                 userInfo = new UserInfo(principal);
                 userInfosByUserId.put(userId, userInfo);
             }
-            userInfo.addGroupInfo(new GroupInfo(key, admins.contains(userId), blackListedUsers.contains(userId)));
+            userInfo.addGroupInfo(new GroupInfo(key, admins.contains(userId)));
         }
         logger.log(LogLevel.INFO, principalsByUserId.size() + " users for group " + key);
     }
 
-    public void manageUsers()
+    public String manageUsers()
     {
         String sessionToken = service.loginAsSystem();
         Map<IPersonId, Person> users = getUsersWithRoleAssigments(sessionToken);
         Map<IAuthorizationGroupId, AuthorizationGroup> authorizationGroups = getAuthorizationGroups(sessionToken);
+        StringBuilder builder = new StringBuilder();
+        for (String groupCode : groupCodes)
+        {
+            try
+            {
+                manageGroup(sessionToken, groupCode, authorizationGroups);
+            } 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");
+                logger.log(LogLevel.ERROR, message, e);
+            }
+        }
+        authorizationGroups = getAuthorizationGroups(sessionToken);
         for (UserInfo userInfo : userInfosByUserId.values())
         {
-            manageUser(userInfo, users, authorizationGroups);
+            try
+            {
+                manageUser(sessionToken, userInfo, users, authorizationGroups);
+            } 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");
+                logger.log(LogLevel.ERROR, message, e);
+            }
         }
         service.logout(sessionToken);
+        return builder.toString();
     }
-    
-    private void manageUser(UserInfo userInfo, Map<IPersonId, Person> knownUsers, 
+
+    private void manageGroup(String sessionToken, String groupCode, Map<IAuthorizationGroupId, AuthorizationGroup> knownAuthorizationGroups)
+    {
+        Context context = new Context(sessionToken);
+        AuthorizationGroup knownGroup = knownAuthorizationGroups.get(new AuthorizationGroupPermId(groupCode));
+        if (knownGroup != null)
+        {
+            manageKnownGroup(context, knownGroup);
+        } else
+        {
+            manageNewGroup(context, groupCode);
+        }
+        context.executeOperations();
+    }
+
+    private void manageNewGroup(Context context, String groupCode)
+    {
+        String adminGroupCode = createAdminGroupCode(groupCode);
+        assertAuthorizationGroupDoesNotExist(context, adminGroupCode);
+        assertNoCommonSpaceExists(context, groupCode);
+
+        createAuthorizationGroup(context, groupCode);
+        createAuthorizationGroup(context, adminGroupCode);
+
+        for (Entry<Role, List<String>> entry : commonSpacesByRole.entrySet())
+        {
+            Role role = entry.getKey();
+            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);
+            }
+        }
+    }
+
+    private void assertAuthorizationGroupDoesNotExist(Context context, String adminGroup)
+    {
+        if (service.getAuthorizationGroups(context.getSessionToken(), Arrays.asList(new AuthorizationGroupPermId(adminGroup)),
+                new AuthorizationGroupFetchOptions()).isEmpty() == false)
+        {
+            throw new IllegalStateException("Authorization group " + adminGroup + " already exists.");
+        }
+    }
+
+    private void assertNoCommonSpaceExists(Context context, String groupCode)
+    {
+        Set<String> commonSpaces = new TreeSet<>();
+        for (List<String> set : commonSpacesByRole.values())
+        {
+            commonSpaces.addAll(set.stream().map(s -> createCommonSpaceCode(groupCode, s)).collect(Collectors.toList()));
+        }
+        Map<ISpaceId, Space> spaces = getSpaces(context.getSessionToken(), commonSpaces);
+        if (spaces.isEmpty())
+        {
+            return;
+        }
+        List<String> existingSpaces = new ArrayList<>(spaces.values()).stream().map(Space::getCode).collect(Collectors.toList());
+        Collections.sort(existingSpaces);
+        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)
+    {
+        Context context = new Context(sessionToken);
+        Person knownUser = knownUsers.get(new PersonPermId(userInfo.getPrincipal().getUserId()));
+        if (knownUser != null)
+        {
+            manageKnownUser(context, userInfo, knownUser, knownAuthorizationGroups);
+        } else
+        {
+            manageNewUser(context, userInfo, knownAuthorizationGroups);
+        }
+        context.executeOperations();
+    }
+
+    private void manageKnownUser(Context context, UserInfo userInfo, Person knownUser,
             Map<IAuthorizationGroupId, AuthorizationGroup> knownAuthorizationGroups)
     {
-        
+        System.out.println("UserManager.manageKnownUser() " + userInfo);
+    }
+
+    private void manageNewUser(Context context, UserInfo userInfo,
+            Map<IAuthorizationGroupId, AuthorizationGroup> knownAuthorizationGroups)
+    {
+        Principal principal = userInfo.getPrincipal();
+        String userId = principal.getUserId();
+        String spaceCode = userId.toUpperCase();
+        if (getSpaces(context.getSessionToken(), Arrays.asList(spaceCode)).isEmpty() == false)
+        {
+            throw new IllegalStateException("There is already a space with code " + spaceCode + ".");
+        }
+
+        ISpaceId homeSpaceId = createSpace(context, spaceCode);
+        IPersonId personId = createPerson(context, userId);
+        assignHomeSpace(context, homeSpaceId, personId);
+
+        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);
+            if (groupInfo.isAdmin())
+            {
+                addPersonToAuthorizationGroup(context, adminGroupId, personId);
+            }
+        }
+    }
+
+    private void assignHomeSpace(Context context, ISpaceId homeSpaceId, IPersonId personId)
+    {
+        PersonUpdate personUpdate = new PersonUpdate();
+        personUpdate.setUserId(personId);
+        personUpdate.setSpaceId(homeSpaceId);
+        context.add(personUpdate);
+        RoleAssignmentCreation roleCreation = new RoleAssignmentCreation();
+        roleCreation.setUserId(personId);
+        roleCreation.setRole(Role.ADMIN);
+        roleCreation.setSpaceId(homeSpaceId);
+        context.add(roleCreation);
+    }
+
+    private void addPersonToAuthorizationGroup(Context context, AuthorizationGroupPermId groupId, IPersonId personId)
+    {
+        AuthorizationGroupUpdate groupUpdate = new AuthorizationGroupUpdate();
+        groupUpdate.setAuthorizationGroupId(groupId);
+        groupUpdate.getUserIds().add(personId);
+        context.add(groupUpdate);
+    }
+
+    private ISpaceId createSpace(Context context, String spaceCode)
+    {
+        SpaceCreation spaceCreation = new SpaceCreation();
+        spaceCreation.setCode(spaceCode);
+        context.add(spaceCreation);
+        return new SpacePermId(spaceCode);
+    }
+
+    private IPersonId createPerson(Context context, String userId)
+    {
+        PersonCreation personCreation = new PersonCreation();
+        personCreation.setUserId(userId);
+        context.add(personCreation);
+        return new PersonPermId(userId);
+    }
+
+    private void createAuthorizationGroup(Context context, String groupCode)
+    {
+        AuthorizationGroupCreation creation = new AuthorizationGroupCreation();
+        creation.setCode(groupCode);
+        context.add(creation);
+    }
+
+    private void createRoleAssignment(Context context, AuthorizationGroupPermId groupId, Role role, ISpaceId spaceId)
+    {
+        RoleAssignmentCreation roleCreation = new RoleAssignmentCreation();
+        roleCreation.setAuthorizationGroupId(groupId);
+        roleCreation.setRole(role);
+        roleCreation.setSpaceId(spaceId);
+        context.add(roleCreation);
+    }
+
+    private String createCommonSpaceCode(String groupCode, String spaceCode)
+    {
+        return groupCode + "_" + spaceCode;
+    }
+
+    private String createAdminGroupCode(String groupCode)
+    {
+        return groupCode + "_ADMIN";
+    }
+
+    private Map<ISpaceId, Space> getSpaces(String sessionToken, Collection<String> spaceCodes)
+    {
+        SpaceFetchOptions fetchOptions = new SpaceFetchOptions();
+        return service.getSpaces(sessionToken, spaceCodes.stream().map(SpacePermId::new).collect(Collectors.toList()), fetchOptions);
     }
 
     private Map<IPersonId, Person> getUsersWithRoleAssigments(String sessionToken)
@@ -113,7 +341,7 @@ class UserManager
         fetchOptions.withUsers();
         return service.getAuthorizationGroups(sessionToken, groupPermIds, fetchOptions);
     }
-    
+
     private Set<String> asSet(List<String> users)
     {
         return users == null ? Collections.emptySet() : new TreeSet<>(users);
@@ -140,6 +368,11 @@ class UserManager
             groupInfosByGroupKey.put(groupInfo.getKey(), groupInfo);
         }
 
+        public Map<String, GroupInfo> getGroupInfosByGroupKey()
+        {
+            return groupInfosByGroupKey;
+        }
+
         @Override
         public String toString()
         {
@@ -153,13 +386,10 @@ class UserManager
 
         private boolean admin;
 
-        private boolean onBlackList;
-
-        GroupInfo(String key, boolean admin, boolean onBlackList)
+        GroupInfo(String key, boolean admin)
         {
             this.key = key;
             this.admin = admin;
-            this.onBlackList = onBlackList;
         }
 
         public String getKey()
@@ -172,17 +402,103 @@ class UserManager
             return admin;
         }
 
-        public boolean isOnBlackList()
-        {
-            return onBlackList;
-        }
-
         @Override
         public String toString()
         {
-            return (onBlackList ? "." : "") + key + (admin ? "*" : "");
+            return key + (admin ? "*" : "");
         }
 
     }
 
+    private final class Context
+    {
+        private String sessionToken;
+
+        private List<PersonCreation> personCreations = new ArrayList<>();
+
+        private List<PersonUpdate> personUpdates = new ArrayList<>();
+
+        private List<SpaceCreation> spaceCreations = new ArrayList<>();
+
+        private List<AuthorizationGroupCreation> groupCreations = new ArrayList<>();
+
+        private List<AuthorizationGroupUpdate> groupUpdates = new ArrayList<>();
+
+        private List<RoleAssignmentCreation> roleCreations = new ArrayList<>();
+
+        Context(String sessionToken)
+        {
+            this.sessionToken = sessionToken;
+        }
+
+        public String getSessionToken()
+        {
+            return sessionToken;
+        }
+
+        public void add(PersonCreation personCreation)
+        {
+            personCreations.add(personCreation);
+        }
+
+        public void add(PersonUpdate personUpdate)
+        {
+            personUpdates.add(personUpdate);
+        }
+
+        public void add(SpaceCreation spaceCreation)
+        {
+            spaceCreations.add(spaceCreation);
+        }
+
+        public void add(AuthorizationGroupCreation creation)
+        {
+            groupCreations.add(creation);
+        }
+
+        public void add(RoleAssignmentCreation roleCreation)
+        {
+            roleCreations.add(roleCreation);
+        }
+
+        public void add(AuthorizationGroupUpdate groupUpdate)
+        {
+            groupUpdates.add(groupUpdate);
+        }
+
+        public void executeOperations()
+        {
+            List<IOperation> operations = new ArrayList<>();
+            if (personCreations.isEmpty() == false)
+            {
+                operations.add(new CreatePersonsOperation(personCreations));
+            }
+            if (personUpdates.isEmpty() == false)
+            {
+                operations.add(new UpdatePersonsOperation(personUpdates));
+            }
+            if (spaceCreations.isEmpty() == false)
+            {
+                operations.add(new CreateSpacesOperation(spaceCreations));
+            }
+            if (groupCreations.isEmpty() == false)
+            {
+                operations.add(new CreateAuthorizationGroupsOperation(groupCreations));
+            }
+            if (groupUpdates.isEmpty() == false)
+            {
+                operations.add(new UpdateAuthorizationGroupsOperation(groupUpdates));
+            }
+            if (roleCreations.isEmpty() == false)
+            {
+                operations.add(new CreateRoleAssignmentsOperation(roleCreations));
+            }
+            if (operations.isEmpty())
+            {
+                return;
+            }
+            SynchronousOperationExecutionOptions options = new SynchronousOperationExecutionOptions();
+            service.executeOperations(sessionToken, operations, options);
+        }
+    }
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagerConfig.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagerConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..328c31e334d92c9ee547d42fa6c6ca55fca634fb
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagerConfig.java
@@ -0,0 +1,51 @@
+/*
+ * 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.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.roleassignment.Role;
+
+class UserManagerConfig
+{
+    private Map<Role, List<String>> commonSpaces = new HashMap<>();
+
+    private List<UserGroup> groups;
+
+    public Map<Role, List<String>> getCommonSpaces()
+    {
+        return commonSpaces;
+    }
+
+    public void setCommonSpaces(Map<Role, List<String>> commonSpaces)
+    {
+        this.commonSpaces = commonSpaces;
+    }
+
+    public List<UserGroup> getGroups()
+    {
+        return groups;
+    }
+
+    public void setGroups(List<UserGroup> groups)
+    {
+        this.groups = groups;
+    }
+
+}
\ No newline at end of file
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/AbstractTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/AbstractTest.java
index 31c0181d0d82b933a1777919d71151d83f3621e9..54498508abdc3bccb99202244dad536f3290f771 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/AbstractTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/AbstractTest.java
@@ -47,7 +47,6 @@ import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 
-import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.attachment.Attachment;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.attachment.create.AttachmentCreation;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.id.IObjectId;
@@ -97,6 +96,7 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.vocabulary.id.VocabularyTermPerm
 import ch.ethz.sis.openbis.generic.asapi.v3.exceptions.NotFetchedException;
 import ch.ethz.sis.openbis.generic.asapi.v3.exceptions.ObjectNotFoundException;
 import ch.ethz.sis.openbis.generic.asapi.v3.exceptions.UnauthorizedObjectAccessException;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.IApplicationServerInternalApi;
 import ch.ethz.sis.openbis.systemtest.asapi.v3.index.IndexOperation;
 import ch.ethz.sis.openbis.systemtest.asapi.v3.index.IndexState;
 import ch.ethz.sis.openbis.systemtest.asapi.v3.index.ReindexingState;
@@ -119,7 +119,6 @@ import ch.systemsx.cisd.openbis.generic.shared.dto.PersonPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.SamplePE;
 import ch.systemsx.cisd.openbis.systemtest.SystemTestCase;
 import ch.systemsx.cisd.openbis.util.LogRecordingUtils;
-
 import junit.framework.Assert;
 
 /**
@@ -139,7 +138,7 @@ public class AbstractTest extends SystemTestCase
     private BufferedAppender logRecorder;
 
     @Autowired
-    protected IApplicationServerApi v3api;
+    protected IApplicationServerInternalApi v3api;
 
     @Autowired
     protected IGeneralInformationService generalInformationService;
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
new file mode 100644
index 0000000000000000000000000000000000000000..4645e33b8da619dfaa9ac5f7903aa1de53dbc53a
--- /dev/null
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/task/UserManagerExpectationsBuilder.java
@@ -0,0 +1,401 @@
+/*
+ * 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.systemtest.task;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.testng.Assert;
+
+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;
+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.roleassignment.Role;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.Space;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.fetchoptions.SpaceFetchOptions;
+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.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.shared.IOpenBisSessionManager;
+import ch.systemsx.cisd.openbis.generic.shared.dto.Session;
+import ch.systemsx.cisd.openbis.generic.shared.dto.SpacePE;
+
+class UserManagerExpectationsBuilder
+{
+    private static final String TEST_USER = "test";
+
+    private static final String PASSWORD = "password";
+
+    private final IApplicationServerInternalApi v3api;
+
+    private final UserManagerTestService testService;
+
+    private final IOpenBisSessionManager sessionManager;
+
+    private final Map<Role, List<String>> commonSpaces;
+
+    private Map<String, List<Principal>> usersByGroup = new TreeMap<>();
+
+    private Map<String, List<Principal>> noUsersByGroup = new TreeMap<>();
+
+    private Map<String, List<Principal>> normalUsersByGroup = new TreeMap<>();
+
+    private Map<String, List<Principal>> adminUsersByGroup = new TreeMap<>();
+
+    UserManagerExpectationsBuilder(IApplicationServerInternalApi v3api, UserManagerTestService testService,
+            IOpenBisSessionManager sessionManager, Map<Role, List<String>> commonSpaces)
+    {
+        this.v3api = v3api;
+        this.testService = testService;
+        this.sessionManager = sessionManager;
+        this.commonSpaces = commonSpaces;
+    }
+
+    UserManagerExpectationsBuilder noUser(Principal user, String... groups)
+    {
+        return addUser(user, noUsersByGroup, groups);
+    }
+
+    UserManagerExpectationsBuilder user(Principal user, String... groups)
+    {
+        return addUser(user, normalUsersByGroup, groups);
+    }
+
+    UserManagerExpectationsBuilder adminUser(Principal user, String... groups)
+    {
+        return addUser(user, adminUsersByGroup, groups);
+    }
+
+    private UserManagerExpectationsBuilder addUser(Principal user, Map<String, List<Principal>> users, String... groups)
+    {
+        for (String group : groups)
+        {
+            addUserToGroup(user, group, users);
+            addUserToGroup(user, group, usersByGroup);
+        }
+        return this;
+    }
+
+    private void addUserToGroup(Principal user, String group, Map<String, List<Principal>> users)
+    {
+        List<Principal> principals = users.get(group);
+        if (principals == null)
+        {
+            principals = new ArrayList<>();
+            users.put(group, principals);
+        }
+        principals.add(user);
+    }
+
+    void assertExpectations()
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+        assertSpaces(sessionToken);
+        assertUsers(sessionToken);
+        assertAuthorization(sessionToken);
+        v3api.logout(sessionToken);
+    }
+
+    private void assertSpaces(String sessionToken)
+    {
+        List<SpacePermId> allCommonSpaces = getAllCommonSpaces();
+        assertSpacesExist(sessionToken, allCommonSpaces);
+        List<SpacePermId> allUserSpaces = applyMapperToAllUsers(user -> new SpacePermId(user.getUserId().toUpperCase()));
+        assertSpacesExist(sessionToken, allUserSpaces);
+    }
+
+    private List<SpacePermId> getAllCommonSpaces()
+    {
+        List<SpacePermId> allCommonSpaces = new ArrayList<>();
+        Set<String> groups = usersByGroup.keySet();
+        for (String group : groups)
+        {
+            for (List<String> list : commonSpaces.values())
+            {
+                for (String commonSpace : list)
+                {
+                    allCommonSpaces.add(new SpacePermId(group + "_" + commonSpace));
+                }
+            }
+        }
+        return allCommonSpaces;
+    }
+
+    private void assertSpacesExist(String sessionToken, List<SpacePermId> spaces)
+    {
+        List<String> expectedSpaces = extractedSortedPermIds(spaces);
+        SpaceFetchOptions fetchOptions = new SpaceFetchOptions();
+        List<String> actualSpaces = extractedSortedCodes(v3api.getSpaces(sessionToken, spaces, fetchOptions).values());
+        assertEquals(actualSpaces.toString(), expectedSpaces.toString());
+    }
+
+    private void assertUsers(String sessionToken)
+    {
+        List<PersonPermId> allUsers = applyMapperToAllUsers(user -> new PersonPermId(user.getUserId()));
+        PersonFetchOptions fetchOptions = new PersonFetchOptions();
+        fetchOptions.withSpace();
+        Map<IPersonId, Person> persons = v3api.getPersons(sessionToken, allUsers, fetchOptions);
+        Function<IPersonId, PersonPermId> mapper = id -> (PersonPermId) id;
+        List<String> expectedUsers = extractedSortedPermIds(allUsers);
+        List<String> actualUsers = extractedSortedPermIds(persons.keySet().stream().map(mapper).collect(Collectors.toSet()));
+        assertEquals(actualUsers.toString(), expectedUsers.toString());
+        for (PersonPermId id : allUsers)
+        {
+            Person person = persons.get(id);
+            assertEquals(person.getUserId(), id.getPermId());
+            assertEquals(person.getEmail(), "franz-josef.elmer@systemsx.ch", "Wrong email of " + person);
+            assertEquals(person.getSpace().getCode(), person.getUserId().toUpperCase(), "Wrong home space of " + person);
+        }
+    }
+
+    private void assertAuthorization(String sessionToken)
+    {
+        AuthorizationExpectations expectations = new AuthorizationExpectations(v3api, sessionManager, testService);
+        for (Entry<String, List<Principal>> entry : normalUsersByGroup.entrySet())
+        {
+            String groupKey = entry.getKey();
+            List<Principal> users = entry.getValue();
+            users.forEach(user -> expectations.expect(user, groupKey, Level.SPACE_USER));
+        }
+    }
+
+    private static final class AuthorizationExpectations
+    {
+        private Map<String, Map<Level, Set<String>>> groupsByLevelsByUsers = new TreeMap<>();
+
+        private IApplicationServerInternalApi v3api;
+
+        private IOpenBisSessionManager sessionManager;
+
+        private UserManagerTestService testService;
+
+        public AuthorizationExpectations(IApplicationServerInternalApi v3api, IOpenBisSessionManager sessionManager,
+                UserManagerTestService testService)
+        {
+            this.v3api = v3api;
+            this.sessionManager = sessionManager;
+            this.testService = testService;
+        }
+
+        void expect(Principal user, String group, Level level)
+        {
+            String userId = user.getUserId();
+            Map<Level, Set<String>> groupsByLevels = groupsByLevelsByUsers.get(userId);
+            if (groupsByLevels == null)
+            {
+                groupsByLevels = new TreeMap<>();
+                groupsByLevelsByUsers.put(userId, groupsByLevels);
+            }
+            Set<String> groups = groupsByLevels.get(level);
+            if (groups == null)
+            {
+                groups = new TreeSet<>();
+                groupsByLevels.put(level, groups);
+            }
+            groups.add(group);
+        }
+
+        void assertExpectations()
+        {
+
+            for (Entry<String, Map<Level, Set<String>>> entry : groupsByLevelsByUsers.entrySet())
+            {
+                String userId = entry.getKey();
+                String sessionToken = v3api.login(userId, PASSWORD);
+
+                try
+                {
+                    Session session = sessionManager.getSession(sessionToken);
+                    IOperationContext context = new OperationContext(session);
+                    Set<Entry<Level, Set<String>>> entrySet = entry.getValue().entrySet();
+                    for (Entry<Level, Set<String>> entry2 : entrySet)
+                    {
+                        Level level = entry2.getKey();
+                        Set<String> groups = entry2.getValue();
+                        for (String group : groups)
+                        {
+//                            getActualLevel(context, spaceCode);
+                        }
+//                        probeAuthorizationLevel(user, spaceCodes);
+                    }
+                } finally
+                {
+                    v3api.logout(sessionToken);
+                }
+
+            }
+        }
+        
+        private Level getActualLevel(IOperationContext context, String spaceCode)
+        {
+            SpacePE space = new SpacePE();
+            space.setCode(spaceCode);
+            Level previousLevel = null;
+            for (Level level : Level.values())
+            {
+                try
+                {
+                    level.probe(testService, context, space);
+                } catch (Exception e)
+                {
+                    break;
+                }
+                previousLevel = level;
+            }
+            return previousLevel;
+        }
+        
+        private static final class LevelAndSpace
+        {
+            private Level level;
+            private String spaceCode;
+
+            LevelAndSpace(Level level, String spaceCode)
+            {
+                this.level = level;
+                this.spaceCode = spaceCode;
+            }
+        }
+
+    }
+
+    private List<String> extractedSortedPermIds(Collection<? extends ObjectPermId> ids)
+    {
+        List<String> result = ids.stream().map(ObjectPermId::getPermId).collect(Collectors.toList());
+        Collections.sort(result);
+        return result;
+    }
+
+    private List<String> extractedSortedCodes(Collection<? extends ICodeHolder> codeHolders)
+    {
+        List<String> result = codeHolders.stream().map(ICodeHolder::getCode).collect(Collectors.toList());
+        Collections.sort(result);
+        return result;
+    }
+
+    private <T> List<T> applyMapperToAllUsers(Function<Principal, T> mapper)
+    {
+        List<T> result = new ArrayList<>();
+        for (List<Principal> users : usersByGroup.values())
+        {
+            result.addAll(users.stream().map(mapper).collect(Collectors.toList()));
+        }
+        return result;
+    }
+
+    private List<Level> probeAuthorizationLevel(String userId, List<String> spaceCodes)
+    {
+        String sessionToken = v3api.login(userId, PASSWORD);
+        try
+        {
+            List<Level> levels = new ArrayList<>();
+            Session session = sessionManager.getSession(sessionToken);
+            IOperationContext context = new OperationContext(session);
+            for (String spaceCode : spaceCodes)
+            {
+                Level previousLevel = getActualLevel(context, spaceCode);
+                levels.add(previousLevel);
+            }
+            return levels;
+        } finally
+        {
+            v3api.logout(sessionToken);
+        }
+    }
+
+    private Level getActualLevel(IOperationContext context, String spaceCode)
+    {
+        SpacePE space = new SpacePE();
+        space.setCode(spaceCode);
+        Level previousLevel = null;
+        for (Level level : Level.values())
+        {
+            try
+            {
+                level.probe(testService, context, space);
+            } catch (Exception e)
+            {
+                break;
+            }
+            previousLevel = level;
+        }
+        return previousLevel;
+    }
+
+    private enum Level
+    {
+        NON
+        {
+            @Override
+            public void probe(UserManagerTestService testService, IOperationContext context, SpacePE space)
+            {
+            }
+        },
+        SPACE_OBSERVER
+        {
+            @Override
+            public void probe(UserManagerTestService testService, IOperationContext context, SpacePE space)
+            {
+                testService.allowedForSpaceObservers(context, space);
+            }
+        },
+        SPACE_USER
+        {
+            @Override
+            public void probe(UserManagerTestService testService, IOperationContext context, SpacePE space)
+            {
+                testService.allowedForSpaceUsers(context, space);
+            }
+        },
+        SPACE_ADMIN
+        {
+            @Override
+            public void probe(UserManagerTestService testService, IOperationContext context, SpacePE space)
+            {
+                testService.allowedForSpaceAdmins(context, space);
+            }
+        },
+        INSTANCE_ADMIN
+        {
+            @Override
+            public void probe(UserManagerTestService testService, IOperationContext context, SpacePE space)
+            {
+                testService.allowedForInstanceAdmins(context, space);
+            }
+        };
+
+        public abstract void probe(UserManagerTestService testService, IOperationContext context, SpacePE space);
+    }
+
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000000000000000000000000000000000..7779ca4ed26c8b234b321e8d295deb4d66cb1112
--- /dev/null
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/task/UserManagerTest.java
@@ -0,0 +1,326 @@
+/*
+ * 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.systemtest.task;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.annotation.Resource;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.search.SearchResult;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.Person;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.fetchoptions.PersonFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.person.id.PersonPermId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.roleassignment.Role;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.Space;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.fetchoptions.SpaceFetchOptions;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.search.SpaceSearchCriteria;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.IOperationContext;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.OperationContext;
+import ch.ethz.sis.openbis.systemtest.asapi.v3.AbstractTest;
+import ch.systemsx.cisd.authentication.Principal;
+import ch.systemsx.cisd.common.logging.MockLogger;
+import ch.systemsx.cisd.common.test.AssertionUtil;
+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.shared.IOpenBisSessionManager;
+import ch.systemsx.cisd.openbis.generic.shared.dto.Session;
+import ch.systemsx.cisd.openbis.generic.shared.dto.SpacePE;
+
+/**
+ * @author Franz-Josef Elmer
+ */
+public class UserManagerTest extends AbstractTest
+{
+    private static final Principal U1 = new Principal("u1", "Albert", "Einstein", "a.e@abc.de");
+
+    private static final Principal U2 = new Principal("u2", "Isaac", "Newton", "i.n@abc.de");
+
+    private static final Principal U3 = new Principal("u3", "Alan", "Turing", "a.t@abc.de");
+
+    @Autowired
+    private UserManagerTestService testService;
+
+    @Resource(name = ComponentNames.SESSION_MANAGER)
+    private IOpenBisSessionManager sessionManager;
+
+    private static Map<Role, List<String>> commonSpaces()
+    {
+        Map<Role, List<String>> commonSpacesByRole = new EnumMap<>(Role.class);
+        commonSpacesByRole.put(Role.USER, Arrays.asList("ALPHA", "BETA"));
+        commonSpacesByRole.put(Role.OBSERVER, Arrays.asList("GAMMA"));
+        return commonSpacesByRole;
+    }
+
+    @Test
+    public void testAddNewGroupWithNewUsers()
+    {
+        // Given
+        MockLogger logger = new MockLogger();
+        UserManager userManager = new UserManager(v3api, 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);
+
+        // When
+        String errorMessages = userManager.manageUsers();
+
+        // Then
+        assertEquals(errorMessages, "");
+        List<String> allSpaces = getAllSpaces();
+        System.err.println("all spaces: " + allSpaces);
+        for (Principal user : principals.values())
+        {
+            List<Person> persons = getPersons(user.getUserId());
+            Person person = persons.get(0);
+            assertEquals(person.getUserId(), user.getUserId());
+            assertEquals(person.getEmail(), "franz-josef.elmer@systemsx.ch");
+            String space = getOwnSpace(user);
+            assertEquals(person.getSpace().getCode(), space);
+            if (group.getAdmins() != null && group.getAdmins().contains(user.getUserId()))
+            {
+                assertAdminUserLevels("G1", user, principals.values());
+            } else
+            {
+                assertNormalUserLevels("G1", user);
+            }
+        }
+    }
+
+    @Test
+    public void testAddUsersToAnExistingGroup()
+    {
+        // 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 = new UserManager(v3api, commonSpaces, logger);
+        userManager.addGroup("G2", group, principals(U2, U3));
+
+        // When
+        String errorMessages = userManager.manageUsers();
+
+        // Then
+        assertEquals(errorMessages, "");
+        Collection<Principal> users = principals(U1, U2, U3).values();
+        for (Principal user : users)
+        {
+            List<Person> persons = getPersons(user.getUserId());
+            Person person = persons.get(0);
+            assertEquals(person.getUserId(), user.getUserId());
+            assertEquals(person.getEmail(), "franz-josef.elmer@systemsx.ch");
+            String space = getOwnSpace(user);
+            assertEquals(person.getSpace().getCode(), space);
+            if (group.getAdmins() != null && group.getAdmins().contains(user.getUserId()))
+            {
+                assertAdminUserLevels("G2", user, users);
+            } else
+            {
+                assertNormalUserLevels("G2", user);
+            }
+        }
+        UserManagerExpectationsBuilder builder = createBuilder(commonSpaces);
+        builder.adminUser(U1, "G2");
+        builder.user(U2, "G2");
+        builder.user(U3, "G2");
+        builder.assertExpectations();
+    }
+
+    private UserManagerExpectationsBuilder createBuilder(Map<Role, List<String>> commonSpaces)
+    {
+        return new UserManagerExpectationsBuilder(v3api, testService, sessionManager, commonSpaces);
+    }
+
+    private void assertNormalUserLevels(String groupCode, Principal user)
+    {
+        String userId = user.getUserId();
+        Map<Role, List<String>> commonSpaces = commonSpaces();
+        assertLevel(userId, Level.SPACE_OBSERVER, getCommonSpaces(groupCode, commonSpaces, Role.OBSERVER));
+        assertLevel(userId, Level.SPACE_USER, getCommonSpaces(groupCode, commonSpaces, Role.USER));
+        assertLevel(userId, Level.SPACE_ADMIN, Arrays.asList(userId.toUpperCase()));
+    }
+
+    private void assertAdminUserLevels(String groupCode, Principal adminUser, Collection<Principal> users)
+    {
+        List<String> spaces = users.stream().map(p -> getOwnSpace(p)).collect(Collectors.toList());
+        Map<Role, List<String>> commonSpaces = commonSpaces();
+        spaces.addAll(getCommonSpaces(groupCode, commonSpaces, Role.OBSERVER));
+        spaces.addAll(getCommonSpaces(groupCode, commonSpaces, Role.USER));
+        assertLevel(adminUser.getUserId(), Level.SPACE_ADMIN, spaces);
+    }
+
+    private List<String> getCommonSpaces(String groupCode, Map<Role, List<String>> commonSpaces, Role role)
+    {
+        return commonSpaces.get(role).stream().map(s -> groupCode + "_" + s).collect(Collectors.toList());
+    }
+
+    private void assertLevel(String userId, Level level, List<String> spaceCodes)
+    {
+        List<Level> levels = probeAuthorizationLevel(userId, spaceCodes);
+        for (int i = 0; i < spaceCodes.size(); i++)
+        {
+            assertLevel(levels.get(i), level, userId, spaceCodes.get(i));
+        }
+    }
+
+    private void assertLevel(Level actualLevel, Level expectedLevel, String userId, String spaceCode)
+    {
+        assertEquals(userId + ":" + spaceCode + ":" + actualLevel, userId + ":" + spaceCode + ":" + expectedLevel);
+    }
+
+    private List<String> getAllSpaces()
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+        SearchResult<Space> spaces = v3api.searchSpaces(sessionToken, new SpaceSearchCriteria(), new SpaceFetchOptions());
+        v3api.logout(sessionToken);
+        List<String> spaceCodes = spaces.getObjects().stream().map(Space::getCode).collect(Collectors.toList());
+        Collections.sort(spaceCodes);
+        return spaceCodes;
+    }
+
+    private List<Person> getPersons(String... userIds)
+    {
+        String sessionToken = v3api.login(TEST_USER, PASSWORD);
+        Function<String, PersonPermId> mapper = u -> new PersonPermId(u);
+        List<PersonPermId> permIds = Arrays.asList(userIds).stream().map(mapper).collect(Collectors.toList());
+        PersonFetchOptions fetchOptions = new PersonFetchOptions();
+        fetchOptions.withSpace();
+        Collection<Person> persons = v3api.getPersons(sessionToken, permIds, fetchOptions).values();
+        v3api.logout(sessionToken);
+        List<Person> result = new ArrayList<>(persons);
+        Collections.sort(result, Comparator.comparing(Person::getUserId));
+        return result;
+    }
+
+    private Map<String, Principal> principals(Principal... principals)
+    {
+        Map<String, Principal> map = new TreeMap<>();
+        for (Principal principal : principals)
+        {
+            map.put(principal.getUserId(), principal);
+        }
+        return map;
+    }
+
+    private String getOwnSpace(Principal principal)
+    {
+        return principal.getUserId().toUpperCase();
+    }
+
+    private List<Level> probeAuthorizationLevel(String userId, List<String> spaceCodes)
+    {
+        String sessionToken = v3api.login(userId, PASSWORD);
+        try
+        {
+            List<Level> levels = new ArrayList<>();
+            Session session = sessionManager.getSession(sessionToken);
+            IOperationContext context = new OperationContext(session);
+            for (String spaceCode : spaceCodes)
+            {
+                SpacePE space = new SpacePE();
+                space.setCode(spaceCode);
+                Level previousLevel = null;
+                for (Level level : Level.values())
+                {
+                    try
+                    {
+                        level.probe(testService, context, space);
+                    } catch (Exception e)
+                    {
+                        break;
+                    }
+                    previousLevel = level;
+                }
+                levels.add(previousLevel);
+            }
+            return levels;
+        } finally
+        {
+            v3api.logout(sessionToken);
+        }
+    }
+
+    private enum Level
+    {
+        NON
+        {
+            @Override
+            public void probe(UserManagerTestService testService, IOperationContext context, SpacePE space)
+            {
+            }
+        },
+        SPACE_OBSERVER
+        {
+            @Override
+            public void probe(UserManagerTestService testService, IOperationContext context, SpacePE space)
+            {
+                testService.allowedForSpaceObservers(context, space);
+            }
+        },
+        SPACE_USER
+        {
+            @Override
+            public void probe(UserManagerTestService testService, IOperationContext context, SpacePE space)
+            {
+                testService.allowedForSpaceUsers(context, space);
+            }
+        },
+        SPACE_ADMIN
+        {
+            @Override
+            public void probe(UserManagerTestService testService, IOperationContext context, SpacePE space)
+            {
+                testService.allowedForSpaceAdmins(context, space);
+            }
+        },
+        INSTANCE_ADMIN
+        {
+            @Override
+            public void probe(UserManagerTestService testService, IOperationContext context, SpacePE space)
+            {
+                testService.allowedForInstanceAdmins(context, space);
+            }
+        };
+
+        public abstract void probe(UserManagerTestService testService, IOperationContext context, SpacePE space);
+    }
+
+}
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/task/UserManagerTestService.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/task/UserManagerTestService.java
new file mode 100644
index 0000000000000000000000000000000000000000..1f8c74c79fe559138103c93a4278e615b9c5b0bb
--- /dev/null
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/task/UserManagerTestService.java
@@ -0,0 +1,55 @@
+/*
+ * 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.systemtest.task;
+
+import org.springframework.stereotype.Component;
+
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.IOperationContext;
+import ch.systemsx.cisd.openbis.generic.server.authorization.annotation.AuthorizationGuard;
+import ch.systemsx.cisd.openbis.generic.server.authorization.annotation.RolesAllowed;
+import ch.systemsx.cisd.openbis.generic.server.authorization.predicate.SpacePEPredicate;
+import ch.systemsx.cisd.openbis.generic.shared.basic.dto.RoleWithHierarchy;
+import ch.systemsx.cisd.openbis.generic.shared.dto.SpacePE;
+
+/**
+ * @author Franz-Josef Elmer
+ *
+ */
+@Component
+public class UserManagerTestService
+{
+    @RolesAllowed(RoleWithHierarchy.SPACE_OBSERVER)
+    public void allowedForSpaceObservers(IOperationContext context, @AuthorizationGuard(guardClass = SpacePEPredicate.class)  SpacePE space)
+    {
+    }
+
+    @RolesAllowed(RoleWithHierarchy.SPACE_USER)
+    public void allowedForSpaceUsers(IOperationContext context, @AuthorizationGuard(guardClass = SpacePEPredicate.class)  SpacePE space)
+    {
+    }
+    
+    @RolesAllowed(RoleWithHierarchy.SPACE_ADMIN)
+    public void allowedForSpaceAdmins(IOperationContext context, @AuthorizationGuard(guardClass = SpacePEPredicate.class)  SpacePE space)
+    {
+    }
+    
+    @RolesAllowed(RoleWithHierarchy.INSTANCE_ADMIN)
+    public void allowedForInstanceAdmins(IOperationContext context, @AuthorizationGuard(guardClass = SpacePEPredicate.class)  SpacePE space)
+    {
+    }
+    
+}