keycloak-uncached

Merge pull request #3501 from patriot1burke/master LDAP

11/14/2016 8:24:39 PM

Changes

core/src/main/java/org/keycloak/representations/idm/StorageProviderRepresentation.java 90(+0 -90)

federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java 428(+0 -428)

federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java 122(+0 -122)

federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/group/GroupLDAPFederationMapperFactory.java 212(+0 -212)

federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/membership/role/RoleLDAPFederationMapperFactory.java 206(+0 -206)

federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java 113(+0 -113)

federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory 23(+0 -23)

services/pom.xml 5(+5 -0)

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/federation/AbstractKerberosLdapAdapterTest.java 129(+0 -129)

Details

diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java
index 7e3d6e7..ed6c495 100644
--- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java
+++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java
@@ -18,6 +18,7 @@
 package org.keycloak.federation.kerberos;
 
 import org.keycloak.common.constants.KerberosConstants;
+import org.keycloak.component.ComponentModel;
 import org.keycloak.models.UserFederationProviderModel;
 
 import java.util.Map;
@@ -29,31 +30,41 @@ import java.util.Map;
  */
 public abstract class CommonKerberosConfig {
 
-    private final UserFederationProviderModel providerModel;
+    protected UserFederationProviderModel providerModel;
+    protected ComponentModel componentModel;
 
     public CommonKerberosConfig(UserFederationProviderModel userFederationProvider) {
         this.providerModel = userFederationProvider;
     }
 
+    public CommonKerberosConfig(ComponentModel componentModel) {
+        this.componentModel = componentModel;
+    }
+
     // Should be always true for KerberosFederationProvider
     public boolean isAllowKerberosAuthentication() {
-        return Boolean.valueOf(getConfig().get(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION));
+        if (providerModel != null) return Boolean.valueOf(getConfig().get(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION));
+        else return Boolean.valueOf(componentModel.getConfig().getFirst(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION));
     }
 
     public String getKerberosRealm() {
-        return getConfig().get(KerberosConstants.KERBEROS_REALM);
+        if (providerModel != null) return getConfig().get(KerberosConstants.KERBEROS_REALM);
+        else return componentModel.getConfig().getFirst(KerberosConstants.KERBEROS_REALM);
     }
 
     public String getServerPrincipal() {
-        return getConfig().get(KerberosConstants.SERVER_PRINCIPAL);
+        if (providerModel != null) return getConfig().get(KerberosConstants.SERVER_PRINCIPAL);
+        else return componentModel.getConfig().getFirst(KerberosConstants.SERVER_PRINCIPAL);
     }
 
     public String getKeyTab() {
-        return getConfig().get(KerberosConstants.KEYTAB);
+        if (providerModel != null) return getConfig().get(KerberosConstants.KEYTAB);
+        else return componentModel.getConfig().getFirst(KerberosConstants.KEYTAB);
     }
 
     public boolean isDebug() {
-        return Boolean.valueOf(getConfig().get(KerberosConstants.DEBUG));
+        if (providerModel != null) return Boolean.valueOf(getConfig().get(KerberosConstants.DEBUG));
+        else return Boolean.valueOf(componentModel.getConfig().getFirst(KerberosConstants.DEBUG));
     }
 
     protected Map<String, String> getConfig() {
diff --git a/federation/ldap/pom.xml b/federation/ldap/pom.xml
index 38fa19d..5cd5ecc 100755
--- a/federation/ldap/pom.xml
+++ b/federation/ldap/pom.xml
@@ -26,9 +26,14 @@
     <modelVersion>4.0.0</modelVersion>
 
     <artifactId>keycloak-ldap-federation</artifactId>
-    <name>Keycloak LDAP Federation</name>
+    <name>Keycloak LDAP UserStoreProvider</name>
     <description />
 
+    <properties>
+        <maven.compiler.target>1.8</maven.compiler.target>
+        <maven.compiler.source>1.8</maven.compiler.source>
+    </properties>
+
     <dependencies>
         <dependency>
             <groupId>org.keycloak</groupId>
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java
new file mode 100755
index 0000000..2401b49
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java
@@ -0,0 +1,545 @@
+/*
+ * 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.storage.ldap;
+
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.common.constants.KerberosConstants;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
+import org.keycloak.federation.kerberos.CommonKerberosConfig;
+import org.keycloak.federation.kerberos.impl.KerberosServerSubjectAuthenticator;
+import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator;
+import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
+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.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderConfigurationBuilder;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.UserStorageProviderFactory;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.ldap.idm.model.LDAPObject;
+import org.keycloak.storage.ldap.idm.query.Condition;
+import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
+import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
+import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
+import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory;
+import org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapperFactory;
+import org.keycloak.storage.user.ImportSynchronization;
+import org.keycloak.storage.user.SynchronizationResult;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LDAPStorageProvider>, ImportSynchronization {
+
+    /**
+     * Optional type that can be by implementations to describe edit mode of federation storage
+     *
+     */
+    public enum EditMode {
+        /**
+         * federation storage is read-only
+         */
+        READ_ONLY,
+        /**
+         * federation storage is writable
+         *
+         */
+        WRITABLE,
+        /**
+         * updates to user are stored locally and not synced with federation storage.
+         *
+         */
+        UNSYNCED
+    }
+
+
+    private static final Logger logger = Logger.getLogger(LDAPStorageProviderFactory.class);
+    public static final String PROVIDER_NAME = LDAPConstants.LDAP_PROVIDER;
+
+    private LDAPIdentityStoreRegistry ldapStoreRegistry;
+
+    protected static final List<ProviderConfigProperty> configProperties;
+
+    static {
+        configProperties = getConfigProps(null);
+    }
+
+    private static List<ProviderConfigProperty> getConfigProps(ComponentModel parent) {
+        boolean readOnly = false;
+        if (parent != null) {
+            LDAPConfig config = new LDAPConfig(parent.getConfig());
+            readOnly = config.getEditMode() != LDAPStorageProviderFactory.EditMode.WRITABLE;
+        }
+
+
+        return ProviderConfigurationBuilder.create()
+                .property().name(LDAPConstants.EDIT_MODE)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .add()
+                .property().name(LDAPConstants.SYNC_REGISTRATIONS)
+                .type(ProviderConfigProperty.BOOLEAN_TYPE)
+                .defaultValue("false")
+                .add()
+                .property().name(LDAPConstants.VENDOR)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .add()
+                .property().name(LDAPConstants.USERNAME_LDAP_ATTRIBUTE)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .add()
+                .property().name(LDAPConstants.RDN_LDAP_ATTRIBUTE)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .add()
+                .property().name(LDAPConstants.UUID_LDAP_ATTRIBUTE)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .add()
+                .property().name(LDAPConstants.USER_OBJECT_CLASSES)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .add()
+                .property().name(LDAPConstants.CONNECTION_URL)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .add()
+                .property().name(LDAPConstants.USERS_DN)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .add()
+                .property().name(LDAPConstants.AUTH_TYPE)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .defaultValue("simple")
+                .add()
+                .property().name(LDAPConstants.BIND_DN)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .add()
+                .property().name(LDAPConstants.BIND_CREDENTIAL)
+                .type(ProviderConfigProperty.PASSWORD)
+                .secret(true)
+                .add()
+                .property().name(LDAPConstants.CUSTOM_USER_SEARCH_FILTER)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .add()
+                .property().name(LDAPConstants.SEARCH_SCOPE)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .defaultValue("1")
+                .add()
+                .property().name(LDAPConstants.USE_TRUSTSTORE_SPI)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .defaultValue("ldapsOnly")
+                .add()
+                .property().name(LDAPConstants.CONNECTION_POOLING)
+                .type(ProviderConfigProperty.BOOLEAN_TYPE)
+                .defaultValue("true")
+                .add()
+                .property().name(LDAPConstants.PAGINATION)
+                .type(ProviderConfigProperty.BOOLEAN_TYPE)
+                .defaultValue("true")
+                .add()
+                .property().name(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION)
+                .type(ProviderConfigProperty.BOOLEAN_TYPE)
+                .defaultValue("false")
+                .add()
+                .property().name(KerberosConstants.SERVER_PRINCIPAL)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .add()
+                .property().name(KerberosConstants.KEYTAB)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .add()
+                .property().name(KerberosConstants.KERBEROS_REALM)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .add()
+                .property().name(KerberosConstants.DEBUG)
+                .type(ProviderConfigProperty.BOOLEAN_TYPE)
+                .defaultValue("false")
+                .add()
+                .property().name(KerberosConstants.USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION)
+                .type(ProviderConfigProperty.BOOLEAN_TYPE)
+                .defaultValue("false")
+                .add()
+                .property().name(KerberosConstants.SERVER_PRINCIPAL)
+                .type(ProviderConfigProperty.STRING_TYPE)
+                .add()
+                .build();
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return configProperties;
+    }
+
+    @Override
+    public LDAPStorageProvider create(KeycloakSession session, ComponentModel model) {
+        LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model);
+        return new LDAPStorageProvider(this, session, model, ldapIdentityStore);
+    }
+
+    @Override
+    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
+        LDAPConfig cfg = new LDAPConfig(config.getConfig());
+        String customFilter = cfg.getCustomUserSearchFilter();
+        LDAPUtils.validateCustomLdapFilter(customFilter);
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+        this.ldapStoreRegistry = new LDAPIdentityStoreRegistry();
+    }
+
+    @Override
+    public void close() {
+        this.ldapStoreRegistry = null;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_NAME;
+    }
+
+    // Best effort to create appropriate mappers according to our LDAP config
+    @Override
+    public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
+        LDAPConfig ldapConfig = new LDAPConfig(model.getConfig());
+
+        boolean activeDirectory = ldapConfig.isActiveDirectory();
+        EditMode editMode = ldapConfig.getEditMode();
+        String readOnly = String.valueOf(editMode == EditMode.READ_ONLY || editMode == EditMode.UNSYNCED);
+        String usernameLdapAttribute = ldapConfig.getUsernameLdapAttribute();
+
+        String alwaysReadValueFromLDAP = String.valueOf(editMode==EditMode.READ_ONLY || editMode== EditMode.WRITABLE);
+
+        ComponentModel mapperModel;
+        mapperModel = KeycloakModelUtils.createComponentModel("username", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(),
+                UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME,
+                UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, usernameLdapAttribute,
+                UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
+                UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false",
+                UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
+        realm.addComponentModel(mapperModel);
+
+        // CN is typically used as RDN for Active Directory deployments
+        if (ldapConfig.getRdnLdapAttribute().equalsIgnoreCase(LDAPConstants.CN)) {
+
+            if (usernameLdapAttribute.equalsIgnoreCase(LDAPConstants.CN)) {
+
+                // For AD deployments with "cn" as username, we will map "givenName" to first name
+                mapperModel = KeycloakModelUtils.createComponentModel("first name", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+                        UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
+                        UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.GIVENNAME,
+                        UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
+                        UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+                        UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
+                realm.addComponentModel(mapperModel);
+
+            } else {
+                if (editMode == EditMode.WRITABLE) {
+
+                    // For AD deployments with "sAMAccountName" as username and writable, we need to map "cn" as username as well (this is needed so we can register new users from KC into LDAP) and we will map "givenName" to first name.
+                    mapperModel = KeycloakModelUtils.createComponentModel("first name", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+                            UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
+                            UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.GIVENNAME,
+                            UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
+                            UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+                            UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
+                    realm.addComponentModel(mapperModel);
+
+                    mapperModel = KeycloakModelUtils.createComponentModel("username-cn", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+                            UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME,
+                            UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.CN,
+                            UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
+                            UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false",
+                            UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
+                    realm.addComponentModel(mapperModel);
+                } else {
+
+                    // For read-only LDAP, we map "cn" as full name
+                    mapperModel = KeycloakModelUtils.createComponentModel("full name", model.getId(), FullNameLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+                            FullNameLDAPStorageMapper.LDAP_FULL_NAME_ATTRIBUTE, LDAPConstants.CN,
+                            FullNameLDAPStorageMapper.READ_ONLY, readOnly,
+                            FullNameLDAPStorageMapper.WRITE_ONLY, "false");
+                    realm.addComponentModel(mapperModel);
+                }
+            }
+        } else {
+            mapperModel = KeycloakModelUtils.createComponentModel("first name", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+                    UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
+                    UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.CN,
+                    UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
+                    UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+                    UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
+            realm.addComponentModel(mapperModel);
+        }
+
+        mapperModel = KeycloakModelUtils.createComponentModel("last name", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+                UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.LAST_NAME,
+                UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.SN,
+                UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
+                UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+                UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "true");
+        realm.addComponentModel(mapperModel);
+
+        mapperModel = KeycloakModelUtils.createComponentModel("email", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+                UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, UserModel.EMAIL,
+                UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, LDAPConstants.EMAIL,
+                UserAttributeLDAPStorageMapper.READ_ONLY, readOnly,
+                UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false",
+                UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "false");
+        realm.addComponentModel(mapperModel);
+
+        String createTimestampLdapAttrName = activeDirectory ? "whenCreated" : LDAPConstants.CREATE_TIMESTAMP;
+        String modifyTimestampLdapAttrName = activeDirectory ? "whenChanged" : LDAPConstants.MODIFY_TIMESTAMP;
+
+        // map createTimeStamp as read-only
+        mapperModel = KeycloakModelUtils.createComponentModel("creation date", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+                UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.CREATE_TIMESTAMP,
+                UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, createTimestampLdapAttrName,
+                UserAttributeLDAPStorageMapper.READ_ONLY, "true",
+                UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+                UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "false");
+        realm.addComponentModel(mapperModel);
+
+        // map modifyTimeStamp as read-only
+        mapperModel = KeycloakModelUtils.createComponentModel("modify date", model.getId(), UserAttributeLDAPStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
+                UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.MODIFY_TIMESTAMP,
+                UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, modifyTimestampLdapAttrName,
+                UserAttributeLDAPStorageMapper.READ_ONLY, "true",
+                UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP,
+                UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "false");
+        realm.addComponentModel(mapperModel);
+
+        // MSAD specific mapper for account state propagation
+        if (activeDirectory) {
+            mapperModel = KeycloakModelUtils.createComponentModel("MSAD account controls", model.getId(), MSADUserAccountControlStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName());
+            realm.addComponentModel(mapperModel);
+        }
+    }
+
+    @Override
+    public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {
+        syncMappers(sessionFactory, realmId, model);
+
+        logger.infof("Sync all users from LDAP to local store: realm: %s, federation provider: %s", realmId, model.getName());
+
+        LDAPQuery userQuery = createQuery(sessionFactory, realmId, model);
+        SynchronizationResult syncResult = syncImpl(sessionFactory, userQuery, realmId, model);
+
+        // TODO: Remove all existing keycloak users, which have federation links, but are not in LDAP. Perhaps don't check users, which were just added or updated during this sync?
+
+        logger.infof("Sync all users finished: %s", syncResult.getStatus());
+        return syncResult;
+    }
+
+    @Override
+    public SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) {
+        syncMappers(sessionFactory, realmId, model);
+
+        logger.infof("Sync changed users from LDAP to local store: realm: %s, federation provider: %s, last sync time: " + lastSync, realmId, model.getName());
+
+        // Sync newly created and updated users
+        LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
+        Condition createCondition = conditionsBuilder.greaterThanOrEqualTo(LDAPConstants.CREATE_TIMESTAMP, lastSync);
+        Condition modifyCondition = conditionsBuilder.greaterThanOrEqualTo(LDAPConstants.MODIFY_TIMESTAMP, lastSync);
+        Condition orCondition = conditionsBuilder.orCondition(createCondition, modifyCondition);
+
+        LDAPQuery userQuery = createQuery(sessionFactory, realmId, model);
+        userQuery.addWhereCondition(orCondition);
+        SynchronizationResult result = syncImpl(sessionFactory, userQuery, realmId, model);
+
+        logger.infof("Sync changed users finished: %s", result.getStatus());
+        return result;
+    }
+
+    protected void syncMappers(KeycloakSessionFactory sessionFactory, final String realmId, final ComponentModel model) {
+        KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+            @Override
+            public void run(KeycloakSession session) {
+                LDAPStorageProvider ldapProvider = (LDAPStorageProvider)session.getProvider(UserStorageProvider.class, model);
+                RealmModel realm = session.realms().getRealm(realmId);
+                List<ComponentModel> mappers = realm.getComponents(model.getId(), LDAPStorageMapper.class.getName());
+                for (ComponentModel mapperModel : mappers) {
+                    LDAPStorageMapper ldapMapper = session.getProvider(LDAPStorageMapper.class, mapperModel);
+                    SynchronizationResult syncResult = ldapMapper.syncDataFromFederationProviderToKeycloak(mapperModel, ldapProvider, session, realm);
+                    if (syncResult.getAdded() > 0 || syncResult.getUpdated() > 0 || syncResult.getRemoved() > 0 || syncResult.getFailed() > 0) {
+                        logger.infof("Sync of federation mapper '%s' finished. Status: %s", mapperModel.getName(), syncResult.toString());
+                    }
+                }
+            }
+
+        });
+    }
+
+    protected SynchronizationResult syncImpl(KeycloakSessionFactory sessionFactory, LDAPQuery userQuery, final String realmId, final ComponentModel fedModel) {
+
+        final SynchronizationResult syncResult = new SynchronizationResult();
+
+        LDAPConfig ldapConfig = new LDAPConfig(fedModel.getConfig());
+        boolean pagination = ldapConfig.isPagination();
+        if (pagination) {
+            int pageSize = ldapConfig.getBatchSizeForSync();
+
+            boolean nextPage = true;
+            while (nextPage) {
+                userQuery.setLimit(pageSize);
+                final List<LDAPObject> users = userQuery.getResultList();
+                nextPage = userQuery.getPaginationContext() != null;
+                SynchronizationResult currentPageSync = importLdapUsers(sessionFactory, realmId, fedModel, users);
+                syncResult.add(currentPageSync);
+            }
+        } else {
+            // LDAP pagination not available. Do everything in single transaction
+            final List<LDAPObject> users = userQuery.getResultList();
+            SynchronizationResult currentSync = importLdapUsers(sessionFactory, realmId, fedModel, users);
+            syncResult.add(currentSync);
+        }
+
+        return syncResult;
+    }
+
+    private LDAPQuery createQuery(KeycloakSessionFactory sessionFactory, final String realmId, final ComponentModel model) {
+        class QueryHolder {
+            LDAPQuery query;
+        }
+
+        final QueryHolder queryHolder = new QueryHolder();
+        KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+            @Override
+            public void run(KeycloakSession session) {
+                LDAPStorageProvider ldapFedProvider = (LDAPStorageProvider)session.getProvider(UserStorageProvider.class, model);
+                RealmModel realm = session.realms().getRealm(realmId);
+                queryHolder.query = LDAPUtils.createQueryForUserSearch(ldapFedProvider, realm);
+            }
+
+        });
+        return queryHolder.query;
+    }
+
+    protected SynchronizationResult importLdapUsers(KeycloakSessionFactory sessionFactory, final String realmId, final ComponentModel fedModel, List<LDAPObject> ldapUsers) {
+        final SynchronizationResult syncResult = new SynchronizationResult();
+
+        class BooleanHolder {
+            private boolean value = true;
+        }
+        final BooleanHolder exists = new BooleanHolder();
+
+        for (final LDAPObject ldapUser : ldapUsers) {
+
+            try {
+
+                // Process each user in it's own transaction to avoid global fail
+                KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+                    @Override
+                    public void run(KeycloakSession session) {
+                        LDAPStorageProvider ldapFedProvider = (LDAPStorageProvider)session.getProvider(UserStorageProvider.class, fedModel);
+                        RealmModel currentRealm = session.realms().getRealm(realmId);
+
+                        String username = LDAPUtils.getUsername(ldapUser, ldapFedProvider.getLdapIdentityStore().getConfig());
+                        exists.value = true;
+                        LDAPUtils.checkUuid(ldapUser, ldapFedProvider.getLdapIdentityStore().getConfig());
+                        UserModel currentUser = session.userLocalStorage().getUserByUsername(username, currentRealm);
+
+                        if (currentUser == null) {
+
+                            // Add new user to Keycloak
+                            exists.value = false;
+                            ldapFedProvider.importUserFromLDAP(session, currentRealm, ldapUser);
+                            syncResult.increaseAdded();
+
+                        } else {
+                            if ((fedModel.getId().equals(currentUser.getFederationLink())) && (ldapUser.getUuid().equals(currentUser.getFirstAttribute(LDAPConstants.LDAP_ID)))) {
+
+                                // Update keycloak user
+                                List<ComponentModel> federationMappers = currentRealm.getComponents(fedModel.getId(), LDAPStorageMapper.class.getName());
+                                List<ComponentModel> sortedMappers = ldapFedProvider.sortMappersDesc(federationMappers);
+                                for (ComponentModel mapperModel : sortedMappers) {
+                                    LDAPStorageMapper ldapMapper = ldapFedProvider.getMapper(mapperModel);
+                                    ldapMapper.onImportUserFromLDAP(mapperModel, ldapFedProvider, ldapUser, currentUser, currentRealm, false);
+                                }
+
+                                logger.debugf("Updated user from LDAP: %s", currentUser.getUsername());
+                                syncResult.increaseUpdated();
+                            } else {
+                                logger.warnf("User '%s' is not updated during sync as he already exists in Keycloak database but is not linked to federation provider '%s'", username, fedModel.getName());
+                                syncResult.increaseFailed();
+                            }
+                        }
+                    }
+
+                });
+            } catch (ModelException me) {
+                logger.error("Failed during import user from LDAP", me);
+                syncResult.increaseFailed();
+
+                // Remove user if we already added him during this transaction
+                if (!exists.value) {
+                    KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+                        @Override
+                        public void run(KeycloakSession session) {
+                            LDAPStorageProvider ldapFedProvider = (LDAPStorageProvider)session.getProvider(UserStorageProvider.class, fedModel);
+                            RealmModel currentRealm = session.realms().getRealm(realmId);
+                            String username = null;
+                            try {
+                                username = LDAPUtils.getUsername(ldapUser, ldapFedProvider.getLdapIdentityStore().getConfig());
+                            } catch (ModelException ignore) {
+                            }
+
+                            if (username != null) {
+                                UserModel existing = session.userLocalStorage().getUserByUsername(username, currentRealm);
+                                if (existing != null) {
+                                    session.getUserCache().evict(currentRealm, existing);
+                                    session.userLocalStorage().removeUser(currentRealm, existing);
+                                }
+                            }
+                        }
+
+                    });
+                }
+            }
+        }
+
+        return syncResult;
+    }
+
+    protected SPNEGOAuthenticator createSPNEGOAuthenticator(String spnegoToken, CommonKerberosConfig kerberosConfig) {
+        KerberosServerSubjectAuthenticator kerberosAuth = createKerberosSubjectAuthenticator(kerberosConfig);
+        return new SPNEGOAuthenticator(kerberosConfig, kerberosAuth, spnegoToken);
+    }
+
+    protected KerberosServerSubjectAuthenticator createKerberosSubjectAuthenticator(CommonKerberosConfig kerberosConfig) {
+        return new KerberosServerSubjectAuthenticator(kerberosConfig);
+    }
+
+    protected KerberosUsernamePasswordAuthenticator createKerberosUsernamePasswordAuthenticator(CommonKerberosConfig kerberosConfig) {
+        return new KerberosUsernamePasswordAuthenticator(kerberosConfig);
+    }
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/FullNameLDAPStorageMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/FullNameLDAPStorageMapperFactory.java
new file mode 100755
index 0000000..3cfa6c5
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/FullNameLDAPStorageMapperFactory.java
@@ -0,0 +1,124 @@
+/*
+ * 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.storage.ldap.mappers;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
+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.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderConfigurationBuilder;
+import org.keycloak.storage.ldap.LDAPConfig;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class FullNameLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
+
+    public static final String PROVIDER_ID =  "full-name-ldap-mapper";
+
+    protected static final List<ProviderConfigProperty> configProperties;
+
+    static {
+        configProperties = getConfigProps(null);
+    }
+
+    private static List<ProviderConfigProperty> getConfigProps(ComponentModel parent) {
+        boolean readOnly = false;
+        if (parent != null) {
+            LDAPConfig config = new LDAPConfig(parent.getConfig());
+            readOnly = config.getEditMode() != LDAPStorageProviderFactory.EditMode.WRITABLE;
+        }
+
+
+        return ProviderConfigurationBuilder.create()
+                .property().name(FullNameLDAPStorageMapper.LDAP_FULL_NAME_ATTRIBUTE)
+                           .label("LDAP Full Name Attribute")
+                           .helpText("Name of LDAP attribute, which contains fullName of user. Usually it will be 'cn' ")
+                           .type(ProviderConfigProperty.STRING_TYPE)
+                           .defaultValue(LDAPConstants.CN)
+                           .add()
+                .property().name(FullNameLDAPStorageMapper.READ_ONLY)
+                           .label("Read Only")
+                           .helpText("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.")
+                           .type(ProviderConfigProperty.BOOLEAN_TYPE)
+                           .defaultValue(String.valueOf(readOnly))
+                .add()
+                .property().name(FullNameLDAPStorageMapper.WRITE_ONLY)
+                           .label("Write Only")
+                           .helpText("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")
+                           .type(ProviderConfigProperty.BOOLEAN_TYPE)
+                           .defaultValue(String.valueOf(!readOnly))
+                            .add()
+                           .build();
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Used to map full-name of user from single attribute in LDAP (usually 'cn' attribute) to firstName and lastName attributes of UserModel in Keycloak DB";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return configProperties;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
+        checkMandatoryConfigAttribute(FullNameLDAPStorageMapper.LDAP_FULL_NAME_ATTRIBUTE, "LDAP Full Name Attribute", config);
+
+
+        boolean readOnly = AbstractLDAPStorageMapper.parseBooleanParameter(config, FullNameLDAPStorageMapper.READ_ONLY);
+        boolean writeOnly = AbstractLDAPStorageMapper.parseBooleanParameter(config, FullNameLDAPStorageMapper.WRITE_ONLY);
+
+        ComponentModel parent = realm.getComponent(config.getParentId());
+        if (parent == null) {
+            throw new ComponentValidationException("can't find parent component model");
+
+        }
+        LDAPConfig cfg = new LDAPConfig(parent.getConfig());
+        LDAPStorageProviderFactory.EditMode editMode = cfg.getEditMode();
+
+        if (writeOnly && cfg.getEditMode() != LDAPStorageProviderFactory.EditMode.WRITABLE) {
+            throw new ComponentValidationException("ldapErrorCantWriteOnlyForReadOnlyLdap");
+        }
+        if (writeOnly && readOnly) {
+            throw new ComponentValidationException("ldapErrorCantWriteOnlyAndReadOnly");
+        }
+    }
+
+    @Override
+    protected AbstractLDAPStorageMapper createMapper(ComponentModel mapperModel, LDAPStorageProvider federationProvider, RealmModel realm) {
+        return new FullNameLDAPStorageMapper(mapperModel, federationProvider, realm);
+    }
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPStorageMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPStorageMapperFactory.java
new file mode 100644
index 0000000..d5e8318
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPStorageMapperFactory.java
@@ -0,0 +1,97 @@
+/*
+ * 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.storage.ldap.mappers;
+
+import org.keycloak.Config;
+import org.keycloak.component.ComponentFactory;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
+import org.keycloak.component.SubComponentFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface LDAPStorageMapperFactory<T extends LDAPStorageMapper> extends SubComponentFactory<T, LDAPStorageMapper> {
+    /**
+     * called per Keycloak transaction.
+     *
+     * @param session
+     * @param model
+     * @return
+     */
+    T create(KeycloakSession session, ComponentModel model);
+
+    /**
+     * This is the name of the provider and will be showed in the admin console as an option.
+     *
+     * @return
+     */
+    @Override
+    String getId();
+
+    @Override
+    default void init(Config.Scope config) {
+
+    }
+
+    @Override
+    default void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    default void close() {
+
+    }
+
+    @Override
+    default String getHelpText() {
+        return "";
+    }
+
+    @Override
+    default List<ProviderConfigProperty> getConfigProperties() {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    default void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
+
+    }
+
+    /**
+     * Called when UserStorageProviderModel is created.  This allows you to do initialization of any additional configuration
+     * you need to add.  For example, you may be introspecting a database or ldap schema to automatically create mappings.
+     *
+     * @param session
+     * @param realm
+     * @param model
+     */
+    @Override
+    default void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
+
+    }
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPStorageMapperSpi.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPStorageMapperSpi.java
new file mode 100644
index 0000000..c7b8185
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/LDAPStorageMapperSpi.java
@@ -0,0 +1,50 @@
+/*
+ * 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.storage.ldap.mappers;
+
+import org.keycloak.credential.CredentialProvider;
+import org.keycloak.credential.CredentialProviderFactory;
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:me@tsudot.com">Kunal Kerkar</a>
+ */
+public class LDAPStorageMapperSpi implements Spi {
+
+    @Override
+    public boolean isInternal() {
+        return false;
+    }
+
+    @Override
+    public String getName() {
+        return "ldap-mapper";
+    }
+
+    @Override
+    public Class<? extends Provider> getProviderClass() {
+        return LDAPStorageMapper.class;
+    }
+
+    @Override
+    public Class<? extends ProviderFactory> getProviderFactoryClass() {
+        return LDAPStorageMapperFactory.class;
+    }
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapperFactory.java
new file mode 100644
index 0000000..b3baf50
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/group/GroupLDAPStorageMapperFactory.java
@@ -0,0 +1,213 @@
+/*
+ * 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.storage.ldap.mappers.membership.group;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
+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.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderConfigurationBuilder;
+import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
+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.mappers.AbstractLDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapperFactory;
+import org.keycloak.storage.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
+import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode;
+import org.keycloak.storage.ldap.mappers.membership.MembershipType;
+import org.keycloak.storage.ldap.mappers.membership.UserRolesRetrieveStrategy;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class GroupLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
+
+    public static final String PROVIDER_ID = "group-ldap-mapper";
+
+    protected static final List<ProviderConfigProperty> configProperties;
+    protected static final Map<String, UserRolesRetrieveStrategy> userGroupsStrategies = new LinkedHashMap<>();
+    protected static final List<String> MEMBERSHIP_TYPES = new LinkedList<>();
+    protected static final List<String> MODES = new LinkedList<>();
+    protected static final List<String> ROLE_RETRIEVERS;
+
+    // TODO: Merge with RoleLDAPFederationMapperFactory as there are lot of similar properties
+    static {
+        userGroupsStrategies.put(GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE, new UserRolesRetrieveStrategy.LoadRolesByMember());
+        userGroupsStrategies.put(GroupMapperConfig.GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE, new UserRolesRetrieveStrategy.GetRolesFromUserMemberOfAttribute());
+        userGroupsStrategies.put(GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY, new UserRolesRetrieveStrategy.LoadRolesByMemberRecursively());
+        for (MembershipType membershipType : MembershipType.values()) {
+            MEMBERSHIP_TYPES.add(membershipType.toString());
+        }
+        for (LDAPGroupMapperMode mode : LDAPGroupMapperMode.values()) {
+            MODES.add(mode.toString());
+        }
+        ROLE_RETRIEVERS = new LinkedList<>(userGroupsStrategies.keySet());
+
+        List<ProviderConfigProperty> config = getProps(null);
+        configProperties = config;
+    }
+
+    private static List<ProviderConfigProperty> getProps(ComponentModel parent) {
+        String roleObjectClasses = LDAPConstants.GROUP_OF_NAMES;
+        String mode = LDAPGroupMapperMode.LDAP_ONLY.toString();
+        if (parent != null) {
+            LDAPConfig config = new LDAPConfig(parent.getConfig());
+            roleObjectClasses = config.isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
+            mode = config.getEditMode() == LDAPStorageProviderFactory.EditMode.WRITABLE ? LDAPGroupMapperMode.LDAP_ONLY.toString() : LDAPGroupMapperMode.READ_ONLY.toString();
+        }
+        return ProviderConfigurationBuilder.create()
+                    .property().name(GroupMapperConfig.GROUPS_DN)
+                               .label("LDAP Groups DN")
+                               .helpText("LDAP DN where are groups of this tree saved. For example 'ou=groups,dc=example,dc=org' ")
+                               .type(ProviderConfigProperty.STRING_TYPE)
+                               .add()
+                    .property().name(GroupMapperConfig.GROUP_NAME_LDAP_ATTRIBUTE)
+                               .label("Group Name LDAP Attribute")
+                               .helpText("Name of LDAP attribute, which is used in group objects for name and RDN of group. Usually it will be 'cn' . In this case typical group/role object may have DN like 'cn=Group1,ou=groups,dc=example,dc=org' ")
+                               .type(ProviderConfigProperty.STRING_TYPE)
+                               .defaultValue(LDAPConstants.CN)
+                               .add()
+                    .property().name(GroupMapperConfig.GROUP_OBJECT_CLASSES)
+                               .label("Group Object Classes")
+                               .helpText("Object class (or classes) of the group object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ")
+                               .type(ProviderConfigProperty.STRING_TYPE)
+                               .defaultValue(roleObjectClasses)
+                               .add()
+                    .property().name(GroupMapperConfig.PRESERVE_GROUP_INHERITANCE)
+                               .label("Preserve Group Inheritance")
+                               .helpText("Flag whether group inheritance from LDAP should be propagated to Keycloak. If false, then all LDAP groups will be mapped as flat top-level groups in Keycloak. Otherwise group inheritance is " +
+                            "preserved into Keycloak, but the group sync might fail if LDAP structure contains recursions or multiple parent groups per child groups")
+                               .type(ProviderConfigProperty.BOOLEAN_TYPE)
+                               .defaultValue("true")
+                               .add()
+                    .property().name(GroupMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE)
+                               .label("Membership LDAP Attribute")
+                               .helpText("Name of LDAP attribute on group, which is used for membership mappings. Usually it will be 'member' ")
+                               .type(ProviderConfigProperty.STRING_TYPE)
+                               .defaultValue(LDAPConstants.MEMBER)
+                               .add()
+                    .property().name(GroupMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE)
+                               .label("Membership Attribute Type")
+                               .helpText("DN means that LDAP group has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " +
+                            "UID means that LDAP group has it's members declared in form of pure user uids. For example 'memberUid: john' .")
+                               .type(ProviderConfigProperty.LIST_TYPE)
+                               .options(MEMBERSHIP_TYPES)
+                               .defaultValue(MembershipType.DN.toString())
+                               .add()
+                    .property().name(GroupMapperConfig.GROUPS_LDAP_FILTER)
+                               .label("LDAP Filter")
+                               .helpText("LDAP Filter adds additional custom filter to the whole query for retrieve LDAP groups. Leave this empty if no additional filtering is needed and you want to retrieve all groups from LDAP. Otherwise make sure that filter starts with '(' and ends with ')'")
+                               .type(ProviderConfigProperty.STRING_TYPE)
+                               .add()
+                    .property().name(GroupMapperConfig.MODE)
+                               .label("Mode")
+                               .helpText("LDAP_ONLY means that all group mappings of users are retrieved from LDAP and saved into LDAP. READ_ONLY is Read-only LDAP mode where group mappings are " +
+                            "retrieved from both LDAP and DB and merged together. New group joins are not saved to LDAP but to DB. IMPORT is Read-only LDAP mode where group mappings are " +
+                            "retrieved from LDAP just at the time when user is imported from LDAP and then " +
+                            "they are saved to local keycloak DB.")
+                               .type(ProviderConfigProperty.LIST_TYPE)
+                               .options(MODES)
+                               .defaultValue(mode)
+                               .add()
+                    .property().name(GroupMapperConfig.USER_ROLES_RETRIEVE_STRATEGY)
+                               .label("User Groups Retrieve Strategy")
+                               .helpText("Specify how to retrieve groups of user. LOAD_GROUPS_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all groups where 'member' is our user. " +
+                            "GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE means that groups of user will be retrieved from 'memberOf' attribute of our user. " +
+                            "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that groups of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension.")
+                               .type(ProviderConfigProperty.LIST_TYPE)
+                               .options(ROLE_RETRIEVERS)
+                               .defaultValue(GroupMapperConfig.LOAD_GROUPS_BY_MEMBER_ATTRIBUTE)
+                               .add()
+                    .property().name(GroupMapperConfig.MAPPED_GROUP_ATTRIBUTES)
+                               .label("Mapped Group Attributes")
+                               .helpText("List of names of attributes divided by comma. This points to the list of attributes on LDAP group, which will be mapped as attributes of Group in Keycloak. " +
+                    "Leave this empty if no additional group attributes are required to be mapped in Keycloak. ")
+                               .type(ProviderConfigProperty.STRING_TYPE)
+                               .add()
+                    .property().name(GroupMapperConfig.DROP_NON_EXISTING_GROUPS_DURING_SYNC)
+                               .label("Drop non-existing groups during sync")
+                               .helpText("If this flag is true, then during sync of groups from LDAP to Keycloak, we will keep just those Keycloak groups, which still exists in LDAP. Rest will be deleted")
+                               .type(ProviderConfigProperty.BOOLEAN_TYPE)
+                               .defaultValue("false")
+                               .add()
+                    .build();
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Used to map group mappings of groups from some LDAP DN to Keycloak group mappings";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return configProperties;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public Map<String, Object> getTypeMetadata() {
+        Map<String, Object> metadata = new HashMap<>();
+        metadata.put("fedToKeycloakSyncSupported", true);
+        metadata.put("fedToKeycloakSyncMessage", "sync-ldap-groups-to-keycloak");
+        metadata.put("keycloakToFedSyncSupported", true);
+        metadata.put("keycloakToFedSyncMessage", "sync-keycloak-groups-to-ldap");
+
+        return metadata;
+    }
+
+
+    @Override
+    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
+        checkMandatoryConfigAttribute(GroupMapperConfig.GROUPS_DN, "LDAP Groups DN", config);
+        checkMandatoryConfigAttribute(GroupMapperConfig.MODE, "Mode", config);
+
+        String mt = config.getConfig().getFirst(CommonLDAPGroupMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE);
+        MembershipType membershipType = mt==null ? MembershipType.DN : Enum.valueOf(MembershipType.class, mt);
+        boolean preserveGroupInheritance = Boolean.parseBoolean(config.getConfig().getFirst(GroupMapperConfig.PRESERVE_GROUP_INHERITANCE));
+        if (preserveGroupInheritance && membershipType != MembershipType.DN) {
+            throw new ComponentValidationException("ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType");
+        }
+
+        LDAPUtils.validateCustomLdapFilter(config.getConfig().getFirst(GroupMapperConfig.GROUPS_LDAP_FILTER));
+    }
+
+    @Override
+    protected AbstractLDAPStorageMapper createMapper(ComponentModel mapperModel, LDAPStorageProvider federationProvider, RealmModel realm) {
+        return new GroupLDAPStorageMapper(mapperModel, federationProvider, realm, this);
+    }
+
+    protected UserRolesRetrieveStrategy getUserGroupsRetrieveStrategy(String strategyKey) {
+        return userGroupsStrategies.get(strategyKey);
+    }
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapperFactory.java
new file mode 100644
index 0000000..e2da595
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/membership/role/RoleLDAPStorageMapperFactory.java
@@ -0,0 +1,207 @@
+/*
+ * 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.storage.ldap.mappers.membership.role;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
+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.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderConfigurationBuilder;
+import org.keycloak.representations.idm.UserFederationMapperSyncConfigRepresentation;
+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.mappers.AbstractLDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.AbstractLDAPStorageMapperFactory;
+import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode;
+import org.keycloak.storage.ldap.mappers.membership.MembershipType;
+import org.keycloak.storage.ldap.mappers.membership.UserRolesRetrieveStrategy;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RoleLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
+
+    public static final String PROVIDER_ID = "role-ldap-mapper";
+
+    protected static final List<ProviderConfigProperty> configProperties;
+    protected static final Map<String, UserRolesRetrieveStrategy> userRolesStrategies = new LinkedHashMap<>();
+    protected static final List<String> MEMBERSHIP_TYPES = new LinkedList<>();
+    protected static final List<String> MODES = new LinkedList<>();
+    protected static final List<String> roleRetrievers;
+
+    static {
+        userRolesStrategies.put(RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE, new UserRolesRetrieveStrategy.LoadRolesByMember());
+        userRolesStrategies.put(RoleMapperConfig.GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE, new UserRolesRetrieveStrategy.GetRolesFromUserMemberOfAttribute());
+        userRolesStrategies.put(RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY, new UserRolesRetrieveStrategy.LoadRolesByMemberRecursively());
+
+        for (MembershipType membershipType : MembershipType.values()) {
+            MEMBERSHIP_TYPES.add(membershipType.toString());
+        }
+
+        for (LDAPGroupMapperMode mode : LDAPGroupMapperMode.values()) {
+            MODES.add(mode.toString());
+        }
+        roleRetrievers = new LinkedList<>(userRolesStrategies.keySet());
+
+        List<ProviderConfigProperty> config = getProps(null);
+        configProperties = config;
+    }
+
+    private static List<ProviderConfigProperty> getProps(ComponentModel parent) {
+        String roleObjectClasses = LDAPConstants.GROUP_OF_NAMES;
+        String mode = LDAPGroupMapperMode.LDAP_ONLY.toString();
+        if (parent != null) {
+            LDAPConfig config = new LDAPConfig(parent.getConfig());
+            roleObjectClasses = config.isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
+            mode = config.getEditMode() == LDAPStorageProviderFactory.EditMode.WRITABLE ? LDAPGroupMapperMode.LDAP_ONLY.toString() : LDAPGroupMapperMode.READ_ONLY.toString();
+        }
+        return ProviderConfigurationBuilder.create()
+                    .property().name(RoleMapperConfig.ROLES_DN)
+                               .label("LDAP Roles DN")
+                               .helpText("LDAP DN where are roles of this tree saved. For example 'ou=finance,dc=example,dc=org' ")
+                               .type(ProviderConfigProperty.STRING_TYPE)
+                               .add()
+                    .property().name(RoleMapperConfig.ROLE_NAME_LDAP_ATTRIBUTE)
+                               .label("Role Name LDAP Attribute")
+                               .helpText("Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be 'cn' . In this case typical group/role object may have DN like 'cn=role1,ou=finance,dc=example,dc=org' ")
+                               .type(ProviderConfigProperty.STRING_TYPE)
+                               .defaultValue(LDAPConstants.CN)
+                               .add()
+                    .property().name(RoleMapperConfig.ROLE_OBJECT_CLASSES)
+                               .label("Role Object Classes")
+                               .helpText("Object class (or classes) of the role object. It's divided by comma if more classes needed. In typical LDAP deployment it could be 'groupOfNames' . In Active Directory it's usually 'group' ")
+                               .type(ProviderConfigProperty.STRING_TYPE)
+                               .defaultValue(roleObjectClasses)
+                               .add()
+                    .property().name(RoleMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE)
+                               .label("Membership LDAP Attribute")
+                               .helpText("Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ")
+                               .type(ProviderConfigProperty.STRING_TYPE)
+                               .defaultValue(LDAPConstants.MEMBER)
+                               .add()
+                    .property().name(RoleMapperConfig.MEMBERSHIP_ATTRIBUTE_TYPE)
+                               .label("Membership Attribute Type")
+                               .helpText("DN means that LDAP role has it's members declared in form of their full DN. For example 'member: uid=john,ou=users,dc=example,dc=com' . " +
+                            "UID means that LDAP role has it's members declared in form of pure user uids. For example 'memberUid: john' .")
+                               .type(ProviderConfigProperty.LIST_TYPE)
+                               .options(MEMBERSHIP_TYPES)
+                               .defaultValue(MembershipType.DN.toString())
+                               .add()
+                    .property().name(RoleMapperConfig.ROLES_LDAP_FILTER)
+                               .label("LDAP Filter")
+                               .helpText("LDAP Filter adds additional custom filter to the whole query for retrieve LDAP roles. Leave this empty if no additional filtering is needed and you want to retrieve all roles from LDAP. Otherwise make sure that filter starts with '(' and ends with ')'")
+                               .type(ProviderConfigProperty.STRING_TYPE)
+                               .add()
+                    .property().name(RoleMapperConfig.MODE)
+                               .label("Mode")
+                               .helpText("LDAP_ONLY means that all role mappings are retrieved from LDAP and saved into LDAP. READ_ONLY is Read-only LDAP mode where role mappings are " +
+                            "retrieved from both LDAP and DB and merged together. New role grants are not saved to LDAP but to DB. IMPORT is Read-only LDAP mode where role mappings are retrieved from LDAP just at the time when user is imported from LDAP and then " +
+                            "they are saved to local keycloak DB.")
+                               .type(ProviderConfigProperty.LIST_TYPE)
+                               .options(MODES)
+                               .defaultValue(mode)
+                               .add()
+                    .property().name(RoleMapperConfig.USER_ROLES_RETRIEVE_STRATEGY)
+                               .label("User Roles Retrieve Strategy")
+                               .helpText("Specify how to retrieve roles of user. LOAD_ROLES_BY_MEMBER_ATTRIBUTE means that roles of user will be retrieved by sending LDAP query to retrieve all roles where 'member' is our user. " +
+                            "GET_ROLES_FROM_USER_MEMBEROF_ATTRIBUTE means that roles of user will be retrieved from 'memberOf' attribute of our user. " +
+                            "LOAD_ROLES_BY_MEMBER_ATTRIBUTE_RECURSIVELY is applicable just in Active Directory and it means that roles of user will be retrieved recursively with usage of LDAP_MATCHING_RULE_IN_CHAIN Ldap extension.")
+                               .type(ProviderConfigProperty.LIST_TYPE)
+                               .options(roleRetrievers)
+                               .defaultValue(RoleMapperConfig.LOAD_ROLES_BY_MEMBER_ATTRIBUTE)
+                               .add()
+                    .property().name(RoleMapperConfig.USE_REALM_ROLES_MAPPING)
+                               .label("Use Realm Roles Mapping")
+                               .helpText("If true, then LDAP role mappings will be mapped to realm role mappings in Keycloak. Otherwise it will be mapped to client role mappings")
+                               .type(ProviderConfigProperty.BOOLEAN_TYPE)
+                               .defaultValue("true")
+                               .add()
+                    .property().name(RoleMapperConfig.CLIENT_ID)
+                               .label("Client ID")
+                               .helpText("Client ID of client to which LDAP role mappings will be mapped. Applicable just if 'Use Realm Roles Mapping' is false")
+                               .type(ProviderConfigProperty.CLIENT_LIST_TYPE)
+                               .add()
+                    .build();
+    }
+
+
+    @Override
+    public String getHelpText() {
+        return "Used to map role mappings of roles from some LDAP DN to Keycloak role mappings of either realm roles or client roles of particular client";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return configProperties;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public Map<String, Object> getTypeMetadata() {
+        Map<String, Object> metadata = new HashMap<>();
+        metadata.put("fedToKeycloakSyncSupported", true);
+        metadata.put("fedToKeycloakSyncMessage", "sync-ldap-roles-to-keycloak");
+        metadata.put("keycloakToFedSyncSupported", true);
+        metadata.put("keycloakToFedSyncMessage", "sync-keycloak-roles-to-ldap");
+
+        return metadata;
+    }
+
+
+    @Override
+    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
+        checkMandatoryConfigAttribute(RoleMapperConfig.ROLES_DN, "LDAP Roles DN", config);
+        checkMandatoryConfigAttribute(RoleMapperConfig.MODE, "Mode", config);
+
+        String realmMappings = config.getConfig().getFirst(RoleMapperConfig.USE_REALM_ROLES_MAPPING);
+        boolean useRealmMappings = Boolean.parseBoolean(realmMappings);
+        if (!useRealmMappings) {
+            String clientId = config.getConfig().getFirst(RoleMapperConfig.CLIENT_ID);
+            if (clientId == null || clientId.trim().isEmpty()) {
+                throw new ComponentValidationException("ldapErrorMissingClientId");
+            }
+        }
+
+        LDAPUtils.validateCustomLdapFilter(config.getConfig().getFirst(RoleMapperConfig.ROLES_LDAP_FILTER));
+    }
+
+    @Override
+    protected AbstractLDAPStorageMapper createMapper(ComponentModel mapperModel, LDAPStorageProvider federationProvider, RealmModel realm) {
+        return new RoleLDAPStorageMapper(mapperModel, federationProvider, realm, this);
+    }
+
+    protected UserRolesRetrieveStrategy getUserRolesRetrieveStrategy(String strategyKey) {
+        return userRolesStrategies.get(strategyKey);
+    }
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapperFactory.java
new file mode 100755
index 0000000..99d9ea1
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapperFactory.java
@@ -0,0 +1,110 @@
+/*
+ * 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.storage.ldap.mappers;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserFederationProvider;
+import org.keycloak.models.UserFederationProviderModel;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderConfigurationBuilder;
+import org.keycloak.storage.ldap.LDAPConfig;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
+
+    public static final String PROVIDER_ID = "user-attribute-ldap-mapper";
+    protected static final List<ProviderConfigProperty> configProperties;
+
+    static {
+        List<ProviderConfigProperty> props = getConfigProps(null);
+        configProperties = props;
+    }
+
+    private static List<ProviderConfigProperty> getConfigProps(ComponentModel parent) {
+        String readOnly = "false";
+        if (parent != null) {
+            LDAPConfig ldapConfig = new LDAPConfig(parent.getConfig());
+            readOnly = ldapConfig.getEditMode() == LDAPStorageProviderFactory.EditMode.WRITABLE ? "false" : "true";
+        }
+        return ProviderConfigurationBuilder.create()
+                    .property().name(UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE)
+                               .label("User Model Attribute")
+                               .helpText("Name of mapped UserModel property or UserModel attribute in Keycloak DB. For example 'firstName', 'lastName, 'email', 'street' etc.")
+                               .type(ProviderConfigProperty.STRING_TYPE)
+                               .add()
+                    .property().name(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE).label("LDAP Attribute").helpText("Name of mapped attribute on LDAP object. For example 'cn', 'sn, 'mail', 'street' etc.")
+                               .type(ProviderConfigProperty.STRING_TYPE)
+                               .add()
+                    .property().name(UserAttributeLDAPStorageMapper.READ_ONLY).label("Read Only")
+                               .helpText("Read-only attribute is imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.")
+                               .type(ProviderConfigProperty.BOOLEAN_TYPE)
+                               .defaultValue(readOnly)
+                               .add()
+                    .property().name(UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP).label("Always Read Value From LDAP")
+                               .helpText("If on, then during reading of the user will be value of attribute from LDAP always used instead of the value from Keycloak DB")
+                               .type(ProviderConfigProperty.BOOLEAN_TYPE).defaultValue("false").add()
+                    .property().name(UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP).label("Is Mandatory In LDAP")
+                               .helpText("If true, attribute is mandatory in LDAP. Hence if there is no value in Keycloak DB, the empty value will be set to be propagated to LDAP")
+                               .type(ProviderConfigProperty.BOOLEAN_TYPE)
+                               .defaultValue("false").add()
+                    .build();
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Used to map single attribute from LDAP user to attribute of UserModel in Keycloak DB";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return configProperties;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
+        checkMandatoryConfigAttribute(UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute", config);
+        checkMandatoryConfigAttribute(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, "LDAP Attribute", config);
+
+    }
+
+    @Override
+    protected AbstractLDAPStorageMapper createMapper(ComponentModel mapperModel, LDAPStorageProvider federationProvider, RealmModel realm) {
+        return new UserAttributeLDAPStorageMapper(mapperModel, federationProvider, realm);
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) {
+        return getConfigProps(parent);
+    }
+}
diff --git a/federation/ldap/src/main/resources/META-INF/services/org.keycloak.storage.ldap.mappers.LDAPStorageMapperFactory b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.storage.ldap.mappers.LDAPStorageMapperFactory
new file mode 100644
index 0000000..ff1f0f6
--- /dev/null
+++ b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.storage.ldap.mappers.LDAPStorageMapperFactory
@@ -0,0 +1,6 @@
+org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory
+org.keycloak.storage.ldap.mappers.HardcodedLDAPRoleStorageMapperFactory
+org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapperFactory
+org.keycloak.storage.ldap.mappers.membership.role.RoleLDAPStorageMapperFactory
+org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapperFactory
+org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory
diff --git a/federation/ldap/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory
new file mode 100644
index 0000000..0d8483f
--- /dev/null
+++ b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory
@@ -0,0 +1 @@
+org.keycloak.storage.ldap.LDAPStorageProviderFactory
\ No newline at end of file
diff --git a/federation/pom.xml b/federation/pom.xml
index 7942fee..a08ef63 100755
--- a/federation/pom.xml
+++ b/federation/pom.xml
@@ -33,8 +33,8 @@
     <description />
 
     <modules>
-        <module>ldap</module>
         <module>kerberos</module>
+        <module>ldap</module>
         <module>sssd</module>
     </modules>
 
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..069b34a 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,35 @@ public class RealmAdapter implements CachedRealmModel {
     @Override
     public ComponentModel addComponentModel(ComponentModel model) {
         getDelegateForUpdate();
+        evictUsers(model);
         return updated.addComponentModel(model);
     }
 
     @Override
+    public ComponentModel importComponentModel(ComponentModel model) {
+        getDelegateForUpdate();
+        evictUsers(model);
+        return updated.importComponentModel(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 +1373,7 @@ public class RealmAdapter implements CachedRealmModel {
     @Override
     public void removeComponent(ComponentModel component) {
         getDelegateForUpdate();
+        evictUsers(component);
         updated.removeComponent(component);
 
     }
@@ -1353,6 +1381,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 1a52aa9..3094521 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
@@ -65,6 +65,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..5531de1 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) {
@@ -498,8 +501,7 @@ public class UserCacheSession implements UserCache {
     @Override
     public UserModel getServiceAccount(ClientModel client) {
         // Just an attempt to find the user from cache by default serviceAccount username
-        String username = ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + client.getClientId();
-        UserModel user = getUserByUsername(username, client.getRealm());
+        UserModel user = findServiceAccount(client);
         if (user != null && user.getServiceAccountClientLink() != null && user.getServiceAccountClientLink().equals(client.getId())) {
             return user;
         }
@@ -507,6 +509,60 @@ public class UserCacheSession implements UserCache {
         return getDelegate().getServiceAccount(client);
     }
 
+    public UserModel findServiceAccount(ClientModel client) {
+        String username = ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + client.getClientId();
+        logger.tracev("getServiceAccount: {0}", username);
+        username = username.toLowerCase();
+        RealmModel realm = client.getRealm();
+        if (realmInvalidations.contains(realm.getId())) {
+            logger.tracev("realmInvalidations");
+            return getDelegate().getServiceAccount(client);
+        }
+        String cacheKey = getUserByUsernameCacheKey(realm.getId(), username);
+        if (invalidations.contains(cacheKey)) {
+            logger.tracev("invalidations");
+            return getDelegate().getServiceAccount(client);
+        }
+        UserListQuery query = cache.get(cacheKey, UserListQuery.class);
+
+        String userId = null;
+        if (query == null) {
+            logger.tracev("query null");
+            Long loaded = cache.getCurrentRevision(cacheKey);
+            UserModel model = getDelegate().getServiceAccount(client);
+            if (model == null) {
+                logger.tracev("model from delegate null");
+                return null;
+            }
+            userId = model.getId();
+            if (invalidations.contains(userId)) return model;
+            if (managedUsers.containsKey(userId)) {
+                logger.tracev("return managed user");
+                return managedUsers.get(userId);
+            }
+
+            UserModel adapter = getUserAdapter(realm, userId, loaded, model);
+            if (adapter instanceof UserAdapter) { // this was cached, so we can cache query too
+                query = new UserListQuery(loaded, cacheKey, realm, model.getId());
+                cache.addRevisioned(query, startupRevision);
+            }
+            managedUsers.put(userId, adapter);
+            return adapter;
+        } else {
+            userId = query.getUsers().iterator().next();
+            if (invalidations.contains(userId)) {
+                logger.tracev("invalidated cache return delegate");
+                return getDelegate().getUserByUsername(username, realm);
+
+            }
+            logger.trace("return getUserById");
+            return getUserById(userId, realm);
+        }
+    }
+
+
+
+
     @Override
     public List<UserModel> getUsers(RealmModel realm, boolean includeServiceAccounts) {
         return getDelegate().getUsers(realm, includeServiceAccounts);
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/AbstractUserFedToComponent.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/AbstractUserFedToComponent.java
new file mode 100644
index 0000000..e86788b
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/AbstractUserFedToComponent.java
@@ -0,0 +1,182 @@
+/*
+ * 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.connections.jpa.updater.liquibase.custom;
+
+import liquibase.exception.CustomChangeException;
+import liquibase.statement.core.DeleteStatement;
+import liquibase.statement.core.InsertStatement;
+import liquibase.structure.core.Table;
+import org.jboss.logging.Logger;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.storage.UserStorageProvider;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public abstract class AbstractUserFedToComponent extends CustomKeycloakTask {
+    private final Logger logger = Logger.getLogger(getClass());
+    protected void convertFedProviderToComponent(String providerId, String newMapperType) throws CustomChangeException {
+        try {
+            PreparedStatement statement = jdbcConnection.prepareStatement("select ID, REALM_ID, PRIORITY, DISPLAY_NAME, FULL_SYNC_PERIOD, CHANGED_SYNC_PERIOD, LAST_SYNC from " + getTableName("USER_FEDERATION_PROVIDER") + " WHERE PROVIDER_NAME='" + providerId + "'");
+
+            try {
+                ResultSet resultSet = statement.executeQuery();
+                try {
+                    while (resultSet.next()) {
+                        int index = 1;
+                        String id = resultSet.getString(index++);
+                        String realmId = resultSet.getString(index++);
+                        int priority = resultSet.getInt(index++);
+                        String displayName = resultSet.getString(index++);
+                        int fullSyncPeriod = resultSet.getInt(index++);
+                        int changedSyncPeriod = resultSet.getInt(index++);
+                        int lastSync = resultSet.getInt(index++);
+
+
+                        InsertStatement insertComponent = new InsertStatement(null, null, database.correctObjectName("COMPONENT", Table.class))
+                                .addColumnValue("ID", id)
+                                .addColumnValue("REALM_ID", realmId)
+                                .addColumnValue("PARENT_ID", realmId)
+                                .addColumnValue("NAME", displayName)
+                                .addColumnValue("PROVIDER_ID", LDAPConstants.LDAP_PROVIDER)
+                                .addColumnValue("PROVIDER_TYPE", UserStorageProvider.class.getName());
+
+                        statements.add(insertComponent);
+
+                        statements.add(componentConfigStatement(id, "priority", Integer.toString(priority)));
+                        statements.add(componentConfigStatement(id, "fullSyncPeriod", Integer.toString(fullSyncPeriod)));
+                        statements.add(componentConfigStatement(id, "changedSyncPeriod", Integer.toString(changedSyncPeriod)));
+                        statements.add(componentConfigStatement(id, "lastSync", Integer.toString(lastSync)));
+                        PreparedStatement configStatement = jdbcConnection.prepareStatement("select name, VALUE from " + getTableName("USER_FEDERATION_CONFIG") + " WHERE USER_FEDERATION_PROVIDER_ID=?");
+                        configStatement.setString(1, id);
+                        try {
+                            ResultSet configSet = configStatement.executeQuery();
+                            try {
+                                while (configSet.next()) {
+                                    String name = configSet.getString(1);
+                                    String value = configSet.getString(2);
+                                    //logger.info("adding component config: " + name + ": " + value);
+                                    statements.add(componentConfigStatement(id, name, value));
+                                }
+                            } finally {
+                                configSet.close();
+                            }
+                        } finally {
+                            configStatement.close();
+                        }
+
+                        if (newMapperType != null) {
+                            convertFedMapperToComponent(realmId, id, newMapperType);
+                        }
+
+                        DeleteStatement configDelete = new DeleteStatement(null, null, database.correctObjectName("USER_FEDERATION_CONFIG", Table.class));
+                        configDelete.setWhere("USER_FEDERATION_PROVIDER_ID='" + id + "'");
+                        statements.add(configDelete);
+                        DeleteStatement deleteStatement = new DeleteStatement(null, null, database.correctObjectName("USER_FEDERATION_PROVIDER", Table.class));
+                        deleteStatement.setWhere("ID='" + id + "'");
+                        statements.add(deleteStatement);
+
+                    }
+                } finally {
+                    resultSet.close();
+                }
+            } finally {
+                statement.close();
+            }
+
+            confirmationMessage.append("Updated " + statements.size() + " records in USER_FEDERATION_PROVIDER table for " + providerId + " conversion to component model");
+        } catch (Exception e) {
+            throw new CustomChangeException(getTaskId() + ": Exception when updating data from previous version", e);
+        }
+    }
+
+    protected InsertStatement componentConfigStatement(String componentId, String name, String value) {
+        return new InsertStatement(null, null, database.correctObjectName("COMPONENT_CONFIG", Table.class))
+                .addColumnValue("ID", KeycloakModelUtils.generateId())
+                .addColumnValue("COMPONENT_ID", componentId)
+                .addColumnValue("NAME", name)
+                .addColumnValue("VALUE", value);
+    }
+
+    protected void convertFedMapperToComponent(String realmId, String parentId, String newMapperType) throws CustomChangeException {
+        try {
+            PreparedStatement statement = jdbcConnection.prepareStatement("select ID, NAME, FEDERATION_MAPPER_TYPE from " + getTableName("USER_FEDERATION_MAPPER") + " WHERE FEDERATION_PROVIDER_ID='" + parentId + "'");
+
+            try {
+                ResultSet resultSet = statement.executeQuery();
+                try {
+                    while (resultSet.next()) {
+                        String id = resultSet.getString(1);
+                        String mapperName = resultSet.getString(2);
+                        String fedMapperType = resultSet.getString(3);
+
+                        InsertStatement insertComponent = new InsertStatement(null, null, database.correctObjectName("COMPONENT", Table.class))
+                                .addColumnValue("ID", id)
+                                .addColumnValue("REALM_ID", realmId)
+                                .addColumnValue("PARENT_ID", parentId)
+                                .addColumnValue("NAME", mapperName)
+                                .addColumnValue("PROVIDER_ID", fedMapperType)
+                                .addColumnValue("PROVIDER_TYPE", newMapperType);
+
+                        statements.add(insertComponent);
+
+
+
+                        PreparedStatement configStatement = jdbcConnection.prepareStatement("select name, VALUE from " + getTableName("USER_FEDERATION_MAPPER_CONFIG") + " WHERE USER_FEDERATION_MAPPER_ID=?");
+                        configStatement.setString(1, id);
+                        try {
+                            ResultSet configSet = configStatement.executeQuery();
+                            try {
+                                while (configSet.next()) {
+                                    String name = configSet.getString(1);
+                                    String value = configSet.getString(2);
+                                    statements.add(componentConfigStatement(id, name, value));
+                                }
+                            } finally {
+                                configSet.close();
+                            }
+                        } finally {
+                            configStatement.close();
+                        }
+                        DeleteStatement configDelete = new DeleteStatement(null, null, database.correctObjectName("USER_FEDERATION_MAPPER_CONFIG", Table.class));
+                        configDelete.setWhere("USER_FEDERATION_MAPPER_ID='" + id + "'");
+                        statements.add(configDelete);
+                        DeleteStatement deleteStatement = new DeleteStatement(null, null, database.correctObjectName("USER_FEDERATION_MAPPER", Table.class));
+                        deleteStatement.setWhere("ID='" + id + "'");
+                        statements.add(deleteStatement);
+
+
+                    }
+                } finally {
+                    resultSet.close();
+                }
+            } finally {
+                statement.close();
+            }
+
+            confirmationMessage.append("Updated " + statements.size() + " records in USER_FEDERATION_MAPPER table for " + parentId + " conversion to component model");
+        } catch (Exception e) {
+            throw new CustomChangeException(getTaskId() + ": Exception when updating data from previous version", e);
+        }
+    }
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/PortLdapUserFedToComponentModel.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/PortLdapUserFedToComponentModel.java
new file mode 100644
index 0000000..cf224b2
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/PortLdapUserFedToComponentModel.java
@@ -0,0 +1,46 @@
+/*
+ * 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.connections.jpa.updater.liquibase.custom;
+
+import liquibase.exception.CustomChangeException;
+import liquibase.statement.core.InsertStatement;
+import liquibase.structure.core.Table;
+import org.keycloak.keys.KeyProvider;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.storage.UserStorageProvider;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class PortLdapUserFedToComponentModel extends AbstractUserFedToComponent {
+
+    @Override
+    protected void generateStatementsImpl() throws CustomChangeException {
+        String providerId = LDAPConstants.LDAP_PROVIDER;
+        convertFedProviderToComponent(LDAPConstants.LDAP_PROVIDER, "org.keycloak.storage.ldap.mappers.LDAPStorageMapper");
+    }
+
+    @Override
+    protected String getTaskId() {
+        return "Update 2.4.0.Final";
+    }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index bd03727..b0ea73a 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -48,6 +48,7 @@ import org.keycloak.models.jpa.entities.UserConsentRoleEntity;
 import org.keycloak.models.jpa.entities.UserEntity;
 import org.keycloak.models.utils.DefaultRoles;
 import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.storage.UserStorageProvider;
 
 import javax.persistence.EntityManager;
 import javax.persistence.TypedQuery;
@@ -395,33 +396,38 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
 
     @Override
     public void preRemove(RealmModel realm, UserFederationProviderModel link) {
+        String linkId = link.getId();
+        removeUserDataByLink(realm, linkId);
+    }
+
+    public void removeUserDataByLink(RealmModel realm, String linkId) {
         int num = em.createNamedQuery("deleteUserRoleMappingsByRealmAndLink")
                 .setParameter("realmId", realm.getId())
-                .setParameter("link", link.getId())
+                .setParameter("link", linkId)
                 .executeUpdate();
         num = em.createNamedQuery("deleteUserRequiredActionsByRealmAndLink")
                 .setParameter("realmId", realm.getId())
-                .setParameter("link", link.getId())
+                .setParameter("link", linkId)
                 .executeUpdate();
         num = em.createNamedQuery("deleteFederatedIdentityByRealmAndLink")
                 .setParameter("realmId", realm.getId())
-                .setParameter("link", link.getId())
+                .setParameter("link", linkId)
                 .executeUpdate();
         num = em.createNamedQuery("deleteCredentialAttributeByRealmAndLink")
                 .setParameter("realmId", realm.getId())
-                .setParameter("link", link.getId())
+                .setParameter("link", linkId)
                 .executeUpdate();
         num = em.createNamedQuery("deleteCredentialsByRealmAndLink")
                 .setParameter("realmId", realm.getId())
-                .setParameter("link", link.getId())
+                .setParameter("link", linkId)
                 .executeUpdate();
         num = em.createNamedQuery("deleteUserAttributesByRealmAndLink")
                 .setParameter("realmId", realm.getId())
-                .setParameter("link", link.getId())
+                .setParameter("link", linkId)
                 .executeUpdate();
         num = em.createNamedQuery("deleteUsersByRealmAndLink")
                 .setParameter("realmId", realm.getId())
-                .setParameter("link", link.getId())
+                .setParameter("link", linkId)
                 .executeUpdate();
     }
 
@@ -718,6 +724,8 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
 
     @Override
     public void preRemove(RealmModel realm, ComponentModel component) {
+        if (!component.getProviderType().equals(UserStorageProvider.class.getName())) return;
+        removeUserDataByLink(realm, component.getId());
 
     }
 
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 ebfa5bc..97aa4bd 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
@@ -2031,12 +2031,20 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
 
     @Override
     public ComponentModel addComponentModel(ComponentModel model) {
+        model = importComponentModel(model);
+        ComponentUtil.notifyCreated(session, this, model);
+
+        return model;
+    }
+
+    @Override
+    public ComponentModel importComponentModel(ComponentModel model) {
         ComponentFactory componentFactory = ComponentUtil.getComponentFactory(session, model);
         if (componentFactory == null) {
             throw new IllegalArgumentException("Invalid component type");
         }
 
-        componentFactory.validateConfiguration(session, model);
+        componentFactory.validateConfiguration(session, this, model);
 
         ComponentEntity c = new ComponentEntity();
         if (model.getId() == null) {
@@ -2046,6 +2054,10 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
         }
         c.setName(model.getName());
         c.setParentId(model.getParentId());
+        if (model.getParentId() == null) {
+            c.setParentId(this.getId());
+            model.setParentId(this.getId());
+        }
         c.setProviderType(model.getProviderType());
         c.setProviderId(model.getProviderId());
         c.setSubType(model.getSubType());
@@ -2053,8 +2065,6 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
         em.persist(c);
         setConfig(model, c);
         model.setId(c.getId());
-        ComponentUtil.notifyCreated(session, this, model);
-
         return model;
     }
 
@@ -2077,7 +2087,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
 
     @Override
     public void updateComponent(ComponentModel component) {
-        ComponentUtil.getComponentFactory(session, component).validateConfiguration(session, component);
+        ComponentUtil.getComponentFactory(session, component).validateConfiguration(session, this, component);
 
         ComponentEntity c = em.find(ComponentEntity.class, component.getId());
         if (c == null) return;
@@ -2098,6 +2108,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
         ComponentEntity c = em.find(ComponentEntity.class, component.getId());
         if (c == null) return;
         session.users().preRemove(this, component);
+        removeComponents(component.getId());
         em.createNamedQuery("deleteComponentConfigByComponent").setParameter("component", c).executeUpdate();
         em.remove(c);
     }
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-2.4.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.4.0.xml
new file mode 100755
index 0000000..0aebd9a
--- /dev/null
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.4.0.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+  ~ 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.
+  -->
+
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
+
+     <changeSet author="bburke@redhat.com" id="2.4.0">
+         <customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.PortLdapUserFedToComponentModel"/>
+     </changeSet>
+
+</databaseChangeLog>
\ No newline at end of file
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
index dc16e0d..ebe8cf3 100755
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
@@ -43,4 +43,5 @@
     <include file="META-INF/jpa-changelog-2.1.0.xml"/>
     <include file="META-INF/jpa-changelog-2.2.0.xml"/>
     <include file="META-INF/jpa-changelog-2.3.0.xml"/>
+    <include file="META-INF/jpa-changelog-2.4.0.xml"/>
 </databaseChangeLog>
diff --git a/model/mongo/src/main/java/org/keycloak/connections/mongo/updater/impl/DefaultMongoUpdaterProvider.java b/model/mongo/src/main/java/org/keycloak/connections/mongo/updater/impl/DefaultMongoUpdaterProvider.java
index 3e429e2..c7f5be8 100755
--- a/model/mongo/src/main/java/org/keycloak/connections/mongo/updater/impl/DefaultMongoUpdaterProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/connections/mongo/updater/impl/DefaultMongoUpdaterProvider.java
@@ -34,6 +34,7 @@ import org.keycloak.connections.mongo.updater.impl.updates.Update1_7_0;
 import org.keycloak.connections.mongo.updater.impl.updates.Update1_8_0;
 import org.keycloak.connections.mongo.updater.impl.updates.Update1_9_2;
 import org.keycloak.connections.mongo.updater.impl.updates.Update2_3_0;
+import org.keycloak.connections.mongo.updater.impl.updates.Update2_4_0;
 import org.keycloak.models.KeycloakSession;
 
 import java.util.Date;
@@ -59,7 +60,8 @@ public class DefaultMongoUpdaterProvider implements MongoUpdaterProvider {
             Update1_7_0.class,
             Update1_8_0.class,
             Update1_9_2.class,
-            Update2_3_0.class
+            Update2_3_0.class,
+            Update2_4_0.class
     };
 
     @Override
diff --git a/model/mongo/src/main/java/org/keycloak/connections/mongo/updater/impl/updates/Update2_4_0.java b/model/mongo/src/main/java/org/keycloak/connections/mongo/updater/impl/updates/Update2_4_0.java
new file mode 100644
index 0000000..d0f9405
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/connections/mongo/updater/impl/updates/Update2_4_0.java
@@ -0,0 +1,181 @@
+/*
+ * 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.connections.mongo.updater.impl.updates;
+
+import com.mongodb.BasicDBList;
+import com.mongodb.BasicDBObject;
+import com.mongodb.DBCollection;
+import com.mongodb.DBCursor;
+import org.jboss.logging.Logger;
+import org.keycloak.keys.KeyProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.storage.UserStorageProvider;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class Update2_4_0 extends Update {
+    private final Logger logger = Logger.getLogger(getClass());
+
+    @Override
+    public String getId() {
+        return "2.4.0";
+    }
+
+    @Override
+    public void update(KeycloakSession session) {
+        portUserFedMappersToComponent(LDAPConstants.LDAP_PROVIDER, "org.keycloak.storage.ldap.mappers.LDAPStorageMapper");
+        portUserFedToComponent(LDAPConstants.LDAP_PROVIDER);
+    }
+
+    public void portUserFedToComponent(String providerId) {
+        DBCollection realms = db.getCollection("realms");
+        DBCursor cursor = realms.find();
+        while (cursor.hasNext()) {
+            BasicDBObject realm = (BasicDBObject) cursor.next();
+
+            String realmId = realm.getString("_id");
+            Set<String> removedProviders = new HashSet<>();
+
+            BasicDBList componentEntities = (BasicDBList) realm.get("componentEntities");
+            BasicDBList federationProviders = (BasicDBList) realm.get("userFederationProviders");
+            for (Object obj : federationProviders) {
+                BasicDBObject fedProvider = (BasicDBObject)obj;
+                if (fedProvider.getString("providerName").equals(providerId)) {
+                    String id = fedProvider.getString("id");
+                    removedProviders.add(id);
+                    int priority = fedProvider.getInt("priority");
+                    String displayName = fedProvider.getString("displayName");
+                    int fullSyncPeriod = fedProvider.getInt("fullSyncPeriod");
+                    int changedSyncPeriod = fedProvider.getInt("changedSyncPeriod");
+                    int lastSync = fedProvider.getInt("lastSync");
+                    BasicDBObject component = new BasicDBObject();
+                    component.put("id", id);
+                    component.put("name", displayName);
+                    component.put("providerType", UserStorageProvider.class.getName());
+                    component.put("providerId", providerId);
+                    component.put("parentId", realmId);
+
+                    BasicDBObject config = new BasicDBObject();
+                    config.put("priority", Collections.singletonList(Integer.toString(priority)));
+                    config.put("fullSyncPeriod", Collections.singletonList(Integer.toString(fullSyncPeriod)));
+                    config.put("changedSyncPeriod", Collections.singletonList(Integer.toString(changedSyncPeriod)));
+                    config.put("lastSync", Collections.singletonList(Integer.toString(lastSync)));
+
+                    BasicDBObject fedConfig = (BasicDBObject)fedProvider.get("config");
+                    if (fedConfig != null) {
+                        for (Map.Entry<String, Object> attr : new HashSet<>(fedConfig.entrySet())) {
+                            String attrName = attr.getKey();
+                            String attrValue = attr.getValue().toString();
+                            config.put(attrName, Collections.singletonList(attrValue));
+
+                        }
+                    }
+
+
+                    component.put("config", config);
+
+                    componentEntities.add(component);
+
+                }
+            }
+            Iterator<Object> it = federationProviders.iterator();
+            while (it.hasNext()) {
+                BasicDBObject fedProvider = (BasicDBObject)it.next();
+                String id = fedProvider.getString("id");
+                if (removedProviders.contains(id)) {
+                    it.remove();
+                }
+
+            }
+            realms.update(new BasicDBObject().append("_id", realmId), realm);
+        }
+    }
+    public void portUserFedMappersToComponent(String providerId, String mapperType) {
+        //logger.info("*** port mappers");
+        DBCollection realms = db.getCollection("realms");
+        DBCursor cursor = realms.find();
+        while (cursor.hasNext()) {
+            BasicDBObject realm = (BasicDBObject) cursor.next();
+
+            String realmId = realm.getString("_id");
+            Set<String> removedProviders = new HashSet<>();
+
+            BasicDBList componentEntities = (BasicDBList) realm.get("componentEntities");
+            BasicDBList federationProviders = (BasicDBList) realm.get("userFederationProviders");
+            BasicDBList fedMappers = (BasicDBList) realm.get("userFederationMappers");
+            for (Object obj : federationProviders) {
+                BasicDBObject fedProvider = (BasicDBObject)obj;
+                String providerName = fedProvider.getString("providerName");
+                //logger.info("looking for mappers of fed provider: " + providerName);
+                if (providerName.equals(providerId)) {
+                    String id = fedProvider.getString("id");
+                    //logger.info("found fed provider: " + id + ", looking at mappers");
+                    for (Object obj2 : fedMappers) {
+                        BasicDBObject fedMapper = (BasicDBObject)obj2;
+                        String federationProviderId = fedMapper.getString("federationProviderId");
+                        //logger.info("looking at mapper with federationProviderId: " + federationProviderId);
+                        if (federationProviderId.equals(id)) {
+                            String name = fedMapper.getString("name");
+                            String mapperId = fedMapper.getString("id");
+                            removedProviders.add(mapperId);
+                            String mapperProviderId = fedMapper.getString("federationMapperType");
+                            BasicDBObject component = new BasicDBObject();
+                            component.put("id", mapperId);
+                            component.put("name", name);
+                            component.put("providerType", mapperType);
+                            component.put("providerId", mapperProviderId);
+                            component.put("parentId", id);
+
+                            BasicDBObject fedConfig = (BasicDBObject)fedMapper.get("config");
+                            BasicDBObject config = new BasicDBObject();
+                            if (fedConfig != null) {
+                                for (Map.Entry<String, Object> attr : new HashSet<>(fedConfig.entrySet())) {
+                                    String attrName = attr.getKey();
+                                    String attrValue = attr.getValue().toString();
+                                    config.put(attrName, Collections.singletonList(attrValue));
+
+                                }
+                            }
+                            component.put("config", config);
+                            componentEntities.add(component);
+                        }
+                    }
+                }
+            }
+            Iterator<Object> it = fedMappers.iterator();
+            while (it.hasNext()) {
+                BasicDBObject fedMapper = (BasicDBObject)it.next();
+                String id = fedMapper.getString("id");
+                if (removedProviders.contains(id)) {
+                    it.remove();
+                }
+
+            }
+            realms.update(new BasicDBObject().append("_id", realmId), realm);
+        }
+    }
+}
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 e60055b..a79c478 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
@@ -1954,7 +1954,14 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
 
     @Override
     public ComponentModel addComponentModel(ComponentModel model) {
-        ComponentUtil.getComponentFactory(session, model).validateConfiguration(session, model);
+        model = importComponentModel(model);
+        ComponentUtil.notifyCreated(session, this, model);
+        return model;
+    }
+
+    @Override
+    public ComponentModel importComponentModel(ComponentModel model) {
+        ComponentUtil.getComponentFactory(session, model).validateConfiguration(session, this, model);
 
         ComponentEntity entity = new ComponentEntity();
         if (model.getId() == null) {
@@ -1964,15 +1971,18 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
         }
         updateComponentEntity(entity, model);
         model.setId(entity.getId());
+        if (model.getParentId() == null) {
+            entity.setParentId(this.getId());
+            model.setParentId(this.getId());
+        }
         realm.getComponentEntities().add(entity);
         updateRealm();
-        ComponentUtil.notifyCreated(session, this, model);
         return model;
     }
 
     @Override
     public void updateComponent(ComponentModel model) {
-        ComponentUtil.getComponentFactory(session, model).validateConfiguration(session, model);
+        ComponentUtil.getComponentFactory(session, model).validateConfiguration(session, this, model);
 
         for (ComponentEntity entity : realm.getComponentEntities()) {
             if (entity.getId().equals(model.getId())) {
@@ -1999,6 +2009,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
         while(it.hasNext()) {
             if (it.next().getId().equals(component.getId())) {
                 session.users().preRemove(this, component);
+                removeComponents(component.getId());
                 it.remove();
                 break;
             }
diff --git a/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java b/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java
index c388756..d519286 100644
--- a/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java
+++ b/server-spi/src/main/java/org/keycloak/component/ComponentFactory.java
@@ -25,6 +25,7 @@ import org.keycloak.provider.ProviderFactory;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -38,7 +39,7 @@ public interface ComponentFactory<CreatedType, ProviderType extends Provider> ex
         return null;
     }
 
-    void validateConfiguration(KeycloakSession session, ComponentModel model) throws ComponentValidationException;
+    void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException;
 
     default
     void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
@@ -55,5 +56,17 @@ public interface ComponentFactory<CreatedType, ProviderType extends Provider> ex
         return Collections.EMPTY_LIST;
     }
 
+    /**
+     * This is metadata about this component type.  Its really configuration information about the component type and not
+     * an individual instance
+     *
+     * @return
+     */
+    default
+    Map<String, Object> getTypeMetadata() {
+        return Collections.EMPTY_MAP;
+
+    }
+
 
 }
