keycloak-aplcache

Changes

Details

diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPQueryConditionsBuilder.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPQueryConditionsBuilder.java
index 0ba9a39..4be4ba4 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPQueryConditionsBuilder.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPQueryConditionsBuilder.java
@@ -61,9 +61,6 @@ public class LDAPQueryConditionsBuilder {
 
     public Condition addCustomLDAPFilter(String filter) {
         filter = filter.trim();
-        if (!filter.startsWith("(") || !filter.endsWith(")")) {
-            throw new ModelException("Custom filter doesn't start with ( or doesn't end with ). ");
-        }
         return new CustomLDAPFilter(filter);
     }
 
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java
index 15af133..eb8eaaa 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java
@@ -131,7 +131,12 @@ public class LDAPConfig {
 
     public boolean isPagination() {
         String pagination = config.get(LDAPConstants.PAGINATION);
-        return pagination==null ? false : Boolean.parseBoolean(pagination);
+        return Boolean.parseBoolean(pagination);
+    }
+
+    public int getBatchSizeForSync() {
+        String pageSizeConfig = config.get(LDAPConstants.BATCH_SIZE_FOR_SYNC);
+        return pageSizeConfig!=null ? Integer.parseInt(pageSizeConfig) : LDAPConstants.DEFAULT_BATCH_SIZE_FOR_SYNC;
     }
 
     public String getUsernameLdapAttribute() {
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
index bfc7fc2..51a3c8c 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
@@ -34,6 +34,7 @@ import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
 import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper;
 import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory;
 import org.keycloak.federation.ldap.mappers.msad.MSADUserAccountControlMapperFactory;
+import org.keycloak.mappers.FederationConfigValidationException;
 import org.keycloak.mappers.UserFederationMapper;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
@@ -46,6 +47,7 @@ import org.keycloak.models.UserFederationMapperModel;
 import org.keycloak.models.UserFederationProvider;
 import org.keycloak.models.UserFederationProviderModel;
 import org.keycloak.models.UserFederationSyncResult;
+import org.keycloak.models.UserFederationValidatingProviderFactory;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 
@@ -59,7 +61,7 @@ import java.util.Set;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class LDAPFederationProviderFactory extends UserFederationEventAwareProviderFactory {
+public class LDAPFederationProviderFactory extends UserFederationEventAwareProviderFactory implements UserFederationValidatingProviderFactory {
     private static final Logger logger = Logger.getLogger(LDAPFederationProviderFactory.class);
     public static final String PROVIDER_NAME = LDAPConstants.LDAP_PROVIDER;
 
@@ -77,6 +79,13 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
     }
 
     @Override
+    public void validateConfig(RealmModel realm, UserFederationProviderModel providerModel) throws FederationConfigValidationException {
+        LDAPConfig cfg = new LDAPConfig(providerModel.getConfig());
+        String customFilter = cfg.getCustomUserSearchFilter();
+        LDAPUtils.validateCustomLdapFilter(customFilter);
+    }
+
+    @Override
     public void init(Config.Scope config) {
         this.ldapStoreRegistry = new LDAPIdentityStoreRegistry();
     }
@@ -156,7 +165,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
                     // For read-only LDAP, we map "cn" as full name
                     mapperModel = KeycloakModelUtils.createUserFederationMapperModel("full name", newProviderModel.getId(), FullNameLDAPFederationMapperFactory.PROVIDER_ID,
                             FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, LDAPConstants.CN,
-                            UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
+                            FullNameLDAPFederationMapper.READ_ONLY, readOnly,
+                            FullNameLDAPFederationMapper.WRITE_ONLY, "false");
                     realm.addUserFederationMapper(mapperModel);
                 }
             }
@@ -274,11 +284,10 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
 
         final UserFederationSyncResult syncResult = new UserFederationSyncResult();
 
-        boolean pagination = Boolean.parseBoolean(fedModel.getConfig().get(LDAPConstants.PAGINATION));
+        LDAPConfig ldapConfig = new LDAPConfig(fedModel.getConfig());
+        boolean pagination = ldapConfig.isPagination();
         if (pagination) {
-
-            String pageSizeConfig = fedModel.getConfig().get(LDAPConstants.BATCH_SIZE_FOR_SYNC);
-            int pageSize = pageSizeConfig!=null ? Integer.parseInt(pageSizeConfig) : LDAPConstants.DEFAULT_BATCH_SIZE_FOR_SYNC;
+            int pageSize = ldapConfig.getBatchSizeForSync();
 
             boolean nextPage = true;
             while (nextPage) {
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java
index 4d64e2c..b075d19 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java
@@ -31,6 +31,7 @@ import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilde
 import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
 import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
 import org.keycloak.federation.ldap.mappers.membership.MembershipType;
+import org.keycloak.mappers.FederationConfigValidationException;
 import org.keycloak.models.LDAPConstants;
 import org.keycloak.models.ModelException;
 import org.keycloak.models.RealmModel;
@@ -226,4 +227,19 @@ public class LDAPUtils {
     public static String getMemberValueOfChildObject(LDAPObject ldapUser, MembershipType membershipType) {
         return membershipType == MembershipType.DN ? ldapUser.getDn().toString() : ldapUser.getAttributeAsString(ldapUser.getRdnAttributeName());
     }
+
+
+    public static void validateCustomLdapFilter(String customFilter) throws FederationConfigValidationException {
+        if (customFilter != null) {
+
+            customFilter = customFilter.trim();
+            if (customFilter.isEmpty()) {
+                return;
+            }
+
+            if (!customFilter.startsWith("(") || !customFilter.endsWith(")")) {
+                throw new FederationConfigValidationException("ldapErrorInvalidCustomFilter");
+            }
+        }
+    }
 }
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java
index 933d614..d681125 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java
@@ -20,7 +20,7 @@ package org.keycloak.federation.ldap.mappers;
 import org.keycloak.Config;
 import org.keycloak.federation.ldap.LDAPFederationProvider;
 import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
-import org.keycloak.mappers.MapperConfigValidationException;
+import org.keycloak.mappers.FederationConfigValidationException;
 import org.keycloak.mappers.UserFederationMapper;
 import org.keycloak.mappers.UserFederationMapperFactory;
 import org.keycloak.models.KeycloakSession;
@@ -85,10 +85,10 @@ public abstract class AbstractLDAPFederationMapperFactory implements UserFederat
         return configProperty;
     }
 
-    protected void checkMandatoryConfigAttribute(String name, String displayName, UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
+    protected void checkMandatoryConfigAttribute(String name, String displayName, UserFederationMapperModel mapperModel) throws FederationConfigValidationException {
         String attrConfigValue = mapperModel.getConfig().get(name);
         if (attrConfigValue == null || attrConfigValue.trim().isEmpty()) {
-            throw new MapperConfigValidationException("Missing configuration for '" + displayName + "'");
+            throw new FederationConfigValidationException("Missing configuration for '" + displayName + "'");
         }
     }
 
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java
index 113a57d..b94b5b4 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java
@@ -40,6 +40,8 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
 
     public static final String LDAP_FULL_NAME_ATTRIBUTE = "ldap.full.name.attribute";
     public static final String READ_ONLY = "read.only";
+    public static final String WRITE_ONLY = "write.only";
+
 
     public FullNameLDAPFederationMapper(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) {
         super(mapperModel, ldapProvider, realm);
@@ -47,6 +49,10 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
 
     @Override
     public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, boolean isCreate) {
+        if (isWriteOnly()) {
+            return;
+        }
+
         String ldapFullNameAttrName = getLdapFullNameAttrName();
         String fullName = ldapUser.getAttributeAsString(ldapFullNameAttrName);
         if (fullName == null) {
@@ -117,6 +123,10 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
 
     @Override
     public void beforeLDAPQuery(LDAPQuery query) {
+        if (isWriteOnly()) {
+            return;
+        }
+
         String ldapFullNameAttrName = getLdapFullNameAttrName();
         query.addReturningLdapAttribute(ldapFullNameAttrName);
 
@@ -178,4 +188,8 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
     private boolean isReadOnly() {
         return parseBooleanParameter(mapperModel, READ_ONLY);
     }
+
+    private boolean isWriteOnly() {
+        return parseBooleanParameter(mapperModel, WRITE_ONLY);
+    }
 }
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java
index 32b0acd..32826b2 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java
@@ -24,7 +24,7 @@ import java.util.Map;
 
 import org.keycloak.federation.ldap.LDAPConfig;
 import org.keycloak.federation.ldap.LDAPFederationProvider;
-import org.keycloak.mappers.MapperConfigValidationException;
+import org.keycloak.mappers.FederationConfigValidationException;
 import org.keycloak.models.LDAPConstants;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserFederationMapperModel;
@@ -43,12 +43,17 @@ public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationM
 
     static {
         ProviderConfigProperty userModelAttribute = createConfigProperty(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, "LDAP Full Name Attribute",
-                "Name of LDAP attribute, which contains fullName of user. In most cases it will be 'cn' ", ProviderConfigProperty.STRING_TYPE, null);
+                "Name of LDAP attribute, which contains fullName of user. Usually it will be 'cn' ", ProviderConfigProperty.STRING_TYPE, null);
         configProperties.add(userModelAttribute);
 
-        ProviderConfigProperty readOnly = createConfigProperty(UserAttributeLDAPFederationMapper.READ_ONLY, "Read Only",
+        ProviderConfigProperty readOnly = createConfigProperty(FullNameLDAPFederationMapper.READ_ONLY, "Read Only",
                 "For Read-only is data imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, null);
         configProperties.add(readOnly);
+
+        ProviderConfigProperty writeOnly = createConfigProperty(FullNameLDAPFederationMapper.WRITE_ONLY, "Write Only",
+                "For Write-only is data propagated to LDAP when user is created or updated in Keycloak. But this mapper is not used to propagate data from LDAP back into Keycloak. " +
+                        "This setting is useful if you configured separate firstName and lastName attribute mappers and you want to use those to read attribute from LDAP into Keycloak", ProviderConfigProperty.BOOLEAN_TYPE, null);
+        configProperties.add(writeOnly);
     }
 
     @Override
@@ -78,8 +83,11 @@ public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationM
 
         defaultValues.put(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, LDAPConstants.CN);
 
-        String readOnly = config.getEditMode() == UserFederationProvider.EditMode.WRITABLE ? "false" : "true";
-        defaultValues.put(UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
+        boolean readOnly = config.getEditMode() != UserFederationProvider.EditMode.WRITABLE;
+        defaultValues.put(FullNameLDAPFederationMapper.READ_ONLY, String.valueOf(readOnly));
+
+        String writeOnly = String.valueOf(!readOnly);
+        defaultValues.put(FullNameLDAPFederationMapper.WRITE_ONLY, writeOnly);
 
         return defaultValues;
     }
@@ -90,8 +98,21 @@ public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationM
     }
 
     @Override
-    public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
+    public void validateConfig(RealmModel realm, UserFederationProviderModel fedProviderModel, UserFederationMapperModel mapperModel) throws FederationConfigValidationException {
         checkMandatoryConfigAttribute(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, "LDAP Full Name Attribute", mapperModel);
+
+        boolean readOnly = AbstractLDAPFederationMapper.parseBooleanParameter(mapperModel, FullNameLDAPFederationMapper.READ_ONLY);
+        boolean writeOnly = AbstractLDAPFederationMapper.parseBooleanParameter(mapperModel, FullNameLDAPFederationMapper.WRITE_ONLY);
+
+        LDAPConfig cfg = new LDAPConfig(fedProviderModel.getConfig());
+        UserFederationProvider.EditMode editMode = cfg.getEditMode();
+
+        if (writeOnly && cfg.getEditMode() != UserFederationProvider.EditMode.WRITABLE) {
+            throw new FederationConfigValidationException("ldapErrorCantWriteOnlyForReadOnlyLdap");
+        }
+        if (writeOnly && readOnly) {
+            throw new FederationConfigValidationException("ldapErrorCantWriteOnlyAndReadOnly");
+        }
     }
 
     @Override
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/HardcodedLDAPRoleMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/HardcodedLDAPRoleMapperFactory.java
index ae07c0f..1ca93f5 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/HardcodedLDAPRoleMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/HardcodedLDAPRoleMapperFactory.java
@@ -23,7 +23,7 @@ import java.util.List;
 import java.util.Map;
 
 import org.keycloak.federation.ldap.LDAPFederationProvider;
-import org.keycloak.mappers.MapperConfigValidationException;
+import org.keycloak.mappers.FederationConfigValidationException;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserFederationMapperModel;
@@ -77,14 +77,14 @@ public class HardcodedLDAPRoleMapperFactory extends AbstractLDAPFederationMapper
     }
 
     @Override
-    public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
+    public void validateConfig(RealmModel realm, UserFederationProviderModel fedProviderModel, UserFederationMapperModel mapperModel) throws FederationConfigValidationException {
         String roleName = mapperModel.getConfig().get(HardcodedLDAPRoleMapper.ROLE);
         if (roleName == null) {
-            throw new MapperConfigValidationException("Role can't be null");
+            throw new FederationConfigValidationException("Role can't be null");
         }
         RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
         if (role == null) {
-            throw new MapperConfigValidationException("There is no role corresponding to configured value");
+            throw new FederationConfigValidationException("There is no role corresponding to configured value");
         }
     }
 
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapper.java
index 2c1c048..00108bc 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapper.java
@@ -27,6 +27,7 @@ import java.util.Map;
 import java.util.Set;
 
 import org.jboss.logging.Logger;
+import org.keycloak.federation.ldap.LDAPConfig;
 import org.keycloak.federation.ldap.LDAPFederationProvider;
 import org.keycloak.federation.ldap.LDAPUtils;
 import org.keycloak.federation.ldap.idm.model.LDAPDn;
@@ -41,9 +42,11 @@ import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode;
 import org.keycloak.federation.ldap.mappers.membership.MembershipType;
 import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy;
 import org.keycloak.models.GroupModel;
+import org.keycloak.models.LDAPConstants;
 import org.keycloak.models.ModelException;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserFederationMapperModel;
+import org.keycloak.models.UserFederationProviderModel;
 import org.keycloak.models.UserFederationSyncResult;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
@@ -149,8 +152,7 @@ public class GroupLDAPFederationMapper extends AbstractLDAPFederationMapper impl
         logger.debugf("Syncing groups from LDAP into Keycloak DB. Mapper is [%s], LDAP provider is [%s]", mapperModel.getName(), ldapProvider.getModel().getDisplayName());
 
         // Get all LDAP groups
-        LDAPQuery ldapQuery = createGroupQuery();
-        List<LDAPObject> ldapGroups = ldapQuery.getResultList();
+        List<LDAPObject> ldapGroups = getAllLDAPGroups();
 
         // Convert to internal format
         Map<String, LDAPObject> ldapGroupsMap = new HashMap<>();
@@ -286,29 +288,46 @@ public class GroupLDAPFederationMapper extends AbstractLDAPFederationMapper impl
         }
     }
 
-    // Override if better effectivity or different algorithm is needed
+
     protected GroupModel findKcGroupByLDAPGroup(LDAPObject ldapGroup) {
         String groupNameAttr = config.getGroupNameLdapAttribute();
         String groupName = ldapGroup.getAttributeAsString(groupNameAttr);
 
-        List<GroupModel> groups = realm.getGroups();
-        for (GroupModel group : groups) {
-            if (group.getName().equals(groupName)) {
-                return group;
+        if (config.isPreserveGroupsInheritance()) {
+            // Override if better effectivity or different algorithm is needed
+            List<GroupModel> groups = realm.getGroups();
+            for (GroupModel group : groups) {
+                if (group.getName().equals(groupName)) {
+                    return group;
+                }
             }
-        }
 
-        return null;
+            return null;
+        } else {
+            // Without preserved inheritance, it's always top-level group
+            return KeycloakModelUtils.findGroupByPath(realm, "/" + groupName);
+        }
     }
 
     protected GroupModel findKcGroupOrSyncFromLDAP(LDAPObject ldapGroup, UserModel user) {
         GroupModel kcGroup = findKcGroupByLDAPGroup(ldapGroup);
 
         if (kcGroup == null) {
-            // Sync groups from LDAP
-            if (!syncFromLDAPPerformedInThisTransaction) {
-                syncDataFromFederationProviderToKeycloak();
-                kcGroup = findKcGroupByLDAPGroup(ldapGroup);
+
+            if (config.isPreserveGroupsInheritance()) {
+
+                // Better to sync all groups from LDAP with preserved inheritance
+                if (!syncFromLDAPPerformedInThisTransaction) {
+                    syncDataFromFederationProviderToKeycloak();
+                    kcGroup = findKcGroupByLDAPGroup(ldapGroup);
+                }
+            } else {
+                String groupNameAttr = config.getGroupNameLdapAttribute();
+                String groupName = ldapGroup.getAttributeAsString(groupNameAttr);
+
+                kcGroup = realm.createGroup(groupName);
+                updateAttributesOfKCGroup(kcGroup, ldapGroup);
+                realm.moveGroup(kcGroup, null);
             }
 
             // Could theoretically happen on some LDAP servers if 'memberof' style is used and 'memberof' attribute of user references non-existing group
@@ -321,6 +340,33 @@ public class GroupLDAPFederationMapper extends AbstractLDAPFederationMapper impl
         return kcGroup;
     }
 
+    // Send LDAP query to retrieve all groups
+    protected List<LDAPObject> getAllLDAPGroups() {
+        LDAPQuery ldapGroupQuery = createGroupQuery();
+
+        LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
+        boolean pagination = ldapConfig.isPagination();
+        if (pagination) {
+            // For now reuse globally configured batch size in LDAP provider page
+            int pageSize = ldapConfig.getBatchSizeForSync();
+
+            List<LDAPObject> result = new LinkedList<>();
+            boolean nextPage = true;
+
+            while (nextPage) {
+                ldapGroupQuery.setLimit(pageSize);
+                final List<LDAPObject> currentPageGroups = ldapGroupQuery.getResultList();
+                result.addAll(currentPageGroups);
+                nextPage = ldapGroupQuery.getPaginationContext() != null;
+            }
+
+            return result;
+        } else {
+            // LDAP pagination not available. Do everything in single transaction
+            return ldapGroupQuery.getResultList();
+        }
+    }
+
 
     // Sync from Keycloak to LDAP
 
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java
index 0304788..fb6056b 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java
@@ -26,6 +26,7 @@ import java.util.Map;
 
 import org.keycloak.federation.ldap.LDAPConfig;
 import org.keycloak.federation.ldap.LDAPFederationProvider;
+import org.keycloak.federation.ldap.LDAPUtils;
 import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
 import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapperFactory;
 import org.keycloak.federation.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
@@ -33,7 +34,7 @@ import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode;
 import org.keycloak.federation.ldap.mappers.membership.MembershipType;
 import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy;
 import org.keycloak.federation.ldap.mappers.membership.role.RoleMapperConfig;
-import org.keycloak.mappers.MapperConfigValidationException;
+import org.keycloak.mappers.FederationConfigValidationException;
 import org.keycloak.models.LDAPConstants;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserFederationMapperModel;
@@ -185,7 +186,7 @@ public class GroupLDAPFederationMapperFactory extends AbstractLDAPFederationMapp
     }
 
     @Override
-    public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
+    public void validateConfig(RealmModel realm, UserFederationProviderModel fedProviderModel, UserFederationMapperModel mapperModel) throws FederationConfigValidationException {
         checkMandatoryConfigAttribute(GroupMapperConfig.GROUPS_DN, "LDAP Groups DN", mapperModel);
         checkMandatoryConfigAttribute(GroupMapperConfig.MODE, "Mode", mapperModel);
 
@@ -193,8 +194,10 @@ public class GroupLDAPFederationMapperFactory extends AbstractLDAPFederationMapp
         MembershipType membershipType = mt==null ? MembershipType.DN : Enum.valueOf(MembershipType.class, mt);
         boolean preserveGroupInheritance = Boolean.parseBoolean(mapperModel.getConfig().get(GroupMapperConfig.PRESERVE_GROUP_INHERITANCE));
         if (preserveGroupInheritance && membershipType != MembershipType.DN) {
-            throw new MapperConfigValidationException("Not possible to preserve group inheritance and use UID membership type together");
+            throw new FederationConfigValidationException("ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType");
         }
+
+        LDAPUtils.validateCustomLdapFilter(mapperModel.getConfig().get(GroupMapperConfig.GROUPS_LDAP_FILTER));
     }
 
     @Override
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapperFactory.java
index ee5140b..25a7ad1 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapperFactory.java
@@ -26,12 +26,14 @@ import java.util.Map;
 
 import org.keycloak.federation.ldap.LDAPConfig;
 import org.keycloak.federation.ldap.LDAPFederationProvider;
+import org.keycloak.federation.ldap.LDAPUtils;
 import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
 import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapperFactory;
 import org.keycloak.federation.ldap.mappers.membership.LDAPGroupMapperMode;
 import org.keycloak.federation.ldap.mappers.membership.MembershipType;
 import org.keycloak.federation.ldap.mappers.membership.UserRolesRetrieveStrategy;
-import org.keycloak.mappers.MapperConfigValidationException;
+import org.keycloak.federation.ldap.mappers.membership.group.GroupMapperConfig;
+import org.keycloak.mappers.FederationConfigValidationException;
 import org.keycloak.models.LDAPConstants;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserFederationMapperModel;
@@ -178,7 +180,7 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe
     }
 
     @Override
-    public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
+    public void validateConfig(RealmModel realm, UserFederationProviderModel fedProviderModel, UserFederationMapperModel mapperModel) throws FederationConfigValidationException {
         checkMandatoryConfigAttribute(RoleMapperConfig.ROLES_DN, "LDAP Roles DN", mapperModel);
         checkMandatoryConfigAttribute(RoleMapperConfig.MODE, "Mode", mapperModel);
 
@@ -187,14 +189,11 @@ public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMappe
         if (!useRealmMappings) {
             String clientId = mapperModel.getConfig().get(RoleMapperConfig.CLIENT_ID);
             if (clientId == null || clientId.trim().isEmpty()) {
-                throw new MapperConfigValidationException("Client ID needs to be provided in config when Realm Roles Mapping is not used");
+                throw new FederationConfigValidationException("ldapErrorMissingClientId");
             }
         }
 
-        String customLdapFilter = mapperModel.getConfig().get(RoleMapperConfig.ROLES_LDAP_FILTER);
-        if ((customLdapFilter != null && customLdapFilter.trim().length() > 0) && (!customLdapFilter.startsWith("(") || !customLdapFilter.endsWith(")"))) {
-            throw new MapperConfigValidationException("Custom Roles LDAP filter must starts with '(' and ends with ')'");
-        }
+        LDAPUtils.validateCustomLdapFilter(mapperModel.getConfig().get(RoleMapperConfig.ROLES_LDAP_FILTER));
     }
 
     @Override
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapperFactory.java
index 2ccc96e..36c494d 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/msad/MSADUserAccountControlMapperFactory.java
@@ -25,7 +25,7 @@ import java.util.Map;
 import org.keycloak.federation.ldap.LDAPFederationProvider;
 import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapper;
 import org.keycloak.federation.ldap.mappers.AbstractLDAPFederationMapperFactory;
-import org.keycloak.mappers.MapperConfigValidationException;
+import org.keycloak.mappers.FederationConfigValidationException;
 import org.keycloak.models.LDAPConstants;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserFederationMapperModel;
@@ -75,7 +75,7 @@ public class MSADUserAccountControlMapperFactory extends AbstractLDAPFederationM
     }
 
     @Override
-    public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
+    public void validateConfig(RealmModel realm, UserFederationProviderModel fedProviderModel, UserFederationMapperModel mapperModel) throws FederationConfigValidationException {
     }
 
     @Override
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java
index 6a85623..b0a7faa 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java
@@ -24,7 +24,7 @@ import java.util.Map;
 
 import org.keycloak.federation.ldap.LDAPConfig;
 import org.keycloak.federation.ldap.LDAPFederationProvider;
-import org.keycloak.mappers.MapperConfigValidationException;
+import org.keycloak.mappers.FederationConfigValidationException;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserFederationMapperModel;
 import org.keycloak.models.UserFederationProvider;
@@ -101,7 +101,7 @@ public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFedera
     }
 
     @Override
-    public void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
+    public void validateConfig(RealmModel realm, UserFederationProviderModel fedProviderModel, UserFederationMapperModel mapperModel) throws FederationConfigValidationException {
         checkMandatoryConfigAttribute(UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute", mapperModel);
         checkMandatoryConfigAttribute(UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, "LDAP Attribute", mapperModel);
     }
diff --git a/server-spi/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java b/server-spi/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java
index 80eec6a..661462c 100644
--- a/server-spi/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java
+++ b/server-spi/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java
@@ -52,10 +52,12 @@ public interface UserFederationMapperFactory extends ProviderFactory<UserFederat
     /**
      * Called when instance of mapperModel is created for this factory through admin endpoint
      *
+     * @param realm
+     * @param fedProviderModel
      * @param mapperModel
-     * @throws MapperConfigValidationException if configuration provided in mapperModel is not valid
+     * @throws FederationConfigValidationException if configuration provided in mapperModel is not valid
      */
-    void validateConfig(RealmModel realm, UserFederationMapperModel mapperModel) throws MapperConfigValidationException;
+    void validateConfig(RealmModel realm, UserFederationProviderModel fedProviderModel, UserFederationMapperModel mapperModel) throws FederationConfigValidationException;
 
     /**
      * Used to detect what are default values for ProviderConfigProperties specified during mapper creation
diff --git a/server-spi/src/main/java/org/keycloak/models/UserFederationValidatingProviderFactory.java b/server-spi/src/main/java/org/keycloak/models/UserFederationValidatingProviderFactory.java
new file mode 100644
index 0000000..9624690
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/models/UserFederationValidatingProviderFactory.java
@@ -0,0 +1,37 @@
+/*
+ * 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.models;
+
+import org.keycloak.mappers.FederationConfigValidationException;
+
+/**
+ * TODO: Merge with UserFederationProviderFactory and add "default" method validateConfig with empty body once we move to source level 1.8
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface UserFederationValidatingProviderFactory extends UserFederationProviderFactory {
+
+    /**
+     * Called when instance of mapperModel is created for this factory through admin endpoint
+     *
+     * @param realm
+     * @param providerModel
+     * @throws FederationConfigValidationException if configuration provided in mapperModel is not valid
+     */
+    void validateConfig(RealmModel realm, UserFederationProviderModel providerModel) throws FederationConfigValidationException;
+}
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 f3606c9..f75bf11 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
@@ -411,8 +411,12 @@ public final class KeycloakModelUtils {
         return mapperModel;
     }
 
+    public static UserFederationProviderFactory getFederationProviderFactory(KeycloakSession session, UserFederationProviderModel model) {
+        return (UserFederationProviderFactory)session.getKeycloakSessionFactory().getProviderFactory(UserFederationProvider.class, model.getProviderName());
+    }
+
     public static UserFederationProvider getFederationProviderInstance(KeycloakSession session, UserFederationProviderModel model) {
-        UserFederationProviderFactory factory = (UserFederationProviderFactory)session.getKeycloakSessionFactory().getProviderFactory(UserFederationProvider.class, model.getProviderName());
+        UserFederationProviderFactory factory = getFederationProviderFactory(session, model);
         return factory.getInstance(session, model);
 
     }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java
index 0eb1748..29b4ae6 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java
@@ -16,12 +16,14 @@
  */
 package org.keycloak.services.resources.admin;
 
+import java.text.MessageFormat;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Properties;
 
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
@@ -40,7 +42,7 @@ import javax.ws.rs.core.UriInfo;
 import org.jboss.resteasy.annotations.cache.NoCache;
 import org.jboss.resteasy.spi.NotFoundException;
 import org.keycloak.events.admin.OperationType;
-import org.keycloak.mappers.MapperConfigValidationException;
+import org.keycloak.mappers.FederationConfigValidationException;
 import org.keycloak.mappers.UserFederationMapper;
 import org.keycloak.mappers.UserFederationMapperFactory;
 import org.keycloak.models.KeycloakSession;
@@ -63,7 +65,6 @@ import org.keycloak.representations.idm.UserFederationProviderRepresentation;
 import org.keycloak.services.ErrorResponseException;
 import org.keycloak.services.ServicesLogger;
 import org.keycloak.services.managers.UsersSyncManager;
-import org.keycloak.timer.TimerProvider;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -105,6 +106,9 @@ public class UserFederationProviderResource {
         }
         UserFederationProviderModel model = new UserFederationProviderModel(rep.getId(), rep.getProviderName(), rep.getConfig(), rep.getPriority(), displayName,
                 rep.getFullSyncPeriod(), rep.getChangedSyncPeriod(), rep.getLastSync());
+
+        UserFederationProvidersResource.validateFederationProviderConfig(session, auth, realm, model);
+
         realm.updateUserFederationProvider(model);
         new UsersSyncManager().notifyToRefreshPeriodicSync(session, realm, model, false);
         boolean kerberosCredsAdded = UserFederationProvidersResource.checkKerberosCredential(session, realm, model);
@@ -369,9 +373,12 @@ public class UserFederationProviderResource {
     private void validateModel(UserFederationMapperModel model) {
         try {
             UserFederationMapperFactory mapperFactory = (UserFederationMapperFactory) session.getKeycloakSessionFactory().getProviderFactory(UserFederationMapper.class, model.getFederationMapperType());
-            mapperFactory.validateConfig(realm, model);
-        } catch (MapperConfigValidationException ex) {
-            throw new ErrorResponseException("Validation error", ex.getMessage(), Response.Status.BAD_REQUEST);
+            mapperFactory.validateConfig(realm, federationProviderModel, model);
+        } catch (FederationConfigValidationException ex) {
+            logger.error(ex.getMessage());
+            Properties messages = AdminRoot.getMessages(session, realm, auth.getAuth().getToken().getLocale());
+            throw new ErrorResponseException(ex.getMessage(), MessageFormat.format(messages.getProperty(ex.getMessage(), ex.getMessage()), ex.getParameters()),
+                    Response.Status.BAD_REQUEST);
         }
     }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java
index 995cda4..9c9ac51 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProvidersResource.java
@@ -21,11 +21,13 @@ import org.jboss.resteasy.spi.NotFoundException;
 import org.jboss.resteasy.spi.ResteasyProviderFactory;
 import org.keycloak.common.constants.KerberosConstants;
 import org.keycloak.events.admin.OperationType;
+import org.keycloak.mappers.FederationConfigValidationException;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserFederationProvider;
 import org.keycloak.models.UserFederationProviderFactory;
 import org.keycloak.models.UserFederationProviderModel;
+import org.keycloak.models.UserFederationValidatingProviderFactory;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.provider.ConfiguredProvider;
@@ -35,6 +37,8 @@ import org.keycloak.representations.idm.ConfigPropertyRepresentation;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.representations.idm.UserFederationProviderFactoryRepresentation;
 import org.keycloak.representations.idm.UserFederationProviderRepresentation;
+import org.keycloak.services.ErrorResponse;
+import org.keycloak.services.ErrorResponseException;
 import org.keycloak.services.ServicesLogger;
 import org.keycloak.services.managers.UsersSyncManager;
 import org.keycloak.timer.TimerProvider;
@@ -51,9 +55,11 @@ import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 
+import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Properties;
 
 /**
  * Base resource for managing users
@@ -101,6 +107,20 @@ public class UserFederationProvidersResource {
         return false;
     }
 
+    public static void validateFederationProviderConfig(KeycloakSession session, RealmAuth auth, RealmModel realm, UserFederationProviderModel model) {
+        UserFederationProviderFactory providerFactory = KeycloakModelUtils.getFederationProviderFactory(session, model);
+        if (providerFactory instanceof UserFederationValidatingProviderFactory) {
+            try {
+                ((UserFederationValidatingProviderFactory) providerFactory).validateConfig(realm, model);
+            } catch (FederationConfigValidationException fcve) {
+                logger.error(fcve.getMessage());
+                Properties messages = AdminRoot.getMessages(session, realm, auth.getAuth().getToken().getLocale());
+                throw new ErrorResponseException(fcve.getMessage(), MessageFormat.format(messages.getProperty(fcve.getMessage(), fcve.getMessage()), fcve.getParameters()),
+                        Response.Status.BAD_REQUEST);
+            }
+        }
+    }
+
     /**
      * Get available provider factories
      *
@@ -176,6 +196,10 @@ public class UserFederationProvidersResource {
         if (displayName != null && displayName.trim().equals("")) {
             displayName = null;
         }
+
+        UserFederationProviderModel tempModel = new UserFederationProviderModel(null, rep.getProviderName(), rep.getConfig(), rep.getPriority(), displayName, rep.getFullSyncPeriod(), rep.getChangedSyncPeriod(), rep.getLastSync());
+        validateFederationProviderConfig(session, auth, realm, tempModel);
+
         UserFederationProviderModel model = realm.addUserFederationProvider(rep.getProviderName(), rep.getConfig(), rep.getPriority(), displayName,
                 rep.getFullSyncPeriod(), rep.getChangedSyncPeriod(), rep.getLastSync());
         new UsersSyncManager().notifyToRefreshPeriodicSync(session, realm, model, false);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java
index 273a125..3ee6b70 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/FederationProvidersIntegrationTest.java
@@ -29,6 +29,7 @@ import org.keycloak.OAuth2Constants;
 import org.keycloak.federation.ldap.LDAPConfig;
 import org.keycloak.federation.ldap.LDAPFederationProvider;
 import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
+import org.keycloak.federation.ldap.LDAPUtils;
 import org.keycloak.federation.ldap.idm.model.LDAPObject;
 import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapper;
 import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory;
@@ -521,7 +522,7 @@ public class FederationProvidersIntegrationTest {
 
             UserFederationMapperModel fullNameMapperModel = KeycloakModelUtils.createUserFederationMapperModel("full name", ldapModel.getId(), FullNameLDAPFederationMapperFactory.PROVIDER_ID,
                     FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, ldapFirstNameAttributeName,
-                    UserAttributeLDAPFederationMapper.READ_ONLY, "false");
+                    FullNameLDAPFederationMapper.READ_ONLY, "false");
             appRealm.addUserFederationMapper(fullNameMapperModel);
         } finally {
             keycloakRule.stopSession(session, true);
@@ -534,6 +535,36 @@ public class FederationProvidersIntegrationTest {
             // Assert user is successfully imported in Keycloak DB now with correct firstName and lastName
             FederationTestUtils.assertUserImported(session.users(), appRealm, "fullname", "James", "Dee", "fullname@email.org", "4578");
 
+            // change mapper to writeOnly
+            UserFederationMapperModel fullNameMapperModel = appRealm.getUserFederationMapperByName(ldapModel.getId(), "full name");
+            fullNameMapperModel.getConfig().put(FullNameLDAPFederationMapper.WRITE_ONLY, "true");
+            appRealm.updateUserFederationMapper(fullNameMapperModel);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+
+        // Assert changing user in Keycloak will change him in LDAP too...
+        session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = new RealmManager(session).getRealmByName("test");
+
+            UserModel fullnameUser = session.users().getUserByUsername("fullname", appRealm);
+            fullnameUser.setFirstName("James2");
+            fullnameUser.setLastName("Dee2");
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+
+        // Assert changed user available in Keycloak
+        session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = new RealmManager(session).getRealmByName("test");
+
+            // Assert user is successfully imported in Keycloak DB now with correct firstName and lastName
+            FederationTestUtils.assertUserImported(session.users(), appRealm, "fullname", "James2", "Dee2", "fullname@email.org", "4578");
+
             // Remove "fullnameUser" to assert he is removed from LDAP. Revert mappers to previous state
             UserModel fullnameUser = session.users().getUserByUsername("fullname", appRealm);
             session.users().removeUser(appRealm, fullnameUser);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapper2WaySyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapper2WaySyncTest.java
index 45ad9e7..493996a 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapper2WaySyncTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapper2WaySyncTest.java
@@ -61,6 +61,7 @@ public class LDAPGroupMapper2WaySyncTest {
             Map<String,String> ldapConfig = ldapRule.getConfig();
             ldapConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "true");
             ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString());
+            ldapConfig.put(LDAPConstants.BATCH_SIZE_FOR_SYNC, "4"); // Issues with pagination on ApacheDS
 
             ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap", -1, -1, 0);
             LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperSyncTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperSyncTest.java
index d218f26..9308e04 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperSyncTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/base/LDAPGroupMapperSyncTest.java
@@ -19,6 +19,7 @@ package org.keycloak.testsuite.federation.ldap.base;
 
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.junit.Assert;
 import org.junit.Before;
@@ -46,6 +47,7 @@ import org.keycloak.models.UserFederationMapperModel;
 import org.keycloak.models.UserFederationProvider;
 import org.keycloak.models.UserFederationProviderModel;
 import org.keycloak.models.UserFederationSyncResult;
+import org.keycloak.models.UserModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.testsuite.federation.ldap.FederationTestUtils;
@@ -258,4 +260,53 @@ public class LDAPGroupMapperSyncTest {
         }
     }
 
+
+
+    @Test
+    public void test04_syncNoPreserveGroupInheritanceWithLazySync() throws Exception {
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel realm = session.realms().getRealmByName("test");
+            UserFederationMapperModel mapperModel = realm.getUserFederationMapperByName(ldapModel.getId(), "groupsMapper");
+            LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
+            GroupLDAPFederationMapper groupMapper = FederationTestUtils.getGroupMapper(mapperModel, ldapProvider, realm);
+
+            // Update group mapper to skip preserve inheritance
+            FederationTestUtils.updateGroupMapperConfigOptions(mapperModel, GroupMapperConfig.PRESERVE_GROUP_INHERITANCE, "false");
+            realm.updateUserFederationMapper(mapperModel);
+
+            // Add user to LDAP and put him as member of group11
+            FederationTestUtils.removeAllLDAPUsers(ldapProvider, realm);
+            LDAPObject johnLdap = FederationTestUtils.addLDAPUser(ldapProvider, realm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
+            FederationTestUtils.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-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/LdapUserProviderForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/LdapUserProviderForm.java
index 5a34641..3503880 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/LdapUserProviderForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/LdapUserProviderForm.java
@@ -49,6 +49,9 @@ public class LdapUserProviderForm extends Form {
     @FindBy(id = "ldapBindCredential")
     private WebElement ldapBindCredentialInput;
 
+    @FindBy(id = "customUserSearchFilter")
+    private WebElement customUserSearchFilterInput;
+
     @FindBy(id = "searchScope")
     private Select searchScopeSelect;
 
@@ -155,6 +158,10 @@ public class LdapUserProviderForm extends Form {
         setInputValue(ldapBindCredentialInput, ldapBindCredential);
     }
 
+    public void setCustomUserSearchFilter(String customUserSearchFilter) {
+        setInputValue(customUserSearchFilterInput, customUserSearchFilter);
+    }
+
     public void setKerberosRealmInput(String kerberosRealm) {
         setInputValue(kerberosRealmInput, kerberosRealm);
     }
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java
index a127e41..a4eb399 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java
@@ -105,8 +105,25 @@ public class LdapUserFederationTest extends AbstractConsoleTest {
         createLdapUserProvider.form().save();
         assertAlertDanger();
         createLdapUserProvider.form().setLdapBindCredentialInput("secret");
+
+        createLdapUserProvider.form().setCustomUserSearchFilter("foo");
+        createLdapUserProvider.form().save();
+        assertAlertDanger();
+        createLdapUserProvider.form().setCustomUserSearchFilter("");
         createLdapUserProvider.form().save();
         assertAlertSuccess();
+
+        // Try updating invalid Custom LDAP Filter
+        createLdapUserProvider.form().setCustomUserSearchFilter("(foo=bar");
+        createLdapUserProvider.form().save();
+        assertAlertDanger();
+        createLdapUserProvider.form().setCustomUserSearchFilter("foo=bar)");
+        createLdapUserProvider.form().save();
+        assertAlertDanger();
+        createLdapUserProvider.form().setCustomUserSearchFilter("(foo=bar)");
+        createLdapUserProvider.form().save();
+        assertAlertSuccess();
+
     }
 
     @Test
diff --git a/themes/src/main/resources/theme/base/admin/messages/messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/messages_en.properties
index ab297dd..95e16db 100644
--- a/themes/src/main/resources/theme/base/admin/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/messages_en.properties
@@ -5,4 +5,10 @@ invalidPasswordMinUpperCaseCharsMessage=Invalid password: must contain at least 
 invalidPasswordMinSpecialCharsMessage=Invalid password: must contain at least {0} special characters.
 invalidPasswordNotUsernameMessage=Invalid password: must not be equal to the username.
 invalidPasswordRegexPatternMessage=Invalid password: fails to match regex pattern(s).
-invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords.
\ No newline at end of file
+invalidPasswordHistoryMessage=Invalid password: must not be equal to any of last {0} passwords.
+
+ldapErrorInvalidCustomFilter=Custom configured LDAP filter does not start with "(" or does not end with ")".
+ldapErrorMissingClientId=Client ID needs to be provided in config when Realm Roles Mapping is not used.
+ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType=Not possible to preserve group inheritance and use UID membership type together.
+ldapErrorCantWriteOnlyForReadOnlyLdap=Can't set write only when LDAP provider mode is not WRITABLE
+ldapErrorCantWriteOnlyAndReadOnly=Can't set write-only and read-only together
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index eb896fa..4ca5bf8 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -705,6 +705,10 @@ module.controller('GenericUserFederationCtrl', function($scope, $location, Notif
 
                 $location.url("/realms/" + realm.realm + "/user-federation/providers/" + $scope.instance.providerName + "/" + id);
                 Notifications.success("The provider has been created.");
+            }, function (errorResponse) {
+                if (errorResponse.data && errorResponse.data['error_description']) {
+                    Notifications.error(errorResponse.data['error_description']);
+                }
             });
         } else {
             UserFederationInstances.update({realm: realm.realm,
@@ -713,6 +717,10 @@ module.controller('GenericUserFederationCtrl', function($scope, $location, Notif
                 $scope.instance,  function () {
                     $route.reload();
                     Notifications.success("The provider has been updated.");
+                }, function (errorResponse) {
+                    if (errorResponse.data && errorResponse.data['error_description']) {
+                        Notifications.error(errorResponse.data['error_description']);
+                    }
                 });
         }
     };
@@ -909,6 +917,10 @@ module.controller('LDAPCtrl', function($scope, $location, $route, Notifications,
 
                 $location.url("/realms/" + realm.realm + "/user-federation/providers/" + $scope.instance.providerName + "/" + id);
                 Notifications.success("The provider has been created.");
+            }, function (errorResponse) {
+                if (errorResponse.data && errorResponse.data['error_description']) {
+                    Notifications.error(errorResponse.data['error_description']);
+                }
             });
         } else {
             UserFederationInstances.update({realm: realm.realm,
@@ -917,8 +929,11 @@ module.controller('LDAPCtrl', function($scope, $location, $route, Notifications,
                 $scope.instance,  function () {
                 $route.reload();
                 Notifications.success("The provider has been updated.");
+            }, function (errorResponse) {
+                if (errorResponse.data && errorResponse.data['error_description']) {
+                    Notifications.error(errorResponse.data['error_description']);
+                }
             });
-
         }
     };
 
@@ -1041,7 +1056,7 @@ module.controller('UserFederationMapperCtrl', function($scope, realm,  provider,
             Notifications.success("Your changes have been saved.");
         }, function(error) {
             if (error.status == 400 && error.data.error_description) {
-                Notifications.error('Error in configuration of mapper: ' + error.data.error_description);
+                Notifications.error(error.data.error_description);
             } else {
                 Notifications.error('Unexpected error when creating mapper');
             }
@@ -1113,7 +1128,7 @@ module.controller('UserFederationMapperCreateCtrl', function($scope, realm, prov
             Notifications.success("Mapper has been created.");
         }, function(error) {
             if (error.status == 400 && error.data.error_description) {
-                Notifications.error('Error in configuration of mapper: ' + error.data.error_description);
+                Notifications.error(error.data.error_description);
             } else {
                 Notifications.error('Unexpected error when creating mapper');
             }