keycloak-uncached

ldap port

11/5/2016 10:04:53 PM

Changes

Details

diff --git a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java
index 6c5fbb2..cd41c5a 100644
--- a/federation/ldap2/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java
+++ b/federation/ldap2/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java
@@ -157,6 +157,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
 
     // throw ModelDuplicateException if there is different user in model with same email
     protected void checkDuplicateEmail(String userModelAttrName, String email, RealmModel realm, KeycloakSession session, UserModel user) {
+        if (email == null) return;
         if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName)) {
             // lowercase before search
             email = KeycloakModelUtils.toLowerCaseSafe(email);
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index 2b2179c..8252f6b 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -28,6 +28,7 @@ import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.GroupModel;
 import org.keycloak.models.IdentityProviderMapperModel;
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.OTPPolicy;
 import org.keycloak.models.PasswordPolicy;
 import org.keycloak.models.RealmModel;
@@ -39,6 +40,7 @@ import org.keycloak.models.UserFederationProviderModel;
 import org.keycloak.models.cache.CachedRealmModel;
 import org.keycloak.models.cache.infinispan.entities.CachedRealm;
 import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.storage.UserStorageProvider;
 
 import java.security.Key;
 import java.security.PrivateKey;
@@ -62,10 +64,12 @@ public class RealmAdapter implements CachedRealmModel {
     protected RealmCacheSession cacheSession;
     protected RealmModel updated;
     protected RealmCache cache;
+    protected KeycloakSession session;
 
-    public RealmAdapter(CachedRealm cached, RealmCacheSession cacheSession) {
+    public RealmAdapter(KeycloakSession session, CachedRealm cached, RealmCacheSession cacheSession) {
         this.cached = cached;
         this.cacheSession = cacheSession;
+        this.session = session;
     }
 
     @Override
@@ -1333,12 +1337,28 @@ public class RealmAdapter implements CachedRealmModel {
     @Override
     public ComponentModel addComponentModel(ComponentModel model) {
         getDelegateForUpdate();
+        evictUsers(model);
         return updated.addComponentModel(model);
     }
 
+    public void evictUsers(ComponentModel model) {
+        String parentId = model.getParentId();
+        evictUsers(parentId);
+    }
+
+    public void evictUsers(String parentId) {
+        if (parentId != null && !parentId.equals(getId())) {
+            ComponentModel parent = getComponent(parentId);
+            if (parent != null && UserStorageProvider.class.getName().equals(parent.getProviderType())) {
+                session.getUserCache().evict(this);
+            }
+        }
+    }
+
     @Override
     public void updateComponent(ComponentModel component) {
         getDelegateForUpdate();
+        evictUsers(component);
         updated.updateComponent(component);
 
     }
@@ -1346,6 +1366,7 @@ public class RealmAdapter implements CachedRealmModel {
     @Override
     public void removeComponent(ComponentModel component) {
         getDelegateForUpdate();
+        evictUsers(component);
         updated.removeComponent(component);
 
     }
@@ -1353,6 +1374,7 @@ public class RealmAdapter implements CachedRealmModel {
     @Override
     public void removeComponents(String parentId) {
         getDelegateForUpdate();
+        evictUsers(parentId);
         updated.removeComponents(parentId);
 
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
index 565fd48..9321f47 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java
@@ -389,7 +389,7 @@ public class RealmCacheSession implements CacheRealmProvider {
         } else if (managedRealms.containsKey(id)) {
             return managedRealms.get(id);
         }
-        RealmAdapter adapter = new RealmAdapter(cached, this);
+        RealmAdapter adapter = new RealmAdapter(session, cached, this);
         if (wasCached) {
             CachedRealmModel.RealmCachedEvent event = new CachedRealmModel.RealmCachedEvent() {
                 @Override
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java
index 6030a66..bc49440 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java
@@ -64,6 +64,11 @@ public class UserAdapter implements CachedUserModel {
     }
 
     @Override
+    public boolean isMarkedForEviction() {
+        return updated != null;
+    }
+
+    @Override
     public void invalidate() {
         getDelegateForUpdate();
     }
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
index 0bb71c0..b51d911 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java
@@ -115,7 +115,10 @@ public class UserCacheSession implements UserCache {
         }
     }
 
-
+    @Override
+    public void evict(RealmModel realm) {
+        realmInvalidations.add(realm.getId());
+    }
 
     protected void runInvalidations() {
         for (String realmId : realmInvalidations) {
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index b8670dc..6de22a9 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -2046,7 +2046,10 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
         }
         c.setName(model.getName());
         c.setParentId(model.getParentId());
-        if (model.getParentId() == null) c.setParentId(this.getId());
+        if (model.getParentId() == null) {
+            c.setParentId(this.getId());
+            model.setParentId(this.getId());
+        }
         c.setProviderType(model.getProviderType());
         c.setProviderId(model.getProviderId());
         c.setSubType(model.getSubType());
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index 2fa7561..205f6b3 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -1964,7 +1964,10 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
         }
         updateComponentEntity(entity, model);
         model.setId(entity.getId());
-        if (model.getParentId() == null) entity.setParentId(this.getId());
+        if (model.getParentId() == null) {
+            entity.setParentId(this.getId());
+            model.setParentId(this.getId());
+        }
         realm.getComponentEntities().add(entity);
         updateRealm();
         ComponentUtil.notifyCreated(session, this, model);
diff --git a/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java b/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java
index 67431a9..8434d9d 100644
--- a/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java
@@ -35,6 +35,8 @@ public interface CachedUserModel extends UserModel {
      */
     UserModel getDelegateForUpdate();
 
+    boolean isMarkedForEviction();
+
     /**
      * Invalidate the cache for this model
      *
diff --git a/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java b/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java
index f309079..260b0be 100755
--- a/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java
+++ b/server-spi/src/main/java/org/keycloak/models/cache/UserCache.java
@@ -32,5 +32,12 @@ public interface UserCache extends UserProvider {
      * @param user
      */
     void evict(RealmModel realm, UserModel user);
+
+    /**
+     * Evict users of a specific realm
+     *
+     * @param realm
+     */
+    void evict(RealmModel realm);
     void clear();
 }
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index a962300..b57d2b1 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -52,6 +52,7 @@ import org.keycloak.models.UserModel;
 import org.keycloak.provider.Provider;
 import org.keycloak.provider.ProviderFactory;
 import org.keycloak.representations.idm.CertificateRepresentation;
+import org.keycloak.storage.UserStorageProviderModel;
 import org.keycloak.transaction.JtaTransactionManagerLookup;
 
 import javax.crypto.spec.SecretKeySpec;
@@ -372,6 +373,29 @@ public final class KeycloakModelUtils {
         }
         return null;
     }
+
+    public static UserStorageProviderModel findUserStorageProviderByName(String displayName, RealmModel realm) {
+        if (displayName == null) {
+            return null;
+        }
+
+        for (UserStorageProviderModel fedProvider : realm.getUserStorageProviders()) {
+            if (displayName.equals(fedProvider.getName())) {
+                return fedProvider;
+            }
+        }
+        return null;
+    }
+
+    public static UserStorageProviderModel findUserStorageProviderById(String fedProviderId, RealmModel realm) {
+        for (UserStorageProviderModel fedProvider : realm.getUserStorageProviders()) {
+            if (fedProviderId.equals(fedProvider.getId())) {
+                return fedProvider;
+            }
+        }
+        return null;
+    }
+
     public static ComponentModel createComponentModel(String name, String parentId, String providerId, String providerType, String... config) {
         ComponentModel mapperModel = new ComponentModel();
         mapperModel.setParentId(parentId);
diff --git a/services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java b/services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java
index f71f84a..fcd1e18 100644
--- a/services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java
+++ b/services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java
@@ -43,8 +43,9 @@ public class OTPCredentialProvider implements CredentialProvider, CredentialInpu
     protected KeycloakSession session;
 
     protected List<CredentialModel> getCachedCredentials(UserModel user, String type) {
-        if (!(user instanceof CachedUserModel)) return Collections.EMPTY_LIST;
+        if (!(user instanceof CachedUserModel)) return null;
         CachedUserModel cached = (CachedUserModel)user;
+        if (cached.isMarkedForEviction()) return null;
         List<CredentialModel> rtn = (List<CredentialModel>)cached.getCachedWith().get(OTPCredentialProvider.class.getName() + "." + type);
         if (rtn == null) return Collections.EMPTY_LIST;
         return rtn;
@@ -186,8 +187,9 @@ public class OTPCredentialProvider implements CredentialProvider, CredentialInpu
     }
 
     protected boolean configuredForTOTP(RealmModel realm, UserModel user) {
-        return !getCachedCredentials(user, CredentialModel.TOTP).isEmpty()
-                || !getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.TOTP).isEmpty();
+        List<CredentialModel> cachedCredentials = getCachedCredentials(user, CredentialModel.TOTP);
+        if (cachedCredentials == null) return !getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.TOTP).isEmpty();
+        return !cachedCredentials.isEmpty();
     }
 
     public static boolean validOTP(RealmModel realm, String token, String secret) {
diff --git a/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java b/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java
index d0558ab..84cd0f0 100644
--- a/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java
+++ b/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java
@@ -59,7 +59,7 @@ public class PasswordCredentialProvider implements CredentialProvider, Credentia
 
     public CredentialModel getPassword(RealmModel realm, UserModel user) {
         List<CredentialModel> passwords = null;
-        if (user instanceof CachedUserModel) {
+        if (user instanceof CachedUserModel && !((CachedUserModel)user).isMarkedForEviction()) {
             CachedUserModel cached = (CachedUserModel)user;
             passwords = (List<CredentialModel>)cached.getCachedWith().get(PASSWORD_CACHE_KEY);
 
diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java
index 8394bbb..e3eeaec 100755
--- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java
+++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java
@@ -138,6 +138,12 @@ public class UserStorageManager implements UserProvider, OnUserCache {
         if (getFederatedStorage() != null) getFederatedStorage().preRemove(realm, user);
         StorageId storageId = new StorageId(user.getId());
         if (storageId.getProviderId() == null) {
+            if (user.getFederationLink() != null) {
+                UserStorageProvider provider = getStorageProvider(session, realm, user.getFederationLink());
+                if (provider != null && provider instanceof UserRegistrationProvider) {
+                    ((UserRegistrationProvider)provider).removeUser(realm, user);
+                }
+            }
             return localStorage().removeUser(realm, user);
         }
         UserRegistrationProvider registry = (UserRegistrationProvider)getStorageProvider(session, realm, storageId.getProviderId());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapper2WaySyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapper2WaySyncTest.java
new file mode 100755
index 0000000..7534a93
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapper2WaySyncTest.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.testsuite.federation.storage.ldap;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.FixMethodOrder;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
+import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode;
+import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapperFactory;
+import org.keycloak.storage.ldap.mappers.membership.group.GroupMapperConfig;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.storage.user.SynchronizationResult;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.LDAPRule;
+
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class LDAPGroupMapper2WaySyncTest {
+
+    @ClassRule
+    public static LDAPRule ldapRule = new LDAPRule();
+
+    private static ComponentModel ldapModel = null;
+    private static String descriptionAttrName = null;
+
+    @Rule
+    public KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            MultivaluedHashMap<String,String> ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule);
+            ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "true");
+            ldapConfig.putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.WRITABLE.toString());
+            ldapConfig.putSingle(LDAPConstants.BATCH_SIZE_FOR_SYNC, "4"); // Issues with pagination on ApacheDS
+            UserStorageProviderModel model = new UserStorageProviderModel();
+            model.setLastSync(0);
+            model.setChangedSyncPeriod(-1);
+            model.setFullSyncPeriod(-1);
+            model.setName("test-ldap");
+            model.setPriority(0);
+            model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME);
+            model.setConfig(ldapConfig);
+
+            ldapModel = appRealm.addComponentModel(model);
+            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            descriptionAttrName = ldapFedProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? "displayName" : "description";
+
+            // Add group mapper
+            LDAPTestUtils.addOrUpdateGroupMapper(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY, descriptionAttrName);
+
+            // Remove all LDAP groups
+            LDAPTestUtils.removeAllLDAPGroups(session, appRealm, ldapModel, "groupsMapper");
+
+            // Add some groups for testing into Keycloak
+            removeAllModelGroups(appRealm);
+
+            GroupModel group1 = appRealm.createGroup("group1");
+            appRealm.moveGroup(group1, null);
+            group1.setSingleAttribute(descriptionAttrName, "group1 - description1");
+
+            GroupModel group11 = appRealm.createGroup("group11");
+            appRealm.moveGroup(group11, group1);
+
+            GroupModel group12 = appRealm.createGroup("group12");
+            appRealm.moveGroup(group12, group1);
+            group12.setSingleAttribute(descriptionAttrName, "group12 - description12");
+
+            GroupModel group2 = appRealm.createGroup("group2");
+            appRealm.moveGroup(group2, null);
+        }
+    });
+
+
+    @Test
+    public void test01_syncNoPreserveGroupInheritance() throws Exception {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ComponentModel mapperModel = LDAPTestUtils.getComponentByName(realm,ldapModel, "groupsMapper");
+            LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+
+            // Update group mapper to skip preserve inheritance and check it will pass now
+            LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "false");
+            realm.updateComponent(mapperModel);
+
+            // Sync from Keycloak into LDAP
+            SynchronizationResult syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromKeycloakToFederationProvider(mapperModel, ldapProvider, session, realm);
+            LDAPTestUtils.assertSyncEquals(syncResult, 4, 0, 0, 0);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel realm = session.realms().getRealmByName("test");
+
+            // Delete all KC groups now
+            removeAllModelGroups(realm);
+            Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1"));
+            Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group11"));
+            Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group2"));
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ComponentModel mapperModel = LDAPTestUtils.getComponentByName(realm,ldapModel, "groupsMapper");
+            LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+
+            // Sync from LDAP back into Keycloak
+            SynchronizationResult syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
+            LDAPTestUtils.assertSyncEquals(syncResult, 4, 0, 0, 0);
+
+            // Assert groups are imported to keycloak. All are at top level
+            GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1");
+            GroupModel kcGroup11 = KeycloakModelUtils.findGroupByPath(realm, "/group11");
+            GroupModel kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group12");
+            GroupModel kcGroup2 = KeycloakModelUtils.findGroupByPath(realm, "/group2");
+
+            Assert.assertEquals(0, kcGroup1.getSubGroups().size());
+
+            Assert.assertEquals("group1 - description1", kcGroup1.getFirstAttribute(descriptionAttrName));
+            Assert.assertNull(kcGroup11.getFirstAttribute(descriptionAttrName));
+            Assert.assertEquals("group12 - description12", kcGroup12.getFirstAttribute(descriptionAttrName));
+            Assert.assertNull(kcGroup2.getFirstAttribute(descriptionAttrName));
+
+            // test drop non-existing works
+            testDropNonExisting(session, realm, mapperModel, ldapProvider);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+    }
+
+    @Test
+    public void test02_syncWithGroupInheritance() throws Exception {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ComponentModel mapperModel = LDAPTestUtils.getComponentByName(realm,ldapModel, "groupsMapper");
+            LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+
+            // Update group mapper to skip preserve inheritance and check it will pass now
+            LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "true");
+            realm.updateComponent(mapperModel);
+
+            // Sync from Keycloak into LDAP
+            SynchronizationResult syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromKeycloakToFederationProvider(mapperModel, ldapProvider, session, realm);
+            LDAPTestUtils.assertSyncEquals(syncResult, 4, 0, 0, 0);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel realm = session.realms().getRealmByName("test");
+
+            // Delete all KC groups now
+            removeAllModelGroups(realm);
+            Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1"));
+            Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group11"));
+            Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group2"));
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ComponentModel mapperModel = LDAPTestUtils.getComponentByName(realm,ldapModel, "groupsMapper");
+            LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+
+            // Sync from LDAP back into Keycloak
+            SynchronizationResult syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
+            LDAPTestUtils.assertSyncEquals(syncResult, 4, 0, 0, 0);
+
+            // Assert groups are imported to keycloak. All are at top level
+            GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1");
+            GroupModel kcGroup11 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group11");
+            GroupModel kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group12");
+            GroupModel kcGroup2 = KeycloakModelUtils.findGroupByPath(realm, "/group2");
+
+            Assert.assertEquals(2, kcGroup1.getSubGroups().size());
+
+            Assert.assertEquals("group1 - description1", kcGroup1.getFirstAttribute(descriptionAttrName));
+            Assert.assertNull(kcGroup11.getFirstAttribute(descriptionAttrName));
+            Assert.assertEquals("group12 - description12", kcGroup12.getFirstAttribute(descriptionAttrName));
+            Assert.assertNull(kcGroup2.getFirstAttribute(descriptionAttrName));
+
+            // test drop non-existing works
+            testDropNonExisting(session, realm, mapperModel, ldapProvider);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+    }
+
+
+    private static void removeAllModelGroups(RealmModel appRealm) {
+        for (GroupModel group : appRealm.getTopLevelGroups()) {
+            appRealm.removeGroup(group);
+        }
+    }
+
+    private void testDropNonExisting(KeycloakSession session, RealmModel realm, ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
+        // Put some group directly to LDAP
+        LDAPTestUtils.createLDAPGroup(session, realm, ldapModel, "group3");
+
+        // Sync and assert our group is still in LDAP
+        SynchronizationResult syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromKeycloakToFederationProvider(mapperModel, ldapProvider, session, realm);
+        LDAPTestUtils.assertSyncEquals(syncResult, 0, 4, 0, 0);
+        Assert.assertNotNull(LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, realm).loadLDAPGroupByName("group3"));
+
+        // Change config to drop non-existing groups
+        LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC, "true");
+        realm.updateComponent(mapperModel);
+
+        // Sync and assert group removed from LDAP
+        syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromKeycloakToFederationProvider(mapperModel, ldapProvider, session, realm);
+        LDAPTestUtils.assertSyncEquals(syncResult, 0, 4, 1, 0);
+        Assert.assertNull(LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, realm).loadLDAPGroupByName("group3"));
+    }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java
new file mode 100755
index 0000000..6632124
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperSyncTest.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.testsuite.federation.storage.ldap;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runners.MethodSorters;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
+import org.keycloak.storage.ldap.LDAPUtils;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode;
+import org.keycloak.storage.ldap.mappers.membership.MembershipType;
+import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapperFactory;
+import org.keycloak.storage.ldap.mappers.membership.group.GroupMapperConfig;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.storage.user.SynchronizationResult;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.LDAPRule;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class LDAPGroupMapperSyncTest {
+
+    private static LDAPRule ldapRule = new LDAPRule();
+
+    private static ComponentModel ldapModel = null;
+    private static String descriptionAttrName = null;
+
+    private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            MultivaluedHashMap<String,String> ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule);
+            ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "true");
+            ldapConfig.putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.WRITABLE.toString());
+            UserStorageProviderModel model = new UserStorageProviderModel();
+            model.setLastSync(0);
+            model.setChangedSyncPeriod(-1);
+            model.setFullSyncPeriod(-1);
+            model.setName("test-ldap");
+            model.setPriority(0);
+            model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME);
+            model.setConfig(ldapConfig);
+
+            ldapModel = appRealm.addComponentModel(model);
+
+            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            descriptionAttrName = ldapFedProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? "displayName" : "description";
+
+            // Add group mapper
+            LDAPTestUtils.addOrUpdateGroupMapper(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY, descriptionAttrName);
+
+            // Remove all LDAP groups
+            LDAPTestUtils.removeAllLDAPGroups(session, appRealm, ldapModel, "groupsMapper");
+
+            // Add some groups for testing
+            LDAPObject group1 = LDAPTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group1", descriptionAttrName, "group1 - description");
+            LDAPObject group11 = LDAPTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group11");
+            LDAPObject group12 = LDAPTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group12", descriptionAttrName, "group12 - description");
+
+            LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group11, false);
+            LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group12, true);
+        }
+    });
+
+    @ClassRule
+    public static TestRule chain = RuleChain
+            .outerRule(ldapRule)
+            .around(keycloakRule);
+
+    @Before
+    public void before() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel realm = session.realms().getRealmByName("test");
+            List<GroupModel> kcGroups = realm.getTopLevelGroups();
+            for (GroupModel kcGroup : kcGroups) {
+                realm.removeGroup(kcGroup);
+            }
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+    }
+
+    @Test
+    public void test01_syncNoPreserveGroupInheritance() throws Exception {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ComponentModel mapperModel = LDAPTestUtils.getComponentByName(realm,ldapModel, "groupsMapper");
+            LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, realm);
+
+            // Add recursive group mapping to LDAP. Check that sync with preserve group inheritance will fail
+            LDAPObject group1 = groupMapper.loadLDAPGroupByName("group1");
+            LDAPObject group12 = groupMapper.loadLDAPGroupByName("group12");
+            LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, group12, group1, true);
+
+            try {
+                new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
+                Assert.fail("Not expected group sync to pass");
+            } catch (ModelException expected) {
+                Assert.assertTrue(expected.getMessage().contains("Recursion detected"));
+            }
+
+            // Update group mapper to skip preserve inheritance and check it will pass now
+            LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "false");
+            realm.updateComponent(mapperModel);
+
+            new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
+
+            // Assert groups are imported to keycloak. All are at top level
+            GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1");
+            GroupModel kcGroup11 = KeycloakModelUtils.findGroupByPath(realm, "/group11");
+            GroupModel kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group12");
+
+            Assert.assertEquals(0, kcGroup1.getSubGroups().size());
+
+            Assert.assertEquals("group1 - description", kcGroup1.getFirstAttribute(descriptionAttrName));
+            Assert.assertNull(kcGroup11.getFirstAttribute(descriptionAttrName));
+            Assert.assertEquals("group12 - description", kcGroup12.getFirstAttribute(descriptionAttrName));
+
+            // Cleanup - remove recursive mapping in LDAP
+            LDAPUtils.deleteMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, group12, group1, true);
+
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+    @Test
+    public void test02_syncWithGroupInheritance() throws Exception {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ComponentModel mapperModel = LDAPTestUtils.getComponentByName(realm,ldapModel, "groupsMapper");
+            LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, realm);
+
+            // Sync groups with inheritance
+            SynchronizationResult syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
+            LDAPTestUtils.assertSyncEquals(syncResult, 3, 0, 0, 0);
+
+            // Assert groups are imported to keycloak including their inheritance from LDAP
+            GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1");
+            Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group11"));
+            Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group12"));
+            GroupModel kcGroup11 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group11");
+            GroupModel kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group12");
+
+            Assert.assertEquals(2, kcGroup1.getSubGroups().size());
+
+            Assert.assertEquals("group1 - description", kcGroup1.getFirstAttribute(descriptionAttrName));
+            Assert.assertNull(kcGroup11.getFirstAttribute(descriptionAttrName));
+            Assert.assertEquals("group12 - description", kcGroup12.getFirstAttribute(descriptionAttrName));
+
+            // Update description attributes in LDAP
+            LDAPObject group1 = groupMapper.loadLDAPGroupByName("group1");
+            group1.setSingleAttribute(descriptionAttrName, "group1 - changed description");
+            ldapProvider.getLdapIdentityStore().update(group1);
+
+            LDAPObject group12 = groupMapper.loadLDAPGroupByName("group12");
+            group12.setAttribute(descriptionAttrName, null);
+            ldapProvider.getLdapIdentityStore().update(group12);
+
+            // Sync and assert groups updated
+            syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
+            LDAPTestUtils.assertSyncEquals(syncResult, 0, 3, 0, 0);
+
+            // Assert attributes changed in keycloak
+            kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1");
+            kcGroup12 = KeycloakModelUtils.findGroupByPath(realm, "/group1/group12");
+            Assert.assertEquals("group1 - changed description", kcGroup1.getFirstAttribute(descriptionAttrName));
+            Assert.assertNull(kcGroup12.getFirstAttribute(descriptionAttrName));
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+    @Test
+    public void test03_syncWithDropNonExistingGroups() throws Exception {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ComponentModel mapperModel = LDAPTestUtils.getComponentByName(realm,ldapModel, "groupsMapper");
+            LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+
+            // Sync groups with inheritance
+            SynchronizationResult syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
+            LDAPTestUtils.assertSyncEquals(syncResult, 3, 0, 0, 0);
+
+            // Assert groups are imported to keycloak including their inheritance from LDAP
+            GroupModel kcGroup1 = KeycloakModelUtils.findGroupByPath(realm, "/group1");
+            Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group11"));
+            Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group12"));
+
+            Assert.assertEquals(2, kcGroup1.getSubGroups().size());
+
+            // Create some new groups in keycloak
+            GroupModel model1 = realm.createGroup("model1");
+            realm.moveGroup(model1, null);
+            GroupModel model2 = realm.createGroup("model2");
+            kcGroup1.addChild(model2);
+
+            // Sync groups again from LDAP. Nothing deleted
+            syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
+            LDAPTestUtils.assertSyncEquals(syncResult, 0, 3, 0, 0);
+
+            Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group11"));
+            Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group12"));
+            Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/model1"));
+            Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/model2"));
+
+            // Update group mapper to drop non-existing groups during sync
+            LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC, "true");
+            realm.updateComponent(mapperModel);
+
+            // Sync groups again from LDAP. Assert LDAP non-existing groups deleted
+            syncResult = new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
+            Assert.assertEquals(3, syncResult.getUpdated());
+            Assert.assertTrue(syncResult.getRemoved() == 2);
+
+            // Sync and assert groups updated
+            Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group11"));
+            Assert.assertNotNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/group12"));
+            Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/model1"));
+            Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1/model2"));
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+
+
+    @Test
+    public void test04_syncNoPreserveGroupInheritanceWithLazySync() throws Exception {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel realm = session.realms().getRealmByName("test");
+            ComponentModel mapperModel = LDAPTestUtils.getComponentByName(realm,ldapModel, "groupsMapper");
+            LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, realm);
+
+            // Update group mapper to skip preserve inheritance
+            LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "false");
+            realm.updateComponent(mapperModel);
+
+            // Add user to LDAP and put him as member of group11
+            LDAPTestUtils.removeAllLDAPUsers(ldapProvider, realm);
+            LDAPObject johnLdap = LDAPTestUtils.addLDAPUser(ldapProvider, realm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
+            LDAPTestUtils.updateLDAPPassword(ldapProvider, johnLdap, "Password1");
+            groupMapper.addGroupMappingInLDAP("group11", johnLdap);
+
+            // Assert groups not yet imported to Keycloak DB
+            Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group1"));
+            Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group11"));
+            Assert.assertNull(KeycloakModelUtils.findGroupByPath(realm, "/group12"));
+
+            // Load user from LDAP to Keycloak DB
+            UserModel john = session.users().getUserByUsername("johnkeycloak", realm);
+            Set<GroupModel> johnGroups = john.getGroups();
+
+            // Assert just those groups, which john was memberOf exists because they were lazily created
+            GroupModel group1 = KeycloakModelUtils.findGroupByPath(realm, "/group1");
+            GroupModel group11 = KeycloakModelUtils.findGroupByPath(realm, "/group11");
+            GroupModel group12 = KeycloakModelUtils.findGroupByPath(realm, "/group12");
+            Assert.assertNull(group1);
+            Assert.assertNotNull(group11);
+            Assert.assertNull(group12);
+
+            Assert.assertEquals(1, johnGroups.size());
+            Assert.assertTrue(johnGroups.contains(group11));
+
+            // Delete group mapping
+            john.leaveGroup(group11);
+
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperTest.java
new file mode 100755
index 0000000..c15a304
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPGroupMapperTest.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.testsuite.federation.storage.ldap;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runners.MethodSorters;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.ldap.LDAPConfig;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
+import org.keycloak.storage.ldap.LDAPUtils;
+import org.keycloak.storage.ldap.idm.model.LDAPDn;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode;
+import org.keycloak.storage.ldap.mappers.membership.MembershipType;
+import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapperFactory;
+import org.keycloak.storage.ldap.mappers.membership.group.GroupMapperConfig;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.LDAPRule;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class LDAPGroupMapperTest {
+
+    private static LDAPRule ldapRule = new LDAPRule();
+
+    private static ComponentModel ldapModel = null;
+    private static String descriptionAttrName = null;
+
+    private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            LDAPTestUtils.addLocalUser(manager.getSession(), appRealm, "mary", "mary@test.com", "password-app");
+            LDAPTestUtils.addLocalUser(manager.getSession(), appRealm, "john", "john@test.com", "password-app");
+
+            MultivaluedHashMap<String,String> ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule);
+            ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "true");
+            ldapConfig.putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.WRITABLE.toString());
+            UserStorageProviderModel model = new UserStorageProviderModel();
+            model.setLastSync(0);
+            model.setChangedSyncPeriod(-1);
+            model.setFullSyncPeriod(-1);
+            model.setName("test-ldap");
+            model.setPriority(0);
+            model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME);
+            model.setConfig(ldapConfig);
+
+            ldapModel = appRealm.addComponentModel(model);
+            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            descriptionAttrName = ldapFedProvider.getLdapIdentityStore().getConfig().isActiveDirectory() ? "displayName" : "description";
+
+            // Add group mapper
+            LDAPTestUtils.addOrUpdateGroupMapper(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY, descriptionAttrName);
+
+            // Remove all LDAP groups
+            LDAPTestUtils.removeAllLDAPGroups(session, appRealm, ldapModel, "groupsMapper");
+
+            // Add some groups for testing
+            LDAPObject group1 = LDAPTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group1", descriptionAttrName, "group1 - description");
+            LDAPObject group11 = LDAPTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group11");
+            LDAPObject group12 = LDAPTestUtils.createLDAPGroup(manager.getSession(), appRealm, ldapModel, "group12", descriptionAttrName, "group12 - description");
+
+            LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group11, false);
+            LDAPUtils.addMember(ldapFedProvider, MembershipType.DN, LDAPConstants.MEMBER, group1, group12, true);
+
+            // Sync LDAP groups to Keycloak DB
+            ComponentModel mapperModel = LDAPTestUtils.getComponentByName(appRealm,ldapModel, "groupsMapper");
+            new GroupLDAPStorageMapperFactory().create(session, mapperModel).syncDataFromFederationProviderToKeycloak(mapperModel, ldapFedProvider, session, appRealm);
+
+            // Delete all LDAP users
+            LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);
+
+            // Add some LDAP users for testing
+            LDAPObject john = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
+            LDAPTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1");
+
+            LDAPObject mary = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary", "Kelly", "mary@email.org", null, "5678");
+            LDAPTestUtils.updateLDAPPassword(ldapFedProvider, mary, "Password1");
+
+            LDAPObject rob = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "robkeycloak", "Rob", "Brown", "rob@email.org", null, "8910");
+            LDAPTestUtils.updateLDAPPassword(ldapFedProvider, rob, "Password1");
+
+            LDAPObject james = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "jameskeycloak", "James", "Brown", "james@email.org", null, "8910");
+            LDAPTestUtils.updateLDAPPassword(ldapFedProvider, james, "Password1");
+
+        }
+    });
+
+    @ClassRule
+    public static TestRule chain = RuleChain
+            .outerRule(ldapRule)
+            .around(keycloakRule);
+
+    @Test
+    public void test01_ldapOnlyGroupMappings() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+
+            ComponentModel mapperModel = LDAPTestUtils.getComponentByName(appRealm,ldapModel, "groupsMapper");
+            LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.MODE, LDAPGroupMapperMode.LDAP_ONLY.toString());
+            appRealm.updateComponent(mapperModel);
+
+            UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
+            UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm);
+
+            // 1 - Grant some groups in LDAP
+
+            // This group should already exists as it was imported from LDAP
+            GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1");
+            john.joinGroup(group1);
+
+            // This group should already exists as it was imported from LDAP
+            GroupModel group11 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11");
+            mary.joinGroup(group11);
+
+            // This group should already exists as it was imported from LDAP
+            GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12");
+            john.joinGroup(group12);
+            mary.joinGroup(group12);
+
+            // 2 - Check that group mappings are not in local Keycloak DB (They are in LDAP).
+
+            UserModel johnDb = session.userLocalStorage().getUserByUsername("johnkeycloak", appRealm);
+            Set<GroupModel> johnDbGroups = johnDb.getGroups();
+            Assert.assertEquals(0, johnDbGroups.size());
+
+            // 3 - Check that group mappings are in LDAP and hence available through federation
+
+            Set<GroupModel> johnGroups = john.getGroups();
+            Assert.assertEquals(2, johnGroups.size());
+            Assert.assertTrue(johnGroups.contains(group1));
+            Assert.assertFalse(johnGroups.contains(group11));
+            Assert.assertTrue(johnGroups.contains(group12));
+
+            // 4 - Check through userProvider
+            List<UserModel> group1Members = session.users().getGroupMembers(appRealm, group1, 0, 10);
+            List<UserModel> group11Members = session.users().getGroupMembers(appRealm, group11, 0, 10);
+            List<UserModel> group12Members = session.users().getGroupMembers(appRealm, group12, 0, 10);
+
+            Assert.assertEquals(1, group1Members.size());
+            Assert.assertEquals("johnkeycloak", group1Members.get(0).getUsername());
+            Assert.assertEquals(1, group11Members.size());
+            Assert.assertEquals("marykeycloak", group11Members.get(0).getUsername());
+            Assert.assertEquals(2, group12Members.size());
+
+            // 4 - Delete some group mappings and check they are deleted
+
+            john.leaveGroup(group1);
+            john.leaveGroup(group12);
+
+            mary.leaveGroup(group1);
+            mary.leaveGroup(group12);
+
+            johnGroups = john.getGroups();
+            Assert.assertEquals(0, johnGroups.size());
+
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+    @Test
+    public void test02_readOnlyGroupMappings() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            System.out.println("starting test02_readOnlyGroupMappings");
+            RealmModel appRealm = session.realms().getRealmByName("test");
+
+            ComponentModel mapperModel = LDAPTestUtils.getComponentByName(appRealm,ldapModel, "groupsMapper");
+            LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.MODE, LDAPGroupMapperMode.READ_ONLY.toString());
+            appRealm.updateComponent(mapperModel);
+
+            UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm);
+
+            GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1");
+            GroupModel group11 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11");
+            GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12");
+
+            // Add some group mappings directly into LDAP
+            LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, appRealm);
+
+            LDAPObject maryLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "marykeycloak");
+            groupMapper.addGroupMappingInLDAP("group1", maryLdap);
+            groupMapper.addGroupMappingInLDAP("group11", maryLdap);
+
+            // Add some group mapping to model
+            mary.joinGroup(group12);
+
+            // Assert that mary has both LDAP and DB mapped groups
+            Set<GroupModel> maryGroups = mary.getGroups();
+            Assert.assertEquals(3, maryGroups.size());
+            Assert.assertTrue(maryGroups.contains(group1));
+            Assert.assertTrue(maryGroups.contains(group11));
+            Assert.assertTrue(maryGroups.contains(group12));
+
+            // Assert that access through DB will have just DB mapped groups
+            System.out.println("******");
+            UserModel maryDB = session.userLocalStorage().getUserByUsername("marykeycloak", appRealm);
+            Set<GroupModel> maryDBGroups = maryDB.getGroups();
+            Assert.assertFalse(maryDBGroups.contains(group1));
+            Assert.assertFalse(maryDBGroups.contains(group11));
+            Assert.assertTrue(maryDBGroups.contains(group12));
+
+            // Check through userProvider
+            List<UserModel> group1Members = session.users().getGroupMembers(appRealm, group1, 0, 10);
+            List<UserModel> group11Members = session.users().getGroupMembers(appRealm, group11, 0, 10);
+            List<UserModel> group12Members = session.users().getGroupMembers(appRealm, group12, 0, 10);
+            Assert.assertEquals(1, group1Members.size());
+            Assert.assertEquals("marykeycloak", group1Members.get(0).getUsername());
+            Assert.assertEquals(1, group11Members.size());
+            Assert.assertEquals("marykeycloak", group11Members.get(0).getUsername());
+            Assert.assertEquals(1, group12Members.size());
+            Assert.assertEquals("marykeycloak", group12Members.get(0).getUsername());
+
+            mary.leaveGroup(group12);
+            try {
+                mary.leaveGroup(group1);
+                Assert.fail("It wasn't expected to successfully delete LDAP group mappings in READ_ONLY mode");
+            } catch (ModelException expected) {
+            }
+
+            // Delete role mappings directly in LDAP
+            deleteGroupMappingsInLDAP(groupMapper, maryLdap, "group1");
+            deleteGroupMappingsInLDAP(groupMapper, maryLdap, "group11");
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+    @Test
+    public void test03_importGroupMappings() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+
+            ComponentModel mapperModel = LDAPTestUtils.getComponentByName(appRealm,ldapModel, "groupsMapper");
+            LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.MODE, LDAPGroupMapperMode.IMPORT.toString());
+            appRealm.updateComponent(mapperModel);
+
+            // Add some group mappings directly in LDAP
+            LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, appRealm);
+
+            LDAPObject robLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "robkeycloak");
+            groupMapper.addGroupMappingInLDAP("group11", robLdap);
+            groupMapper.addGroupMappingInLDAP("group12", robLdap);
+
+            // Get user and check that he has requested groupa from LDAP
+            UserModel rob = session.users().getUserByUsername("robkeycloak", appRealm);
+            Set<GroupModel> robGroups = rob.getGroups();
+
+            GroupModel group1 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1");
+            GroupModel group11 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group11");
+            GroupModel group12 = KeycloakModelUtils.findGroupByPath(appRealm, "/group1/group12");
+
+            Assert.assertFalse(robGroups.contains(group1));
+            Assert.assertTrue(robGroups.contains(group11));
+            Assert.assertTrue(robGroups.contains(group12));
+
+            // Delete some group mappings in LDAP and check that it doesn't have any effect and user still has groups
+            deleteGroupMappingsInLDAP(groupMapper, robLdap, "group11");
+            deleteGroupMappingsInLDAP(groupMapper, robLdap, "group12");
+            robGroups = rob.getGroups();
+            Assert.assertTrue(robGroups.contains(group11));
+            Assert.assertTrue(robGroups.contains(group12));
+
+            // Delete group mappings through model and verifies that user doesn't have them anymore
+            rob.leaveGroup(group11);
+            rob.leaveGroup(group12);
+            robGroups = rob.getGroups();
+            Assert.assertEquals(0, robGroups.size());
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+
+    // KEYCLOAK-2682
+    @Test
+    public void test04_groupReferencingNonExistentMember() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            // Ignoring this test on ActiveDirectory as it's not allowed to have LDAP group referencing nonexistent member. KEYCLOAK-2682 was related to OpenLDAP TODO: Better solution than programmatic...
+            LDAPConfig config = LDAPTestUtils.getLdapProvider(session, ldapModel).getLdapIdentityStore().getConfig();
+            if (config.isActiveDirectory()) {
+                return;
+            }
+
+            RealmModel appRealm = session.realms().getRealmByName("test");
+
+            ComponentModel mapperModel = LDAPTestUtils.getComponentByName(appRealm,ldapModel, "groupsMapper");
+            LDAPTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.MODE, LDAPGroupMapperMode.LDAP_ONLY.toString());
+            appRealm.updateComponent(mapperModel);
+
+            // 1 - Add some group to LDAP for testing
+            LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            GroupLDAPStorageMapper groupMapper = LDAPTestUtils.getGroupMapper(mapperModel, ldapProvider, appRealm);
+            LDAPObject group2 = LDAPTestUtils.createLDAPGroup(session, appRealm, ldapModel, "group2", descriptionAttrName, "group2 - description");
+
+            // 2 - Add one existing user rob to LDAP group
+            LDAPObject jamesLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "jameskeycloak");
+            LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, group2, jamesLdap, false);
+
+            // 3 - Add non-existing user to LDAP group
+            LDAPDn nonExistentDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn());
+            nonExistentDn.addFirst(jamesLdap.getRdnAttributeName(), "nonexistent");
+            LDAPObject nonExistentLdapUser = new LDAPObject();
+            nonExistentLdapUser.setDn(nonExistentDn);
+            LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, group2, nonExistentLdapUser, true);
+
+            // 4 - Check group members. Just existing user rob should be present
+            groupMapper.syncDataFromFederationProviderToKeycloak();
+            GroupModel kcGroup2 = KeycloakModelUtils.findGroupByPath(appRealm, "/group2");
+            List<UserModel> groupUsers = session.users().getGroupMembers(appRealm, kcGroup2, 0, 5);
+            Assert.assertEquals(1, groupUsers.size());
+            UserModel rob = groupUsers.get(0);
+            Assert.assertEquals("jameskeycloak", rob.getUsername());
+
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+    private void deleteGroupMappingsInLDAP(GroupLDAPStorageMapper groupMapper, LDAPObject ldapUser, String groupName) {
+        LDAPObject ldapGroup = groupMapper.loadLDAPGroupByName(groupName);
+        groupMapper.deleteGroupMappingInLDAP(ldapUser, ldapGroup);
+    }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPMultipleAttributesTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPMultipleAttributesTest.java
new file mode 100755
index 0000000..c054e08
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPMultipleAttributesTest.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.testsuite.federation.storage.ldap;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.FixMethodOrder;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runners.MethodSorters;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserFederationProvider;
+import org.keycloak.models.UserFederationProviderModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.LDAPRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.openqa.selenium.WebDriver;
+
+import javax.ws.rs.core.UriBuilder;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class LDAPMultipleAttributesTest {
+
+    protected String APP_SERVER_BASE_URL = "http://localhost:8081";
+    protected String LOGIN_URL = OIDCLoginProtocolService.authUrl(UriBuilder.fromUri(APP_SERVER_BASE_URL + "/auth")).build("test").toString();
+
+    private static LDAPRule ldapRule = new LDAPRule();
+
+    private static ComponentModel ldapModel = null;
+
+    private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            MultivaluedHashMap<String,String> ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule);
+            ldapConfig.putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.WRITABLE.toString());
+
+            UserStorageProviderModel model = new UserStorageProviderModel();
+            model.setLastSync(0);
+            model.setChangedSyncPeriod(-1);
+            model.setFullSyncPeriod(-1);
+            model.setName("test-ldap");
+            model.setPriority(0);
+            model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME);
+            model.setConfig(ldapConfig);
+            ldapModel = appRealm.addComponentModel(model);
+
+            LDAPTestUtils.addZipCodeLDAPMapper(appRealm, ldapModel);
+            LDAPTestUtils.addUserAttributeMapper(appRealm, ldapModel, "streetMapper", "street", LDAPConstants.STREET);
+
+            // Remove current users and add default users
+            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);
+
+            LDAPObject james = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "jbrown", "James", "Brown", "jbrown@keycloak.org", null, "88441");
+            LDAPTestUtils.updateLDAPPassword(ldapFedProvider, james, "Password1");
+
+            // User for testing duplicating surname and postalCode
+            LDAPObject bruce = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "bwilson", "Bruce", "Wilson", "bwilson@keycloak.org", "Elm 5", "88441", "77332");
+            bruce.setAttribute("sn", new LinkedHashSet<>(Arrays.asList("Wilson", "Schneider")));
+            ldapFedProvider.getLdapIdentityStore().update(bruce);
+            LDAPTestUtils.updateLDAPPassword(ldapFedProvider, bruce, "Password1");
+
+            // Create ldap-portal client
+            ClientModel ldapClient = KeycloakModelUtils.createClient(appRealm, "ldap-portal");
+            ldapClient.addRedirectUri("/ldap-portal");
+            ldapClient.addRedirectUri("/ldap-portal/*");
+            ldapClient.setManagementUrl("/ldap-portal");
+            ldapClient.addProtocolMapper(UserAttributeMapper.createClaimMapper("postalCode", "postal_code", "postal_code", "String", true, "", true, true, true));
+            ldapClient.addProtocolMapper(UserAttributeMapper.createClaimMapper("street", "street", "street", "String", true, "", true, true, false));
+            ldapClient.addScopeMapping(appRealm.getRole("user"));
+            ldapClient.setSecret("password");
+
+            // Deploy ldap-portal client
+            URL url = getClass().getResource("/ldap/ldap-app-keycloak.json");
+            keycloakRule.createApplicationDeployment()
+                    .name("ldap-portal").contextPath("/ldap-portal")
+                    .servletClass(LDAPExampleServlet.class).adapterConfigPath(url.getPath())
+                    .role("user").deployApplication();
+        }
+    });
+
+    @ClassRule
+    public static TestRule chain = RuleChain
+            .outerRule(ldapRule)
+            .around(keycloakRule);
+
+    @Rule
+    public WebRule webRule = new WebRule(this);
+
+    @WebResource
+    protected WebDriver driver;
+
+    @WebResource
+    protected OAuthClient oauth;
+
+    @WebResource
+    protected LoginPage loginPage;
+
+    @Test
+    public void testModel() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+
+            LDAPTestUtils.assertUserImported(session.users(), appRealm, "jbrown", "James", "Brown", "jbrown@keycloak.org", "88441");
+
+            UserModel user = session.users().getUserByUsername("bwilson", appRealm);
+            Assert.assertEquals("bwilson@keycloak.org", user.getEmail());
+            Assert.assertEquals("Bruce", user.getFirstName());
+
+            // There are 2 lastnames in ldif
+            Assert.assertTrue("Wilson".equals(user.getLastName()) || "Schneider".equals(user.getLastName()));
+
+            // Actually there are 2 postalCodes
+            List<String> postalCodes = user.getAttribute("postal_code");
+            assertPostalCodes(postalCodes, "88441", "77332");
+            List<String> tmp = new LinkedList<>();
+            tmp.addAll(postalCodes);
+            postalCodes = tmp;
+            postalCodes.remove("77332");
+            user.setAttribute("postal_code", postalCodes);
+
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+            UserModel user = session.users().getUserByUsername("bwilson", appRealm);
+            List<String> postalCodes = user.getAttribute("postal_code");
+            assertPostalCodes(postalCodes, "88441");
+            List<String> tmp = new LinkedList<>();
+            tmp.addAll(postalCodes);
+            postalCodes = tmp;
+            postalCodes.add("77332");
+            user.setAttribute("postal_code", postalCodes);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+            UserModel user = session.users().getUserByUsername("bwilson", appRealm);
+            assertPostalCodes(user.getAttribute("postal_code"), "88441", "77332");
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+    }
+
+    private void assertPostalCodes(List<String> postalCodes, String... expectedPostalCodes) {
+        if (expectedPostalCodes == null && postalCodes.isEmpty()) {
+            return;
+        }
+
+
+        Assert.assertEquals(expectedPostalCodes.length, postalCodes.size());
+        for (String expected : expectedPostalCodes) {
+            if (!postalCodes.contains(expected)) {
+                Assert.fail("postalCode '" + expected + "' not in postalCodes: " + postalCodes);
+            }
+        }
+    }
+
+    @Test
+    public void ldapPortalEndToEndTest() {
+        // Login as bwilson
+        driver.navigate().to(APP_SERVER_BASE_URL + "/ldap-portal");
+        Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+        loginPage.login("bwilson", "Password1");
+        Assert.assertTrue(driver.getCurrentUrl().startsWith(APP_SERVER_BASE_URL + "/ldap-portal"));
+        String pageSource = driver.getPageSource();
+        System.out.println(pageSource);
+        Assert.assertTrue(pageSource.contains("bwilson") && pageSource.contains("Bruce"));
+        Assert.assertTrue(pageSource.contains("street") && pageSource.contains("Elm 5"));
+        Assert.assertTrue(pageSource.contains("postal_code") && pageSource.contains("88441") && pageSource.contains("77332"));
+
+        // Logout
+        String logoutUri = OIDCLoginProtocolService.logoutUrl(UriBuilder.fromUri(APP_SERVER_BASE_URL + "/auth"))
+                .queryParam(OAuth2Constants.REDIRECT_URI, APP_SERVER_BASE_URL + "/ldap-portal").build("test").toString();
+        driver.navigate().to(logoutUri);
+
+        // Login as jbrown
+        driver.navigate().to(APP_SERVER_BASE_URL + "/ldap-portal");
+        Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+        loginPage.login("jbrown", "Password1");
+        Assert.assertTrue(driver.getCurrentUrl().startsWith(APP_SERVER_BASE_URL + "/ldap-portal"));
+        pageSource = driver.getPageSource();
+        System.out.println(pageSource);
+        Assert.assertTrue(pageSource.contains("jbrown") && pageSource.contains("James Brown"));
+        Assert.assertFalse(pageSource.contains("street"));
+        Assert.assertTrue(pageSource.contains("postal_code") && pageSource.contains("88441"));
+        Assert.assertFalse(pageSource.contains("77332"));
+
+        // Logout
+        driver.navigate().to(logoutUri);
+    }
+
+
+
+}
+
+
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPProvidersIntegrationTest.java
index 85f7812..13735e6 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPProvidersIntegrationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPProvidersIntegrationTest.java
@@ -79,16 +79,6 @@ public class LDAPProvidersIntegrationTest {
 
     private static ComponentModel ldapModel = null;
 
-    private static MultivaluedHashMap<String, String> getLdapRuleConfig() {
-        MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
-        Map<String,String> ldapConfig = ldapRule.getConfig();
-        for (Map.Entry<String, String> entry : ldapConfig.entrySet()) {
-            config.add(entry.getKey(), entry.getValue());
-
-        }
-        return config;
-
-    }
 
     private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
 
@@ -96,7 +86,7 @@ public class LDAPProvidersIntegrationTest {
         public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
             LDAPTestUtils.addLocalUser(manager.getSession(), appRealm, "marykeycloak", "mary@test.com", "password-app");
 
-            MultivaluedHashMap<String,String> ldapConfig = getLdapRuleConfig();
+            MultivaluedHashMap<String,String> ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule);
             ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "true");
             ldapConfig.putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.WRITABLE.toString());
             UserStorageProviderModel model = new UserStorageProviderModel();