diff --git a/server-spi/src/main/java/org/keycloak/component/ComponentModel.java b/server-spi/src/main/java/org/keycloak/component/ComponentModel.java
index dc5c680..f6c26d8 100755
--- a/server-spi/src/main/java/org/keycloak/component/ComponentModel.java
+++ b/server-spi/src/main/java/org/keycloak/component/ComponentModel.java
@@ -48,7 +48,7 @@ public class ComponentModel implements Serializable {
         this.providerType = copy.providerType;
         this.parentId = copy.parentId;
         this.subType = copy.subType;
-        this.config = copy.config;
+        this.config.addAll(copy.config);
     }
 
 
diff --git a/server-spi/src/main/java/org/keycloak/component/SubComponentFactory.java b/server-spi/src/main/java/org/keycloak/component/SubComponentFactory.java
new file mode 100644
index 0000000..56c012d
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/component/SubComponentFactory.java
@@ -0,0 +1,53 @@
+/*
+ * 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.component;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ConfiguredProvider;
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderFactory;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Useful when you want to describe config properties that are effected by the parent ComponentModel
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface SubComponentFactory<CreatedType, ProviderType extends Provider> extends ComponentFactory<CreatedType, ProviderType> {
+    default
+    List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) {
+        return getConfigProperties();
+    }
+
+    /**
+     * This is metadata about this component type.  Its really configuration information about the component type and not
+     * an individual instance
+     *
+     * @return
+     */
+    default Map<String, Object> getTypeMetadata(RealmModel realm, ComponentModel parent) {
+        return getTypeMetadata();
+
+    }
+
+}
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/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index aa8b440..e9df047 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
@@ -274,7 +274,22 @@ public interface RealmModel extends RoleContainerModel {
     public IdentityProviderMapperModel getIdentityProviderMapperByName(String brokerAlias, String name);
 
 
+    /**
+     * Adds component model.  Will call onCreate() method of ComponentFactory
+     *
+     * @param model
+     * @return
+     */
     ComponentModel addComponentModel(ComponentModel model);
+
+    /**
+     * Adds component model.  Will NOT call onCreate() method of ComponentFactory
+     *
+     * @param model
+     * @return
+     */
+    ComponentModel importComponentModel(ComponentModel model);
+
     void updateComponent(ComponentModel component);
     void removeComponent(ComponentModel component);
     void removeComponents(String parentId);
diff --git a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigurationBuilder.java b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigurationBuilder.java
index 607a41b..194e8e8 100644
--- a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigurationBuilder.java
+++ b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigurationBuilder.java
@@ -102,6 +102,12 @@ public class ProviderConfigurationBuilder {
             return this;
         }
 
+        public ProviderConfigPropertyBuilder options(List<String> options) {
+            this.options = options;
+            return this;
+        }
+
+
         public ProviderConfigPropertyBuilder secret(boolean secret) {
             this.secret = secret;
             return this;
diff --git a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderFactory.java b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderFactory.java
index 121a5ea..18e291a 100755
--- a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderFactory.java
+++ b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderFactory.java
@@ -26,10 +26,13 @@ import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.RealmModel;
 import org.keycloak.provider.ProviderConfigProperty;
 import org.keycloak.provider.ProviderConfigurationBuilder;
+import org.keycloak.storage.user.ImportSynchronization;
 
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -81,7 +84,7 @@ public interface UserStorageProviderFactory<T extends UserStorageProvider> exten
     }
 
     @Override
-    default void validateConfiguration(KeycloakSession session, ComponentModel config) throws ComponentValidationException {
+    default void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
 
     }
 
@@ -108,4 +111,14 @@ public interface UserStorageProviderFactory<T extends UserStorageProvider> exten
     List<ProviderConfigProperty> getCommonProviderConfigProperties() {
         return UserStorageProviderSpi.commonConfig();
     }
+
+    @Override
+    default
+    Map<String, Object> getTypeMetadata() {
+        Map<String, Object> metadata = new HashMap<>();
+        if (this instanceof ImportSynchronization) {
+            metadata.put("synchronizable", true);
+        }
+        return metadata;
+    }
 }
