diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPAuthenticationService.java
index 9652cd18ec44e30daefa0f139253835c055742cb..ce85b4b1410bf5bd5be837988f4bc1dce9d67781 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPAuthenticationService.java
@@ -207,4 +207,8 @@ public class LDAPAuthenticationService implements IAuthenticationService
         return configured;
     }
 
+    public List<Principal> listPrincipalsByGroup(String group)
+    {
+        return query.listPrincipalsByKeyValue("memberOf", group);
+    }
 }
diff --git a/commonbase/sourceTest/java/ch/systemsx/cisd/common/test/ToStringMatcher.java b/commonbase/sourceTest/java/ch/systemsx/cisd/common/test/ToStringMatcher.java
new file mode 100644
index 0000000000000000000000000000000000000000..328b82754304b6d0677f941a3e251d100238ee60
--- /dev/null
+++ b/commonbase/sourceTest/java/ch/systemsx/cisd/common/test/ToStringMatcher.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.common.test;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+
+/**
+ * @author Franz-Josef Elmer
+ */
+public class ToStringMatcher<T> extends BaseMatcher<T>
+{
+    private String expectedToStringString;
+
+    public ToStringMatcher(T expectedItem)
+    {
+        this(String.valueOf(expectedItem));
+    }
+
+    public ToStringMatcher(String expectedToStringString)
+    {
+        this.expectedToStringString = expectedToStringString;
+    }
+
+    @Override
+    public boolean matches(Object item)
+    {
+        return String.valueOf(item).equals(expectedToStringString);
+    }
+
+    @Override
+    public void describeTo(Description description)
+    {
+        description.appendText(expectedToStringString);
+    }
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApi.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApi.java
index 6065148dce5d454bbf4d69b34f294bb95830b09e..200dd53708fcf5bfdd68b710f14455a82a430e2e 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApi.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApi.java
@@ -453,7 +453,7 @@ import ch.systemsx.cisd.openbis.generic.shared.managed_property.IManagedProperty
  */
 @Component(ApplicationServerApi.INTERNAL_SERVICE_NAME)
 public class ApplicationServerApi extends AbstractServer<IApplicationServerApi> implements
-        IApplicationServerApi
+        IApplicationServerInternalApi
 {
     /**
      * Name of this service for which it is registered as Spring bean
@@ -486,6 +486,12 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
         return session == null ? null : session.getSessionToken();
     }
 
+    @Override
+    public String loginAsSystem()
+    {
+        return tryToAuthenticateAsSystem().getSessionToken();
+    }
+
     @Override
     @Transactional
     public String loginAsAnonymousUser()
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/IApplicationServerInternalApi.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/IApplicationServerInternalApi.java
new file mode 100644
index 0000000000000000000000000000000000000000..6e623105fb4aad9348231d0af71b37e2bcb57004
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/IApplicationServerInternalApi.java
@@ -0,0 +1,33 @@
+/*
+ * 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.ethz.sis.openbis.generic.server.asapi.v3;
+
+import org.springframework.transaction.annotation.Transactional;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi;
+
+/**
+ * Extension of {@link IApplicationServerApi} which are only for internal use. These methods are not accessible remotely.
+ * 
+ * @author Franz-Josef Elmer
+ */
+public interface IApplicationServerInternalApi extends IApplicationServerApi
+{
+    @Transactional
+    public String loginAsSystem();
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java
index d17c567bd75ed5b73d867adb9b616f1f5c367042..0c8be1bc0d4ff0ca450ad880f2a0e0454fa8c139 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java
@@ -465,6 +465,22 @@ public abstract class AbstractServer<T> extends AbstractServiceWithLogger<T> imp
         return getDAOFactory().getPersonDAO().countActivePersons();
     }
 
+    public SessionContextDTO tryToAuthenticateAsSystem()
+    {
+        final PersonPE systemUser = getSystemUser();
+        HibernateUtils.initialize(systemUser.getAllPersonRoles());
+        RoleAssignmentPE role = new RoleAssignmentPE();
+        role.setRole(RoleCode.ADMIN);
+        systemUser.addRoleAssignment(role);
+        String sessionToken =
+                sessionManager.tryToOpenSession(systemUser.getUserId(),
+                        new AuthenticatedPersonBasedPrincipalProvider(systemUser));
+        Session session = sessionManager.getSession(sessionToken);
+        session.setPerson(systemUser);
+        session.setCreatorPerson(systemUser);
+        return tryGetSession(sessionToken);
+    }
+
     @Override
     public SessionContextDTO tryAuthenticateAnonymously()
     {
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java
index b3bbb5e222a37fc108d5ff524695a3425b167fcc..585e93d97d65573b195fb37c12ec1f4035bd242a 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServer.java
@@ -211,7 +211,6 @@ import ch.systemsx.cisd.openbis.generic.shared.dto.SampleUpdatesDTO;
 import ch.systemsx.cisd.openbis.generic.shared.dto.ScriptPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.SearchableEntity;
 import ch.systemsx.cisd.openbis.generic.shared.dto.Session;
-import ch.systemsx.cisd.openbis.generic.shared.dto.SessionContextDTO;
 import ch.systemsx.cisd.openbis.generic.shared.dto.SpacePE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.VocabularyPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.VocabularyTermWithStats;
@@ -323,27 +322,6 @@ public final class CommonServer extends AbstractCommonServer<ICommonServerForInt
         return new CommonServerLogger(getSessionManager(), context);
     }
 
-    //
-    // ISystemAuthenticator
-    //
-
-    @Override
-    public SessionContextDTO tryToAuthenticateAsSystem()
-    {
-        final PersonPE systemUser = getSystemUser();
-        HibernateUtils.initialize(systemUser.getAllPersonRoles());
-        RoleAssignmentPE role = new RoleAssignmentPE();
-        role.setRole(RoleCode.ADMIN);
-        systemUser.addRoleAssignment(role);
-        String sessionToken =
-                sessionManager.tryToOpenSession(systemUser.getUserId(),
-                        new AuthenticatedPersonBasedPrincipalProvider(systemUser));
-        Session session = sessionManager.getSession(sessionToken);
-        session.setPerson(systemUser);
-        session.setCreatorPerson(systemUser);
-        return tryGetSession(sessionToken);
-    }
-
     //
     // IGenericServer
     //
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServiceProvider.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServiceProvider.java
index b311ae93581827d73962be8973c7ee904c168b4a..e5dbc63c5a9b7c2b97ac51211b109e59234c87cd 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServiceProvider.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/CommonServiceProvider.java
@@ -18,8 +18,8 @@ package ch.systemsx.cisd.openbis.generic.server;
 
 import org.springframework.context.ApplicationContext;
 
-import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.ApplicationServerApi;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.IApplicationServerInternalApi;
 import ch.systemsx.cisd.common.mail.IMailClient;
 import ch.systemsx.cisd.common.mail.MailClient;
 import ch.systemsx.cisd.common.mail.MailClientParameters;
@@ -66,9 +66,9 @@ public class CommonServiceProvider
         return new MailClient(mailClientParameters);
     }
 
-    public static IApplicationServerApi getApplicationServerApi()
+    public static IApplicationServerInternalApi getApplicationServerApi()
     {
-        return (IApplicationServerApi) applicationContext.getBean(ApplicationServerApi.INTERNAL_SERVICE_NAME);
+        return (IApplicationServerInternalApi) applicationContext.getBean(ApplicationServerApi.INTERNAL_SERVICE_NAME);
     }
 
     public static Object tryToGetBean(String beanName)
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/Group.java
new file mode 100644
index 0000000000000000000000000000000000000000..db1fe91cf09dea6233ac680df34b6779da3e2bc1
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/Group.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.List;
+
+class Group
+{
+    private String name;
+
+    private List<String> ldapGroupKeys;
+
+    private List<String> admins;
+
+    private List<String> usersBlackList;
+
+    public String getName()
+    {
+        return name;
+    }
+
+    public List<String> getAdmins()
+    {
+        return admins;
+    }
+
+    public List<String> getLdapGroupKeys()
+    {
+        return ldapGroupKeys;
+    }
+
+    public List<String> getUsersBlackList()
+    {
+        return usersBlackList;
+    }
+
+}
\ 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
new file mode 100644
index 0000000000000000000000000000000000000000..f836afd0ba7d33ce2fbaccde71d00a2ac6a1990d
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagementMaintenanceTask.java
@@ -0,0 +1,156 @@
+/*
+ * 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.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;
+import ch.systemsx.cisd.authentication.ldap.LDAPAuthenticationService;
+import ch.systemsx.cisd.authentication.ldap.LDAPDirectoryConfiguration;
+import ch.systemsx.cisd.authentication.ldap.LDAPPrincipalQuery;
+import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
+import ch.systemsx.cisd.common.filesystem.FileUtilities;
+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.openbis.generic.server.CommonServiceProvider;
+
+/**
+ * @author Franz-Josef Elmer
+ */
+public class UserManagementMaintenanceTask implements IMaintenanceTask
+{
+    private static final String DISTINGUISHED_NAME_TEMPLATE_PROPERTY = "distinguished-name-template";
+
+    private static final String DEFAULT_DISTINGUISHED_NAME_TEMPLATE = "CN=%s,OU=EthLists,DC=d,DC=ethz,DC=ch";
+
+    private static final String CONFIGURATION_FILE_PATH_PROPERTY = "configuration-file-path";
+
+    private static final String DEFAULT_CONFIGURATION_FILE_PATH = "etc/user-management-maintenance-config.json";
+
+    private static final Logger operationLog = LogFactory.getLogger(LogCategory.OPERATION,
+            UserManagementMaintenanceTask.class);
+
+    private File configurationFile;
+
+    private LDAPAuthenticationService ldapService;
+
+    private String dnTemplate;
+
+    @Override
+    public void setUp(String pluginName, Properties properties)
+    {
+        operationLog.info("Setup plugin " + pluginName);
+        configurationFile = new File(properties.getProperty(CONFIGURATION_FILE_PATH_PROPERTY, DEFAULT_CONFIGURATION_FILE_PATH));
+        if (configurationFile.isFile() == false)
+        {
+            throw new ConfigurationFailureException("Configuration file '" + configurationFile.getAbsolutePath()
+                    + "' doesn't exist or is a directory.");
+        }
+        dnTemplate = properties.getProperty(DISTINGUISHED_NAME_TEMPLATE_PROPERTY, DEFAULT_DISTINGUISHED_NAME_TEMPLATE);
+        if (dnTemplate.contains("%s") == false)
+        {
+            throw new ConfigurationFailureException("Property '" + DISTINGUISHED_NAME_TEMPLATE_PROPERTY + "' doesn't contain '%s' as placeholder.");
+        }
+        ldapService = (LDAPAuthenticationService) CommonServiceProvider.getApplicationContext().getBean("ldap-authentication-service");
+        operationLog.info("Plugin '" + pluginName + "' initialized. Configuration file: " + configurationFile.getAbsolutePath());
+        
+    }
+
+    @Override
+    public void execute()
+    {
+        Map<String, Group> groups = readGroupDefinitions();
+        if (groups == null)
+        {
+            return;
+        }
+        Log4jSimpleLogger logger = new Log4jSimpleLogger(operationLog);
+        UserManager userManager = new UserManager(CommonServiceProvider.getApplicationServerApi(), logger);
+        for (Entry<String, Group> entry : groups.entrySet())
+        {
+            String key = entry.getKey();
+            Group group = entry.getValue();
+            List<String> ldapGroupKeys = group.getLdapGroupKeys();
+            if (ldapGroupKeys == null || ldapGroupKeys.isEmpty())
+            {
+                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.listPrincipalsByGroup(String.format(dnTemplate, 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);
+        }
+        userManager.manageUsers();
+        operationLog.info("finished");
+    }
+    
+    private Map<String, Group> readGroupDefinitions()
+    {
+        if (configurationFile.isFile() == false)
+        {
+            operationLog.error("Configuration file '" + configurationFile.getAbsolutePath() + "' doesn't exist or is a directory.");
+            return null;
+        }
+        String serializedConfig = FileUtilities.loadToString(configurationFile);
+        try
+        {
+            return deserialize(serializedConfig);
+        } catch (Exception e)
+        {
+            operationLog.error("Invalid content of configuration file '" + configurationFile.getAbsolutePath() + "': " + e, e);
+            return null;
+        }
+    }
+
+    private Map<String, Group> deserialize(String serializedConfig) throws Exception
+    {
+        ObjectMapper mapper = new ObjectMapper();
+        return mapper.readValue(serializedConfig, new TypeReference<Map<String, Group>>(){});
+    }
+    
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..12effa23e0a731ec0140264e0f36beb0c51fb1c0
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/task/UserManager.java
@@ -0,0 +1,188 @@
+/*
+ * 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.ArrayList;
+import java.util.Collections;
+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 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.authorizationgroup.id.IAuthorizationGroupId;
+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.server.asapi.v3.IApplicationServerInternalApi;
+import ch.systemsx.cisd.authentication.Principal;
+import ch.systemsx.cisd.common.logging.ISimpleLogger;
+import ch.systemsx.cisd.common.logging.LogLevel;
+
+/**
+ * @author Franz-Josef Elmer
+ */
+class UserManager
+{
+    private final IApplicationServerInternalApi service;
+
+    private final ISimpleLogger logger;
+    
+    private final Map<String, UserInfo> userInfosByUserId = new TreeMap<>();
+    
+    private final List<String> groupCodes = new ArrayList<>();
+
+    UserManager(IApplicationServerInternalApi service, ISimpleLogger logger)
+    {
+        this.service = service;
+        this.logger = logger;
+    }
+
+    public void addGroup(String key, Group group, Map<String, Principal> principalsByUserId)
+    {
+        groupCodes.add(key);
+        Set<String> admins = asSet(group.getAdmins());
+        Set<String> blackListedUsers = asSet(group.getUsersBlackList());
+        for (Principal principal : principalsByUserId.values())
+        {
+            String userId = principal.getUserId();
+            UserInfo userInfo = userInfosByUserId.get(userId);
+            if (userInfo == null)
+            {
+                userInfo = new UserInfo(principal);
+                userInfosByUserId.put(userId, userInfo);
+            }
+            userInfo.addGroupInfo(new GroupInfo(key, admins.contains(userId), blackListedUsers.contains(userId)));
+        }
+        logger.log(LogLevel.INFO, principalsByUserId.size() + " users for group " + key);
+    }
+
+    public void manageUsers()
+    {
+        String sessionToken = service.loginAsSystem();
+        Map<IPersonId, Person> users = getUsersWithRoleAssigments(sessionToken);
+        Map<IAuthorizationGroupId, AuthorizationGroup> authorizationGroups = getAuthorizationGroups(sessionToken);
+        for (UserInfo userInfo : userInfosByUserId.values())
+        {
+            manageUser(userInfo, users, authorizationGroups);
+        }
+        service.logout(sessionToken);
+    }
+    
+    private void manageUser(UserInfo userInfo, Map<IPersonId, Person> knownUsers, 
+            Map<IAuthorizationGroupId, AuthorizationGroup> knownAuthorizationGroups)
+    {
+        
+    }
+
+    private Map<IPersonId, Person> getUsersWithRoleAssigments(String sessionToken)
+    {
+        Function<String, PersonPermId> mapper = userId -> new PersonPermId(userId);
+        List<PersonPermId> userIds = userInfosByUserId.keySet().stream().map(mapper).collect(Collectors.toList());
+        PersonFetchOptions fetchOptions = new PersonFetchOptions();
+        fetchOptions.withRoleAssignments().withSpace();
+        Map<IPersonId, Person> users = service.getPersons(sessionToken, userIds, fetchOptions);
+        return users;
+    }
+
+    private Map<IAuthorizationGroupId, AuthorizationGroup> getAuthorizationGroups(String sessionToken)
+    {
+        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);
+    }
+    
+    private Set<String> asSet(List<String> users)
+    {
+        return users == null ? Collections.emptySet() : new TreeSet<>(users);
+    }
+
+    private static class UserInfo
+    {
+        private Principal principal;
+
+        private Map<String, GroupInfo> groupInfosByGroupKey = new TreeMap<>();
+
+        public UserInfo(Principal principal)
+        {
+            this.principal = principal;
+        }
+
+        public Principal getPrincipal()
+        {
+            return principal;
+        }
+
+        public void addGroupInfo(GroupInfo groupInfo)
+        {
+            groupInfosByGroupKey.put(groupInfo.getKey(), groupInfo);
+        }
+
+        @Override
+        public String toString()
+        {
+            return principal.getUserId() + " " + groupInfosByGroupKey.values();
+        }
+    }
+
+    private static class GroupInfo
+    {
+        private String key;
+
+        private boolean admin;
+
+        private boolean onBlackList;
+
+        GroupInfo(String key, boolean admin, boolean onBlackList)
+        {
+            this.key = key;
+            this.admin = admin;
+            this.onBlackList = onBlackList;
+        }
+
+        public String getKey()
+        {
+            return key;
+        }
+
+        public boolean isAdmin()
+        {
+            return admin;
+        }
+
+        public boolean isOnBlackList()
+        {
+            return onBlackList;
+        }
+
+        @Override
+        public String toString()
+        {
+            return (onBlackList ? "." : "") + key + (admin ? "*" : "");
+        }
+
+    }
+
+}
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagerTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..e0fe3386b39b0cfb6e9051eacd946116f932c381
--- /dev/null
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/task/UserManagerTest.java
@@ -0,0 +1,295 @@
+/*
+ * 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 static org.testng.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+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.authorizationgroup.id.IAuthorizationGroupId;
+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.roleassignment.RoleAssignment;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.roleassignment.RoleLevel;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.roleassignment.fetchoptions.RoleAssignmentFetchOptions;
+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.SpacePermId;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.IApplicationServerInternalApi;
+import ch.systemsx.cisd.authentication.Principal;
+import ch.systemsx.cisd.common.logging.MockLogger;
+import ch.systemsx.cisd.common.test.RecordingMatcher;
+import ch.systemsx.cisd.common.test.ToStringMatcher;
+
+/**
+ * @author Franz-Josef Elmer
+ */
+public class UserManagerTest
+{
+    private static final String SESSION_TOKEN = "session-123";
+
+    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");
+
+    private Mockery context;
+
+    private IApplicationServerInternalApi service;
+
+    private UserManager userManager;
+
+    private MockLogger logger;
+
+    @BeforeMethod
+    public void setUp()
+    {
+        context = new Mockery();
+        service = context.mock(IApplicationServerInternalApi.class);
+        context.checking(new Expectations()
+            {
+                {
+                    one(service).loginAsSystem();
+                    will(returnValue(SESSION_TOKEN));
+
+                    one(service).logout(SESSION_TOKEN);
+                }
+            });
+        logger = new MockLogger();
+        userManager = new UserManager(service, logger);
+    }
+
+    @AfterMethod
+    public void tearDown()
+    {
+        // To following line of code should also be called at the end of each test method.
+        // Otherwise one does not known which test failed.
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testAddNewNormalUser()
+    {
+        // Given
+        RecordingMatcher<List<IPersonId>> personsMatcher = prepareGetUsersWithRoleAssigments(new PersonBuilder(U1).get());
+        RecordingMatcher<List<AuthorizationGroupPermId>> groupsMatcher = prepareGetAuthorizationGroups();
+
+        userManager.addGroup("G1", new Group(), principals(U2, U1));
+
+        // When
+        userManager.manageUsers();
+
+        // Then
+        assertEquals(personsMatcher.recordedObject().toString(), "[u1, u2]");
+        assertEquals(groupsMatcher.recordedObject().toString(), "[G1]");
+        context.assertIsSatisfied();
+    }
+
+    // @Test
+    public void test2()
+    {
+        // Given
+
+        // When
+        userManager.manageUsers();
+
+        // Then
+        context.assertIsSatisfied();
+    }
+
+    // @Test
+    public void test3()
+    {
+        // Given
+
+        // When
+        userManager.manageUsers();
+
+        // Then
+        context.assertIsSatisfied();
+    }
+
+    private RecordingMatcher<List<IPersonId>> prepareGetUsersWithRoleAssigments(Person... persons)
+    {
+        Map<IPersonId, Person> result = new LinkedHashMap<>();
+        for (Person person : persons)
+        {
+            result.put(person.getPermId(), person);
+        }
+        RecordingMatcher<List<IPersonId>> matcher = new RecordingMatcher<>();
+        context.checking(new Expectations()
+            {
+                {
+                    PersonFetchOptions fetchOptions = new PersonFetchOptions();
+                    fetchOptions.withRoleAssignments().withSpace();
+                    one(service).getPersons(with(SESSION_TOKEN), with(matcher), with(new ToStringMatcher<>(fetchOptions)));
+                    will(returnValue(result));
+                }
+            });
+        return matcher;
+    }
+
+    private RecordingMatcher<List<AuthorizationGroupPermId>> prepareGetAuthorizationGroups(AuthorizationGroup... authorizationGroups)
+    {
+        Map<IAuthorizationGroupId, AuthorizationGroup> result = new LinkedHashMap<>();
+        for (AuthorizationGroup authorizationGroup : authorizationGroups)
+        {
+            result.put(authorizationGroup.getPermId(), authorizationGroup);
+        }
+        RecordingMatcher<List<AuthorizationGroupPermId>> matcher = new RecordingMatcher<>();
+        context.checking(new Expectations()
+            {
+                {
+                    AuthorizationGroupFetchOptions fetchOptions = new AuthorizationGroupFetchOptions();
+                    fetchOptions.withUsers();
+                    one(service).getAuthorizationGroups(with(SESSION_TOKEN), with(matcher), with(new ToStringMatcher<>(fetchOptions)));
+                    will(returnValue(result));
+                }
+            });
+        return matcher;
+    }
+
+    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 RoleAssignment ra(Role role)
+    {
+        return ra(role, null);
+    }
+
+    private RoleAssignment ra(Role role, String spaceCodeOrNull)
+    {
+        RoleAssignment roleAssignment = new RoleAssignment();
+        RoleAssignmentFetchOptions fetchOptions = new RoleAssignmentFetchOptions();
+        fetchOptions.withSpace();
+        roleAssignment.setFetchOptions(fetchOptions);
+        roleAssignment.setRole(role);
+        roleAssignment.setRoleLevel(RoleLevel.INSTANCE);
+        if (spaceCodeOrNull != null)
+        {
+            Space space = new Space();
+            space.setCode(spaceCodeOrNull);
+            space.setPermId(new SpacePermId(spaceCodeOrNull));
+            space.setFetchOptions(new SpaceFetchOptions());
+            roleAssignment.setSpace(space);
+            roleAssignment.setRoleLevel(RoleLevel.SPACE);
+        }
+        return roleAssignment;
+    }
+
+    private static Person createPerson(Principal principal, PersonFetchOptions fetchOptions)
+    {
+        Person person = new Person();
+        person.setFetchOptions(fetchOptions);
+        person.setUserId(principal.getUserId());
+        person.setPermId(new PersonPermId(principal.getUserId()));
+        person.setEmail(principal.getEmail());
+        person.setFirstName(principal.getFirstName());
+        person.setLastName(principal.getLastName());
+        person.setRoleAssignments(new ArrayList<RoleAssignment>());
+        person.setActive(true);
+        return person;
+    }
+
+    private static final class PersonBuilder
+    {
+        private Person person;
+
+        PersonBuilder(Principal principal)
+        {
+            PersonFetchOptions fetchOptions = new PersonFetchOptions();
+            fetchOptions.withRoleAssignments().withSpace();
+            person = createPerson(principal, fetchOptions);
+        }
+
+        Person get()
+        {
+            return person;
+        }
+
+        PersonBuilder roleAssignments(RoleAssignment... roleAssignments)
+        {
+            for (RoleAssignment roleAssignment : roleAssignments)
+            {
+                person.getRoleAssignments().add(roleAssignment);
+            }
+            return this;
+        }
+
+        PersonBuilder deactive()
+        {
+            person.setActive(false);
+            return this;
+        }
+    }
+
+    private static final class AuthorizationGroupBuilder
+    {
+        private AuthorizationGroup authorizationGroup;
+
+        AuthorizationGroupBuilder(String code)
+        {
+            authorizationGroup = new AuthorizationGroup();
+            AuthorizationGroupFetchOptions fetchOptions = new AuthorizationGroupFetchOptions();
+            fetchOptions.withUsers();
+            authorizationGroup.setFetchOptions(fetchOptions);
+            authorizationGroup.setCode(code);
+            authorizationGroup.setPermId(new AuthorizationGroupPermId(code));
+            authorizationGroup.setUsers(new ArrayList<>());
+        }
+
+        public AuthorizationGroupBuilder users(Principal... principals)
+        {
+            PersonFetchOptions personFetchOptions = new PersonFetchOptions();
+            Function<Principal, Person> mapper = p -> createPerson(p, personFetchOptions);
+            authorizationGroup.getUsers().addAll(Arrays.asList(principals).stream().map(mapper).collect(Collectors.toList()));
+            return this;
+        }
+
+        AuthorizationGroup get()
+        {
+            return authorizationGroup;
+        }
+    }
+}