keycloak-aplcache

Merge pull request #1285 from mposolda/ldap KEYCLOAK-886

5/26/2015 1:28:43 PM

Changes

Details

diff --git a/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java
index f03b376..f7f594a 100644
--- a/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/UserFederationMapperTypeRepresentation.java
@@ -1,5 +1,6 @@
 package org.keycloak.representations.idm;
 
+import java.util.LinkedList;
 import java.util.List;
 
 /**
@@ -11,7 +12,7 @@ public class UserFederationMapperTypeRepresentation {
     protected String category;
     protected String helpText;
 
-    protected List<ConfigPropertyRepresentation> properties;
+    protected List<ConfigPropertyRepresentation> properties  = new LinkedList<>();
 
     public String getId() {
         return id;
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
index 166c5bf..c020f51 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
@@ -24,7 +24,6 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserFederationEventAwareProviderFactory;
 import org.keycloak.models.UserFederationMapperModel;
 import org.keycloak.models.UserFederationProvider;
-import org.keycloak.models.UserFederationProviderFactory;
 import org.keycloak.models.UserFederationProviderModel;
 import org.keycloak.models.UserFederationSyncResult;
 import org.keycloak.models.UserModel;
@@ -89,7 +88,7 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
         String usernameLdapAttribute = ldapConfig.getUsernameLdapAttribute();
 
         UserFederationMapperModel mapperModel;
-        mapperModel = KeycloakModelUtils.createUserFederationMapperModel("usernameMapper", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.ID,
+        mapperModel = KeycloakModelUtils.createUserFederationMapperModel("username", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                 UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME,
                 UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, usernameLdapAttribute,
                 UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
@@ -97,25 +96,25 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
 
         // For AD deployments with sAMAccountName is probably more common to map "cn" to full name of user
         if (activeDirectory && usernameLdapAttribute.equalsIgnoreCase(LDAPConstants.SAM_ACCOUNT_NAME)) {
-            mapperModel = KeycloakModelUtils.createUserFederationMapperModel("fullNameMapper", newProviderModel.getId(), FullNameLDAPFederationMapperFactory.ID,
+            mapperModel = KeycloakModelUtils.createUserFederationMapperModel("full name", newProviderModel.getId(), FullNameLDAPFederationMapperFactory.PROVIDER_ID,
                     FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, LDAPConstants.CN,
                     UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
             realm.addUserFederationMapper(mapperModel);
         } else {
-            mapperModel = KeycloakModelUtils.createUserFederationMapperModel("firstNameMapper", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.ID,
+            mapperModel = KeycloakModelUtils.createUserFederationMapperModel("first name", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                     UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
                     UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.CN,
                     UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
             realm.addUserFederationMapper(mapperModel);
         }
 
-        mapperModel = KeycloakModelUtils.createUserFederationMapperModel("lastNameMapper", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.ID,
+        mapperModel = KeycloakModelUtils.createUserFederationMapperModel("last name", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                 UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.LAST_NAME,
                 UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.SN,
                 UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
         realm.addUserFederationMapper(mapperModel);
 
-        mapperModel = KeycloakModelUtils.createUserFederationMapperModel("emailMapper", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.ID,
+        mapperModel = KeycloakModelUtils.createUserFederationMapperModel("email", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                 UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.EMAIL,
                 UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.EMAIL,
                 UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
@@ -125,14 +124,14 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
         String modifyTimestampLdapAttrName = activeDirectory ? "whenChanged" : LDAPConstants.MODIFY_TIMESTAMP;
 
         // map createTimeStamp as read-only
-        mapperModel = KeycloakModelUtils.createUserFederationMapperModel("creationDateMapper", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.ID,
+        mapperModel = KeycloakModelUtils.createUserFederationMapperModel("creation date", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                 UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.CREATE_TIMESTAMP,
                 UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, createTimestampLdapAttrName,
                 UserAttributeLDAPFederationMapper.READ_ONLY, "true");
         realm.addUserFederationMapper(mapperModel);
 
         // map modifyTimeStamp as read-only
-        mapperModel = KeycloakModelUtils.createUserFederationMapperModel("modifyDateMapper", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.ID,
+        mapperModel = KeycloakModelUtils.createUserFederationMapperModel("modify date", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                 UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.MODIFY_TIMESTAMP,
                 UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, modifyTimestampLdapAttrName,
                 UserAttributeLDAPFederationMapper.READ_ONLY, "true");
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java
index 6b8f186..33ace78 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java
@@ -1,25 +1,65 @@
 package org.keycloak.federation.ldap.mappers;
 
+import java.util.List;
+import java.util.Map;
+
 import org.keycloak.Config;
+import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
+import org.keycloak.mappers.MapperConfigValidationException;
 import org.keycloak.mappers.UserFederationMapperFactory;
 import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.UserFederationMapperModel;
+import org.keycloak.provider.ProviderConfigProperty;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 public abstract class AbstractLDAPFederationMapperFactory implements UserFederationMapperFactory {
 
+    // Used to map attributes from LDAP to UserModel attributes
+    public static final String ATTRIBUTE_MAPPER_CATEGORY = "Attribute Mapper";
+
+    // Used to map roles from LDAP to UserModel users
+    public static final String ROLE_MAPPER_CATEGORY = "Role Mapper";
+
     @Override
     public void init(Config.Scope config) {
     }
 
     @Override
+    public String getFederationProviderType() {
+        return LDAPFederationProviderFactory.PROVIDER_NAME;
+    }
+
+    @Override
     public void postInit(KeycloakSessionFactory factory) {
     }
 
     @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        throw new IllegalStateException("Method not supported for this implementation");
+    }
+
+    @Override
     public void close() {
     }
 
+    public static ProviderConfigProperty createConfigProperty(String name, String label, String helpText, String type, Object defaultValue) {
+        ProviderConfigProperty configProperty = new ProviderConfigProperty();
+        configProperty.setName(name);
+        configProperty.setLabel(label);
+        configProperty.setHelpText(helpText);
+        configProperty.setType(type);
+        configProperty.setDefaultValue(defaultValue);
+        return configProperty;
+    }
+
+    protected void checkMandatoryConfigAttribute(String name, String displayName, UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
+        String attrConfigValue = mapperModel.getConfig().get(name);
+        if (attrConfigValue == null || attrConfigValue.trim().isEmpty()) {
+            throw new MapperConfigValidationException("Missing configuration for '" + displayName + "'");
+        }
+    }
+
 
 }
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java
index 6ef6979..d0d6230 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java
@@ -1,9 +1,14 @@
 package org.keycloak.federation.ldap.mappers;
 
+import java.util.ArrayList;
 import java.util.List;
 
+import org.keycloak.mappers.MapperConfigValidationException;
 import org.keycloak.mappers.UserFederationMapper;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserFederationMapperModel;
 import org.keycloak.provider.ProviderConfigProperty;
 
 /**
@@ -11,21 +16,48 @@ import org.keycloak.provider.ProviderConfigProperty;
  */
 public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory {
 
-    public static final String ID =  "full-name-ldap-mapper";
+    public static final String PROVIDER_ID =  "full-name-ldap-mapper";
+
+    protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+    static {
+        ProviderConfigProperty userModelAttribute = createConfigProperty(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, "LDAP Full Name Attribute",
+                "Name of LDAP attribute, which contains fullName of user. In most cases it will be 'cn' ", ProviderConfigProperty.STRING_TYPE, LDAPConstants.CN);
+        configProperties.add(userModelAttribute);
+
+        ProviderConfigProperty readOnly = createConfigProperty(UserAttributeLDAPFederationMapper.READ_ONLY, "Read Only",
+                "For Read-only is data imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, "false");
+        configProperties.add(readOnly);
+    }
 
     @Override
     public String getHelpText() {
-        return "Some help text - full name mapper - TODO";
+        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 String getDisplayCategory() {
+        return ATTRIBUTE_MAPPER_CATEGORY;
     }
 
     @Override
-    public List<ProviderConfigProperty> getConfigProperties() {
-        return null;
+    public String getDisplayType() {
+        return "Full Name";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties(RealmModel realm) {
+        return configProperties;
     }
 
     @Override
     public String getId() {
-        return ID;
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
+        checkMandatoryConfigAttribute(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE, "LDAP Full Name Attribute", mapperModel);
     }
 
     @Override
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java
index 47f288d..084b255 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java
@@ -178,7 +178,6 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
         }
         String[] objClasses = objectClasses.split(",");
 
-        // TODO: util method for trim and convert array to collection?
         Set<String> trimmed = new HashSet<String>();
         for (String objectClass : objClasses) {
             objectClass = objectClass.trim();
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java
index cbae850..a5eadd9 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java
@@ -1,9 +1,18 @@
 package org.keycloak.federation.ldap.mappers;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 
+import org.keycloak.mappers.MapperConfigValidationException;
 import org.keycloak.mappers.UserFederationMapper;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserFederationMapperModel;
 import org.keycloak.provider.ProviderConfigProperty;
 
 /**
@@ -11,21 +20,95 @@ import org.keycloak.provider.ProviderConfigProperty;
  */
 public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory {
 
-    public static final String ID = "role-ldap-mapper";
+    public static final String PROVIDER_ID = "role-ldap-mapper";
+
+    protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+    static {
+        ProviderConfigProperty rolesDn = createConfigProperty(RoleLDAPFederationMapper.ROLES_DN, "LDAP Roles DN",
+                "LDAP DN where are roles of this tree saved. For example 'ou=finance,dc=example,dc=org' ", ProviderConfigProperty.STRING_TYPE, null);
+        configProperties.add(rolesDn);
+
+        ProviderConfigProperty roleNameLDAPAttribute = createConfigProperty(RoleLDAPFederationMapper.ROLE_NAME_LDAP_ATTRIBUTE, "Role Name LDAP Attribute",
+                "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' ",
+                ProviderConfigProperty.STRING_TYPE, LDAPConstants.CN);
+        configProperties.add(roleNameLDAPAttribute);
+
+        ProviderConfigProperty membershipLDAPAttribute = createConfigProperty(RoleLDAPFederationMapper.MEMBERSHIP_LDAP_ATTRIBUTE, "Membership LDAP Attribute",
+                "Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ",
+                ProviderConfigProperty.STRING_TYPE, LDAPConstants.MEMBER);
+        configProperties.add(membershipLDAPAttribute);
+
+        ProviderConfigProperty roleObjectClasses = createConfigProperty(RoleLDAPFederationMapper.ROLE_OBJECT_CLASSES, "Role Object Classes",
+                "Object classes of the role object divided by comma (if more values needed). In typical LDAP deployment it could be 'groupOfNames' or 'groupOfEntries' ",
+                ProviderConfigProperty.STRING_TYPE, LDAPConstants.GROUP_OF_NAMES);
+        configProperties.add(roleObjectClasses);
+
+        List<String> modes = new LinkedList<String>();
+        for (RoleLDAPFederationMapper.Mode mode : RoleLDAPFederationMapper.Mode.values()) {
+            modes.add(mode.toString());
+        }
+        ProviderConfigProperty mode = createConfigProperty(RoleLDAPFederationMapper.MODE, "Mode",
+                "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.",
+                ProviderConfigProperty.LIST_TYPE, modes);
+        configProperties.add(mode);
+
+        ProviderConfigProperty useRealmRolesMappings = createConfigProperty(RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "Use Realm Roles Mapping",
+                "If true, then LDAP role mappings will be mapped to realm role mappings in Keycloak. Otherwise it will be mapped to client role mappings", ProviderConfigProperty.BOOLEAN_TYPE, "true");
+        configProperties.add(useRealmRolesMappings);
+
+        // NOTE: ClientID will be computed dynamically from available clients
+    }
 
     @Override
     public String getHelpText() {
-        return "Some help text - role mapper - TODO";
+        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 String getDisplayCategory() {
+        return ROLE_MAPPER_CATEGORY;
     }
 
     @Override
-    public List<ProviderConfigProperty> getConfigProperties() {
-        return null;
+    public String getDisplayType() {
+        return "Role mappings";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties(RealmModel realm) {
+        List<ProviderConfigProperty> props = new ArrayList<ProviderConfigProperty>(configProperties);
+
+        Map<String, ClientModel> clients = realm.getClientNameMap();
+        List<String> clientIds = new ArrayList<String>(clients.keySet());
+
+        ProviderConfigProperty clientIdProperty = createConfigProperty(RoleLDAPFederationMapper.CLIENT_ID, "Client ID",
+                "Client ID of client to which LDAP role mappings will be mapped. Applicable just if 'Use Realm Roles Mapping' is false",
+                ProviderConfigProperty.LIST_TYPE, clientIds);
+        props.add(clientIdProperty);
+
+        return props;
     }
 
     @Override
     public String getId() {
-        return ID ;
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
+        checkMandatoryConfigAttribute(RoleLDAPFederationMapper.ROLES_DN, "LDAP Roles DN", mapperModel);
+
+        String realmMappings = mapperModel.getConfig().get(RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING);
+        boolean useRealmMappings = Boolean.parseBoolean(realmMappings);
+        if (!useRealmMappings) {
+            String clientId = mapperModel.getConfig().get(RoleLDAPFederationMapper.CLIENT_ID);
+            if (clientId == null || clientId.trim().isEmpty()) {
+                throw new MapperConfigValidationException("Client ID needs to be provided in config when Realm Roles Mapping is not used");
+            }
+        }
     }
 
     @Override
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java
index 564b012..c0b9d79 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java
@@ -1,9 +1,13 @@
 package org.keycloak.federation.ldap.mappers;
 
+import java.util.ArrayList;
 import java.util.List;
 
+import org.keycloak.mappers.MapperConfigValidationException;
 import org.keycloak.mappers.UserFederationMapper;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserFederationMapperModel;
 import org.keycloak.provider.ProviderConfigProperty;
 
 /**
@@ -11,21 +15,52 @@ import org.keycloak.provider.ProviderConfigProperty;
  */
 public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory {
 
-    public static final String ID = "user-attribute-ldap-mapper";
+    public static final String PROVIDER_ID = "user-attribute-ldap-mapper";
+    protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+    static {
+        ProviderConfigProperty userModelAttribute = createConfigProperty(UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute",
+                "Name of mapped UserModel property or UserModel attribute in Keycloak DB. For example 'firstName', 'lastName, 'email', 'street' etc.", ProviderConfigProperty.STRING_TYPE, null);
+        configProperties.add(userModelAttribute);
+
+        ProviderConfigProperty ldapAttribute = createConfigProperty(UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, "LDAP Attribute",
+                "Name of mapped attribute on LDAP object. For example 'cn', 'sn, 'mail', 'street' etc.", ProviderConfigProperty.STRING_TYPE, null);
+        configProperties.add(ldapAttribute);
+
+        ProviderConfigProperty readOnly = createConfigProperty(UserAttributeLDAPFederationMapper.READ_ONLY, "Read Only",
+                "Read-only attribute is imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, "false");
+        configProperties.add(readOnly);
+    }
 
     @Override
     public String getHelpText() {
-        return "Some help text TODO";
+        return "Used to map single attribute from LDAP user to attribute of UserModel in Keycloak DB";
+    }
+
+    @Override
+    public String getDisplayCategory() {
+        return ATTRIBUTE_MAPPER_CATEGORY;
     }
 
     @Override
-    public List<ProviderConfigProperty> getConfigProperties() {
-        return null;
+    public String getDisplayType() {
+        return "User Attribute";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties(RealmModel realm) {
+        return configProperties;
     }
 
     @Override
     public String getId() {
-        return ID;
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException {
+        checkMandatoryConfigAttribute(UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute", mapperModel);
+        checkMandatoryConfigAttribute(UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, "LDAP Attribute", mapperModel);
     }
 
     @Override
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
index 56f6008..7a0ae1a 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -952,6 +952,58 @@ module.config([ '$routeProvider', function($routeProvider) {
             },
             controller : 'GenericUserFederationCtrl'
         })
+        .when('/realms/:realm/user-federation/providers/:provider/:instance/mappers', {
+            templateUrl : function(params){ return resourceUrl + '/partials/federated-mappers.html'; },
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                provider : function(UserFederationInstanceLoader) {
+                    return UserFederationInstanceLoader();
+                },
+                mapperTypes : function(UserFederationMapperTypesLoader) {
+                    return UserFederationMapperTypesLoader();
+                },
+                mappers : function(UserFederationMappersLoader) {
+                    return UserFederationMappersLoader();
+                }
+            },
+            controller : 'UserFederationMapperListCtrl'
+        })
+        .when('/realms/:realm/user-federation/providers/:provider/:instance/mappers/:mapperId', {
+            templateUrl : function(params){ return resourceUrl + '/partials/federated-mapper-detail.html'; },
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                provider : function(UserFederationInstanceLoader) {
+                    return UserFederationInstanceLoader();
+                },
+                mapperTypes : function(UserFederationMapperTypesLoader) {
+                    return UserFederationMapperTypesLoader();
+                },
+                mapper : function(UserFederationMapperLoader) {
+                    return UserFederationMapperLoader();
+                }
+            },
+            controller : 'UserFederationMapperCtrl'
+        })
+        .when('/create/user-federation-mappers/:realm/:provider/:instance', {
+            templateUrl : function(params){ return resourceUrl + '/partials/federated-mapper-detail.html'; },
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                provider : function(UserFederationInstanceLoader) {
+                    return UserFederationInstanceLoader();
+                },
+                mapperTypes : function(UserFederationMapperTypesLoader) {
+                    return UserFederationMapperTypesLoader();
+                },
+            },
+            controller : 'UserFederationMapperCreateCtrl'
+        })
+
         .when('/realms/:realm/defense/headers', {
             templateUrl : resourceUrl + '/partials/defense-headers.html',
             resolve : {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index 2444be4..bc723f3 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -511,8 +511,8 @@ module.controller('GenericUserFederationCtrl', function($scope, $location, Notif
     }
 
     function triggerSync(action) {
-        UserFederationSync.get({ action: action, realm: $scope.realm.realm, provider: $scope.instance.id }, function() {
-            Notifications.success("Sync of users finished successfully");
+        UserFederationSync.save({ action: action, realm: $scope.realm.realm, provider: $scope.instance.id }, {}, function(syncResult) {
+            Notifications.success("Sync of users finished successfully. " + syncResult.status);
         }, function() {
             Notifications.error("Error during sync of users");
         });
@@ -734,3 +734,128 @@ module.controller('LDAPCtrl', function($scope, $location, $route, Notifications,
 
 });
 
+
+module.controller('UserFederationMapperListCtrl', function($scope, $location, Notifications, $route, Dialog, realm, provider, mapperTypes, mappers) {
+    console.log('UserFederationMapperListCtrl');
+
+    $scope.realm = realm;
+    $scope.provider = provider;
+
+    $scope.mapperTypes = mapperTypes;
+    $scope.mappers = mappers;
+
+    $scope.hasAnyMapperTypes = false;
+    for (var property in mapperTypes) {
+        if (!(property.startsWith('$'))) {
+            $scope.hasAnyMapperTypes = true;
+            break;
+        }
+    }
+
+});
+
+module.controller('UserFederationMapperCtrl', function($scope, realm,  provider, mapperTypes, mapper, UserFederationMapper, Notifications, Dialog, $location) {
+    console.log('UserFederationMapperCtrl');
+    $scope.realm = realm;
+    $scope.provider = provider;
+    $scope.create = false;
+    $scope.mapper = angular.copy(mapper);
+    $scope.changed = false;
+    $scope.mapperType = mapperTypes[mapper.federationMapperType];
+
+    $scope.$watch('mapper', function() {
+        if (!angular.equals($scope.mapper, mapper)) {
+            $scope.changed = true;
+        }
+    }, true);
+
+    $scope.save = function() {
+        UserFederationMapper.update({
+            realm : realm.realm,
+            provider: provider.id,
+            mapperId : mapper.id
+        }, $scope.mapper, function() {
+            $scope.changed = false;
+            mapper = angular.copy($scope.mapper);
+            $location.url("/realms/" + realm.realm + '/user-federation/providers/' + provider.providerName + '/' + provider.id + '/mappers/' + mapper.id);
+            Notifications.success("Your changes have been saved.");
+        }, function(error) {
+            if (error.status == 400) {
+                Notifications.error('Error in configuration of mapper: ' + error.data.error_description);
+            } else {
+                Notification.error('Unexpected error when creating mapper');
+            }
+        });
+    };
+
+    $scope.reset = function() {
+        $scope.mapper = angular.copy(mapper);
+        $scope.changed = false;
+    };
+
+    $scope.cancel = function() {
+        window.history.back();
+    };
+
+    $scope.remove = function() {
+        Dialog.confirmDelete($scope.mapper.name, 'mapper', function() {
+            UserFederationMapper.remove({ realm: realm.realm, provider: provider.id, mapperId : $scope.mapper.id }, function() {
+                Notifications.success("The mapper has been deleted.");
+                $location.url("/realms/" + realm.realm + '/user-federation/providers/' + provider.providerName + '/' + provider.id + '/mappers');
+            });
+        });
+    };
+
+});
+
+module.controller('UserFederationMapperCreateCtrl', function($scope, realm, provider, mapperTypes, UserFederationMapper, Notifications, Dialog, $location) {
+    console.log('UserFederationMapperCreateCtrl');
+    $scope.realm = realm;
+    $scope.provider = provider;
+    $scope.create = true;
+    $scope.mapper = { federationProviderDisplayName: provider.displayName, config: {}};
+    $scope.mapperTypes = mapperTypes;
+    $scope.mapperType = null;
+
+    $scope.$watch('mapperType', function() {
+        if ($scope.mapperType != null) {
+            $scope.mapper.config = {};
+            for ( var i = 0; i < $scope.mapperType.properties.length; i++) {
+                var property = $scope.mapperType.properties[i];
+                if (property.type === 'String' || property.type === 'boolean') {
+                    $scope.mapper.config[ property.name ] = property.defaultValue;
+                }
+            }
+        }
+    }, true);
+
+    $scope.save = function() {
+        if ($scope.mapperType == null) {
+            Notifications.error("You need to select mapper type!");
+            return;
+        }
+
+        $scope.mapper.federationMapperType = $scope.mapperType.id;
+        UserFederationMapper.save({
+            realm : realm.realm, provider: provider.id
+        }, $scope.mapper, function(data, headers) {
+            var l = headers().location;
+            var id = l.substring(l.lastIndexOf("/") + 1);
+            $location.url('/realms/' + realm.realm +'/user-federation/providers/' + provider.providerName + '/' + provider.id + '/mappers/' + id);
+            Notifications.success("Mapper has been created.");
+        }, function(error) {
+            if (error.status == 400) {
+                Notifications.error('Error in configuration of mapper: ' + error.data.error_description);
+            } else {
+                Notification.error('Unexpected error when creating mapper');
+            }
+        });
+    };
+
+    $scope.cancel = function() {
+        window.history.back();
+    };
+
+
+});
+
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
index 3f72ffe..3a492bb 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
@@ -116,6 +116,34 @@ module.factory('UserFederationFactoryLoader', function(Loader, UserFederationPro
     });
 });
 
+module.factory('UserFederationMapperTypesLoader', function(Loader, UserFederationMapperTypes, $route, $q) {
+    return Loader.get(UserFederationMapperTypes, function () {
+        return {
+            realm: $route.current.params.realm,
+            provider: $route.current.params.instance
+        }
+    });
+});
+
+module.factory('UserFederationMappersLoader', function(Loader, UserFederationMappers, $route, $q) {
+    return Loader.query(UserFederationMappers, function () {
+        return {
+            realm: $route.current.params.realm,
+            provider: $route.current.params.instance
+        }
+    });
+});
+
+module.factory('UserFederationMapperLoader', function(Loader, UserFederationMapper, $route, $q) {
+    return Loader.get(UserFederationMapper, function () {
+        return {
+            realm: $route.current.params.realm,
+            provider: $route.current.params.instance,
+            mapperId: $route.current.params.mapperId
+        }
+    });
+});
+
 
 module.factory('UserSessionStatsLoader', function(Loader, UserSessionStats, $route, $q) {
     return Loader.get(UserSessionStats, function() {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
index e9f09a2..f192516 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -238,7 +238,33 @@ module.factory('UserFederationProviders', function($resource) {
 });
 
 module.factory('UserFederationSync', function($resource) {
-    return $resource(authUrl + '/admin/realms/:realm/user-federation/sync/:provider');
+    return $resource(authUrl + '/admin/realms/:realm/user-federation/instances/:provider/sync');
+});
+
+module.factory('UserFederationMapperTypes', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/user-federation/instances/:provider/mapper-types', {
+        realm : '@realm',
+        provider : '@provider'
+    });
+});
+
+module.factory('UserFederationMappers', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/user-federation/instances/:provider/mappers', {
+        realm : '@realm',
+        provider : '@provider'
+    });
+});
+
+module.factory('UserFederationMapper', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/user-federation/instances/:provider/mappers/:mapperId', {
+        realm : '@realm',
+        provider : '@provider',
+        mapperId: '@mapperId'
+    }, {
+        update: {
+            method : 'PUT'
+        }
+    });
 });
 
 
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html
index b2c7da1..f0d8774 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html
@@ -5,8 +5,13 @@
         <li data-ng-show="create">Add User Federation Provider</li>
     </ol>
 
-    <h1 data-ng-hide="create"><strong>User Federation Provider</strong> {{instance.displayName|capitalize}}</h1>
-    <h1 data-ng-show="create"><strong>Add User Federation Provider</strong></h1>
+    <h1 data-ng-hide="create"><strong>{{instance.providerName|capitalize}} User Federation Provider</strong> {{instance.displayName|capitalize}}</h1>
+    <h1 data-ng-show="create"><strong>Add {{instance.providerName|capitalize}} User Federation Provider</strong></h1>
+
+    <ul class="nav nav-tabs" data-ng-hide="create">
+        <li class="active"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">Settings</a></li>
+        <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}/mappers">Mappers</a></li>
+    </ul>
 
     <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
         <fieldset>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-kerberos.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-kerberos.html
index 2294abb..b2f4701 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-kerberos.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-kerberos.html
@@ -8,6 +8,11 @@
     <h1 data-ng-hide="create"><strong>Kerberos User Federation Provider</strong> {{instance.displayName|capitalize}}</h1>
     <h1 data-ng-show="create"><strong>Add Kerberos User Federation Provider</strong></h1>
 
+    <ul class="nav nav-tabs" data-ng-hide="create">
+        <li class="active"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">Settings</a></li>
+        <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}/mappers">Mappers</a></li>
+    </ul>
+
     <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
         <fieldset>
             <legend><span class="text">Required Settings</span></legend>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html
index ec916a0..2b86f09 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html
@@ -8,6 +8,11 @@
     <h1 data-ng-hide="create"><strong>LDAP User Federation Provider</strong> {{instance.displayName|capitalize}}</h1>
     <h1 data-ng-show="create"><strong>Add LDAP User Federation Provider</strong></h1>
 
+    <ul class="nav nav-tabs" data-ng-hide="create">
+        <li class="active"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}">Settings</a></li>
+        <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{instance.providerName}}/{{instance.id}}/mappers">Mappers</a></li>
+    </ul>
+
     <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
 
         <fieldset>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html
new file mode 100644
index 0000000..0b9c144
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mapper-detail.html
@@ -0,0 +1,78 @@
+<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</a></li>
+        <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{provider.providerName}}/{{provider.id}}">{{provider.displayName|capitalize}}</a></li>
+        <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{provider.providerName}}/{{provider.id}}/mappers">User Federation Mappers</a></li>
+        <li class="active" data-ng-show="create">Create User Federation Mapper</li>
+        <li class="active" data-ng-hide="create">{{mapper.name}}</li>
+    </ol>
+
+    <h1 data-ng-hide="create"><strong>User Federation Mapper</strong> {{mapper.name}}</h1>
+    <h1 data-ng-show="create"><strong>Add User Federation Mapper</strong></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 </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 <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>Name of the mapper.</kc-tooltip>
+            </div>
+            <div class="form-group" data-ng-show="create">
+                <label class="col-md-2 control-label" for="mapperTypeCreate">Mapper Type</label>
+                <div class="col-sm-6">
+                    <div>
+                        <select class="form-control" id="mapperTypeCreate"
+                                ng-model="mapperType"
+                                ng-options="mapperType.name for (mapperKey, 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</label>
+                <div class="col-md-6">
+                    <input class="form-control" id="mapperType" type="text" ng-model="mapperType.name" data-ng-readonly="true">
+                </div>
+                <kc-tooltip>{{mapperType.helpText}}</kc-tooltip>
+            </div>
+            <div data-ng-repeat="option in mapperType.properties" class="form-group">
+                <label class="col-md-2 control-label">{{option.label}}</label>
+
+                <div class="col-sm-4" data-ng-hide="option.type == 'boolean' || option.type == 'List'">
+                    <input class="form-control" type="text" data-ng-model="mapper.config[ option.name ]">
+                </div>
+                <div class="col-sm-4" data-ng-show="option.type == 'boolean'">
+                    <input ng-model="mapper.config[ option.name ]" value="'true'" name="option.name" id="option.name" onoffswitchmodel />
+                </div>
+                <div class="col-sm-4" data-ng-show="option.type == 'List'">
+                    <select ng-model="mapper.config[ option.name ]" ng-options="data for data in option.defaultValue">
+                        <option value="" selected> Select one... </option>
+                    </select>
+                </div>
+                <kc-tooltip>{{option.helpText}}</kc-tooltip>
+            </div>
+
+        </fieldset>
+        <div class="pull-right form-actions" data-ng-show="create && access.manageRealm">
+            <button kc-cancel data-ng-click="cancel()">Cancel</button>
+            <button kc-save>Save</button>
+        </div>
+
+        <div class="pull-right form-actions" data-ng-show="!create && access.manageRealm">
+            <button kc-reset data-ng-show="changed">Clear changes</button>
+            <button kc-save  data-ng-show="changed">Save</button>
+            <button kc-delete data-ng-click="remove()" data-ng-hide="changed">Delete</button>
+        </div>
+    </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mappers.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mappers.html
new file mode 100644
index 0000000..d650100
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-mappers.html
@@ -0,0 +1,53 @@
+<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</a></li>
+        <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{provider.providerName}}/{{provider.id}}">{{provider.displayName|capitalize}}</a></li>
+        <li>User Federation Mappers</li>
+    </ol>
+
+    <h1><strong>{{provider.providerName === 'ldap' ? 'LDAP' : (provider.providerName|capitalize)}} User Federation Provider</strong> {{provider.displayName|capitalize}}</h1>
+
+    <ul class="nav nav-tabs" data-ng-hide="create">
+        <li><a href="#/realms/{{realm.realm}}/user-federation/providers/{{provider.providerName}}/{{provider.id}}">Settings</a></li>
+        <li class="active"><a href="#/realms/{{realm.realm}}/user-federation/providers/{{provider.providerName}}/{{provider.id}}/mappers">Mappers</a></li>
+    </ul>
+
+    <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..." 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" data-ng-show="hasAnyMapperTypes">
+                        <a class="btn btn-primary" href="#/create/user-federation-mappers/{{realm.realm}}/{{provider.providerName}}/{{provider.id}}">Create</a>
+                    </div>
+                </div>
+            </th>
+        </tr>
+        <tr data-ng-hide="mappers.length == 0">
+            <th>Name</th>
+            <th>Category</th>
+            <th>Type</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr ng-repeat="mapper in mappers | filter:search">
+            <td><a href="#/realms/{{realm.realm}}/user-federation/providers/{{provider.providerName}}/{{provider.id}}/mappers/{{mapper.id}}">{{mapper.name}}</a></td>
+            <td>{{mapperTypes[mapper.federationMapperType].category}}</td>
+            <td>{{mapperTypes[mapper.federationMapperType].name}}</td>
+        </tr>
+        <tr data-ng-show="mappers.length == 0">
+            <td>No mappers available</td>
+        </tr>
+        </tbody>
+    </table>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/model/api/src/main/java/org/keycloak/mappers/MapperConfigValidationException.java b/model/api/src/main/java/org/keycloak/mappers/MapperConfigValidationException.java
new file mode 100644
index 0000000..ca714a9
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/mappers/MapperConfigValidationException.java
@@ -0,0 +1,15 @@
+package org.keycloak.mappers;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class MapperConfigValidationException extends Exception {
+
+    public MapperConfigValidationException(String message) {
+        super(message);
+    }
+
+    public MapperConfigValidationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java
index 386ce13..309036c 100644
--- a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java
+++ b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapperFactory.java
@@ -1,10 +1,36 @@
 package org.keycloak.mappers;
 
+import java.util.List;
+
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserFederationMapperModel;
 import org.keycloak.provider.ConfiguredProvider;
+import org.keycloak.provider.ProviderConfigProperty;
 import org.keycloak.provider.ProviderFactory;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 public interface UserFederationMapperFactory extends ProviderFactory<UserFederationMapper>, ConfiguredProvider {
+
+    /**
+     * Refers to providerName (type) of the federation provider, which this mapper can be used for. For example "ldap" or "kerberos"
+     *
+     * @return providerName
+     */
+    String getFederationProviderType();
+
+    String getDisplayCategory();
+    String getDisplayType();
+
+    /**
+     * Called when instance of mapperModel is created for this factory through admin endpoint
+     *
+     * @param mapperModel
+     * @throws MapperConfigValidationException if configuration provided in mapperModel is not valid
+     */
+    void validateConfig(UserFederationMapperModel mapperModel) throws MapperConfigValidationException;
+
+    // TODO: Remove this and add realm to the method on ConfiguredProvider?
+    List<ProviderConfigProperty> getConfigProperties(RealmModel realm);
 }
diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index 988d9ec..77f825f 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -1,6 +1,7 @@
 package org.keycloak.models.utils;
 
 import org.bouncycastle.openssl.PEMWriter;
+import org.keycloak.constants.KerberosConstants;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
@@ -8,6 +9,7 @@ import org.keycloak.models.KeycloakSessionTask;
 import org.keycloak.models.KeycloakTransaction;
 import org.keycloak.models.ModelDuplicateException;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.RequiredCredentialModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserFederationMapperModel;
@@ -349,4 +351,31 @@ public final class KeycloakModelUtils {
 
         return mapperModel;
     }
+
+    /**
+     * Automatically add "kerberos" to required realm credentials if it's supported by saved provider
+     *
+     * @param realm
+     * @param model
+     * @return true if kerberos credentials were added
+     */
+    public static boolean checkKerberosCredential(RealmModel realm, UserFederationProviderModel model) {
+        String allowKerberosCfg = model.getConfig().get(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION);
+        if (Boolean.valueOf(allowKerberosCfg)) {
+            boolean found = false;
+            List<RequiredCredentialModel> currentCreds = realm.getRequiredCredentials();
+            for (RequiredCredentialModel cred : currentCreds) {
+                if (cred.getType().equals(UserCredentialModel.KERBEROS)) {
+                    found = true;
+                }
+            }
+
+            if (!found) {
+                realm.addRequiredCredential(UserCredentialModel.KERBEROS);
+                return true;
+            }
+        }
+
+        return false;
+    }
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index c2a4730..e2899fe 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -235,8 +235,8 @@ public class RealmAdminResource {
     }
 
     @Path("user-federation")
-    public UserFederationResource userFederation() {
-        UserFederationResource fed = new UserFederationResource(realm, auth, adminEvent);
+    public UserFederationProvidersResource userFederation() {
+        UserFederationProvidersResource fed = new UserFederationProvidersResource(realm, auth, adminEvent);
         ResteasyProviderFactory.getInstance().injectProperties(fed);
         //resourceContext.initResource(fed);
         return fed;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java
new file mode 100644
index 0000000..e5deb3d
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationProviderResource.java
@@ -0,0 +1,298 @@
+package org.keycloak.services.resources.admin;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.jboss.resteasy.spi.NotFoundException;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.mappers.MapperConfigValidationException;
+import org.keycloak.mappers.UserFederationMapper;
+import org.keycloak.mappers.UserFederationMapperFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserFederationMapperModel;
+import org.keycloak.models.UserFederationProviderModel;
+import org.keycloak.models.UserFederationSyncResult;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.models.utils.RepresentationToModel;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.representations.idm.ConfigPropertyRepresentation;
+import org.keycloak.representations.idm.UserFederationMapperRepresentation;
+import org.keycloak.representations.idm.UserFederationMapperTypeRepresentation;
+import org.keycloak.representations.idm.UserFederationProviderRepresentation;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.managers.UsersSyncManager;
+import org.keycloak.timer.TimerProvider;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserFederationProviderResource {
+
+    protected static final Logger logger = Logger.getLogger(UserFederationProviderResource.class);
+
+    private final KeycloakSession session;
+    private final RealmModel realm;
+    private final RealmAuth auth;
+    private final UserFederationProviderModel federationProviderModel;
+    private final AdminEventBuilder adminEvent;
+
+    @Context
+    private UriInfo uriInfo;
+
+    public UserFederationProviderResource(KeycloakSession session, RealmModel realm, RealmAuth auth, UserFederationProviderModel federationProviderModel, AdminEventBuilder adminEvent) {
+        this.session = session;
+        this.realm = realm;
+        this.auth = auth;
+        this.federationProviderModel = federationProviderModel;
+        this.adminEvent = adminEvent;
+    }
+
+    /**
+     * Update a provider
+     *
+     * @param rep
+     */
+    @PUT
+    @NoCache
+    @Consumes(MediaType.APPLICATION_JSON)
+    public void updateProviderInstance(UserFederationProviderRepresentation rep) {
+        auth.requireManage();
+        String displayName = rep.getDisplayName();
+        if (displayName != null && displayName.trim().equals("")) {
+            displayName = null;
+        }
+        UserFederationProviderModel model = new UserFederationProviderModel(rep.getId(), rep.getProviderName(), rep.getConfig(), rep.getPriority(), displayName,
+                rep.getFullSyncPeriod(), rep.getChangedSyncPeriod(), rep.getLastSync());
+        realm.updateUserFederationProvider(model);
+        new UsersSyncManager().refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), model, realm.getId());
+        boolean kerberosCredsAdded = KeycloakModelUtils.checkKerberosCredential(realm, model);
+        if (kerberosCredsAdded) {
+            logger.info("Added 'kerberos' to required realm credentials");
+        }
+
+        adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
+
+    }
+
+    /**
+     * get a provider
+     *
+     */
+    @GET
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public UserFederationProviderRepresentation getProviderInstance() {
+        auth.requireView();
+        return ModelToRepresentation.toRepresentation(this.federationProviderModel);
+    }
+
+    /**
+     * Delete a provider
+     *
+     */
+    @DELETE
+    @NoCache
+    public void deleteProviderInstance() {
+        auth.requireManage();
+
+        realm.removeUserFederationProvider(this.federationProviderModel);
+        new UsersSyncManager().removePeriodicSyncForProvider(session.getProvider(TimerProvider.class), this.federationProviderModel);
+
+        adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
+
+    }
+
+    /**
+     * trigger sync of users
+     *
+     * @return
+     */
+    @POST
+    @Path("sync")
+    @NoCache
+    public UserFederationSyncResult syncUsers(@QueryParam("action") String action) {
+        logger.debug("Syncing users");
+        auth.requireManage();
+
+        UsersSyncManager syncManager = new UsersSyncManager();
+        UserFederationSyncResult syncResult = null;
+        if ("triggerFullSync".equals(action)) {
+            syncResult = syncManager.syncAllUsers(session.getKeycloakSessionFactory(), realm.getId(), this.federationProviderModel);
+        } else if ("triggerChangedUsersSync".equals(action)) {
+            syncResult = syncManager.syncChangedUsers(session.getKeycloakSessionFactory(), realm.getId(), this.federationProviderModel);
+        }
+
+        adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
+        return syncResult;
+    }
+
+    /**
+     * List of available User Federation mapper types
+     *
+     * @return
+     */
+    @GET
+    @Path("mapper-types")
+    @NoCache
+    public Map<String, UserFederationMapperTypeRepresentation> getMapperTypes() {
+        this.auth.requireView();
+        KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+        Map<String, UserFederationMapperTypeRepresentation> types = new HashMap<>();
+        List<ProviderFactory> factories = sessionFactory.getProviderFactories(UserFederationMapper.class);
+
+        for (ProviderFactory factory : factories) {
+            UserFederationMapperFactory mapperFactory = (UserFederationMapperFactory)factory;
+            if (mapperFactory.getFederationProviderType().equals(this.federationProviderModel.getProviderName())) {
+
+                UserFederationMapperTypeRepresentation rep = new UserFederationMapperTypeRepresentation();
+                rep.setId(mapperFactory.getId());
+                rep.setCategory(mapperFactory.getDisplayCategory());
+                rep.setName(mapperFactory.getDisplayType());
+                rep.setHelpText(mapperFactory.getHelpText());
+                List<ProviderConfigProperty> configProperties = mapperFactory.getConfigProperties(realm);
+                for (ProviderConfigProperty prop : configProperties) {
+                    ConfigPropertyRepresentation propRep = new ConfigPropertyRepresentation();
+                    propRep.setName(prop.getName());
+                    propRep.setLabel(prop.getLabel());
+                    propRep.setType(prop.getType());
+                    propRep.setDefaultValue(prop.getDefaultValue());
+                    propRep.setHelpText(prop.getHelpText());
+                    rep.getProperties().add(propRep);
+                }
+                types.put(rep.getId(), rep);
+            }
+        }
+        return types;
+    }
+
+    /**
+     * Get mappers configured for this provider
+     *
+     * @return
+     */
+    @GET
+    @Path("mappers")
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    public List<UserFederationMapperRepresentation> getMappers() {
+        this.auth.requireView();
+        List<UserFederationMapperRepresentation> mappers = new LinkedList<>();
+        for (UserFederationMapperModel model : realm.getUserFederationMappersByFederationProvider(this.federationProviderModel.getId())) {
+            mappers.add(ModelToRepresentation.toRepresentation(realm, model));
+        }
+        return mappers;
+    }
+
+    /**
+     * Create mapper
+     *
+     * @param mapper
+     * @return
+     */
+    @POST
+    @Path("mappers")
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response addMapper(UserFederationMapperRepresentation mapper) {
+        auth.requireManage();
+        UserFederationMapperModel model = RepresentationToModel.toModel(realm, mapper);
+
+        validateModel(model);
+
+        model = realm.addUserFederationMapper(model);
+
+        adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, model.getId())
+                .representation(mapper).success();
+
+        return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build();
+
+    }
+
+    /**
+     * Get mapper
+     *
+     * @param id mapperId
+     * @return
+     */
+    @GET
+    @NoCache
+    @Path("mappers/{id}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public UserFederationMapperRepresentation getMapperById(@PathParam("id") String id) {
+        auth.requireView();
+        UserFederationMapperModel model = realm.getUserFederationMapperById(id);
+        if (model == null) throw new NotFoundException("Model not found");
+        return ModelToRepresentation.toRepresentation(realm, model);
+    }
+
+    /**
+     * Update mapper
+     *
+     * @param id
+     * @param rep
+     */
+    @PUT
+    @NoCache
+    @Path("mappers/{id}")
+    @Consumes(MediaType.APPLICATION_JSON)
+    public void update(@PathParam("id") String id, UserFederationMapperRepresentation rep) {
+        auth.requireManage();
+        UserFederationMapperModel model = realm.getUserFederationMapperById(id);
+        if (model == null) throw new NotFoundException("Model not found");
+        model = RepresentationToModel.toModel(realm, rep);
+
+        validateModel(model);
+
+        realm.updateUserFederationMapper(model);
+        adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
+
+    }
+
+    /**
+     * Delete mapper with given ID
+     *
+     * @param id
+     */
+    @DELETE
+    @NoCache
+    @Path("mappers/{id}")
+    public void delete(@PathParam("id") String id) {
+        auth.requireManage();
+        UserFederationMapperModel model = realm.getUserFederationMapperById(id);
+        if (model == null) throw new NotFoundException("Model not found");
+        realm.removeUserFederationMapper(model);
+        adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
+
+    }
+
+    private void validateModel(UserFederationMapperModel model) {
+        try {
+            UserFederationMapperFactory mapperFactory = (UserFederationMapperFactory) session.getKeycloakSessionFactory().getProviderFactory(UserFederationMapper.class, model.getFederationMapperType());
+            mapperFactory.validateConfig(model);
+        } catch (MapperConfigValidationException ex) {
+            throw new ErrorResponseException("Validation error", ex.getMessage(), Response.Status.BAD_REQUEST);
+        }
+    }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java
index ddda4ff..d3dd9ab 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java
@@ -91,7 +91,7 @@ class FederationTestUtils {
     }
 
     public static void addZipCodeLDAPMapper(RealmModel realm, UserFederationProviderModel providerModel) {
-        UserFederationMapperModel mapperModel = KeycloakModelUtils.createUserFederationMapperModel("zipCodeMapper", providerModel.getId(), UserAttributeLDAPFederationMapperFactory.ID,
+        UserFederationMapperModel mapperModel = KeycloakModelUtils.createUserFederationMapperModel("zipCodeMapper", providerModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                 UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, "postal_code",
                 UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.POSTAL_CODE,
                 UserAttributeLDAPFederationMapper.READ_ONLY, "false");
@@ -104,7 +104,7 @@ class FederationTestUtils {
             mapperModel.getConfig().put(RoleLDAPFederationMapper.MODE, mode.toString());
             realm.updateUserFederationMapper(mapperModel);
         } else {
-            mapperModel = KeycloakModelUtils.createUserFederationMapperModel("realmRolesMapper", providerModel.getId(), RoleLDAPFederationMapperFactory.ID,
+            mapperModel = KeycloakModelUtils.createUserFederationMapperModel("realmRolesMapper", providerModel.getId(), RoleLDAPFederationMapperFactory.PROVIDER_ID,
                     RoleLDAPFederationMapper.ROLES_DN, "ou=RealmRoles,dc=keycloak,dc=org",
                     RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "true",
                     RoleLDAPFederationMapper.MODE, mode.toString());
@@ -116,7 +116,7 @@ class FederationTestUtils {
             mapperModel.getConfig().put(RoleLDAPFederationMapper.MODE, mode.toString());
             realm.updateUserFederationMapper(mapperModel);
         } else {
-            mapperModel = KeycloakModelUtils.createUserFederationMapperModel("financeRolesMapper", providerModel.getId(), RoleLDAPFederationMapperFactory.ID,
+            mapperModel = KeycloakModelUtils.createUserFederationMapperModel("financeRolesMapper", providerModel.getId(), RoleLDAPFederationMapperFactory.PROVIDER_ID,
                     RoleLDAPFederationMapper.ROLES_DN, "ou=FinanceRoles,dc=keycloak,dc=org",
                     RoleLDAPFederationMapper.USE_REALM_ROLES_MAPPING, "false",
                     RoleLDAPFederationMapper.CLIENT_ID, "finance",
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 4d20bc8..276698d 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
@@ -233,7 +233,7 @@ public class ImportTest extends AbstractModelTest {
         Assert.assertTrue(fedMappers1.size() == 1);
         UserFederationMapperModel fullNameMapper = fedMappers1.iterator().next();
         Assert.assertEquals("FullNameMapper", fullNameMapper.getName());
-        Assert.assertEquals(FullNameLDAPFederationMapperFactory.ID, fullNameMapper.getFederationMapperType());
+        Assert.assertEquals(FullNameLDAPFederationMapperFactory.PROVIDER_ID, fullNameMapper.getFederationMapperType());
         Assert.assertEquals(ldap1.getId(), fullNameMapper.getFederationProviderId());
         Assert.assertEquals("cn", fullNameMapper.getConfig().get(FullNameLDAPFederationMapper.LDAP_FULL_NAME_ATTRIBUTE));