diff --git a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderSpi.java b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderSpi.java
index 625adeb..06bc256 100755
--- a/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderSpi.java
+++ b/server-spi/src/main/java/org/keycloak/storage/UserStorageProviderSpi.java
@@ -64,6 +64,8 @@ public class UserStorageProviderSpi implements Spi {
                 .property()
                 .name("lastSync").type(ProviderConfigProperty.STRING_TYPE).add()
                 .property()
+                .name("batchSizeForSync").type(ProviderConfigProperty.STRING_TYPE).add()
+                .property()
                 .name("importEnabled").type(ProviderConfigProperty.BOOLEAN_TYPE).add()
                 .property()
                 .name("cachePolicy").type(ProviderConfigProperty.STRING_TYPE).add()
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index 4f355a5..e265233 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/server-spi-private/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;
@@ -304,6 +305,52 @@ 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);
+        mapperModel.setName(name);
+        mapperModel.setProviderId(providerId);
+        mapperModel.setProviderType(providerType);
+
+        String key = null;
+        for (String configEntry : config) {
+            if (key == null) {
+                key = configEntry;
+            } else {
+                mapperModel.getConfig().add(key, configEntry);
+                key = null;
+            }
+        }
+        if (key != null) {
+            throw new IllegalStateException("Invalid count of arguments for config. Maybe mistake?");
+        }
+
+        return mapperModel;
+    }
+
+
 
     public static UserFederationMapperModel createUserFederationMapperModel(String name, String federationProviderId, String mapperType, String... config) {
         UserFederationMapperModel mapperModel = new UserFederationMapperModel();
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index e35da61..bf4a6dc 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -352,19 +352,7 @@ public class ModelToRepresentation {
             }
         }
 