@@ -441,7 +431,7 @@ public class LDAPProvidersIntegrationTest {
         }
     }
 
-    @Test
+    //@Test // don't think we should support this, bburke
     public void testDirectLDAPUpdate() {
         KeycloakSession session = keycloakRule.startSession();
 
@@ -728,6 +718,17 @@ public class LDAPProvidersIntegrationTest {
 
     @Test
     public void testRemoveFederatedUser() {
+        /*
+        {
+            KeycloakSession session = keycloakRule.startSession();
+            RealmModel appRealm = session.realms().getRealmByName("test");
+            UserModel user = session.users().getUserByUsername("registerUserSuccess2", appRealm);
+            keycloakRule.stopSession(session, true);
+            if (user == null) {
+                registerUserLdapSuccess();
+            }
+        }
+        */
         KeycloakSession session = keycloakRule.startSession();
         try {
             RealmModel appRealm = session.realms().getRealmByName("test");
@@ -756,26 +757,26 @@ public class LDAPProvidersIntegrationTest {
             LDAPTestUtils.addLDAPUser(ldapProvider, appRealm, "username4", "John4", "Doel4", "user4@email.org", null, "124");
 
             // Users are not at local store at this moment
-            Assert.assertNull(session.userStorage().getUserByUsername("username1", appRealm));
-            Assert.assertNull(session.userStorage().getUserByUsername("username2", appRealm));
-            Assert.assertNull(session.userStorage().getUserByUsername("username3", appRealm));
-            Assert.assertNull(session.userStorage().getUserByUsername("username4", appRealm));
+            Assert.assertNull(session.userLocalStorage().getUserByUsername("username1", appRealm));
+            Assert.assertNull(session.userLocalStorage().getUserByUsername("username2", appRealm));
+            Assert.assertNull(session.userLocalStorage().getUserByUsername("username3", appRealm));
+            Assert.assertNull(session.userLocalStorage().getUserByUsername("username4", appRealm));
 
             // search by username
             session.users().searchForUser("username1", appRealm);
-            LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username1", "John1", "Doel1", "user1@email.org", "121");
+            LDAPTestUtils.assertUserImported(session.userLocalStorage(), appRealm, "username1", "John1", "Doel1", "user1@email.org", "121");
 
             // search by email
             session.users().searchForUser("user2@email.org", appRealm);
-            LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username2", "John2", "Doel2", "user2@email.org", "122");
+            LDAPTestUtils.assertUserImported(session.userLocalStorage(), appRealm, "username2", "John2", "Doel2", "user2@email.org", "122");
 
             // search by lastName
             session.users().searchForUser("Doel3", appRealm);
-            LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username3", "John3", "Doel3", "user3@email.org", "123");
+            LDAPTestUtils.assertUserImported(session.userLocalStorage(), appRealm, "username3", "John3", "Doel3", "user3@email.org", "123");
 
             // search by firstName + lastName
             session.users().searchForUser("John4 Doel4", appRealm);
-            LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username4", "John4", "Doel4", "user4@email.org", "124");
+            LDAPTestUtils.assertUserImported(session.userLocalStorage(), appRealm, "username4", "John4", "Doel4", "user4@email.org", "124");
         } finally {
             keycloakRule.stopSession(session, true);
         }
@@ -803,15 +804,15 @@ public class LDAPProvidersIntegrationTest {
             LDAPTestUtils.addLDAPUser(ldapProvider, appRealm, "username7", "John7", "Doel7", "user7@email.org", null, "127");
 
             // search by email
-            session.users().searchForUser("user5@email.org", appRealm);
-            LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username5", "John5", "Doel5", "user5@email.org", "125");
+            List<UserModel> list = session.users().searchForUser("user5@email.org", appRealm);
+            LDAPTestUtils.assertUserImported(session.userLocalStorage(), appRealm, "username5", "John5", "Doel5", "user5@email.org", "125");
 
             session.users().searchForUser("John6 Doel6", appRealm);
-            LDAPTestUtils.assertUserImported(session.userStorage(), appRealm, "username6", "John6", "Doel6", "user6@email.org", "126");
+            LDAPTestUtils.assertUserImported(session.userLocalStorage(), appRealm, "username6", "John6", "Doel6", "user6@email.org", "126");
 
             session.users().searchForUser("user7@email.org", appRealm);
             session.users().searchForUser("John7 Doel7", appRealm);
-            Assert.assertNull(session.userStorage().getUserByUsername("username7", appRealm));
+            Assert.assertNull(session.userLocalStorage().getUserByUsername("username7", appRealm));
 
             // Remove custom filter
             ldapModel.getConfig().remove(LDAPConstants.CUSTOM_USER_SEARCH_FILTER);
@@ -850,7 +851,7 @@ public class LDAPProvidersIntegrationTest {
             Assert.assertTrue(session.users().removeUser(appRealm, user));
 
             // Assert user not available locally, but will be reimported from LDAP once searched
-            Assert.assertNull(session.userStorage().getUserByUsername("johnkeycloak", appRealm));
+            Assert.assertNull(session.userLocalStorage().getUserByUsername("johnkeycloak", appRealm));
             Assert.assertNotNull(session.users().getUserByUsername("johnkeycloak", appRealm));
         } finally {
             keycloakRule.stopSession(session, false);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPRoleMappingsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPRoleMappingsTest.java
new file mode 100644
index 0000000..19c2b6b
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPRoleMappingsTest.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.testsuite.federation.storage.ldap;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.FixMethodOrder;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runners.MethodSorters;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode;
+import org.keycloak.models.AccountRoles;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.storage.ldap.mappers.membership.role.RoleLDAPStorageMapper;
+import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.LDAPRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.openqa.selenium.WebDriver;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class LDAPRoleMappingsTest {
+
+    private static LDAPRule ldapRule = new LDAPRule();
+
+    private static ComponentModel ldapModel = null;
+
+    private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            LDAPTestUtils.addLocalUser(manager.getSession(), appRealm, "mary", "mary@test.com", "password-app");
+
+            MultivaluedHashMap<String,String> ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule);
+            ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "true");
+            ldapConfig.putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.WRITABLE.toString());
+            UserStorageProviderModel model = new UserStorageProviderModel();
+            model.setLastSync(0);
+            model.setChangedSyncPeriod(-1);
+            model.setFullSyncPeriod(-1);
+            model.setName("test-ldap");
+            model.setPriority(0);
+            model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME);
+            model.setConfig(ldapConfig);
+
+            ldapModel = appRealm.addComponentModel(model);
+
+            // Delete all LDAP users
+            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);
+
+            // Add sample application
+            ClientModel finance = appRealm.addClient("finance");
+
+            // Delete all LDAP roles
+            LDAPTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY);
+            LDAPTestUtils.removeAllLDAPRoles(manager.getSession(), appRealm, ldapModel, "realmRolesMapper");
+            LDAPTestUtils.removeAllLDAPRoles(manager.getSession(), appRealm, ldapModel, "financeRolesMapper");
+
+            // Add some users for testing
+            LDAPObject john = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
+            LDAPTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1");
+
+            LDAPObject mary = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary", "Kelly", "mary@email.org", null, "5678");
+            LDAPTestUtils.updateLDAPPassword(ldapFedProvider, mary, "Password1");
+
+            LDAPObject rob = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "robkeycloak", "Rob", "Brown", "rob@email.org", null, "8910");
+            LDAPTestUtils.updateLDAPPassword(ldapFedProvider, rob, "Password1");
+
+            // Add some roles for testing
+            LDAPTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "realmRolesMapper", "realmRole1");
+            LDAPTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "realmRolesMapper", "realmRole2");
+            LDAPTestUtils.createLDAPRole(manager.getSession(), appRealm, ldapModel, "financeRolesMapper", "financeRole1");
+
+            // Sync LDAP roles to Keycloak DB
+            LDAPTestUtils.syncRolesFromLDAP(appRealm, ldapFedProvider, ldapModel);
+        }
+    });
+
+    @ClassRule
+    public static TestRule chain = RuleChain
+            .outerRule(ldapRule)
+            .around(keycloakRule);
+
+    @Rule
+    public WebRule webRule = new WebRule(this);
+
+    @WebResource
+    protected OAuthClient oauth;
+
+    @WebResource
+    protected WebDriver driver;
+
+    @WebResource
+    protected AppPage appPage;
+
+    @WebResource
+    protected LoginPage loginPage;
+
+    @Test
+    public void test01_ldapOnlyRoleMappings() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+
+            LDAPTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, LDAPGroupMapperMode.LDAP_ONLY);
+
+            UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
+            UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm);
+
+            // 1 - Grant some roles in LDAP
+
+            // This role should already exists as it was imported from LDAP
+            RoleModel realmRole1 = appRealm.getRole("realmRole1");
+            john.grantRole(realmRole1);
+
+            // This role should already exists as it was imported from LDAP
+            RoleModel realmRole2 = appRealm.getRole("realmRole2");
+            mary.grantRole(realmRole2);
+
+            // This role may already exists from previous test (was imported from LDAP), but may not
+            RoleModel realmRole3 = appRealm.getRole("realmRole3");
+            if (realmRole3 == null) {
+                realmRole3 = appRealm.addRole("realmRole3");
+            }
+
+            john.grantRole(realmRole3);
+            mary.grantRole(realmRole3);
+
+            ClientModel accountApp = appRealm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
+            ClientModel financeApp = appRealm.getClientByClientId("finance");
+
+            RoleModel manageAccountRole = accountApp.getRole(AccountRoles.MANAGE_ACCOUNT);
+            RoleModel financeRole1 = financeApp.getRole("financeRole1");
+            john.grantRole(financeRole1);
+
+            // 2 - Check that role mappings are not in local Keycloak DB (They are in LDAP).
+
+            UserModel johnDb = session.userLocalStorage().getUserByUsername("johnkeycloak", appRealm);
+            Set<RoleModel> johnDbRoles = johnDb.getRoleMappings();
+            Assert.assertFalse(johnDbRoles.contains(realmRole1));
+            Assert.assertFalse(johnDbRoles.contains(realmRole2));
+            Assert.assertFalse(johnDbRoles.contains(realmRole3));
+            Assert.assertFalse(johnDbRoles.contains(financeRole1));
+            Assert.assertTrue(johnDbRoles.contains(manageAccountRole));
+
+            // 3 - Check that role mappings are in LDAP and hence available through federation
+
+            Set<RoleModel> johnRoles = john.getRoleMappings();
+            Assert.assertTrue(johnRoles.contains(realmRole1));
+            Assert.assertFalse(johnRoles.contains(realmRole2));
+            Assert.assertTrue(johnRoles.contains(realmRole3));
+            Assert.assertTrue(johnRoles.contains(financeRole1));
+            Assert.assertTrue(johnRoles.contains(manageAccountRole));
+
+            Set<RoleModel> johnRealmRoles = john.getRealmRoleMappings();
+            Assert.assertEquals(2, johnRealmRoles.size());
+            Assert.assertTrue(johnRealmRoles.contains(realmRole1));
+            Assert.assertTrue(johnRealmRoles.contains(realmRole3));
+
+            // account roles are not mapped in LDAP. Those are in Keycloak DB
+            Set<RoleModel> johnAccountRoles = john.getClientRoleMappings(accountApp);
+            Assert.assertTrue(johnAccountRoles.contains(manageAccountRole));
+
+            Set<RoleModel> johnFinanceRoles = john.getClientRoleMappings(financeApp);
+            Assert.assertEquals(1, johnFinanceRoles.size());
+            Assert.assertTrue(johnFinanceRoles.contains(financeRole1));
+
+            // 4 - Delete some role mappings and check they are deleted
+
+            john.deleteRoleMapping(realmRole3);
+            john.deleteRoleMapping(realmRole1);
+            john.deleteRoleMapping(financeRole1);
+            john.deleteRoleMapping(manageAccountRole);
+
+            johnRoles = john.getRoleMappings();
+            Assert.assertFalse(johnRoles.contains(realmRole1));
+            Assert.assertFalse(johnRoles.contains(realmRole2));
+            Assert.assertFalse(johnRoles.contains(realmRole3));
+            Assert.assertFalse(johnRoles.contains(financeRole1));
+            Assert.assertFalse(johnRoles.contains(manageAccountRole));
+
+            // Cleanup
+            mary.deleteRoleMapping(realmRole2);
+            mary.deleteRoleMapping(realmRole3);
+            john.grantRole(manageAccountRole);
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+    @Test
+    public void test02_readOnlyRoleMappings() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+
+            LDAPTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, LDAPGroupMapperMode.READ_ONLY);
+
+            UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm);
+
+            RoleModel realmRole1 = appRealm.getRole("realmRole1");
+            RoleModel realmRole2 = appRealm.getRole("realmRole2");
+            RoleModel realmRole3 = appRealm.getRole("realmRole3");
+            if (realmRole3 == null) {
+                realmRole3 = appRealm.addRole("realmRole3");
+            }
+
+            // Add some role mappings directly into LDAP
+            ComponentModel roleMapperModel = LDAPTestUtils.getComponentByName(appRealm, ldapModel, "realmRolesMapper");
+            LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            RoleLDAPStorageMapper roleMapper = LDAPTestUtils.getRoleMapper(roleMapperModel, ldapProvider, appRealm);
+
+            LDAPObject maryLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "marykeycloak");
+            roleMapper.addRoleMappingInLDAP("realmRole1", maryLdap);
+            roleMapper.addRoleMappingInLDAP("realmRole2", maryLdap);
+
+            // Add some role to model
+            mary.grantRole(realmRole3);
+
+            // Assert that mary has both LDAP and DB mapped roles
+            Set<RoleModel> maryRoles = mary.getRealmRoleMappings();
+            Assert.assertTrue(maryRoles.contains(realmRole1));
+            Assert.assertTrue(maryRoles.contains(realmRole2));
+            Assert.assertTrue(maryRoles.contains(realmRole3));
+
+            // Assert that access through DB will have just DB mapped role
+            UserModel maryDB = session.userLocalStorage().getUserByUsername("marykeycloak", appRealm);
+            Set<RoleModel> maryDBRoles = maryDB.getRealmRoleMappings();
+            Assert.assertFalse(maryDBRoles.contains(realmRole1));
+            Assert.assertFalse(maryDBRoles.contains(realmRole2));
+            Assert.assertTrue(maryDBRoles.contains(realmRole3));
+
+            mary.deleteRoleMapping(realmRole3);
+            try {
+                mary.deleteRoleMapping(realmRole1);
+                Assert.fail("It wasn't expected to successfully delete LDAP role mappings in READ_ONLY mode");
+            } catch (ModelException expected) {
+            }
+
+            // Delete role mappings directly in LDAP
+            deleteRoleMappingsInLDAP(roleMapper, maryLdap, "realmRole1");
+            deleteRoleMappingsInLDAP(roleMapper, maryLdap, "realmRole2");
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+            UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm);
+
+            // Assert role mappings is not available
+            Set<RoleModel> maryRoles = mary.getRealmRoleMappings();
+            Assert.assertFalse(maryRoles.contains(appRealm.getRole("realmRole1")));
+            Assert.assertFalse(maryRoles.contains(appRealm.getRole("realmRole2")));
+            Assert.assertFalse(maryRoles.contains(appRealm.getRole("realmRole3")));
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+    @Test
+    public void test03_importRoleMappings() {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+
+            LDAPTestUtils.addOrUpdateRoleLDAPMappers(appRealm, ldapModel, LDAPGroupMapperMode.IMPORT);
+
+            // Add some role mappings directly in LDAP
+            ComponentModel roleMapperModel = LDAPTestUtils.getComponentByName(appRealm, ldapModel, "realmRolesMapper");
+            LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            RoleLDAPStorageMapper roleMapper = LDAPTestUtils.getRoleMapper(roleMapperModel, ldapProvider, appRealm);
+
+            LDAPObject robLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "robkeycloak");
+            roleMapper.addRoleMappingInLDAP("realmRole1", robLdap);
+            roleMapper.addRoleMappingInLDAP("realmRole2", robLdap);
+
+            // Get user and check that he has requested roles from LDAP
+            UserModel rob = session.users().getUserByUsername("robkeycloak", appRealm);
+            RoleModel realmRole1 = appRealm.getRole("realmRole1");
+            RoleModel realmRole2 = appRealm.getRole("realmRole2");
+            RoleModel realmRole3 = appRealm.getRole("realmRole3");
+            if (realmRole3 == null) {
+                realmRole3 = appRealm.addRole("realmRole3");
+            }
+            Set<RoleModel> robRoles = rob.getRealmRoleMappings();
+            Assert.assertTrue(robRoles.contains(realmRole1));
+            Assert.assertTrue(robRoles.contains(realmRole2));
+            Assert.assertFalse(robRoles.contains(realmRole3));
+
+            // Add some role mappings in model and check that user has it
+            rob.grantRole(realmRole3);
+            robRoles = rob.getRealmRoleMappings();
+            Assert.assertTrue(robRoles.contains(realmRole3));
+
+            // Delete some role mappings in LDAP and check that it doesn't have any effect and user still has role
+            deleteRoleMappingsInLDAP(roleMapper, robLdap, "realmRole1");
+            deleteRoleMappingsInLDAP(roleMapper, robLdap, "realmRole2");
+            robRoles = rob.getRealmRoleMappings();
+            Assert.assertTrue(robRoles.contains(realmRole1));
+            Assert.assertTrue(robRoles.contains(realmRole2));
+
+            // Delete role mappings through model and verifies that user doesn't have them anymore
+            rob.deleteRoleMapping(realmRole1);
+            rob.deleteRoleMapping(realmRole2);
+            rob.deleteRoleMapping(realmRole3);
+            robRoles = rob.getRealmRoleMappings();
+            Assert.assertFalse(robRoles.contains(realmRole1));
+            Assert.assertFalse(robRoles.contains(realmRole2));
+            Assert.assertFalse(robRoles.contains(realmRole3));
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+    private void deleteRoleMappingsInLDAP(RoleLDAPStorageMapper roleMapper, LDAPObject ldapUser, String roleName) {
+        LDAPObject ldapRole1 = roleMapper.loadLDAPRoleByName(roleName);
+        roleMapper.deleteRoleMappingInLDAP(ldapUser, ldapRole1);
+    }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPSyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPSyncTest.java
new file mode 100755
index 0000000..c1d55c1
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPSyncTest.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.testsuite.federation.storage.ldap;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
+import org.junit.runners.MethodSorters;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.common.util.Time;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.services.managers.UserStorageSyncManager;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserProvider;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
+import org.keycloak.storage.user.SynchronizationResult;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.LDAPRule;
+
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class LDAPSyncTest {
+
+    private static LDAPRule ldapRule = new LDAPRule();
+
+    private static UserStorageProviderModel ldapModel = null;
+
+    private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            // Other tests may left Time offset uncleared, which could cause issues
+            Time.setOffset(0);
+
+            MultivaluedHashMap<String,String> ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule);
+            ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "false");
+            ldapConfig.putSingle(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.WRITABLE.toString());
+            UserStorageProviderModel model = new UserStorageProviderModel();
+            model.setLastSync(0);
+            model.setChangedSyncPeriod(-1);
+            model.setFullSyncPeriod(-1);
+            model.setName("test-ldap");
+            model.setPriority(0);
+            model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME);
+            model.setConfig(ldapConfig);
+
+            ldapModel = new UserStorageProviderModel(appRealm.addComponentModel(model));
+
+
+            LDAPTestUtils.addZipCodeLDAPMapper(appRealm, ldapModel);
+
+            // Delete all LDAP users and add 5 new users for testing
+            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);
+
+            for (int i=1 ; i<=5 ; i++) {
+                LDAPObject ldapUser = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "user" + i, "User" + i + "FN", "User" + i + "LN", "user" + i + "@email.org", null, "12" + i);
+                LDAPTestUtils.updateLDAPPassword(ldapFedProvider, ldapUser, "Password1");
+            }
+        }
+    });
+
+    @ClassRule
+    public static TestRule chain = RuleChain
+            .outerRule(ldapRule)
+            .around(keycloakRule);
+
+//    @Test
+//    public void test01runit() throws Exception {
+//        Thread.sleep(10000000);
+//    }
+
+    @Test
+    public void test01LDAPSync() {
+        UserStorageSyncManager usersSyncManager = new UserStorageSyncManager();
+
+        // wait a bit
+        sleep(ldapRule.getSleepTime());
+
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+            SynchronizationResult syncResult = usersSyncManager.syncAllUsers(sessionFactory, "test", ldapModel);
+            LDAPTestUtils.assertSyncEquals(syncResult, 5, 0, 0, 0);
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel testRealm = session.realms().getRealm("test");
+            UserProvider userProvider = session.userLocalStorage();
+            // Assert users imported
+            LDAPTestUtils.assertUserImported(userProvider, testRealm, "user1", "User1FN", "User1LN", "user1@email.org", "121");
+            LDAPTestUtils.assertUserImported(userProvider, testRealm, "user2", "User2FN", "User2LN", "user2@email.org", "122");
+            LDAPTestUtils.assertUserImported(userProvider, testRealm, "user3", "User3FN", "User3LN", "user3@email.org", "123");
+            LDAPTestUtils.assertUserImported(userProvider, testRealm, "user4", "User4FN", "User4LN", "user4@email.org", "124");
+            LDAPTestUtils.assertUserImported(userProvider, testRealm, "user5", "User5FN", "User5LN", "user5@email.org", "125");
+
+            // Assert lastSync time updated
+            Assert.assertTrue(ldapModel.getLastSync() > 0);
+            for (UserStorageProviderModel persistentFedModel : testRealm.getUserStorageProviders()) {
+                if (LDAPStorageProviderFactory.PROVIDER_NAME.equals(persistentFedModel.getProviderId())) {
+                    Assert.assertTrue(persistentFedModel.getLastSync() > 0);
+                } else {
+                    // Dummy provider has still 0
+                    Assert.assertEquals(0, persistentFedModel.getLastSync());
+                }
+            }
+
+            // wait a bit
+            sleep(ldapRule.getSleepTime());
+
+            // Add user to LDAP and update 'user5' in LDAP
+            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            LDAPTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user6", "User6FN", "User6LN", "user6@email.org", null, "126");
+            LDAPObject ldapUser5 = ldapFedProvider.loadLDAPUserByUsername(testRealm, "user5");
+            // NOTE: Changing LDAP attributes directly here
+            ldapUser5.setSingleAttribute(LDAPConstants.EMAIL, "user5Updated@email.org");
+            ldapUser5.setSingleAttribute(LDAPConstants.POSTAL_CODE, "521");
+            ldapFedProvider.getLdapIdentityStore().update(ldapUser5);
+
+            // Assert still old users in local provider
+            LDAPTestUtils.assertUserImported(userProvider, testRealm, "user5", "User5FN", "User5LN", "user5@email.org", "125");
+            Assert.assertNull(userProvider.getUserByUsername("user6", testRealm));
+
+            // Trigger partial sync
+            KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+            SynchronizationResult syncResult = usersSyncManager.syncChangedUsers(sessionFactory, "test", ldapModel);
+            LDAPTestUtils.assertSyncEquals(syncResult, 1, 1, 0, 0);
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel testRealm = session.realms().getRealm("test");
+            UserProvider userProvider = session.userLocalStorage();
+            // Assert users updated in local provider
+            LDAPTestUtils.assertUserImported(userProvider, testRealm, "user5", "User5FN", "User5LN", "user5updated@email.org", "521");
+            LDAPTestUtils.assertUserImported(userProvider, testRealm, "user6", "User6FN", "User6LN", "user6@email.org", "126");
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+    @Test
+    public void test02duplicateUsernameAndEmailSync() {
+        LDAPObject duplicatedLdapUser;
+
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel testRealm = session.realms().getRealm("test");
+
+            LDAPTestUtils.addLocalUser(session, testRealm, "user7", "user7@email.org", "password");
+            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+
+            // Add user to LDAP with duplicated username "user7"
+            duplicatedLdapUser = LDAPTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user7", "User7FN", "User7LN", "user7-something@email.org", null, "126");
+
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel testRealm = session.realms().getRealm("test");
+
+            // Assert syncing from LDAP fails due to duplicated username
+            SynchronizationResult result = new UserStorageSyncManager().syncAllUsers(session.getKeycloakSessionFactory(), "test", ldapModel);
+            Assert.assertEquals(1, result.getFailed());
+
+            // Remove "user7" from LDAP
+            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            ldapFedProvider.getLdapIdentityStore().remove(duplicatedLdapUser);
+
+            // Add user to LDAP with duplicated email "user7@email.org"
+            duplicatedLdapUser = LDAPTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user7-something", "User7FNN", "User7LNL", "user7@email.org", null, "126");
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel testRealm = session.realms().getRealm("test");
+
+            // Assert syncing from LDAP fails due to duplicated email
+            SynchronizationResult result = new UserStorageSyncManager().syncAllUsers(session.getKeycloakSessionFactory(), "test", ldapModel);
+            Assert.assertEquals(1, result.getFailed());
+            Assert.assertNull(session.userLocalStorage().getUserByUsername("user7-something", testRealm));
+
+            // Update LDAP user to avoid duplicated email
+            duplicatedLdapUser.setSingleAttribute(LDAPConstants.EMAIL, "user7-changed@email.org");
+            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            ldapFedProvider.getLdapIdentityStore().update(duplicatedLdapUser);
+
+            // Assert user successfully synced now
+            result = new UserStorageSyncManager().syncAllUsers(session.getKeycloakSessionFactory(), "test", ldapModel);
+            Assert.assertEquals(0, result.getFailed());
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        // Assert user imported in another transaction
+        session = keycloakRule.startSession();
+        try {
+            RealmModel testRealm = session.realms().getRealm("test");
+            LDAPTestUtils.assertUserImported(session.userLocalStorage(), testRealm, "user7-something", "User7FNN", "User7LNL", "user7-changed@email.org", "126");
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+    // KEYCLOAK-1571
+    @Test
+    public void test03SameUUIDAndUsernameSync() {
+        KeycloakSession session = keycloakRule.startSession();
+        String origUuidAttrName;
+
+        try {
+            RealmModel testRealm = session.realms().getRealm("test");
+
+            // Remove all users from model
+            for (UserModel user : session.userLocalStorage().getUsers(testRealm, true)) {
+                session.userLocalStorage().removeUser(testRealm, user);
+            }
+
+            UserStorageProviderModel providerModel = KeycloakModelUtils.findUserStorageProviderByName(ldapModel.getName(), testRealm);
+
+            // Change name of UUID attribute to same like usernameAttribute
+            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            String uidAttrName = ldapFedProvider.getLdapIdentityStore().getConfig().getUsernameLdapAttribute();
+            origUuidAttrName = providerModel.getConfig().getFirst(LDAPConstants.UUID_LDAP_ATTRIBUTE);
+            providerModel.getConfig().putSingle(LDAPConstants.UUID_LDAP_ATTRIBUTE, uidAttrName);
+
+            // Need to change this due to ApacheDS pagination bug (For other LDAP servers, pagination works fine) TODO: Remove once ApacheDS upgraded and pagination is fixed
+            providerModel.getConfig().putSingle(LDAPConstants.BATCH_SIZE_FOR_SYNC, "10");
+            testRealm.updateComponent(providerModel);
+
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel testRealm = session.realms().getRealm("test");
+            UserStorageProviderModel providerModel = KeycloakModelUtils.findUserStorageProviderByName(ldapModel.getName(), testRealm);
+
+            KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+            SynchronizationResult syncResult = new UserStorageSyncManager().syncAllUsers(sessionFactory, "test", providerModel);
+            Assert.assertEquals(0, syncResult.getFailed());
+
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel testRealm = session.realms().getRealm("test");
+
+            // Assert users imported with correct LDAP_ID
+            LDAPTestUtils.assertUserImported(session.users(), testRealm, "user1", "User1FN", "User1LN", "user1@email.org", "121");
+            LDAPTestUtils.assertUserImported(session.users(), testRealm, "user2", "User2FN", "User2LN", "user2@email.org", "122");
+            UserModel user1 = session.users().getUserByUsername("user1", testRealm);
+            Assert.assertEquals("user1", user1.getFirstAttribute(LDAPConstants.LDAP_ID));
+
+            // Revert config changes
+            UserStorageProviderModel providerModel = KeycloakModelUtils.findUserStorageProviderByName(ldapModel.getName(), testRealm);
+            providerModel.getConfig().putSingle(LDAPConstants.UUID_LDAP_ATTRIBUTE, origUuidAttrName);
+            testRealm.updateComponent(providerModel);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+    }
+
+    // KEYCLOAK-1728
+    @Test
+    public void test04MissingLDAPUsernameSync() {
+        KeycloakSession session = keycloakRule.startSession();
+        String origUsernameAttrName;
+
+        try {
+            RealmModel testRealm = session.realms().getRealm("test");
+
+            // Remove all users from model
+            for (UserModel user : session.userLocalStorage().getUsers(testRealm, true)) {
+                System.out.println("trying to delete user: " + user.getUsername());
+                session.getUserCache().evict(testRealm, user);
+                session.userLocalStorage().removeUser(testRealm, user);
+            }
+
+            UserStorageProviderModel providerModel = KeycloakModelUtils.findUserStorageProviderByName(ldapModel.getName(), testRealm);
+
+            // Add street mapper and add some user including street
+            ComponentModel streetMapper = LDAPTestUtils.addUserAttributeMapper(testRealm, ldapModel, "streetMapper", "street", LDAPConstants.STREET);
+            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            LDAPObject streetUser = LDAPTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user8", "User8FN", "User8LN", "user8@email.org", "user8street", "126");
+
+            // Change name of username attribute name to street
+            origUsernameAttrName = providerModel.getConfig().getFirst(LDAPConstants.USERNAME_LDAP_ATTRIBUTE);
+            providerModel.getConfig().putSingle(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, "street");
+
+            // Need to change this due to ApacheDS pagination bug (For other LDAP servers, pagination works fine) TODO: Remove once ApacheDS upgraded and pagination is fixed
+            providerModel.getConfig().putSingle(LDAPConstants.BATCH_SIZE_FOR_SYNC, "10");
+            testRealm.updateComponent(providerModel);
+
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        // Just user8 synced. All others failed to sync
+        session = keycloakRule.startSession();
+        try {
+            RealmModel testRealm = session.realms().getRealm("test");
+            UserStorageProviderModel providerModel = KeycloakModelUtils.findUserStorageProviderByName(ldapModel.getName(), testRealm);
+
+            KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+            SynchronizationResult syncResult = new UserStorageSyncManager().syncAllUsers(sessionFactory, "test", providerModel);
+            Assert.assertEquals(1, syncResult.getAdded());
+            Assert.assertTrue(syncResult.getFailed() > 0);
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel testRealm = session.realms().getRealm("test");
+
+            // Revert config changes
+            UserStorageProviderModel providerModel = KeycloakModelUtils.findUserStorageProviderByName(ldapModel.getName(), testRealm);
+            providerModel.getConfig().putSingle(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, origUsernameAttrName);
+            testRealm.updateComponent(providerModel);
+            ComponentModel streetMapper = LDAPTestUtils.getComponentByName(testRealm, providerModel, "streetMapper");
+            testRealm.removeComponent(streetMapper);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+    }
+
+    private void sleep(int time) {
+        try {
+            Thread.sleep(time);
+        } catch (InterruptedException ie) {
+            throw new RuntimeException(ie);
+        }
+    }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPTestUtils.java
index f4bf182..4b612f2 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPTestUtils.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPTestUtils.java
@@ -18,6 +18,7 @@
 package org.keycloak.testsuite.federation.storage.ldap;
 
 import org.junit.Assert;
+import org.keycloak.common.util.MultivaluedHashMap;
 import org.keycloak.component.ComponentModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.LDAPConstants;
@@ -46,6 +47,7 @@ import org.keycloak.storage.ldap.mappers.membership.role.RoleLDAPStorageMapper;
 import org.keycloak.storage.ldap.mappers.membership.role.RoleLDAPStorageMapperFactory;
 import org.keycloak.storage.ldap.mappers.membership.role.RoleMapperConfig;
 import org.keycloak.storage.user.SynchronizationResult;
+import org.keycloak.testsuite.rule.LDAPRule;
 
 import java.util.Arrays;
 import java.util.Collections;
@@ -58,9 +60,20 @@ import java.util.Set;
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 public class LDAPTestUtils {
+    public static MultivaluedHashMap<String, String> getLdapRuleConfig(LDAPRule ldapRule) {
+        MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
+        Map<String,String> ldapConfig = ldapRule.getConfig();
+        for (Map.Entry<String, String> entry : ldapConfig.entrySet()) {
+            config.add(entry.getKey(), entry.getValue());
+
+        }
+        return config;
+
+    }
+
 
     public static UserModel addLocalUser(KeycloakSession session, RealmModel realm, String username, String email, String password) {
-        UserModel user = session.userStorage().addUser(realm, username);
+        UserModel user = session.userLocalStorage().addUser(realm, username);
         user.setEmail(email);
         user.setEnabled(true);