-        List<UserFederationProviderModel> fedProviderModels = realm.getUserFederationProviders();
-        if (fedProviderModels.size() > 0) {
-            List<UserFederationProviderRepresentation> fedProviderReps = new ArrayList<UserFederationProviderRepresentation>();
-            for (UserFederationProviderModel model : fedProviderModels) {
-                UserFederationProviderRepresentation fedProvRep = toRepresentation(model);
-                fedProviderReps.add(fedProvRep);
-            }
-            rep.setUserFederationProviders(fedProviderReps);
-        }
-
-        for (UserFederationMapperModel mapper : realm.getUserFederationMappers()) {
-            rep.addUserFederationMapper(toRepresentation(realm, mapper));
-        }
+        exportUserFederationProvidersAndMappers(realm, rep);
 
         for (IdentityProviderModel provider : realm.getIdentityProviders()) {
             rep.addIdentityProvider(toRepresentation(realm, provider));
@@ -396,6 +384,22 @@ public class ModelToRepresentation {
         return rep;
     }
 
+    public static void exportUserFederationProvidersAndMappers(RealmModel realm, RealmRepresentation rep) {
+        List<UserFederationProviderModel> fedProviderModels = realm.getUserFederationProviders();
+        if (fedProviderModels.size() > 0) {
+            List<UserFederationProviderRepresentation> fedProviderReps = new ArrayList<UserFederationProviderRepresentation>();
+            for (UserFederationProviderModel model : fedProviderModels) {
+                UserFederationProviderRepresentation fedProvRep = toRepresentation(model);
+                fedProviderReps.add(fedProvRep);
+            }
+            rep.setUserFederationProviders(fedProviderReps);
+        }
+
+        for (UserFederationMapperModel mapper : realm.getUserFederationMappers()) {
+            rep.addUserFederationMapper(toRepresentation(realm, mapper));
+        }
+    }
+
     public static void exportGroups(RealmModel realm, RealmRepresentation rep) {
         List<GroupRepresentation> groups = toGroupHierarchy(realm, true);
         rep.setGroups(groups);
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 1261a41..ee4b561 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -51,6 +51,7 @@ import org.keycloak.models.GroupModel;
 import org.keycloak.models.IdentityProviderMapperModel;
 import org.keycloak.models.IdentityProviderModel;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
 import org.keycloak.models.ModelException;
 import org.keycloak.models.OTPPolicy;
 import org.keycloak.models.PasswordPolicy;
@@ -98,6 +99,8 @@ import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentatio
 import org.keycloak.representations.idm.authorization.ResourceRepresentation;
 import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
 import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.UserStorageProviderModel;
 import org.keycloak.storage.federated.UserFederatedStorageProvider;
 import org.keycloak.util.JsonSerialization;
 
@@ -298,36 +301,8 @@ public class RepresentationToModel {
             String parentId = newRealm.getId();
             importComponents(newRealm, components, parentId);
         }
+        importUserFederationProvidersAndMappers(rep, newRealm);
 
-        List<UserFederationProviderModel> providerModels = null;
-        if (rep.getUserFederationProviders() != null) {
-            providerModels = convertFederationProviders(rep.getUserFederationProviders());
-            newRealm.setUserFederationProviders(providerModels);
-        }
-        if (rep.getUserFederationMappers() != null) {
-
-            // Remove builtin mappers for federation providers, which have some mappers already provided in JSON (likely due to previous export)
-            if (rep.getUserFederationProviders() != null) {
-                Set<String> providerNames = new TreeSet<String>();
-                for (UserFederationMapperRepresentation representation : rep.getUserFederationMappers()) {
-                    providerNames.add(representation.getFederationProviderDisplayName());
-                }
-                for (String providerName : providerNames) {
-                    for (UserFederationProviderModel providerModel : providerModels) {
-                        if (providerName.equals(providerModel.getDisplayName())) {
-                            Set<UserFederationMapperModel> toDelete = newRealm.getUserFederationMappersByFederationProvider(providerModel.getId());
-                            for (UserFederationMapperModel mapperModel : toDelete) {
-                                newRealm.removeUserFederationMapper(mapperModel);
-                            }
-                        }
-                    }
-                }
-            }
-
-            for (UserFederationMapperRepresentation representation : rep.getUserFederationMappers()) {
-                newRealm.addUserFederationMapper(toModel(newRealm, representation));
-            }
-        }
 
         if (rep.getGroups() != null) {
             importGroups(newRealm, rep);
@@ -383,6 +358,64 @@ public class RepresentationToModel {
         }
     }
 
+    public static void importUserFederationProvidersAndMappers(RealmRepresentation rep, RealmModel newRealm) {
+        // providers to convert to component model
+        Set<String> convertSet = new HashSet<>();
+        convertSet.add(LDAPConstants.LDAP_PROVIDER);
+        Map<String, String> mapperConvertSet = new HashMap<>();
+        mapperConvertSet.put(LDAPConstants.LDAP_PROVIDER, "org.keycloak.storage.ldap.mappers.LDAPStorageMapper");
+
+
+        List<UserFederationProviderModel> providerModels = null;
+        Map<String, ComponentModel> userStorageModels = new HashMap<>();
+
+        if (rep.getUserFederationProviders() != null) {
+            providerModels = new LinkedList<>();
+            for (UserFederationProviderRepresentation fedRep : rep.getUserFederationProviders()) {
+                if (convertSet.contains(fedRep.getProviderName())) {
+                    ComponentModel component = convertFedProviderToComponent(newRealm.getId(), fedRep);
+                    userStorageModels.put(fedRep.getDisplayName(), newRealm.importComponentModel(component));
+                } else {
+                    providerModels.add(convertFederationProvider(fedRep));
+                }
+
+            }
+            newRealm.setUserFederationProviders(providerModels);
+        }
+        if (rep.getUserFederationMappers() != null) {
+
+            // Remove builtin mappers for federation providers, which have some mappers already provided in JSON (likely due to previous export)
+            if (rep.getUserFederationProviders() != null) {
+                Set<String> providerNames = new TreeSet<String>();
+                for (UserFederationMapperRepresentation representation : rep.getUserFederationMappers()) {
+                    providerNames.add(representation.getFederationProviderDisplayName());
+                }
+                for (String providerName : providerNames) {
+                    for (UserFederationProviderModel providerModel : providerModels) {
+                        if (providerName.equals(providerModel.getDisplayName())) {
+                            Set<UserFederationMapperModel> toDelete = newRealm.getUserFederationMappersByFederationProvider(providerModel.getId());
+                            for (UserFederationMapperModel mapperModel : toDelete) {
+                                newRealm.removeUserFederationMapper(mapperModel);
+                            }
+                        }
+                    }
+                }
+            }
+
+            for (UserFederationMapperRepresentation representation : rep.getUserFederationMappers()) {
+                if (userStorageModels.containsKey(representation.getFederationProviderDisplayName())) {
+                    ComponentModel parent = userStorageModels.get(representation.getFederationProviderDisplayName());
+                    String newMapperType = mapperConvertSet.get(parent.getProviderId());
+                    ComponentModel mapper = convertFedMapperToComponent(newRealm, parent, representation, newMapperType);
+                    newRealm.importComponentModel(mapper);
+
+                } else {
+                    newRealm.addUserFederationMapper(toModel(newRealm, representation));
+                }
+            }
+        }
+    }
+
     protected static void importComponents(RealmModel newRealm, MultivaluedHashMap<String, ComponentExportRepresentation> components, String parentId) {
         for (Map.Entry<String, List<ComponentExportRepresentation>> entry : components.entrySet()) {
             String providerType = entry.getKey();
@@ -395,7 +428,7 @@ public class RepresentationToModel {
                 component.setProviderId(compRep.getProviderId());
                 component.setSubType(compRep.getSubType());
                 component.setParentId(parentId);
-                component = newRealm.addComponentModel(component);
+                component = newRealm.importComponentModel(component);
                 if (compRep.getSubComponents() != null) {
                     importComponents(newRealm, compRep.getSubComponents(), component.getId());
                 }
@@ -858,14 +891,53 @@ public class RepresentationToModel {
         List<UserFederationProviderModel> result = new ArrayList<UserFederationProviderModel>();
 
         for (UserFederationProviderRepresentation representation : providers) {
-            UserFederationProviderModel model = new UserFederationProviderModel(representation.getId(), representation.getProviderName(),
-                    representation.getConfig(), representation.getPriority(), representation.getDisplayName(),
-                    representation.getFullSyncPeriod(), representation.getChangedSyncPeriod(), representation.getLastSync());
+            UserFederationProviderModel model = convertFederationProvider(representation);
             result.add(model);
         }
         return result;
     }
 
+    private static UserFederationProviderModel convertFederationProvider(UserFederationProviderRepresentation representation) {
+        return new UserFederationProviderModel(representation.getId(), representation.getProviderName(),
+                        representation.getConfig(), representation.getPriority(), representation.getDisplayName(),
+                        representation.getFullSyncPeriod(), representation.getChangedSyncPeriod(), representation.getLastSync());
+    }
+
+    public static ComponentModel convertFedProviderToComponent(String realmId, UserFederationProviderRepresentation fedModel) {
+        UserStorageProviderModel model = new UserStorageProviderModel();
+        model.setId(fedModel.getId());
+        model.setName(fedModel.getDisplayName());
+        model.setParentId(realmId);
+        model.setProviderId(fedModel.getProviderName());
+        model.setProviderType(UserStorageProvider.class.getName());
+        model.setFullSyncPeriod(fedModel.getFullSyncPeriod());
+        model.setPriority(fedModel.getPriority());
+        model.setChangedSyncPeriod(fedModel.getChangedSyncPeriod());
+        model.setLastSync(fedModel.getLastSync());
+        if (fedModel.getConfig() != null) {
+            for (Map.Entry<String, String> entry : fedModel.getConfig().entrySet()) {
+                model.getConfig().putSingle(entry.getKey(), entry.getValue());
+            }
+        }
+        return model;
+    }
+
+    public static ComponentModel convertFedMapperToComponent(RealmModel realm, ComponentModel parent, UserFederationMapperRepresentation rep, String newMapperType) {
+        ComponentModel mapper = new ComponentModel();
+        mapper.setId(rep.getId());
+        mapper.setName(rep.getName());
+        mapper.setProviderId(rep.getFederationMapperType());
+        mapper.setProviderType(newMapperType);
+        mapper.setParentId(parent.getId());
+        if (rep.getConfig() != null) {
+            for (Map.Entry<String, String> entry : rep.getConfig().entrySet()) {
+                mapper.getConfig().putSingle(entry.getKey(), entry.getValue());
+            }
+        }
+        return mapper;
+    }
+
+
     public static UserFederationMapperModel toModel(RealmModel realm, UserFederationMapperRepresentation rep) {
         UserFederationMapperModel model = new UserFederationMapperModel();
         model.setId(rep.getId());

services/pom.xml 5(+5 -0)

diff --git a/services/pom.xml b/services/pom.xml
index 0fb68f7..f6fdb74 100755
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -69,6 +69,11 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-ldap-federation</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>org.jboss.spec.javax.servlet</groupId>
             <artifactId>jboss-servlet-api_3.0_spec</artifactId>
         </dependency>
diff --git a/services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java b/services/src/main/java/org/keycloak/credential/OTPCredentialProvider.java
index f71f84a..9635146 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) {
@@ -228,7 +230,7 @@ public class OTPCredentialProvider implements CredentialProvider, CredentialInpu
         } else {
             TimeBasedOTP validator = new TimeBasedOTP(policy.getAlgorithm(), policy.getDigits(), policy.getPeriod(), policy.getLookAheadWindow());
             List<CredentialModel> creds = getCachedCredentials(user, CredentialModel.TOTP);
-            if (creds.isEmpty()) {
+            if (creds == null) {
                 creds = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.TOTP);
             } else {
                 logger.debugv("Cache hit for TOTP for user {0}", user.getUsername());
diff --git a/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java b/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java
index 97a92ea..bdc32e7 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/keys/AbstractRsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProviderFactory.java
index 7b2d526..1c2af4f 100644
--- a/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProviderFactory.java
+++ b/services/src/main/java/org/keycloak/keys/AbstractRsaKeyProviderFactory.java
@@ -20,6 +20,7 @@ package org.keycloak.keys;
 import org.keycloak.component.ComponentModel;
 import org.keycloak.component.ComponentValidationException;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
 import org.keycloak.provider.ConfigurationValidationHelper;
 import org.keycloak.provider.ProviderConfigurationBuilder;
 
@@ -36,7 +37,7 @@ public abstract class AbstractRsaKeyProviderFactory implements KeyProviderFactor
     }
 
     @Override
-    public void validateConfiguration(KeycloakSession session, ComponentModel model) throws ComponentValidationException {
+    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
         ConfigurationValidationHelper.check(model)
                 .checkLong(Attributes.PRIORITY_PROPERTY, false)
                 .checkBoolean(Attributes.ENABLED_PROPERTY, false)
diff --git a/services/src/main/java/org/keycloak/keys/GeneratedRsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/GeneratedRsaKeyProviderFactory.java
index 9042b48..cba36d0 100644
--- a/services/src/main/java/org/keycloak/keys/GeneratedRsaKeyProviderFactory.java
+++ b/services/src/main/java/org/keycloak/keys/GeneratedRsaKeyProviderFactory.java
@@ -57,8 +57,8 @@ public class GeneratedRsaKeyProviderFactory extends AbstractRsaKeyProviderFactor
     }
 
     @Override
-    public void validateConfiguration(KeycloakSession session, ComponentModel model) throws ComponentValidationException {
-        super.validateConfiguration(session, model);
+    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
+        super.validateConfiguration(session, realm, model);
 
         ConfigurationValidationHelper.check(model)
                 .checkInt(Attributes.KEY_SIZE_PROPERTY, false);
@@ -75,7 +75,6 @@ public class GeneratedRsaKeyProviderFactory extends AbstractRsaKeyProviderFactor
         }
 
         if (!(model.contains(Attributes.PRIVATE_KEY_KEY) && model.contains(Attributes.CERTIFICATE_KEY))) {
-            RealmModel realm = session.realms().getRealm(model.getParentId());
             generateKeys(realm, model, size);
 
             logger.debugv("Generated keys for {0}", realm.getName());
@@ -83,7 +82,6 @@ public class GeneratedRsaKeyProviderFactory extends AbstractRsaKeyProviderFactor
             PrivateKey privateKey = PemUtils.decodePrivateKey(model.get(Attributes.PRIVATE_KEY_KEY));
             int currentSize = ((RSAPrivateKey) privateKey).getModulus().bitLength();
             if (currentSize != size) {
-                RealmModel realm = session.realms().getRealm(model.getParentId());
                 generateKeys(realm, model, size);
 
                 logger.debugv("Key size changed, generating new keys for {0}", realm.getName());
diff --git a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java
index cbc003e..51b726a 100644
--- a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java
+++ b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java
@@ -22,6 +22,7 @@ import org.keycloak.component.ComponentModel;
 import org.keycloak.component.ComponentValidationException;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
 import org.keycloak.provider.ConfigurationValidationHelper;
 import org.keycloak.provider.ProviderConfigProperty;
 
@@ -65,8 +66,8 @@ public class JavaKeystoreKeyProviderFactory extends AbstractRsaKeyProviderFactor
     }
 
     @Override
-    public void validateConfiguration(KeycloakSession session, ComponentModel model) throws ComponentValidationException {
-        super.validateConfiguration(session, model);
+    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
+        super.validateConfiguration(session, realm, model);
 
         ConfigurationValidationHelper.check(model)
                 .checkSingle(KEYSTORE_PROPERTY, true)
diff --git a/services/src/main/java/org/keycloak/keys/RsaKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/RsaKeyProviderFactory.java
index f6814da..aac9fbd 100644
--- a/services/src/main/java/org/keycloak/keys/RsaKeyProviderFactory.java
+++ b/services/src/main/java/org/keycloak/keys/RsaKeyProviderFactory.java
@@ -56,8 +56,8 @@ public class RsaKeyProviderFactory extends AbstractRsaKeyProviderFactory {
     }
 
     @Override
-    public void validateConfiguration(KeycloakSession session, ComponentModel model) throws ComponentValidationException {
-        super.validateConfiguration(session, model);
+    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
+        super.validateConfiguration(session, realm, model);
 
         ConfigurationValidationHelper.check(model)
                 .checkSingle(Attributes.PRIVATE_KEY_PROPERTY, true)
@@ -89,7 +89,6 @@ public class RsaKeyProviderFactory extends AbstractRsaKeyProviderFactory {
             }
         } else {
             try {
-                RealmModel realm = session.realms().getRealm(model.getParentId());
                 Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, realm.getName());
                 model.put(Attributes.CERTIFICATE_KEY, PemUtils.encodeCertificate(certificate));
             } catch (Throwable t) {
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/policy/AbstractClientRegistrationPolicyFactory.java b/services/src/main/java/org/keycloak/services/clientregistration/policy/AbstractClientRegistrationPolicyFactory.java
index 572a712..e29848c 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/policy/AbstractClientRegistrationPolicyFactory.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/policy/AbstractClientRegistrationPolicyFactory.java
@@ -24,6 +24,7 @@ import org.keycloak.component.ComponentModel;
 import org.keycloak.component.ComponentValidationException;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
 import org.keycloak.provider.ProviderConfigProperty;
 
 /**
@@ -47,7 +48,7 @@ public abstract class AbstractClientRegistrationPolicyFactory implements ClientR
     }
 
     @Override
-    public void validateConfiguration(KeycloakSession session, ComponentModel config) throws ComponentValidationException {
+    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/MaxClientsClientRegistrationPolicyFactory.java b/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/MaxClientsClientRegistrationPolicyFactory.java
index 00ebe05..120f280 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/MaxClientsClientRegistrationPolicyFactory.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/MaxClientsClientRegistrationPolicyFactory.java
@@ -23,6 +23,7 @@ import java.util.List;
 import org.keycloak.component.ComponentModel;
 import org.keycloak.component.ComponentValidationException;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
 import org.keycloak.provider.ConfigurationValidationHelper;
 import org.keycloak.provider.ProviderConfigProperty;
 import org.keycloak.services.clientregistration.policy.AbstractClientRegistrationPolicyFactory;
@@ -72,7 +73,7 @@ public class MaxClientsClientRegistrationPolicyFactory extends AbstractClientReg
     }
 
     @Override
-    public void validateConfiguration(KeycloakSession session, ComponentModel config) throws ComponentValidationException {
+    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
         ConfigurationValidationHelper.check(config)
                 .checkInt(MAX_CLIENTS_PROPERTY, true);
     }
diff --git a/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/TrustedHostClientRegistrationPolicyFactory.java b/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/TrustedHostClientRegistrationPolicyFactory.java
index a5436c1..c881f64 100644
--- a/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/TrustedHostClientRegistrationPolicyFactory.java
+++ b/services/src/main/java/org/keycloak/services/clientregistration/policy/impl/TrustedHostClientRegistrationPolicyFactory.java
@@ -23,6 +23,7 @@ import java.util.List;
 import org.keycloak.component.ComponentModel;
 import org.keycloak.component.ComponentValidationException;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
 import org.keycloak.provider.ConfigurationValidationHelper;
 import org.keycloak.provider.ProviderConfigProperty;
 import org.keycloak.services.clientregistration.policy.AbstractClientRegistrationPolicyFactory;
@@ -70,7 +71,7 @@ public class TrustedHostClientRegistrationPolicyFactory extends AbstractClientRe
     }
 
     @Override
-    public void validateConfiguration(KeycloakSession session, ComponentModel config) throws ComponentValidationException {
+    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
         ConfigurationValidationHelper.check(config)
                 .checkBoolean(HOST_SENDING_REGISTRATION_REQUEST_MUST_MATCH_PROPERTY, true)
                 .checkBoolean(CLIENT_URIS_MUST_MATCH_PROPERTY, true);
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java
index 0039fa0..4ddcaf2 100755
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java
@@ -92,9 +92,11 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan
 
         if (jtaPolicy == JTAPolicy.REQUIRES_NEW) {
             JtaTransactionManagerLookup jtaLookup = session.getProvider(JtaTransactionManagerLookup.class);
-            TransactionManager tm = jtaLookup.getTransactionManager();
-            if (tm != null) {
-                enlist(new JtaTransactionWrapper(tm));
+            if (jtaLookup != null) {
+                TransactionManager tm = jtaLookup.getTransactionManager();
+                if (tm != null) {
+                   enlist(new JtaTransactionWrapper(tm));
+                }
             }
         }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java
index 5919b2d..56c7ce7 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java
@@ -20,15 +20,22 @@ import org.jboss.logging.Logger;
 import org.jboss.resteasy.annotations.cache.NoCache;
 import org.jboss.resteasy.spi.NotFoundException;
 import org.keycloak.common.ClientConnection;
+import org.keycloak.component.ComponentFactory;
 import org.keycloak.component.ComponentModel;
 import org.keycloak.component.ComponentValidationException;
+import org.keycloak.component.SubComponentFactory;
 import org.keycloak.events.admin.OperationType;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.models.utils.RepresentationToModel;
 import org.keycloak.models.utils.StripSecretsUtils;
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderFactory;
 import org.keycloak.representations.idm.ComponentRepresentation;
+import org.keycloak.representations.idm.ComponentTypeRepresentation;
+import org.keycloak.representations.idm.ConfigPropertyRepresentation;
 import org.keycloak.services.ErrorResponse;
 import org.keycloak.services.ErrorResponseException;
 
@@ -53,6 +60,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Properties;
 import java.util.stream.Collectors;
 
@@ -128,7 +136,7 @@ public class ComponentResource {
         } catch (ComponentValidationException e) {
             return localizedErrorResponse(e);
         } catch (IllegalArgumentException e) {
-            throw new BadRequestException();
+            throw new BadRequestException(e);
         }
     }
 
@@ -197,4 +205,61 @@ public class ComponentResource {
         return ErrorResponse.error(message, Response.Status.BAD_REQUEST);
     }
 
+    /**
+     * List of subcomponent types that are available to configure for a particular parent component.
+     *
+     * @param parentId
+     * @param subtype
+     * @return
+     */
+    @GET
+    @Path("{id}/sub-component-types")
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    public List<ComponentTypeRepresentation> getSubcomponentConfig(@PathParam("id") String parentId, @QueryParam("type") String subtype) {
+        auth.requireView();
+        ComponentModel parent = realm.getComponent(parentId);
+        if (parent == null) {
+            throw new NotFoundException("Could not find parent component");
+        }
+        if (subtype == null) {
+            throw new BadRequestException("must specify a subtype");
+        }
+        Class<? extends Provider> providerClass = null;
+        try {
+            providerClass = (Class<? extends Provider>)Class.forName(subtype);
+        } catch (ClassNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+        List<ComponentTypeRepresentation> subcomponents = new LinkedList<>();
+        for (ProviderFactory factory : session.getKeycloakSessionFactory().getProviderFactories(providerClass)) {
+            ComponentTypeRepresentation rep = new ComponentTypeRepresentation();
+            rep.setId(factory.getId());
+            if (!(factory instanceof ComponentFactory)) {
+                continue;
+            }
+            ComponentFactory componentFactory = (ComponentFactory)factory;
+
+            rep.setHelpText(componentFactory.getHelpText());
+            List<ProviderConfigProperty> props = null;
+            Map<String, Object> metadata = null;
+            if (factory instanceof SubComponentFactory) {
+                props = ((SubComponentFactory)factory).getConfigProperties(realm, parent);
+                metadata = ((SubComponentFactory)factory).getTypeMetadata(realm, parent);
+
+            } else {
+                props = componentFactory.getConfigProperties();
+                metadata = componentFactory.getTypeMetadata();
+            }
+
+            List<ConfigPropertyRepresentation> propReps =  ModelToRepresentation.toRepresentation(props);
+            rep.setProperties(propReps);
+            rep.setMetadata(metadata);
+            subcomponents.add(rep);
+        }
+        return subcomponents;
+    }
+
+
+
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
index 8120e7f..8017b04 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java
@@ -20,6 +20,7 @@ package org.keycloak.services.resources.admin.info;
 import org.keycloak.broker.provider.IdentityProvider;
 import org.keycloak.broker.provider.IdentityProviderFactory;
 import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.component.ComponentFactory;
 import org.keycloak.events.EventType;
 import org.keycloak.events.admin.OperationType;
 import org.keycloak.events.admin.ResourceType;
@@ -136,8 +137,8 @@ public class ServerInfoAdminResource {
                         List<ProviderConfigProperty> configProperties = configured.getConfigProperties();
                         if (configProperties == null) configProperties = Collections.EMPTY_LIST;
                         rep.setProperties(ModelToRepresentation.toRepresentation(configProperties));
-                        if (pi instanceof ImportSynchronization) {
-                            rep.getMetadata().put("synchronizable", true);
+                        if (pi instanceof ComponentFactory) {
+                            rep.setMetadata(((ComponentFactory)pi).getTypeMetadata());
                         }
                         List<ComponentTypeRepresentation> reps = info.getComponentTypes().get(spi.getProviderClass().getName());
                         if (reps == null) {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderResource.java
index 9fde51f..8e29779 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserStorageProviderResource.java
@@ -24,9 +24,14 @@ import org.keycloak.component.ComponentModel;
 import org.keycloak.events.admin.OperationType;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.services.ServicesLogger;
 import org.keycloak.services.managers.UserStorageSyncManager;
 import org.keycloak.storage.UserStorageProvider;
 import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.ldap.LDAPStorageProvider;
+import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
 import org.keycloak.storage.user.SynchronizationResult;
 
 import javax.ws.rs.POST;
@@ -119,6 +124,47 @@ public class UserStorageProviderResource {
         return syncResult;
     }
 
+    /**
+     * Trigger sync of mapper data related to ldap mapper (roles, groups, ...)
+     *
+     * @return
+     */
+    @POST
+    @Path("{parentId}/mappers/{id}/sync")
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public SynchronizationResult syncMapperData(@PathParam("parentId") String parentId, @PathParam("id") String mapperId, @QueryParam("direction") String direction) {
+        auth.requireManage();
+
+        ComponentModel parentModel = realm.getComponent(parentId);
+        if (parentModel == null) throw new NotFoundException("Parent model not found");
+        ComponentModel mapperModel = realm.getComponent(mapperId);
+        if (mapperModel == null) throw new NotFoundException("Mapper model not found");
+        LDAPStorageMapper mapper = session.getProvider(LDAPStorageMapper.class, mapperModel);
+        ProviderFactory factory = session.getKeycloakSessionFactory().getProviderFactory(LDAPStorageProvider.class, parentModel.getProviderId());
+
+        LDAPStorageProviderFactory providerFactory = (LDAPStorageProviderFactory)factory;
+        LDAPStorageProvider federationProvider = providerFactory.create(session, parentModel);
+
+        ServicesLogger.LOGGER.syncingDataForMapper(mapperModel.getName(), mapperModel.getProviderId(), direction);
+
+        SynchronizationResult syncResult;
+        if ("fedToKeycloak".equals(direction)) {
+            syncResult = mapper.syncDataFromFederationProviderToKeycloak(mapperModel, federationProvider, session, realm);
+        } else if ("keycloakToFed".equals(direction)) {
+            syncResult = mapper.syncDataFromKeycloakToFederationProvider(mapperModel, federationProvider, session, realm);
+        } else {
+            throw new NotFoundException("Unknown direction: " + direction);
+        }
+
+        Map<String, Object> eventRep = new HashMap<>();
+        eventRep.put("action", direction);
+        eventRep.put("result", syncResult);
+        adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).representation(eventRep).success();
+        return syncResult;
+    }
+
+
 
 
 }
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/LDAPLegacyImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPLegacyImportTest.java
new file mode 100755
index 0000000..fc34c86
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPLegacyImportTest.java
@@ -0,0 +1,184 @@
+/*
+ * 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.credential.CredentialModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.ModelReadOnlyException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.models.utils.RepresentationToModel;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.storage.UserStorageProvider;
+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.idm.model.LDAPObject;
+import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
+import org.keycloak.storage.ldap.mappers.HardcodedLDAPRoleStorageMapper;
+import org.keycloak.storage.ldap.mappers.HardcodedLDAPRoleStorageMapperFactory;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
+import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.pages.AccountPasswordPage;
+import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.RegisterPage;
+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.keycloak.util.JsonSerialization;
+import org.openqa.selenium.WebDriver;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests that legacy UserFederationProvider json export is converted to ComponentModel
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class LDAPLegacyImportTest {
+
+    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, "marykeycloak", "mary@test.com", "password-app");
+
+            RealmRepresentation imported = null;
+            try {
+                imported = JsonSerialization.readValue(getClass().getResourceAsStream("/ldap/fed-provider-export.json"), RealmRepresentation.class);
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+            RepresentationToModel.importUserFederationProvidersAndMappers(imported, appRealm);
+            ldapModel = appRealm.getComponents(appRealm.getId(), UserStorageProvider.class.getName()).get(0);
+            // Delete all LDAP users and add some new for testing
+            LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
+            LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);
+
+            LDAPObject john = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
+            LDAPTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1");
+
+            LDAPObject existing = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "existing", "Existing", "Foo", "existing@email.org", null, "5678");
+
+            appRealm.getClientByClientId("test-app").setDirectAccessGrantsEnabled(true);
+        }
+    });
+
+    @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 RegisterPage registerPage;
+
+    @WebResource
+    protected LoginPage loginPage;
+
+    @WebResource
+    protected AccountUpdateProfilePage profilePage;
+
+    @WebResource
+    protected AccountPasswordPage changePasswordPage;
+
+    //@Test
+    public void runit() throws Exception {
+        Thread.sleep(10000000);
+
+    }
+
+    private void loginSuccessAndLogout(String username, String password) {
+        loginPage.open();
+        loginPage.login(username, password);
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+        oauth.openLogout();
+    }
+
+
+    @Test
+    public void loginClassic() {
+        loginPage.open();
+        loginPage.login("marykeycloak", "password-app");
+
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+
+    }
+
+    @Test
+    public void loginLdap() {
+        loginPage.open();
+        loginPage.login("johnkeycloak", "Password1");
+
+        Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+        Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+
+        profilePage.open();
+        Assert.assertEquals("John", profilePage.getFirstName());
+        Assert.assertEquals("Doe", profilePage.getLastName());
+        Assert.assertEquals("john@email.org", profilePage.getEmail());
+    }
+
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPTestConfiguration.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPTestConfiguration.java
new file mode 100644
index 0000000..53a6294
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPTestConfiguration.java
@@ -0,0 +1,149 @@
+/*
+ * 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.jboss.logging.Logger;
+import org.keycloak.common.constants.KerberosConstants;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
+
+import java.io.File;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class LDAPTestConfiguration {
+
+    private static final Logger log = Logger.getLogger(LDAPTestConfiguration.class);
+
+    private String connectionPropertiesLocation;
+    private int sleepTime;
+    private boolean startEmbeddedLdapLerver = true;
+    private Map<String, String> config;
+
+    protected static final Map<String, String> PROP_MAPPINGS = new HashMap<String, String>();
+    protected static final Map<String, String> DEFAULT_VALUES = new HashMap<String, String>();
+
+    static {
+        PROP_MAPPINGS.put(LDAPConstants.CONNECTION_URL, "idm.test.ldap.connection.url");
+        PROP_MAPPINGS.put(LDAPConstants.BASE_DN, "idm.test.ldap.base.dn");
+        PROP_MAPPINGS.put(LDAPConstants.USERS_DN, "idm.test.ldap.user.dn.suffix");
+        PROP_MAPPINGS.put(LDAPConstants.BIND_DN, "idm.test.ldap.bind.dn");
+        PROP_MAPPINGS.put(LDAPConstants.BIND_CREDENTIAL, "idm.test.ldap.bind.credential");
+        PROP_MAPPINGS.put(LDAPConstants.VENDOR, "idm.test.ldap.vendor");
+        PROP_MAPPINGS.put(LDAPConstants.CONNECTION_POOLING, "idm.test.ldap.connection.pooling");
+        PROP_MAPPINGS.put(LDAPConstants.PAGINATION, "idm.test.ldap.pagination");
+        PROP_MAPPINGS.put(LDAPConstants.BATCH_SIZE_FOR_SYNC, "idm.test.ldap.batch.size.for.sync");
+        PROP_MAPPINGS.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, "idm.test.ldap.username.ldap.attribute");
+        PROP_MAPPINGS.put(LDAPConstants.RDN_LDAP_ATTRIBUTE, "idm.test.ldap.rdn.ldap.attribute");
+        PROP_MAPPINGS.put(LDAPConstants.USER_OBJECT_CLASSES, "idm.test.ldap.user.object.classes");
+        PROP_MAPPINGS.put(LDAPConstants.EDIT_MODE, "idm.test.ldap.edit.mode");
+
+        PROP_MAPPINGS.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "idm.test.kerberos.allow.kerberos.authentication");
+        PROP_MAPPINGS.put(KerberosConstants.KERBEROS_REALM, "idm.test.kerberos.realm");
+        PROP_MAPPINGS.put(KerberosConstants.SERVER_PRINCIPAL, "idm.test.kerberos.server.principal");
+        PROP_MAPPINGS.put(KerberosConstants.KEYTAB, "idm.test.kerberos.keytab");
+        PROP_MAPPINGS.put(KerberosConstants.DEBUG, "idm.test.kerberos.debug");
+        PROP_MAPPINGS.put(KerberosConstants.ALLOW_PASSWORD_AUTHENTICATION, "idm.test.kerberos.allow.password.authentication");
+        PROP_MAPPINGS.put(KerberosConstants.UPDATE_PROFILE_FIRST_LOGIN, "idm.test.kerberos.update.profile.first.login");
+        PROP_MAPPINGS.put(KerberosConstants.USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION, "idm.test.kerberos.use.kerberos.for.password.authentication");
+
+        DEFAULT_VALUES.put(LDAPConstants.CONNECTION_URL, "ldap://localhost:10389");
+        DEFAULT_VALUES.put(LDAPConstants.BASE_DN, "dc=keycloak,dc=org");
+        DEFAULT_VALUES.put(LDAPConstants.USERS_DN, "ou=People,dc=keycloak,dc=org");
+        DEFAULT_VALUES.put(LDAPConstants.BIND_DN, "uid=admin,ou=system");
+        DEFAULT_VALUES.put(LDAPConstants.BIND_CREDENTIAL, "secret");
+        DEFAULT_VALUES.put(LDAPConstants.VENDOR, LDAPConstants.VENDOR_OTHER);
+        DEFAULT_VALUES.put(LDAPConstants.CONNECTION_POOLING, "true");
+        DEFAULT_VALUES.put(LDAPConstants.PAGINATION, "true");
+        DEFAULT_VALUES.put(LDAPConstants.BATCH_SIZE_FOR_SYNC, String.valueOf(LDAPConstants.DEFAULT_BATCH_SIZE_FOR_SYNC));
+        DEFAULT_VALUES.put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, null);
+        DEFAULT_VALUES.put(LDAPConstants.USER_OBJECT_CLASSES, null);
+        DEFAULT_VALUES.put(LDAPConstants.EDIT_MODE, LDAPStorageProviderFactory.EditMode.READ_ONLY.toString());
+
+        DEFAULT_VALUES.put(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION, "false");
+        DEFAULT_VALUES.put(KerberosConstants.KERBEROS_REALM, "KEYCLOAK.ORG");
+        DEFAULT_VALUES.put(KerberosConstants.SERVER_PRINCIPAL, "HTTP/localhost@KEYCLOAK.ORG");
+        URL keytabUrl = LDAPTestConfiguration.class.getResource("/kerberos/http.keytab");
+        String keyTabPath = new File(keytabUrl.getFile()).getAbsolutePath();
+        DEFAULT_VALUES.put(KerberosConstants.KEYTAB, keyTabPath);
+        DEFAULT_VALUES.put(KerberosConstants.DEBUG, "true");
+        DEFAULT_VALUES.put(KerberosConstants.ALLOW_PASSWORD_AUTHENTICATION, "true");
+        DEFAULT_VALUES.put(KerberosConstants.UPDATE_PROFILE_FIRST_LOGIN, "true");
+        DEFAULT_VALUES.put(KerberosConstants.USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION, "false");
+    }
+
+    public static LDAPTestConfiguration readConfiguration(String connectionPropertiesLocation) {
+        LDAPTestConfiguration ldapTestConfiguration = new LDAPTestConfiguration();
+        ldapTestConfiguration.setConnectionPropertiesLocation(connectionPropertiesLocation);
+        ldapTestConfiguration.loadConnectionProperties();
+        return ldapTestConfiguration;
+    }
+
+    protected void loadConnectionProperties() {
+        Properties p = new Properties();
+        try {
+            log.info("Reading LDAP configuration from: " + connectionPropertiesLocation);
+            InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(connectionPropertiesLocation);
+            p.load(is);
+        }
+        catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+        config = new HashMap<String, String>();
+        for (Map.Entry<String, String> property : PROP_MAPPINGS.entrySet()) {
+            String propertyName = property.getKey();
+            String configName = property.getValue();
+
+            String value = (String) p.get(configName);
+            if (value == null) {
+                value = DEFAULT_VALUES.get(propertyName);
+            }
+
+            config.put(propertyName, value);
+        }
+
+        startEmbeddedLdapLerver = Boolean.parseBoolean(p.getProperty("idm.test.ldap.start.embedded.ldap.server", "true"));
+        sleepTime = Integer.parseInt(p.getProperty("idm.test.ldap.sleepTime", "1000"));
+        log.info("Start embedded server: " + startEmbeddedLdapLerver);
+        log.info("Read config: " + config);
+    }
+
+    public Map<String,String> getLDAPConfig() {
+        return config;
+    }
+
+    public void setConnectionPropertiesLocation(String connectionPropertiesLocation) {
+        this.connectionPropertiesLocation = connectionPropertiesLocation;
+    }
+
+    public boolean isStartEmbeddedLdapLerver() {
+        return startEmbeddedLdapLerver;
+    }
+
+    public int getSleepTime() {
+        return sleepTime;
+    }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
index 98c7437..f193645 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
@@ -22,8 +22,7 @@ import org.junit.FixMethodOrder;
 import org.junit.Test;
 import org.junit.runners.MethodSorters;
 import org.keycloak.common.constants.KerberosConstants;
-import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapper;
-import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory;
+import org.keycloak.component.ComponentModel;
 import org.keycloak.models.AuthenticationFlowModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientTemplateModel;
@@ -49,6 +48,9 @@ import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
 import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.services.managers.RealmManager;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
 
 import java.util.List;
 import java.util.Map;
@@ -278,31 +280,28 @@ public class ImportTest extends AbstractModelTest {
 
         // Test federation providers
         List<UserFederationProviderModel> fedProviders = realm.getUserFederationProviders();
-        Assert.assertTrue(fedProviders.size() == 2);
-        UserFederationProviderModel ldap1 = fedProviders.get(0);
-        Assert.assertEquals("MyLDAPProvider1", ldap1.getDisplayName());
-        Assert.assertEquals("ldap", ldap1.getProviderName());
+        Assert.assertTrue(fedProviders.size() == 0);
+        List<UserStorageProviderModel> storageProviders = realm.getUserStorageProviders();
+        Assert.assertTrue(storageProviders.size() == 2);
+        UserStorageProviderModel ldap1 = storageProviders.get(0);
+        Assert.assertEquals("MyLDAPProvider1", ldap1.getName());
+        Assert.assertEquals("ldap", ldap1.getProviderId());
         Assert.assertEquals(1, ldap1.getPriority());
-        Assert.assertEquals("ldap://foo", ldap1.getConfig().get(LDAPConstants.CONNECTION_URL));
+        Assert.assertEquals("ldap://foo", ldap1.getConfig().getFirst(LDAPConstants.CONNECTION_URL));
 
-        UserFederationProviderModel ldap2 = fedProviders.get(1);
-        Assert.assertEquals("MyLDAPProvider2", ldap2.getDisplayName());
-        Assert.assertEquals("ldap://bar", ldap2.getConfig().get(LDAPConstants.CONNECTION_URL));
+        UserStorageProviderModel ldap2 = storageProviders.get(1);
+        Assert.assertEquals("MyLDAPProvider2", ldap2.getName());
+        Assert.assertEquals("ldap://bar", ldap2.getConfig().getFirst(LDAPConstants.CONNECTION_URL));
 
         // Test federation mappers
-        Set<UserFederationMapperModel> fedMappers1 = realm.getUserFederationMappersByFederationProvider(ldap1.getId());
-        Assert.assertTrue(fedMappers1.size() == 1);
-        UserFederationMapperModel fullNameMapper = fedMappers1.iterator().next();
+        Set<UserFederationMapperModel> userFedMappers1 = realm.getUserFederationMappers();
+        Assert.assertTrue(userFedMappers1.size() == 0);
+        List<ComponentModel> fedMappers1 = realm.getComponents(ldap1.getId());
+        ComponentModel fullNameMapper = fedMappers1.iterator().next();
         Assert.assertEquals("FullNameMapper", fullNameMapper.getName());
-        Assert.assertEquals(FullNameLDAPFederationMapperFactory.PROVIDER_ID, fullNameMapper.getFederationMapperType());
-        Assert.assertEquals(ldap1.getId(), fullNameMapper.getFederationProviderId());
-        Assert.assertEquals("cn", fullNameMapper.getConfig().get(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE));
-
-        // All builtin LDAP mappers should be here
-        Set<UserFederationMapperModel> fedMappers2 = realm.getUserFederationMappersByFederationProvider(ldap2.getId());
-        Assert.assertTrue(fedMappers2.size() > 3);
-        Set<UserFederationMapperModel> allMappers = realm.getUserFederationMappers();
-        Assert.assertEquals(allMappers.size(), fedMappers1.size() + fedMappers2.size());
+        Assert.assertEquals(FullNameLDAPStorageMapperFactory.PROVIDER_ID, fullNameMapper.getProviderId());
+        Assert.assertEquals(ldap1.getId(), fullNameMapper.getParentId());
+        Assert.assertEquals("cn", fullNameMapper.getConfig().getFirst(FullNameLDAPStorageMapper.LDAP_FULL_NAME_ATTRIBUTE));
 
         // Assert that federation link wasn't created during import
         UserFederationProviderFactory factory = (UserFederationProviderFactory)session.getKeycloakSessionFactory().getProviderFactory(UserFederationProvider.class, "dummy");
diff --git a/testsuite/integration/src/test/resources/ldap/fed-provider-export.json b/testsuite/integration/src/test/resources/ldap/fed-provider-export.json
new file mode 100644
index 0000000..4bbe374
--- /dev/null
+++ b/testsuite/integration/src/test/resources/ldap/fed-provider-export.json
@@ -0,0 +1,622 @@
+{
+  "id": "test",
+  "realm": "test",
+  "notBefore": 0,
+  "revokeRefreshToken": false,
+  "accessTokenLifespan": 300,
+  "accessTokenLifespanForImplicitFlow": 900,
+  "ssoSessionIdleTimeout": 1800,
+  "ssoSessionMaxLifespan": 36000,
+  "offlineSessionIdleTimeout": 2592000,
+  "accessCodeLifespan": 60,
+  "accessCodeLifespanUserAction": 300,
+  "accessCodeLifespanLogin": 1800,
+  "enabled": true,
+  "sslRequired": "external",
+  "registrationAllowed": true,
+  "registrationEmailAsUsername": false,
+  "rememberMe": false,
+  "verifyEmail": false,
+  "resetPasswordAllowed": true,
+  "editUsernameAllowed": true,
+  "bruteForceProtected": false,
+  "maxFailureWaitSeconds": 900,
+  "minimumQuickLoginWaitSeconds": 60,
+  "waitIncrementSeconds": 60,
+  "quickLoginCheckMilliSeconds": 1000,
+  "maxDeltaTimeSeconds": 43200,
+  "failureFactor": 30,
+  "groups": [
+    {
+      "id": "2aa57ddd-e48f-4a62-bb8e-53ebe2ff1057",
+      "name": "topGroup",
+      "path": "/topGroup",
+      "attributes": {
+        "topAttribute": [
+          "true"
+        ]
+      },
+      "realmRoles": [
+        "user"
+      ],
+      "clientRoles": {},
+      "subGroups": [
+        {
+          "id": "8e91afd4-b8e4-4de4-ba37-1edc7298d518",
+          "name": "level2group",
+          "path": "/topGroup/level2group",
+          "attributes": {
+            "level2Attribute": [
+              "true"
+            ]
+          },
+          "realmRoles": [
+            "admin"
+          ],
+          "clientRoles": {
+            "test-app": [
+              "customer-user"
+            ]
+          },
+          "subGroups": []
+        }
+      ]
+    }
+  ],
+  "defaultRoles": [
+    "user",
+    "offline_access",
+    "uma_authorization"
+  ],
+  "requiredCredentials": [
+    "password"
+  ],
+  "passwordPolicy": "hashIterations(20000)",
+  "otpPolicyType": "totp",
+  "otpPolicyAlgorithm": "HmacSHA1",
+  "otpPolicyInitialCounter": 0,
+  "otpPolicyDigits": 6,
+  "otpPolicyLookAheadWindow": 1,
+  "otpPolicyPeriod": 30,
+  "browserSecurityHeaders": {
+    "xContentTypeOptions": "nosniff",
+    "xFrameOptions": "SAMEORIGIN",
+    "contentSecurityPolicy": "frame-src 'self'"
+  },
+  "smtpServer": {
+    "host": "localhost",
+    "from": "auto@keycloak.org",
+    "port": "3025"
+  },
+  "userFederationProviders": [
+    {
+      "id": "1fc3afd2-4c18-48dd-9055-b4bbae9229b7",
+      "displayName": "test-ldap",
+      "providerName": "ldap",
+      "config": {
+        "serverPrincipal": "HTTP/localhost@KEYCLOAK.ORG",
+        "debug": "true",
+        "pagination": "true",
+        "keyTab": "/Users/williamburke/jboss/keycloak/p1b-repo/keycloak/testsuite/integration/target/test-classes/kerberos/http.keytab",
+        "connectionPooling": "true",
+        "usersDn": "ou=People,dc=keycloak,dc=org",
+        "useKerberosForPasswordAuthentication": "false",
+        "kerberosRealm": "KEYCLOAK.ORG",
+        "bindCredential": "secret",
+        "bindDn": "uid=admin,ou=system",
+        "allowPasswordAuthentication": "true",
+        "vendor": "other",
+        "editMode": "WRITABLE",
+        "allowKerberosAuthentication": "false",
+        "connectionUrl": "ldap://localhost:10389",
+        "syncRegistrations": "true",
+        "baseDn": "dc=keycloak,dc=org",
+        "batchSizeForSync": "3",
+        "updateProfileFirstLogin": "true"
+      },
+      "priority": 0,
+      "fullSyncPeriod": -1,
+      "changedSyncPeriod": -1,
+      "lastSync": 0
+    }
+  ],
+  "userFederationMappers": [
+    {
+      "id": "b2fc2d9c-2ea8-417f-96db-2565be62a646",
+      "name": "last name",
+      "federationProviderDisplayName": "test-ldap",
+      "federationMapperType": "user-attribute-ldap-mapper",
+      "config": {
+        "always.read.value.from.ldap": "true",
+        "read.only": "false",
+        "ldap.attribute": "sn",
+        "is.mandatory.in.ldap": "true",
+        "user.model.attribute": "lastName"
+      }
+    },
+    {
+      "id": "6dc25318-dc20-4927-ba19-9293ab31aa28",
+      "name": "zipCodeMapper",
+      "federationProviderDisplayName": "test-ldap",
+      "federationMapperType": "user-attribute-ldap-mapper",
+      "config": {
+        "always.read.value.from.ldap": "false",
+        "read.only": "false",
+        "ldap.attribute": "postalCode",
+        "is.mandatory.in.ldap": "false",
+        "user.model.attribute": "postal_code"
+      }
+    },
+    {
+      "id": "7afa12a2-f36e-4f87-b715-e941773c8534",
+      "name": "username",
+      "federationProviderDisplayName": "test-ldap",
+      "federationMapperType": "user-attribute-ldap-mapper",
+      "config": {
+        "always.read.value.from.ldap": "false",
+        "read.only": "false",
+        "ldap.attribute": "uid",
+        "is.mandatory.in.ldap": "true",
+        "user.model.attribute": "username"
+      }
+    },
+    {
+      "id": "abfe054c-6d2a-4870-a239-1a312c3e5a94",
+      "name": "creation date",
+      "federationProviderDisplayName": "test-ldap",
+      "federationMapperType": "user-attribute-ldap-mapper",
+      "config": {
+        "always.read.value.from.ldap": "true",
+        "read.only": "true",
+        "ldap.attribute": "createTimestamp",
+        "is.mandatory.in.ldap": "false",
+        "user.model.attribute": "createTimestamp"
+      }
+    },
+    {
+      "id": "6aef95e5-736e-4b1e-98d0-332f61f94ff9",
+      "name": "first name",
+      "federationProviderDisplayName": "test-ldap",
+      "federationMapperType": "user-attribute-ldap-mapper",
+      "config": {
+        "always.read.value.from.ldap": "true",
+        "read.only": "false",
+        "ldap.attribute": "cn",
+        "is.mandatory.in.ldap": "true",
+        "user.model.attribute": "firstName"
+      }
+    },
+    {
+      "id": "0601e4a2-fd63-4f6a-ae3b-13cc6f4f4f1c",
+      "name": "email",
+      "federationProviderDisplayName": "test-ldap",
+      "federationMapperType": "user-attribute-ldap-mapper",
+      "config": {
+        "always.read.value.from.ldap": "false",
+        "read.only": "false",
+        "ldap.attribute": "mail",
+        "is.mandatory.in.ldap": "false",
+        "user.model.attribute": "email"
+      }
+    },
+    {
+      "id": "fa308910-3be9-4bd8-8256-66cf04d8fcd2",
+      "name": "modify date",
+      "federationProviderDisplayName": "test-ldap",
+      "federationMapperType": "user-attribute-ldap-mapper",
+      "config": {
+        "always.read.value.from.ldap": "true",
+        "read.only": "true",
+        "ldap.attribute": "modifyTimestamp",
+        "is.mandatory.in.ldap": "false",
+        "user.model.attribute": "modifyTimestamp"
+      }
+    }
+  ],
+  "eventsEnabled": false,
+  "eventsListeners": [
+    "jboss-logging"
+  ],
+  "enabledEventTypes": [],
+  "adminEventsEnabled": false,
+  "adminEventsDetailsEnabled": false,
+  "internationalizationEnabled": true,
+  "supportedLocales": [
+    "de",
+    "en"
+  ],
+  "defaultLocale": "en",
+  "authenticationFlows": [
+    {
+      "id": "b12463a9-5d33-4f27-b010-4005db77e602",
+      "alias": "Handle Existing Account",
+      "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "idp-confirm-link",
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticator": "idp-email-verification",
+          "requirement": "ALTERNATIVE",
+          "priority": 20,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "requirement": "ALTERNATIVE",
+          "priority": 30,
+          "flowAlias": "Verify Existing Account by Re-authentication",
+          "userSetupAllowed": false,
+          "autheticatorFlow": true
+        }
+      ]
+    },
+    {
+      "id": "c1684fc8-a99d-4e19-a795-478e4d793fb5",
+      "alias": "Verify Existing Account by Re-authentication",
+      "description": "Reauthentication of existing account",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "idp-username-password-form",
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticator": "auth-otp-form",
+          "requirement": "OPTIONAL",
+          "priority": 20,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        }
+      ]
+    },
+    {
+      "id": "09af30d8-8c2a-45a4-a2be-b7617e9d0185",
+      "alias": "browser",
+      "description": "browser based authentication",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "auth-cookie",
+          "requirement": "ALTERNATIVE",
+          "priority": 10,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticator": "auth-spnego",
+          "requirement": "DISABLED",
+          "priority": 20,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticator": "identity-provider-redirector",
+          "requirement": "ALTERNATIVE",
+          "priority": 25,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "requirement": "ALTERNATIVE",
+          "priority": 30,
+          "flowAlias": "forms",
+          "userSetupAllowed": false,
+          "autheticatorFlow": true
+        }
+      ]
+    },
+    {
+      "id": "6cdf31d0-9c91-4ea6-8e37-da6e8fa7544c",
+      "alias": "clients",
+      "description": "Base authentication for clients",
+      "providerId": "client-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "client-secret",
+          "requirement": "ALTERNATIVE",
+          "priority": 10,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticator": "client-jwt",
+          "requirement": "ALTERNATIVE",
+          "priority": 20,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        }
+      ]
+    },
+    {
+      "id": "c9a38de8-4c0c-496a-9936-b9753f73bfcc",
+      "alias": "direct grant",
+      "description": "OpenID Connect Resource Owner Grant",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "direct-grant-validate-username",
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticator": "direct-grant-validate-password",
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticator": "direct-grant-validate-otp",
+          "requirement": "OPTIONAL",
+          "priority": 30,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        }
+      ]
+    },
+    {
+      "id": "3755e297-7907-4c14-8c5f-d77e2bfe4b5d",
+      "alias": "first broker login",
+      "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticatorConfig": "review profile config",
+          "authenticator": "idp-review-profile",
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticatorConfig": "create unique user config",
+          "authenticator": "idp-create-user-if-unique",
+          "requirement": "ALTERNATIVE",
+          "priority": 20,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "requirement": "ALTERNATIVE",
+          "priority": 30,
+          "flowAlias": "Handle Existing Account",
+          "userSetupAllowed": false,
+          "autheticatorFlow": true
+        }
+      ]
+    },
+    {
+      "id": "f35b2f00-3e84-4f2e-b48e-3e4159d88a06",
+      "alias": "forms",
+      "description": "Username, password, otp and other auth forms.",
+      "providerId": "basic-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "auth-username-password-form",
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticator": "auth-otp-form",
+          "requirement": "OPTIONAL",
+          "priority": 20,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        }
+      ]
+    },
+    {
+      "id": "441b4480-1ace-483a-bffb-f0cb6659fe32",
+      "alias": "registration",
+      "description": "registration flow",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "registration-page-form",
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "flowAlias": "registration form",
+          "userSetupAllowed": false,
+          "autheticatorFlow": true
+        }
+      ]
+    },
+    {
+      "id": "c7de2a37-29a1-471a-9b51-699a69032b00",
+      "alias": "registration form",
+      "description": "registration form",
+      "providerId": "form-flow",
+      "topLevel": false,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "registration-user-creation",
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticator": "registration-profile-action",
+          "requirement": "REQUIRED",
+          "priority": 40,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticator": "registration-password-action",
+          "requirement": "REQUIRED",
+          "priority": 50,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticator": "registration-recaptcha-action",
+          "requirement": "DISABLED",
+          "priority": 60,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        }
+      ]
+    },
+    {
+      "id": "d362be0a-df20-4ce7-9288-f8448e0c4647",
+      "alias": "reset credentials",
+      "description": "Reset credentials for a user if they forgot their password or something",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "reset-credentials-choose-user",
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticator": "reset-credential-email",
+          "requirement": "REQUIRED",
+          "priority": 20,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticator": "reset-password",
+          "requirement": "REQUIRED",
+          "priority": 30,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        },
+        {
+          "authenticator": "reset-otp",
+          "requirement": "OPTIONAL",
+          "priority": 40,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        }
+      ]
+    },
+    {
+      "id": "c2d7a1ae-57c9-4f3b-a4ce-55c3f0d9869f",
+      "alias": "saml ecp",
+      "description": "SAML ECP Profile Authentication Flow",
+      "providerId": "basic-flow",
+      "topLevel": true,
+      "builtIn": true,
+      "authenticationExecutions": [
+        {
+          "authenticator": "http-basic-authenticator",
+          "requirement": "REQUIRED",
+          "priority": 10,
+          "userSetupAllowed": false,
+          "autheticatorFlow": false
+        }
+      ]
+    }
+  ],
+  "authenticatorConfig": [
+    {
+      "id": "a2490828-becb-435f-9c3c-318b3939bf64",
+      "alias": "create unique user config",
+      "config": {
+        "require.password.update.after.registration": "false"
+      }
+    },
+    {
+      "id": "78421671-f733-4901-82bc-58bf50c43206",
+      "alias": "review profile config",
+      "config": {
+        "update.profile.on.first.login": "missing"
+      }
+    }
+  ],
+  "requiredActions": [
+    {
+      "alias": "CONFIGURE_TOTP",
+      "name": "Configure OTP",
+      "providerId": "CONFIGURE_TOTP",
+      "enabled": true,
+      "defaultAction": false,
+      "config": {}
+    },
+    {
+      "alias": "UPDATE_PASSWORD",
+      "name": "Update Password",
+      "providerId": "UPDATE_PASSWORD",
+      "enabled": true,
+      "defaultAction": false,
+      "config": {}
+    },
+    {
+      "alias": "UPDATE_PROFILE",
+      "name": "Update Profile",
+      "providerId": "UPDATE_PROFILE",
+      "enabled": true,
+      "defaultAction": false,
+      "config": {}
+    },
+    {
+      "alias": "VERIFY_EMAIL",
+      "name": "Verify Email",
+      "providerId": "VERIFY_EMAIL",
+      "enabled": true,
+      "defaultAction": false,
+      "config": {}
+    },
+    {
+      "alias": "terms_and_conditions",
+      "name": "Terms and Conditions",
+      "providerId": "terms_and_conditions",
+      "enabled": false,
+      "defaultAction": false,
+      "config": {}
+    }
+  ],
+  "browserFlow": "browser",
+  "registrationFlow": "registration",
+  "directGrantFlow": "direct grant",
+  "resetCredentialsFlow": "reset credentials",
+  "clientAuthenticationFlow": "clients",
+  "attributes": {
+    "_browser_header.xFrameOptions": "SAMEORIGIN",
+    "failureFactor": "30",
+    "quickLoginCheckMilliSeconds": "1000",
+    "maxDeltaTimeSeconds": "43200",
+    "_browser_header.xContentTypeOptions": "nosniff",
+    "bruteForceProtected": "false",
+    "maxFailureWaitSeconds": "900",
+    "_browser_header.contentSecurityPolicy": "frame-src 'self'",
+    "minimumQuickLoginWaitSeconds": "60",
+    "waitIncrementSeconds": "60"
+  }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/components/TestImplProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/components/TestImplProviderFactory.java
index 4092501..b3de69e 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/components/TestImplProviderFactory.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/components/TestImplProviderFactory.java
@@ -22,6 +22,7 @@ import org.keycloak.component.ComponentModel;
 import org.keycloak.component.ComponentValidationException;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
 import org.keycloak.provider.ConfigurationValidationHelper;
 import org.keycloak.provider.ProviderConfigProperty;
 import org.keycloak.provider.ProviderConfigurationBuilder;
@@ -50,7 +51,7 @@ public class TestImplProviderFactory implements TestProviderFactory {
     }
 
     @Override
-    public void validateConfiguration(KeycloakSession session, ComponentModel model) throws ComponentValidationException {
+    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
         ConfigurationValidationHelper.check(model)
                 .checkRequired("required", "Required")
                 .checkInt("number", "Number", false);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java
index 7d005e9..b23194a 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java
@@ -1509,77 +1509,6 @@ public class PermissionsTest extends AbstractKeycloakTest {
     }
 
     @Test
-    public void userFederation() {
-        invoke(new Invocation() {
-            public void invoke(RealmResource realm) {
-                realm.userFederation().getProviderInstances();
-            }
-        }, Resource.REALM, false);
-        invoke(new Invocation() {
-            public void invoke(RealmResource realm) {
-                realm.userFederation().getProviderFactories();
-            }
-        }, Resource.REALM, false);
-        invoke(new Invocation() {
-            public void invoke(RealmResource realm) {
-                realm.userFederation().getProviderFactory("nosuch");
-            }
-        }, Resource.REALM, false);
-        invoke(new InvocationWithResponse() {
-            public void invoke(RealmResource realm, AtomicReference<Response> response) {
-                UserFederationProviderRepresentation rep = new UserFederationProviderRepresentation();
-                rep.setProviderName("ldap");
-                response.set(realm.userFederation().create(rep));
-            }
-        }, Resource.REALM, true);
-        invoke(new Invocation() {
-            public void invoke(RealmResource realm) {
-                realm.userFederation().get("nosuch").toRepresentation();
-            }
-        }, Resource.REALM, false);
-        invoke(new Invocation() {
-            public void invoke(RealmResource realm) {
-                realm.userFederation().get("nosuch").update(new UserFederationProviderRepresentation());
-            }
-        }, Resource.REALM, true);
-        invoke(new Invocation() {
-            public void invoke(RealmResource realm) {
-                realm.userFederation().get("nosuch").remove();
-            }
-        }, Resource.REALM, true);
-        invoke(new Invocation() {
-            public void invoke(RealmResource realm) {
-                realm.userFederation().get("nosuch").syncUsers("nosuch");
-            }
-        }, Resource.REALM, true);
-        invoke(new Invocation() {
-            public void invoke(RealmResource realm) {
-                realm.userFederation().get("nosuch").getMapperTypes();
-            }
-        }, Resource.REALM, false);
-        invoke(new Invocation() {
-            public void invoke(RealmResource realm) {
-                realm.userFederation().get("nosuch").getMappers();
-            }
-        }, Resource.REALM, false);
-        invoke(new InvocationWithResponse() {
-            public void invoke(RealmResource realm, AtomicReference<Response> response) {
-                response.set(realm.userFederation().get("nosuch").addMapper(new UserFederationMapperRepresentation()));
-            }
-        }, Resource.REALM, true);
-        invoke(new Invocation() {
-            public void invoke(RealmResource realm) {
-                realm.userFederation().get("nosuch").getMapperById("nosuch");
-            }
-        }, Resource.REALM, false);
-        invoke(new Invocation() {
-            public void invoke(RealmResource realm) {
-                realm.userFederation().get("nosuch").syncMapperData("nosuch", "nosuch");
-            }
-        }, Resource.REALM, true);
-    }
-
-    @Test
     public void components() {
         invoke(new Invocation() {
             public void invoke(RealmResource realm) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
index 2c45818..001ed86 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
@@ -25,10 +25,11 @@ import org.keycloak.admin.client.resource.ClientTemplateResource;
 import org.keycloak.admin.client.resource.RealmResource;
 import org.keycloak.admin.client.resource.UserResource;
 import org.keycloak.common.constants.KerberosConstants;
-import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapper;
-import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory;
+import org.keycloak.component.ComponentModel;
 import org.keycloak.models.Constants;
 import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.UserFederationMapperModel;
+import org.keycloak.models.UserFederationProviderModel;
 import org.keycloak.models.utils.DefaultAuthenticationFlows;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
@@ -37,6 +38,8 @@ import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
 import org.keycloak.representations.idm.ClientMappingsRepresentation;
 import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.ClientTemplateRepresentation;
+import org.keycloak.representations.idm.ComponentExportRepresentation;
+import org.keycloak.representations.idm.ComponentRepresentation;
 import org.keycloak.representations.idm.FederatedIdentityRepresentation;
 import org.keycloak.representations.idm.IdentityProviderRepresentation;
 import org.keycloak.representations.idm.ProtocolMapperRepresentation;
@@ -49,6 +52,11 @@ import org.keycloak.representations.idm.authorization.PolicyRepresentation;
 import org.keycloak.representations.idm.authorization.ResourceRepresentation;
 import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
 import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
+import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
+import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
 import org.keycloak.testsuite.admin.ApiUtil;
 import org.keycloak.testsuite.client.KeycloakTestingClient;
 import org.keycloak.testsuite.util.RealmRepUtil;
@@ -260,33 +268,34 @@ public class ExportImportUtil {
         Assert.assertEquals("googleId", google.getConfig().get("clientId"));
         Assert.assertEquals("googleSecret", google.getConfig().get("clientSecret"));
 
+        //////////////////
         // Test federation providers
+        // on import should convert UserfederationProviderRepresentation to Component model
         List<UserFederationProviderRepresentation> fedProviders = realm.getUserFederationProviders();
-        Assert.assertTrue(fedProviders.size() == 2);
-        UserFederationProviderRepresentation ldap1 = fedProviders.get(0);
-        Assert.assertEquals("MyLDAPProvider1", ldap1.getDisplayName());
-        Assert.assertEquals("ldap", ldap1.getProviderName());
-        Assert.assertEquals(1, ldap1.getPriority());
-        Assert.assertEquals("ldap://foo", ldap1.getConfig().get(LDAPConstants.CONNECTION_URL));
+        Assert.assertTrue(fedProviders == null || fedProviders.size() == 0);
+        List<ComponentRepresentation> storageProviders = realmRsc.components().query(realm.getId(), UserStorageProvider.class.getName());
+        Assert.assertTrue(storageProviders.size() == 2);
+        ComponentRepresentation ldap1 = storageProviders.get(0);
+        ComponentRepresentation ldap2 = storageProviders.get(1);
+        if (!"MyLDAPProvider1".equals(ldap1.getName())) {
+            ldap2 = ldap1;
+            ldap1 = storageProviders.get(1);
+        }
+        Assert.assertEquals("MyLDAPProvider1", ldap1.getName());
+        Assert.assertEquals("ldap", ldap1.getProviderId());
+        Assert.assertEquals("1", ldap1.getConfig().getFirst("priority"));
+        Assert.assertEquals("ldap://foo", ldap1.getConfig().getFirst(LDAPConstants.CONNECTION_URL));
 
-        UserFederationProviderRepresentation ldap2 = fedProviders.get(1);
-        Assert.assertEquals("MyLDAPProvider2", ldap2.getDisplayName());
-        Assert.assertEquals("ldap://bar", ldap2.getConfig().get(LDAPConstants.CONNECTION_URL));
+        Assert.assertEquals("MyLDAPProvider2", ldap2.getName());
+        Assert.assertEquals("ldap://bar", ldap2.getConfig().getFirst(LDAPConstants.CONNECTION_URL));
 
         // Test federation mappers
-        List<UserFederationMapperRepresentation> fedMappers1 = realmRsc.userFederation().get(ldap1.getId()).getMappers();
-        Assert.assertTrue(fedMappers1.size() == 1);
-        UserFederationMapperRepresentation fullNameMapper = fedMappers1.iterator().next();
+        List<ComponentRepresentation> fedMappers1 = realmRsc.components().query(ldap1.getId(), LDAPStorageMapper.class.getName());
+        ComponentRepresentation fullNameMapper = fedMappers1.iterator().next();
         Assert.assertEquals("FullNameMapper", fullNameMapper.getName());
-        Assert.assertEquals(FullNameLDAPFederationMapperFactory.PROVIDER_ID, fullNameMapper.getFederationMapperType());
-        //Assert.assertEquals(ldap1.getId(), fullNameMapper.getFederationProviderId());
-        Assert.assertEquals("cn", fullNameMapper.getConfig().get(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE));
-
-        // All builtin LDAP mappers should be here
-        List<UserFederationMapperRepresentation> fedMappers2 = realmRsc.userFederation().get(ldap2.getId()).getMappers();
-        Assert.assertTrue(fedMappers2.size() > 3);
-        List<UserFederationMapperRepresentation> allMappers = realm.getUserFederationMappers();
-        Assert.assertEquals(allMappers.size(), fedMappers1.size() + fedMappers2.size());
+        Assert.assertEquals(FullNameLDAPStorageMapperFactory.PROVIDER_ID, fullNameMapper.getProviderId());
+        Assert.assertEquals("cn", fullNameMapper.getConfig().getFirst(FullNameLDAPStorageMapper.LDAP_FULL_NAME_ATTRIBUTE));
+        /////////////////
 
         // Assert that federation link wasn't created during import
         Assert.assertNull(testingClient.testing().getUserByUsernameFromFedProviderFactory(realm.getRealm(), "wburke"));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
index eba2baf..0e78d2e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
@@ -158,7 +158,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
 
         defaultUser = UserBuilder.create()
                 .id(KeycloakModelUtils.generateId())
-                .serviceAccountId(app1.getClientId())
+                //.serviceAccountId(app1.getClientId())
                 .username("test-user@localhost")
                 .password("password")
                 .build();
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index a0b0d78..6c696c9 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -1217,6 +1217,10 @@ credential-types=Credential Types
 manage-user-password=Manage Password
 disable-credentials=Disable Credentials
 credential-reset-actions=Credential Reset
+ldap-mappers=LDAP Mappers
+create-ldap-mapper=Create LDAP mapper
+
+
 
 
 
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/app.js b/themes/src/main/resources/theme/base/admin/resources/js/app.js
index f936fcd..e3b5cc5 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -1469,6 +1469,26 @@ module.config([ '$routeProvider', function($routeProvider) {
             },
             controller : 'RealmSessionStatsCtrl'
         })
+        .when('/create/user-storage/:realm/providers/ldap', {
+            templateUrl : resourceUrl + '/partials/user-storage-ldap.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                instance : function() {
+                    return {
+
+                    };
+                },
+                providerId : function($route) {
+                    return $route.current.params.provider;
+                },
+                serverInfo : function(ServerInfoLoader) {
+                    return ServerInfoLoader();
+                }
+            },
+            controller : 'LDAPUserStorageCtrl'
+        })
         .when('/create/user-storage/:realm/providers/:provider', {
             templateUrl : resourceUrl + '/partials/user-storage-generic.html',
             resolve : {
@@ -1489,6 +1509,24 @@ module.config([ '$routeProvider', function($routeProvider) {
             },
             controller : 'GenericUserStorageCtrl'
         })
+        .when('/realms/:realm/user-storage/providers/ldap/:componentId', {
+            templateUrl : resourceUrl + '/partials/user-storage-ldap.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                instance : function(ComponentLoader) {
+                    return ComponentLoader();
+                },
+                providerId : function($route) {
+                    return $route.current.params.provider;
+                },
+                serverInfo : function(ServerInfoLoader) {
+                    return ServerInfoLoader();
+                }
+            },
+            controller : 'LDAPUserStorageCtrl'
+        })
         .when('/realms/:realm/user-storage/providers/:provider/:componentId', {
             templateUrl : resourceUrl + '/partials/user-storage-generic.html',
             resolve : {
@@ -1507,6 +1545,60 @@ module.config([ '$routeProvider', function($routeProvider) {
             },
             controller : 'GenericUserStorageCtrl'
         })
+        .when('/realms/:realm/ldap-mappers/:componentId', {
+            templateUrl : function(params){ return resourceUrl + '/partials/user-storage-ldap-mappers.html'; },
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                provider : function(ComponentLoader) {
+                    return ComponentLoader();
+                },
+                mappers : function(ComponentsLoader, $route) {
+                    return ComponentsLoader.loadComponents($route.current.params.componentId, 'org.keycloak.storage.ldap.mappers.LDAPStorageMapper');
+                }
+            },
+            controller : 'LDAPMapperListCtrl'
+        })
+        .when('/create/ldap-mappers/:realm/:componentId', {
+            templateUrl : function(params){ return resourceUrl + '/partials/user-storage-ldap-mapper-detail.html'; },
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                provider : function(ComponentLoader) {
+                    return ComponentLoader();
+                },
+                mapperTypes : function(SubComponentTypesLoader, $route) {
+                    return SubComponentTypesLoader.loadComponents($route.current.params.componentId, 'org.keycloak.storage.ldap.mappers.LDAPStorageMapper');
+                },
+                clients : function(ClientListLoader) {
+                    return ClientListLoader();
+                }
+            },
+            controller : 'LDAPMapperCreateCtrl'
+        })
+        .when('/realms/:realm/ldap-mappers/:componentId/mappers/:mapperId', {
+            templateUrl : function(params){ return resourceUrl + '/partials/user-storage-ldap-mapper-detail.html'; },
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                provider : function(ComponentLoader) {
+                    return ComponentLoader();
+                },
+                mapperTypes : function(SubComponentTypesLoader, $route) {
+                    return SubComponentTypesLoader.loadComponents($route.current.params.componentId, 'org.keycloak.storage.ldap.mappers.LDAPStorageMapper');
+                },
+                mapper : function(LDAPMapperLoader) {
+                    return LDAPMapperLoader();
+                },
+                clients : function(ClientListLoader) {
+                    return ClientListLoader();
+                }
+            },
+            controller : 'LDAPMapperCtrl'
+        })
         .when('/realms/:realm/user-federation', {
             templateUrl : resourceUrl + '/partials/user-federation.html',
             resolve : {
@@ -2420,6 +2512,15 @@ module.directive('kcTabsUserFederation', function () {
     }
 });
 
+module.directive('kcTabsLdap', function () {
+    return {
+        scope: true,
+        restrict: 'E',
+        replace: true,
+        templateUrl: resourceUrl + '/templates/kc-tabs-ldap.html'
+    }
+});
+
 module.controller('RoleSelectorModalCtrl', function($scope, realm, config, configName, RealmRoles, Client, ClientRole, $modalInstance) {
     $scope.selectedRealmRole = {
         role: undefined
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 03addd9..c3b7e1f 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
@@ -627,12 +627,14 @@ module.controller('UserFederationCtrl', function($scope, $location, $route, real
     for (var i = 0; i < $scope.providers.length; i++) {
         $scope.providers[i].isUserFederationProvider = false;
     }
+    /*
     UserFederationProviders.query({realm: realm.realm}, function(data) {
         for (var i = 0; i < data.length; i++) {
             data[i].isUserFederationProvider = true;
             $scope.providers.push(data[i]);
         }
     });
+    */
 
     $scope.addProvider = function(provider) {
         console.log('Add provider: ' + provider.id);
@@ -761,13 +763,13 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
             if (providerFactory.metadata.synchronizable) {
                 instance.config['fullSyncPeriod'] = ['-1'];
                 instance.config['changedSyncPeriod'] = ['-1'];
-                instance.config['cachePolicy'] = ['DEFAULT'];
-                instance.config['evictionDay'] = [''];
-                instance.config['evictionHour'] = [''];
-                instance.config['evictionMinute'] = [''];
-                instance.config['maxLifespan'] = [''];
 
             }
+            instance.config['cachePolicy'] = ['DEFAULT'];
+            instance.config['evictionDay'] = [''];
+            instance.config['evictionHour'] = [''];
+            instance.config['evictionMinute'] = [''];
+            instance.config['maxLifespan'] = [''];
             if (providerFactory.properties) {
 
                 for (var i = 0; i < providerFactory.properties.length; i++) {
@@ -816,17 +818,10 @@ module.controller('GenericUserStorageCtrl', function($scope, $location, Notifica
                 instance.config['maxLifespan'] = [''];
 
             }
-
-            /*
-            console.log('Manage instance');
-            console.log(instance.name);
-            console.log(instance.providerId);
-            console.log(instance.providerType);
-            console.log(instance.parentId);
-            for (var k in instance.config) {
-                console.log('config[' + k + "] =");
+            if (!instance.config['priority']) {
+                instance.config['priority'] = ['0'];
             }
-            */
+
         }
         if (providerFactory.metadata.synchronizable) {
             if (instance.config && instance.config['importEnabled']) {
@@ -1560,4 +1555,498 @@ module.controller('UserGroupMembershipCtrl', function($scope, $route, realm, gro
 
 });
 
+module.controller('LDAPUserStorageCtrl', function($scope, $location, Notifications, $route, Dialog, realm,
+                                                     serverInfo, instance, Components, UserStorageSync, RealmLDAPConnectionTester) {
+    console.log('LDAPUserStorageCtrl');
+    var providerId = 'ldap';
+    console.log('providerId: ' + providerId);
+    $scope.create = !instance.providerId;
+    console.log('create: ' + $scope.create);
+    var providers = serverInfo.componentTypes['org.keycloak.storage.UserStorageProvider'];
+    console.log('providers length ' + providers.length);
+    var providerFactory = null;
+    for (var i = 0; i < providers.length; i++) {
+        var p = providers[i];
+        console.log('provider: ' + p.id);
+        if (p.id == providerId) {
+            $scope.providerFactory = p;
+            providerFactory = p;
+            break;
+        }
+
+    }
+
+    $scope.provider = instance;
+    $scope.showSync = false;
+
+    $scope.ldapVendors = [
+        { "id": "ad", "name": "Active Directory" },
+        { "id": "rhds", "name": "Red Hat Directory Server" },
+        { "id": "tivoli", "name": "Tivoli" },
+        { "id": "edirectory", "name": "Novell eDirectory" },
+        { "id": "other", "name": "Other" }
+    ];
+
+    $scope.authTypes = [
+        { "id": "none", "name": "none" },
+        { "id": "simple", "name": "simple" }
+    ];
+
+    $scope.searchScopes = [
+        { "id": "1", "name": "One Level" },
+        { "id": "2", "name": "Subtree" }
+    ];
+
+    $scope.useTruststoreOptions = [
+        { "id": "always", "name": "Always" },
+        { "id": "ldapsOnly", "name": "Only for ldaps" },
+        { "id": "never", "name": "Never" }
+    ];
+
+    var DEFAULT_BATCH_SIZE = "1000";
+
+
+    console.log("providerFactory: " + providerFactory.id);
+
+    function initUserStorageSettings() {
+        if ($scope.create) {
+            instance.name = 'ldap';
+            instance.providerId = 'ldap';
+            instance.providerType = 'org.keycloak.storage.UserStorageProvider';
+            instance.parentId = realm.id;
+            instance.config = {
+
+            };
+            instance.config['priority'] = ["0"];
+
+            $scope.fullSyncEnabled = false;
+            $scope.changedSyncEnabled = false;
+            instance.config['fullSyncPeriod'] = ['-1'];
+            instance.config['changedSyncPeriod'] = ['-1'];
+            instance.config['cachePolicy'] = ['DEFAULT'];
+            instance.config['evictionDay'] = [''];
+            instance.config['evictionHour'] = [''];
+            instance.config['evictionMinute'] = [''];
+            instance.config['maxLifespan'] = [''];
+            instance.config['batchSizeForSync'] = [DEFAULT_BATCH_SIZE];
+
+            if (providerFactory.properties) {
+
+                for (var i = 0; i < providerFactory.properties.length; i++) {
+                    var configProperty = providerFactory.properties[i];
+                    if (configProperty.defaultValue) {
+                        instance.config[configProperty.name] = [configProperty.defaultValue];
+                    } else {
+                        instance.config[configProperty.name] = [''];
+                    }
+
+                }
+            }
+
+
+        } else {
+            $scope.fullSyncEnabled = (instance.config['fullSyncPeriod'] && instance.config['fullSyncPeriod'][0] > 0);
+            $scope.changedSyncEnabled = (instance.config['changedSyncPeriod'] && instance.config['changedSyncPeriod'][0]> 0);
+            if (!instance.config['fullSyncPeriod']) {
+                console.log('setting to -1');
+                instance.config['fullSyncPeriod'] = ['-1'];
+
+            }
+            if (!instance.config['changedSyncPeriod']) {
+                console.log('setting to -1');
+                instance.config['changedSyncPeriod'] = ['-1'];
+
+            }
+            if (!instance.config['cachePolicy']) {
+                instance.config['cachePolicy'] = ['DEFAULT'];
+
+            }
+            if (!instance.config['evictionDay']) {
+                instance.config['evictionDay'] = [''];
+
+            }
+            if (!instance.config['evictionHour']) {
+                instance.config['evictionHour'] = [''];
+
+            }
+            if (!instance.config['evictionMinute']) {
+                instance.config['evictionMinute'] = [''];
+
+            }
+            if (!instance.config['maxLifespan']) {
+                instance.config['maxLifespan'] = [''];
+
+            }
+            if (!instance.config['priority']) {
+                instance.config['priority'] = ['0'];
+            }
+
+            if (providerFactory.properties) {
+
+                for (var i = 0; i < providerFactory.properties.length; i++) {
+                    var configProperty = providerFactory.properties[i];
+                    if (!instance.config[configProperty.name]) {
+                        if (configProperty.defaultValue) {
+                            instance.config[configProperty.name] = [configProperty.defaultValue];
+                        } else {
+                            instance.config[configProperty.name] = [''];
+                        }
+                    }
+
+                }
+            }
+
+            for (var i=0 ; i<$scope.ldapVendors.length ; i++) {
+                if ($scope.ldapVendors[i].id === instance.config['vendor'][0]) {
+                    $scope.vendorName = $scope.ldapVendors[i].name;
+                }
+            };
+
+
+
+        }
+        if (instance.config && instance.config['importEnabled']) {
+            $scope.showSync = instance.config['importEnabled'][0] == 'true';
+        } else {
+            $scope.showSync = true;
+        }
+
+        $scope.changed = false;
+    }
+
+    initUserStorageSettings();
+    $scope.instance = angular.copy(instance);
+    $scope.realm = realm;
+
+    $scope.$watch('instance', function() {
+        if (!angular.equals($scope.instance, instance)) {
+            $scope.changed = true;
+        }
+
+        if (!angular.equals($scope.instance.config['vendor'][0], $scope.lastVendor)) {
+            console.log("LDAP vendor changed");
+            $scope.lastVendor = $scope.instance.config['vendor'][0];
+
+            if ($scope.lastVendor === "ad") {
+                $scope.instance.config['usernameLDAPAttribute'][0] = "cn";
+                $scope.instance.config['userObjectClasses'][0] = "person, organizationalPerson, user";
+            } else {
+                $scope.instance.config['usernameLDAPAttribute'][0] = "uid";
+                $scope.instance.config['userObjectClasses'][0] = "inetOrgPerson, organizationalPerson";
+            }
+
+            $scope.instance.config['rdnLDAPAttribute'][0] = $scope.instance.config['usernameLDAPAttribute'][0];
+
+            var vendorToUUID = {
+                rhds: "nsuniqueid",
+                tivoli: "uniqueidentifier",
+                edirectory: "guid",
+                ad: "objectGUID",
+                other: "entryUUID"
+            };
+            $scope.instance.config['uuidLDAPAttribute'][0] = vendorToUUID[$scope.lastVendor];
+        }
+
+
+    }, true);
+
+    $scope.$watch('fullSyncEnabled', function(newVal, oldVal) {
+        if (oldVal == newVal) {
+            return;
+        }
+
+        $scope.instance.config['fullSyncPeriod'][0] = $scope.fullSyncEnabled ? "604800" : "-1";
+        $scope.changed = true;
+    });
+
+    $scope.$watch('changedSyncEnabled', function(newVal, oldVal) {
+        if (oldVal == newVal) {
+            return;
+        }
+
+        $scope.instance.config['changedSyncPeriod'][0] = $scope.changedSyncEnabled ? "86400" : "-1";
+        $scope.changed = true;
+    });
+
+
+    $scope.save = function() {
+        $scope.changed = false;
+        if (!parseInt($scope.instance.config['batchSizeForSync'[0]])) {
+            $scope.instance.config['batchSizeForSync'][0] = DEFAULT_BATCH_SIZE;
+        } else {
+            $scope.instance.config['batchSizeForSync'][0] = parseInt($scope.instance.config.batchSizeForSync).toString();
+        }
+
+        if ($scope.create) {
+            Components.save({realm: realm.realm}, $scope.instance,  function (data, headers) {
+                var l = headers().location;
+                var id = l.substring(l.lastIndexOf("/") + 1);
+
+                $location.url("/realms/" + realm.realm + "/user-storage/providers/" + $scope.instance.providerId + "/" + id);
+                Notifications.success("The provider has been created.");
+            }, function (errorResponse) {
+                if (errorResponse.data && errorResponse.data['error_description']) {
+                    Notifications.error(errorResponse.data['error_description']);
+                }
+            });
+        } else {
+            Components.update({realm: realm.realm,
+                    componentId: instance.id
+                },
+                $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']);
+                    }
+                });
+        }
+    };
+
+    $scope.reset = function() {
+        initUserStorageSettings();
+        $scope.instance = angular.copy(instance);
+    };
+
+    $scope.cancel = function() {
+        if ($scope.create) {
+            $location.url("/realms/" + realm.realm + "/user-storage");
+        } else {
+            $route.reload();
+        }
+    };
+
+    $scope.triggerFullSync = function() {
+        console.log('GenericCtrl: triggerFullSync');
+        triggerSync('triggerFullSync');
+    }
+
+    $scope.triggerChangedUsersSync = function() {
+        console.log('GenericCtrl: triggerChangedUsersSync');
+        triggerSync('triggerChangedUsersSync');
+    }
+
+    function triggerSync(action) {
+        UserStorageSync.save({ action: action, realm: $scope.realm.realm, componentId: $scope.instance.id }, {}, function(syncResult) {
+            $route.reload();
+            Notifications.success("Sync of users finished successfully. " + syncResult.status);
+        }, function() {
+            $route.reload();
+            Notifications.error("Error during sync of users");
+        });
+    }
+
+    var initConnectionTest = function(testAction, ldapConfig) {
+        return {
+            action: testAction,
+            realm: $scope.realm.realm,
+            connectionUrl: ldapConfig.connectionUrl,
+            bindDn: ldapConfig.bindDn,
+            bindCredential: ldapConfig.bindCredential,
+            useTruststoreSpi: ldapConfig.useTruststoreSpi
+        };
+    };
+
+    $scope.testConnection = function() {
+        console.log('LDAPCtrl: testConnection');
+        RealmLDAPConnectionTester.get(initConnectionTest("testConnection", $scope.instance.config), function() {
+            Notifications.success("LDAP connection successful.");
+        }, function() {
+            Notifications.error("Error when trying to connect to LDAP. See server.log for details.");
+        });
+    }
+
+    $scope.testAuthentication = function() {
+        console.log('LDAPCtrl: testAuthentication');
+        RealmLDAPConnectionTester.get(initConnectionTest("testAuthentication", $scope.instance.config), function() {
+            Notifications.success("LDAP authentication successful.");
+        }, function() {
+            Notifications.error("LDAP authentication failed. See server.log for details");
+        });
+    }
+
+
+
+});
+
+module.controller('LDAPTabCtrl', function(Dialog, $scope, Current, Notifications, $location) {
+    $scope.removeUserFederation = function() {
+        Dialog.confirmDelete($scope.instance.name, 'ldap provider', function() {
+            $scope.instance.$remove({
+                realm : Current.realm.realm,
+                componentId : $scope.instance.id
+            }, function() {
+                $location.url("/realms/" + Current.realm.realm + "/user-federation");
+                Notifications.success("The provider has been deleted.");
+            });
+        });
+    };
+});
+
+
+module.controller('LDAPMapperListCtrl', function($scope, $location, Notifications, $route, Dialog, realm, provider, mappers) {
+    console.log('LDAPMapperListCtrl');
+
+    $scope.realm = realm;
+    $scope.provider = provider;
+    $scope.instance = provider;
+
+    $scope.mappers = mappers;
+
+});
+
+module.controller('LDAPMapperCtrl', function($scope, $route, realm,  provider, mapperTypes, mapper, clients, Components, LDAPMapperSync, Notifications, Dialog, $location) {
+    console.log('LDAPMapperCtrl');
+    $scope.realm = realm;
+    $scope.provider = provider;
+    $scope.clients = clients;
+    $scope.create = false;
+    $scope.changed = false;
+
+    for (var i = 0; i < mapperTypes.length; i++) {
+        console.log('mapper.providerId: ' + mapper.providerId);
+        console.log('mapperTypes[i].id ' + mapperTypes[i].id);
+        if (mapperTypes[i].id == mapper.providerId) {
+            $scope.mapperType = mapperTypes[i];
+            break;
+        }
+    }
+
+    if ($scope.mapperType.properties) {
+
+        for (var i = 0; i < $scope.mapperType.properties.length; i++) {
+            var configProperty = $scope.mapperType.properties[i];
+            if (!mapper.config[configProperty.name]) {
+                if (configProperty.defaultValue) {
+                    mapper.config[configProperty.name] = [configProperty.defaultValue];
+                } else {
+                    mapper.config[configProperty.name] = [''];
+                }
+            }
+
+        }
+    }
+    $scope.mapper = angular.copy(mapper);
+
+
+    $scope.$watch('mapper', function() {
+        if (!angular.equals($scope.mapper, mapper)) {
+            $scope.changed = true;
+        }
+    }, true);
+
+    $scope.save = function() {
+        Components.update({realm: realm.realm,
+                componentId: mapper.id
+            },
+            $scope.mapper,  function () {
+                $route.reload();
+                Notifications.success("The mapper has been updated.");
+            }, function (errorResponse) {
+                if (errorResponse.data && errorResponse.data['error_description']) {
+                    Notifications.error(errorResponse.data['error_description']);
+                }
+            });
+    };
+
+    $scope.reset = function() {
+        $scope.mapper = angular.copy(mapper);
+        $scope.changed = false;
+    };
+
+    $scope.remove = function() {
+        Dialog.confirmDelete($scope.mapper.name, 'ldap mapper', function() {
+            Components.remove({
+                realm : realm.realm,
+                componentId : mapper.id
+            }, function() {
+                $location.url("/realms/" + realm.realm + '/ldap-mappers/' + provider.id);
+                Notifications.success("The provider has been deleted.");
+            });
+        });
+    };
+
+    $scope.triggerFedToKeycloakSync = function() {
+        triggerMapperSync("fedToKeycloak")
+    }
+
+    $scope.triggerKeycloakToFedSync = function() {
+        triggerMapperSync("keycloakToFed");
+    }
+
+    function triggerMapperSync(direction) {
+        LDAPMapperSync.save({ direction: direction, realm: realm.realm, parentId: provider.id, mapperId : $scope.mapper.id }, {}, function(syncResult) {
+            Notifications.success("Data synced successfully. " + syncResult.status);
+        }, function(error) {
+            Notifications.error(error.data.errorMessage);
+        });
+    }
+
+});
+
+module.controller('LDAPMapperCreateCtrl', function($scope, realm, provider, mapperTypes, clients, Components, Notifications, Dialog, $location) {
+    console.log('LDAPMapperCreateCtrl');
+    $scope.realm = realm;
+    $scope.provider = provider;
+    $scope.clients = clients;
+    $scope.create = true;
+    $scope.mapper = { config: {}};
+    $scope.mapperTypes = mapperTypes;
+    $scope.mapperType = null;
+    $scope.changed = true;
+
+    $scope.$watch('mapperType', function() {
+        if ($scope.mapperType != null) {
+            $scope.mapper.config = {};
+            if ($scope.mapperType.properties) {
+
+                for (var i = 0; i < $scope.mapperType.properties.length; i++) {
+                    var configProperty = $scope.mapperType.properties[i];
+                    if (!$scope.mapper.config[configProperty.name]) {
+                        if (configProperty.defaultValue) {
+                            $scope.mapper.config[configProperty.name] = [configProperty.defaultValue];
+                        } else {
+                            $scope.mapper.config[configProperty.name] = [''];
+                        }
+                    }
+
+                }
+            }
+        }
+    }, true);
+
+    $scope.save = function() {
+        if ($scope.mapperType == null) {
+            Notifications.error("You need to select mapper type!");
+            return;
+        }
+
+        $scope.mapper.providerId = $scope.mapperType.id;
+        $scope.mapper.providerType = 'org.keycloak.storage.ldap.mappers.LDAPStorageMapper';
+        $scope.mapper.parentId = provider.id;
+
+        Components.save({realm: realm.realm}, $scope.mapper,  function (data, headers) {
+            var l = headers().location;
+            var id = l.substring(l.lastIndexOf("/") + 1);
+
+            $location.url("/realms/" + realm.realm + "/ldap-mappers/" + $scope.mapper.parentId + "/mappers/" + id);
+            Notifications.success("The mapper has been created.");
+        }, function (errorResponse) {
+            if (errorResponse.data && errorResponse.data['error_description']) {
+                Notifications.error(errorResponse.data['error_description']);
+            }
+        });
+    };
+
+    $scope.reset = function() {
+        $location.url("/realms/" + realm.realm + '/ldap-mappers/' + provider.id);
+    };
+
+
+});
+
+
+
+
 
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/themes/src/main/resources/theme/base/admin/resources/js/loaders.js
index 78fb99f..6d8c3fa 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/loaders.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/loaders.js
@@ -143,6 +143,15 @@ module.factory('ComponentLoader', function(Loader, Components, $route, $q) {
     });
 });
 
+module.factory('LDAPMapperLoader', function(Loader, Components, $route, $q) {
+    return Loader.get(Components, function() {
+        return {
+            realm : $route.current.params.realm,
+            componentId: $route.current.params.mapperId
+        }
+    });
+});
+
 module.factory('ComponentsLoader', function(Loader, Components, $route, $q) {
     var componentsLoader = {};
 
@@ -159,6 +168,22 @@ module.factory('ComponentsLoader', function(Loader, Components, $route, $q) {
     return componentsLoader;
 });
 
+module.factory('SubComponentTypesLoader', function(Loader, SubComponentTypes, $route, $q) {
+    var componentsLoader = {};
+
+    componentsLoader.loadComponents = function(parent, componentType) {
+        return Loader.query(SubComponentTypes, function() {
+            return {
+                realm : $route.current.params.realm,
+                componentId : parent,
+                type: componentType
+            }
+        })();
+    };
+
+    return componentsLoader;
+});
+
 module.factory('UserFederationInstanceLoader', function(Loader, UserFederationInstances, $route, $q) {
     return Loader.get(UserFederationInstances, function() {
         return {
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js
index 5b44382..abacb07 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -1699,6 +1699,13 @@ module.factory('DefaultGroups', function($resource) {
     });
 });
 
+module.factory('SubComponentTypes', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/components/:componentId/sub-component-types', {
+        realm: '@realm',
+        componentId: '@componentId'
+    });
+});
+
 module.factory('Components', function($resource, ComponentUtils) {
     return $resource(authUrl + '/admin/realms/:realm/components/:componentId', {
         realm : '@realm',
@@ -1742,4 +1749,14 @@ module.factory('ClientRegistrationPolicyProviders', function($resource) {
     });
 });
 
+module.factory('LDAPMapperSync', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/user-storage/:parentId/mappers/:mapperId/sync', {
+        realm : '@realm',
+        componentId : '@componentId',
+        mapperId: '@mapperId'
+    });
+});
+
+
+
 
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html
index ccfb203..1b545f7 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html
@@ -62,7 +62,7 @@
                 <kc-tooltip>{{:: 'credentials.disable.tooltip' | translate}}</kc-tooltip>
             </div>
         </fieldset>
-        
+
         <fieldset class="border-top" data-ng-show="user.email">
             <legend><span class="text">{{:: 'credential-reset-actions' | translate}}</span></legend>
             <div class="form-group clearfix">
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap.html
new file mode 100755
index 0000000..060d7d7
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap.html
@@ -0,0 +1,449 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+    <ol class="breadcrumb">
+        <li><a href="#/realms/{{realm.realm}}/user-federation">{{:: 'user-federation' | translate}}</a></li>
+        <li data-ng-hide="create">{{instance.name|capitalize}}</li>
+        <li data-ng-show="create">{{:: 'add-user-storage-provider' | translate}}</li>
+    </ol>
+
+    <kc-tabs-ldap></kc-tabs-ldap>
+
+    <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
+        <input type="text" readonly value="this is not a login form" style="display: none;">
+        <input type="password" readonly value="this is not a login form" style="display: none;">
+
+        <fieldset>
+            <legend><span class="text">{{:: 'required-settings' | translate}}</span></legend>
+            <div class="form-group clearfix" data-ng-show="!create">
+                <label class="col-md-2 control-label" for="providerId">{{:: 'provider-id' | translate}} </label>
+                <div class="col-md-6">
+                    <input class="form-control" id="providerId" type="text" ng-model="instance.id" readonly>
+                </div>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="consoleDisplayName">{{:: 'console-display-name' | translate}} </label>
+                <div class="col-md-6">
+                    <input class="form-control" id="consoleDisplayName" type="text" ng-model="instance.name" placeholder="{{:: 'defaults-to-id' | translate}}">
+                </div>
+                <kc-tooltip>{{:: 'console-display-name.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="priority">{{:: 'priority' | translate}} </label>
+                <div class="col-md-6">
+                    <input class="form-control" id="priority" type="text" ng-model="instance.config['priority'][0]">
+                </div>
+                <kc-tooltip>{{:: 'priority.tooltip' | translate}}</kc-tooltip>
+            </div>
+
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="editMode">{{:: 'edit-mode' | translate}}</label>
+                <div class="col-md-6">
+                    <div>
+                        <select class="form-control" id="editMode"
+                                ng-model="instance.config['editMode'][0]">
+                            <option>READ_ONLY</option>
+                            <option>WRITABLE</option>
+                            <option>UNSYNCED</option>
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>{{:: 'ldap.edit-mode.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix block">
+                <label class="col-md-2 control-label" for="syncRegistrations">{{:: 'sync-registrations' | translate}}</label>
+                <div class="col-md-6">
+                    <input ng-model="instance.config['syncRegistrations'][0]" name="syncRegistrations" id="syncRegistrations" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+                </div>
+                <kc-tooltip>{{:: 'ldap.sync-registrations.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="vendor"><span class="required">*</span> {{:: 'vendor' | translate}}</label>
+                <div class="col-md-6">
+                    <div data-ng-show="create">
+                        <select class="form-control" id="vendor"
+                                ng-model="instance.config['vendor'][0]"
+                                ng-options="vendor.id as vendor.name for vendor in ldapVendors"
+                                required>
+                        </select>
+                    </div>
+                    <div data-ng-show="!create">
+                        <input class="form-control" id="vendor-ro" type="text" ng-model="vendorName" readonly>
+                    </div>
+                </div>
+                <kc-tooltip>{{:: 'ldap.vendor.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="usernameLDAPAttribute"><span class="required">*</span> {{:: 'username-ldap-attribute' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" id="usernameLDAPAttribute" type="text" ng-model="instance.config['usernameLDAPAttribute'][0]" placeholder="{{:: 'ldap-attribute-name-for-username' | translate}}" required>
+                </div>
+                <kc-tooltip>{{:: 'username-ldap-attribute.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="rdnLDAPAttribute"><span class="required">*</span> {{:: 'rdn-ldap-attribute' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" id="rdnLDAPAttribute" type="text" ng-model="instance.config['rdnLDAPAttribute'][0]" placeholder="{{:: 'ldap-attribute-name-for-user-rdn' | translate}}" required>
+                </div>
+                <kc-tooltip>{{:: 'rdn-ldap-attribute.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="uuidLDAPAttribute"><span class="required">*</span> {{:: 'uuid-ldap-attribute' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" id="uuidLDAPAttribute" type="text" ng-model="instance.config['uuidLDAPAttribute'][0]" placeholder="{{:: 'ldap-attribute-name-for-uuid' | translate}}" required>
+                </div>
+                <kc-tooltip>{{:: 'uuid-ldap-attribute.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="userObjectClasses"><span class="required">*</span> {{:: 'user-object-classes' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" id="userObjectClasses" type="text" ng-model="instance.config['userObjectClasses'][0]" placeholder="{{:: 'ldap-user-object-classes.placeholder' | translate}}" required>
+                </div>
+                <kc-tooltip>{{:: 'ldap.user-object-classes.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="ldapConnectionUrl"><span class="required">*</span> {{:: 'connection-url' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" id="ldapConnectionUrl" type="text" ng-model="instance.config['connectionUrl'][0]" placeholder="{{:: 'ldap-connection-url' | translate}}" required>
+                </div>
+                <kc-tooltip>{{:: 'ldap.connection-url.tooltip' | translate}}</kc-tooltip>
+                <div class="col-sm-4" data-ng-show="access.manageRealm">
+                    <a class="btn btn-primary" data-ng-click="testConnection()">{{:: 'test-connection' | translate}}</a>
+                </div>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="ldapUsersDn"><span class="required">*</span> {{:: 'users-dn' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" id="ldapUsersDn" type="text" ng-model="instance.config['usersDn'][0]" placeholder="{{:: 'ldap-users-dn' | translate}}" required>
+                </div>
+                <kc-tooltip>{{:: 'ldap.users-dn.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="authType"><span class="required">*</span> {{:: 'authentication-type' | translate}}</label>
+                <div class="col-md-6">
+                    <div>
+                        <select class="form-control" id="authType"
+                                ng-model="instance.config['authType'][0]"
+                                ng-options="authType.id as authType.name for authType in authTypes"
+                                required>
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>{{:: 'ldap.authentication-type.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix" data-ng-hide="instance.config['authType'][0] == 'none'">
+                <label class="col-md-2 control-label" for="ldapBindDn"><span class="required">*</span> {{:: 'bind-dn' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" id="ldapBindDn" type="text" ng-model="instance.config['bindDn'][0]" placeholder="{{:: 'ldap-bind-dn' | translate}}" data-ng-required="instance.config['authType'][0] != 'none'">
+                </div>
+                <kc-tooltip>{{:: 'ldap.bind-dn.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix" data-ng-hide="instance.config['authType'][0] == 'none'">
+                <label class="col-md-2 control-label" for="ldapBindCredential"><span class="required">*</span> {{:: 'bind-credential' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" id="ldapBindCredential" type="password" ng-model="instance.config['bindCredential'][0]" placeholder="{{:: 'ldap-bind-credentials' | translate}}" data-ng-required="instance.config['authType'][0] != 'none'">
+                </div>
+                <kc-tooltip>{{:: 'ldap.bind-credential.tooltip' | translate}}</kc-tooltip>
+                <div class="col-sm-4" data-ng-show="access.manageRealm">
+                    <a class="btn btn-primary" data-ng-click="testAuthentication()">{{:: 'test-authentication' | translate}}</a>
+                </div>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="customUserSearchFilter">{{:: 'custom-user-ldap-filter' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" id="customUserSearchFilter" type="text" ng-model="instance.config['customUserSearchFilter'][0]" placeholder="{{:: 'ldap-filter' | translate}}">
+                </div>
+                <kc-tooltip>{{:: 'ldap.custom-user-ldap-filter.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="searchScope">{{:: 'search-scope' | translate}}</label>
+                <div class="col-md-6">
+                    <div>
+                        <select class="form-control" id="searchScope"
+                                ng-model="instance.config['searchScope'][0]"
+                                ng-options="searchScope.id as searchScope.name for searchScope in searchScopes"
+                                required>
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>{{:: 'ldap.search-scope.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="useTruststoreSpi">{{:: 'use-truststore-spi' | translate}}</label>
+                <div class="col-md-6">
+                    <div>
+                        <select class="form-control" id="useTruststoreSpi"
+                                ng-model="instance.config['useTruststoreSpi'][0]"
+                                ng-options="useTruststoreSpi.id as useTruststoreSpi.name for useTruststoreSpi in useTruststoreOptions"
+                                required>
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>{{:: 'ldap.use-truststore-spi.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="connectionPooling">{{:: 'connection-pooling' | translate}}</label>
+                <div class="col-md-6">
+                    <input ng-model="instance.config['connectionPooling'][0]" name="connectionPooling" id="connectionPooling" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+                </div>
+                <kc-tooltip>{{:: 'ldap.connection-pooling.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="pagination">{{:: 'pagination' | translate}}</label>
+                <div class="col-md-6">
+                    <input ng-model="instance.config['pagination'][0]" name="pagination" id="pagination" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+                </div>
+                <kc-tooltip>{{:: 'ldap.pagination.tooltip' | translate}}</kc-tooltip>
+            </div>
+        </fieldset>
+
+        <fieldset>
+            <legend><span class="text">{{:: 'kerberos-integration' | translate}}</span></legend>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="allowKerberosAuthentication">{{:: 'allow-kerberos-authentication' | translate}} </label>
+                <div class="col-md-6">
+                    <input ng-model="instance.config['allowKerberosAuthentication'][0]" name="allowKerberosAuthentication" id="allowKerberosAuthentication" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+                </div>
+                <kc-tooltip>{{:: 'ldap.allow-kerberos-authentication.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix" data-ng-show="instance.config['allowKerberosAuthentication'][0] == 'true'">
+                <label class="col-md-2 control-label" for="kerberosRealm"><span class="required">*</span> {{:: 'kerberos-realm' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" id="kerberosRealm" type="text" ng-model="instance.config['kerberosRealm'][0]" ng-required="instance.config['allowKerberosAuthentication'][0] == 'true'">
+                </div>
+                <kc-tooltip>{{:: 'kerberos-realm.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix" data-ng-show="instance.config['allowKerberosAuthentication'][0] == 'true'">
+                <label class="col-md-2 control-label" for="serverPrincipal"><span class="required">*</span> {{:: 'server-principal' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" id="serverPrincipal" type="text" ng-model="instance.config['serverPrincipal'][0]" ng-required="instance.config['allowKerberosAuthentication'][0] == 'true'">
+                </div>
+                <kc-tooltip>{{:: 'server-principal.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix" data-ng-show="instance.config['allowKerberosAuthentication'][0] == 'true'">
+                <label class="col-md-2 control-label" for="keyTab"><span class="required">*</span> {{:: 'keytab' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" id="keyTab" type="text" ng-model="instance.config['keyTab'][0]" ng-required="instance.config['allowKerberosAuthentication'][0] == 'true'">
+                </div>
+                <kc-tooltip>{{:: 'keytab.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group" data-ng-show="instance.config['allowKerberosAuthentication'][0] == 'true'">
+                <label class="col-md-2 control-label" for="debug">{{:: 'debug' | translate}} </label>
+                <div class="col-md-6">
+                    <input ng-model="instance.config['debug'][0]" name="debug" id="debug" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+                </div>
+                <kc-tooltip>{{:: 'debug.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group" data-ng-show="instance.config['allowKerberosAuthentication'][0]">
+                <label class="col-md-2 control-label" for="useKerberosForPasswordAuthentication">{{:: 'use-kerberos-for-password-authentication' | translate}} </label>
+                <div class="col-md-6">
+                    <input ng-model="instance.config['useKerberosForPasswordAuthentication'][0]" id="useKerberosForPasswordAuthentication" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+                </div>
+                <kc-tooltip>{{:: 'ldap.use-kerberos-for-password-authentication.tooltip' | translate}}</kc-tooltip>
+            </div>
+        </fieldset>
+
+        <fieldset>
+            <legend><span class="text">{{:: 'sync-settings' | translate}}</span></legend>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="batchSizeForSync">{{:: 'batch-size' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" type="text" ng-model="instance.config['batchSizeForSync'][0]" id="batchSizeForSync" />
+                </div>
+                <kc-tooltip>{{:: 'ldap.batch-size.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="fullSyncEnabled">{{:: 'periodic-full-sync' | translate}}</label>
+                <div class="col-md-6">
+                    <input ng-model="fullSyncEnabled" name="fullSyncEnabled" id="fullSyncEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+                </div>
+                <kc-tooltip>{{:: 'ldap.periodic-full-sync.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix" data-ng-show="fullSyncEnabled">
+                <label class="col-md-2 control-label" for="fullSyncPeriod">{{:: 'full-sync-period' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" type="text" ng-model="instance.config['fullSyncPeriod'][0]" id="fullSyncPeriod" />
+                </div>
+                <kc-tooltip>{{:: 'full-sync-period.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="changedSyncEnabled">{{:: 'periodic-changed-users-sync' | translate}}</label>
+                <div class="col-md-6">
+                    <input ng-model="changedSyncEnabled" name="changedSyncEnabled" id="changedSyncEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+                </div>
+                <kc-tooltip>{{:: 'ldap.periodic-changed-users-sync.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix" data-ng-show="changedSyncEnabled">
+                <label class="col-md-2 control-label" for="changedSyncPeriod">{{:: 'changed-users-sync-period' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" type="text" ng-model="instance.config['changedSyncPeriod'][0]" id="changedSyncPeriod" />
+                </div>
+                <kc-tooltip>{{:: 'ldap.changed-users-sync-period.tooltip' | translate}}</kc-tooltip>
+            </div>
+        </fieldset>
+
+        <fieldset>
+            <legend><span class="text">{{:: 'user-storage-cache-policy' | translate}}</span></legend>
+            <div class="form-group">
+                <label for="cachePolicy" class="col-md-2 control-label">{{:: 'userStorage.cachePolicy' | translate}}</label>
+                <div class="col-md-2">
+                    <div>
+                        <select id="cachePolicy" ng-model="instance.config['cachePolicy'][0]" class="form-control">
+                            <option value="DEFAULT">{{:: 'userStorage.cachePolicy.option.DEFAULT' | translate}}</option>
+                            <option value="EVICT_DAILY">{{:: 'userStorage.cachePolicy.option.EVICT_DAILY' | translate}}</option>
+                            <option value="EVICT_WEEKLY">{{:: 'userStorage.cachePolicy.option.EVICT_WEEKLY' | translate}}</option>
+                            <option value="MAX_LIFESPAN">{{:: 'userStorage.cachePolicy.option.MAX_LIFESPAN' | translate}}</option>
+                            <option value="NO_CACHE">{{:: 'userStorage.cachePolicy.option.NO_CACHE' | translate}}</option>
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>{{:: 'userStorage.cachePolicy.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group" data-ng-show="instance.config['cachePolicy'][0] == 'EVICT_WEEKLY'">
+                <label for="evictionDay" class="col-md-2 control-label">{{:: 'userStorage.evictionDay' | translate}}</label>
+                <div class="col-md-2">
+                    <div>
+                        <select id="evictionDay" ng-model="instance.config['evictionDay'][0]" class="form-control">
+                            <option value="1">{{:: 'Sunday' | translate}}</option>
+                            <option value="2">{{:: 'Monday' | translate}}</option>
+                            <option value="3">{{:: 'Tuesday' | translate}}</option>
+                            <option value="4">{{:: 'Wednesday' | translate}}</option>
+                            <option value="5">{{:: 'Thursday' | translate}}</option>
+                            <option value="6">{{:: 'Friday' | translate}}</option>
+                            <option value="7">{{:: 'Saturday' | translate}}</option>
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>{{:: 'userStorage.cachePolicy.evictionDay.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix" data-ng-show="instance.config['cachePolicy'][0] == 'EVICT_WEEKLY' || instance.config['cachePolicy'][0] == 'EVICT_DAILY'">
+                <label class="col-md-2 control-label" for="evictionHour">{{:: 'userStorage.cachePolicy.evictionHour' | translate}}</label>
+                <div class="col-md-2">
+                    <div>
+                        <select id="evictionHour" ng-model="instance.config['evictionHour'][0]" class="form-control">
+                            <option value="0">00</option>
+                            <option value="1">01</option>
+                            <option value="2">02</option>
+                            <option value="3">03</option>
+                            <option value="4">04</option>
+                            <option value="5">05</option>
+                            <option value="6">06</option>
+                            <option value="7">07</option>
+                            <option value="8">08</option>
+                            <option value="9">09</option>
+                            <option value="10">10</option>
+                            <option value="11">11</option>
+                            <option value="12">12</option>
+                            <option value="13">13</option>
+                            <option value="14">14</option>
+                            <option value="15">15</option>
+                            <option value="16">16</option>
+                            <option value="17">17</option>
+                            <option value="18">18</option>
+                            <option value="19">19</option>
+                            <option value="20">20</option>
+                            <option value="21">21</option>
+                            <option value="22">22</option>
+                            <option value="23">23</option>
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>{{:: 'userStorage.cachePolicy.evictionHour.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix" data-ng-show="instance.config['cachePolicy'][0] == 'EVICT_WEEKLY' || instance.config['cachePolicy'][0] == 'EVICT_DAILY'">
+                <label class="col-md-2 control-label" for="evictionMinute">{{:: 'userStorage.cachePolicy.evictionMinute' | translate}}</label>
+                <div class="col-md-2">
+                    <div>
+                        <select id="evictionMinute" ng-model="instance.config['evictionMinute'][0]" class="form-control">
+                            <option value="0">00</option>
+                            <option value="1">01</option>
+                            <option value="2">02</option>
+                            <option value="3">03</option>
+                            <option value="4">04</option>
+                            <option value="5">05</option>
+                            <option value="6">06</option>
+                            <option value="7">07</option>
+                            <option value="8">08</option>
+                            <option value="9">09</option>
+                            <option value="10">10</option>
+                            <option value="11">11</option>
+                            <option value="12">12</option>
+                            <option value="13">13</option>
+                            <option value="14">14</option>
+                            <option value="15">15</option>
+                            <option value="16">16</option>
+                            <option value="17">17</option>
+                            <option value="18">18</option>
+                            <option value="19">19</option>
+                            <option value="20">20</option>
+                            <option value="21">21</option>
+                            <option value="22">22</option>
+                            <option value="23">23</option>
+                            <option value="24">24</option>
+                            <option value="25">25</option>
+                            <option value="26">26</option>
+                            <option value="27">27</option>
+                            <option value="28">28</option>
+                            <option value="29">29</option>
+                            <option value="30">30</option>
+                            <option value="31">31</option>
+                            <option value="32">32</option>
+                            <option value="33">33</option>
+                            <option value="34">34</option>
+                            <option value="35">35</option>
+                            <option value="36">36</option>
+                            <option value="37">37</option>
+                            <option value="38">38</option>
+                            <option value="39">39</option>
+                            <option value="40">40</option>
+                            <option value="41">41</option>
+                            <option value="42">42</option>
+                            <option value="43">43</option>
+                            <option value="44">44</option>
+                            <option value="45">45</option>
+                            <option value="46">46</option>
+                            <option value="47">47</option>
+                            <option value="48">48</option>
+                            <option value="49">49</option>
+                            <option value="50">50</option>
+                            <option value="51">51</option>
+                            <option value="52">52</option>
+                            <option value="53">53</option>
+                            <option value="54">54</option>
+                            <option value="55">55</option>
+                            <option value="56">56</option>
+                            <option value="57">57</option>
+                            <option value="58">58</option>
+                            <option value="59">59</option>
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>{{:: 'userStorage.cachePolicy.evictionMinute.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix" data-ng-show="instance.config['cachePolicy'][0] == 'MAX_LIFESPAN'">
+                <label class="col-md-2 control-label" for="maxLifespan">{{:: 'userStorage.cachePolicy.maxLifespan' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" type="text"  ng-model="instance.config['maxLifespan'][0]" id="maxLifespan" />
+                </div>
+                <kc-tooltip>{{:: 'userStorage.cachePolicy.maxLifespan.tooltip' | translate}}</kc-tooltip>
+            </div>
+        </fieldset>
+
+
+        <div class="form-group">
+            <div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageRealm">
+                <button kc-save>{{:: 'save' | translate}}</button>
+                <button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
+            </div>
+        </div>
+
+        <div class="form-group">
+            <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageRealm">
+                <button kc-save  data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+                <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
+                <button class="btn btn-primary" data-ng-click="triggerChangedUsersSync()" data-ng-hide="changed">{{:: 'synchronize-changed-users' | translate}}</button>
+                <button class="btn btn-primary" data-ng-click="triggerFullSync()" data-ng-hide="changed">{{:: 'synchronize-all-users' | translate}}</button>
+            </div>
+        </div>
+    </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap-mapper-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap-mapper-detail.html
new file mode 100644
index 0000000..72cfea4
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap-mapper-detail.html
@@ -0,0 +1,64 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+    <ol class="breadcrumb">
+        <li><a href="#/realms/{{realm.realm}}/user-federation">{{:: 'user-federation' | translate}}</a></li>
+        <li><a href="#/realms/{{realm.realm}}/user-storage/providers/ldap/{{provider.id}}">{{provider.providerId|capitalize}}</a></li>
+        <li><a href="#/realms/{{realm.realm}}/ldap-mappers/{{provider.id}}">{{:: 'ldap-mappers' | translate}}</a></li>
+        <li class="active" data-ng-show="create">{{:: 'create-ldap-mapper' | translate}}</li>
+        <li class="active" data-ng-hide="create">{{mapper.name}}</li>
+    </ol>
+
+    <h1 data-ng-hide="create">{{mapper.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!create && access.manageRealm" 
+    	data-ng-hide="changed" data-ng-click="remove()"></i></h1>
+    <h1 data-ng-show="create">{{:: 'add-user-federation-mapper' | translate}}</h1>
+
+    <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
+        <fieldset>
+            <div class="form-group clearfix" data-ng-show="!create">
+                <label class="col-md-2 control-label" for="mapperId">{{:: 'id' | translate}} </label>
+                <div class="col-md-6">
+                    <input class="form-control" id="mapperId" type="text" ng-model="mapper.id" readonly>
+                </div>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="name">{{:: 'name' | translate}} <span class="required">*</span></label>
+                <div class="col-md-6">
+                    <input class="form-control" id="name" type="text" ng-model="mapper.name" data-ng-readonly="!create" required>
+                </div>
+                <kc-tooltip>{{:: 'mapper.name.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group" data-ng-show="create">
+                <label class="col-md-2 control-label" for="mapperTypeCreate">{{:: 'mapper-type' | translate}}</label>
+                <div class="col-sm-6">
+                    <div>
+                        <select class="form-control" id="mapperTypeCreate"
+                                ng-model="mapperType"
+                                ng-options="mapperType.id for mapperType in mapperTypes">
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>{{mapperType.helpText}}</kc-tooltip>
+            </div>
+            <div class="form-group clearfix" data-ng-hide="create">
+                <label class="col-md-2 control-label" for="mapperType">{{:: 'mapper-type' | translate}}</label>
+                <div class="col-md-6">
+                    <input class="form-control" id="mapperType" type="text" ng-model="mapperType.id" data-ng-readonly="true">
+                </div>
+                <kc-tooltip>{{mapperType.helpText}}</kc-tooltip>
+            </div>
+
+            <kc-component-config realm="realm" config="mapper.config" properties="mapperType.properties"></kc-component-config>
+
+        </fieldset>
+
+        <div class="form-group">
+            <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
+                <button kc-save  data-ng-disabled="!changed">Save</button>
+                <button kc-reset data-ng-disabled="!changed">Cancel</button>
+                <button class="btn btn-primary" data-ng-click="triggerFedToKeycloakSync()" data-ng-hide="create || !mapperType.metadata.fedToKeycloakSyncSupported" data-ng-disabled="changed">{{:: mapperType.metadata.fedToKeycloakSyncMessage | translate}}</button>
+                <button class="btn btn-primary" data-ng-click="triggerKeycloakToFedSync()" data-ng-hide="create || !mapperType.metadata.keycloakToFedSyncSupported" data-ng-disabled="changed">{{:: mapperType.metadata.keycloakToFedSyncMessage | translate}}</button>
+            </div>
+        </div>
+    </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap-mappers.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap-mappers.html
new file mode 100644
index 0000000..794e83e
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap-mappers.html
@@ -0,0 +1,46 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+    <ol class="breadcrumb">
+        <li><a href="#/realms/{{realm.realm}}/user-federation">{{:: 'user-federation' | translate}}</a></li>
+        <li><a href="#/realms/{{realm.realm}}/user-storage/providers/ldap/{{provider.id}}">{{provider.name|capitalize}}</a></li>
+        <li>{{:: 'ldap-mappers' | translate}}</li>
+    </ol>
+
+    <kc-tabs-ldap></kc-tabs-ldap>
+
+    <table class="table table-striped table-bordered">
+        <thead>
+        <tr>
+            <th class="kc-table-actions" colspan="4">
+                <div class="form-inline">
+                    <div class="form-group">
+                        <div class="input-group">
+                            <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search.name" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
+                            <div class="input-group-addon">
+                                <i class="fa fa-search" type="submit"></i>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="pull-right">
+                        <a class="btn btn-primary" href="#/create/ldap-mappers/{{realm.realm}}/{{provider.id}}">{{:: 'create' | translate}}</a>
+                    </div>
+                </div>
+            </th>
+        </tr>
+        <tr data-ng-hide="mappers.length == 0">
+            <th>{{:: 'name' | translate}}</th>
+            <th>{{:: 'type' | translate}}</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr ng-repeat="mapper in mappers | filter:search">
+            <td><a href="#/realms/{{realm.realm}}/ldap-mappers/{{provider.id}}/mappers/{{mapper.id}}">{{mapper.name}}</a></td>
+            <td>{{mapper.providerId}}</td>
+        </tr>
+        <tr data-ng-show="mappers.length == 0">
+            <td>{{:: 'no-mappers-available' | translate}}</td>
+        </tr>
+        </tbody>
+    </table>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-ldap.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-ldap.html
new file mode 100644
index 0000000..089a65f
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-ldap.html
@@ -0,0 +1,12 @@
+<div data-ng-controller="LDAPTabCtrl">
+    <h1 data-ng-hide="create">
+        {{instance.displayName|capitalize}}
+        <i class="pficon pficon-delete clickable" data-ng-show="!create && access.manageUsers" data-ng-click="removeUserFederation()"></i>
+    </h1>
+    <h1 data-ng-show="create">{{:: 'add-user-federation-provider' | translate}}</h1>
+
+    <ul class="nav nav-tabs" data-ng-hide="create">
+        <li ng-class="{active: path[4] == 'ldap'}"><a href="#/realms/{{realm.realm}}/user-storage/providers/ldap/{{instance.id}}">{{:: 'settings' | translate}}</a></li>
+        <li ng-class="{active: path[2] == 'ldap-mappers'}"><a href="#/realms/{{realm.realm}}/ldap-mappers/{{instance.id}}">{{:: 'mappers' | translate}}</a></li>
+    </ul>
+</div>