keycloak-aplcache

Changes

federation/ldap/src/main/resources/META-INF/services/org.keycloak.models.UserFederationMapper 2(+0 -2)

Details

diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java
index 999f133..5d58438 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPDn.java
@@ -1,14 +1,16 @@
 package org.keycloak.federation.ldap.idm.model;
 
+import java.util.Deque;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Queue;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 public class LDAPDn {
 
-    private final List<Entry> entries = new LinkedList<Entry>();
+    private final Deque<Entry> entries = new LinkedList<Entry>();
 
     public static LDAPDn fromString(String dnString) {
         LDAPDn dn = new LDAPDn();
@@ -43,7 +45,7 @@ public class LDAPDn {
      * @return string like "uid=joe" from the DN like "uid=joe,dc=something,dc=org"
      */
     public String getFirstRdn() {
-        Entry firstEntry = entries.get(0);
+        Entry firstEntry = entries.getFirst();
         return firstEntry.attrName + "=" + firstEntry.attrValue;
     }
 
@@ -51,7 +53,7 @@ public class LDAPDn {
      * @return string attribute name like "uid" from the DN like "uid=joe,dc=something,dc=org"
      */
     public String getFirstRdnAttrName() {
-        Entry firstEntry = entries.get(0);
+        Entry firstEntry = entries.getFirst();
         return firstEntry.attrName;
     }
 
@@ -60,28 +62,15 @@ public class LDAPDn {
      * @return string like "dc=something,dc=org" from the DN like "uid=joe,dc=something,dc=org"
      */
     public String getParentDn() {
-        StringBuilder builder = new StringBuilder();
-
-        int n = 0;
-        for (Entry rdn : entries) {
-            n++;
-            if (n > 2) {
-                builder.append(",");
-            }
-            if (n >= 2) {
-                builder.append(rdn.attrName).append("=").append(rdn.attrValue);
-            }
-        }
-
-        return builder.toString();
+        return new LinkedList<Entry>(entries).remove().toString();
     }
 
     public void addToHead(String rdnName, String rdnValue) {
-        entries.add(0, new Entry(rdnName, rdnValue));
+        entries.addFirst(new Entry(rdnName, rdnValue));
     }
 
     public void addToBottom(String rdnName, String rdnValue) {
-        entries.add(new Entry(rdnName, rdnValue));
+        entries.addLast(new Entry(rdnName, rdnValue));
     }
 
 
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java
index 61b631d..587a81f 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java
@@ -18,7 +18,7 @@ public class LDAPObject {
 
     private final List<String> objectClasses = new LinkedList<String>();
     private final List<String> readOnlyAttributeNames = new LinkedList<String>();
-    private final Map<String, Serializable> attributes = new HashMap<String, Serializable>();
+    private final Map<String, Object> attributes = new HashMap<String, Object>();
 
     public String getUuid() {
         return uuid;
@@ -61,7 +61,7 @@ public class LDAPObject {
         this.rdnAttributeName = rdnAttributeName;
     }
 
-    public void setAttribute(String attributeName, Serializable attributeValue) {
+    public void setAttribute(String attributeName, Object attributeValue) {
         attributes.put(attributeName, attributeValue);
     }
 
@@ -70,12 +70,21 @@ public class LDAPObject {
     }
 
 
-    public Serializable getAttribute(String name) {
+    public Object getAttribute(String name) {
         return attributes.get(name);
     }
 
+    public String getAttributeAsString(String name) {
+        Object attrValue = attributes.get(name);
+        if (attrValue != null && !(attrValue instanceof String)) {
+            throw new IllegalStateException("Expected String but attribute was " + attrValue + " of type " + attrValue.getClass().getName());
+        }
+
+        return (String) attrValue;
+    }
+
 
-    public Map<String, Serializable> getAttributes() {
+    public Map<String, Object> getAttributes() {
         return attributes;
     }
 
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java
index 04e6d61..023d143 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java
@@ -9,6 +9,8 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.TreeSet;
 
 import javax.naming.NamingEnumeration;
 import javax.naming.NamingException;
@@ -389,10 +391,9 @@ public class LDAPIdentityStore implements IdentityStore {
 
             while (ldapAttributes.hasMore()) {
                 Attribute ldapAttribute = ldapAttributes.next();
-                Serializable ldapAttributeValue;
 
                 try {
-                    ldapAttributeValue = (Serializable) ldapAttribute.get();
+                    ldapAttribute.get();
                 } catch (NoSuchElementException nsee) {
                     continue;
                 }
@@ -400,23 +401,31 @@ public class LDAPIdentityStore implements IdentityStore {
                 String ldapAttributeName = ldapAttribute.getID();
 
                 if (ldapAttributeName.toLowerCase().equals(getConfig().getUuidAttributeName().toLowerCase())) {
-                    ldapObject.setUuid(this.operationManager.decodeEntryUUID(ldapAttributeValue));
-                } else if (ldapAttributeName.toLowerCase().equals(LDAPConstants.OBJECT_CLASS)) {
-                    List<String> objectClasses = new LinkedList<String>();
+                    Object uuidValue = ldapAttribute.get();
+                    ldapObject.setUuid(this.operationManager.decodeEntryUUID(uuidValue));
+                } else {
+                    Set<String> attrValues = new TreeSet<String>();
                     NamingEnumeration<?> enumm = ldapAttribute.getAll();
                     while (enumm.hasMoreElements()) {
                         String objectClass = enumm.next().toString();
-                        objectClasses.add(objectClass);
-                    }
-                    ldapObject.setObjectClasses(objectClasses);
-                } else {
-                    if (logger.isTraceEnabled()) {
-                        logger.tracef("Populating ldap attribute [%s] with value [%s] for DN [%s].", ldapAttributeName, ldapAttributeValue, entryDN);
+                        attrValues.add(objectClass);
                     }
 
-                    ldapObject.setAttribute(ldapAttributeName, ldapAttributeValue);
-                    if (uppercasedReadOnlyAttrNames.contains(ldapAttributeName.toUpperCase())) {
-                        ldapObject.addReadOnlyAttributeName(ldapAttributeName);
+                    if (ldapAttributeName.toLowerCase().equals(LDAPConstants.OBJECT_CLASS)) {
+                        ldapObject.setObjectClasses(attrValues);
+                    } else {
+                        if (logger.isTraceEnabled()) {
+                            logger.tracef("Populating ldap attribute [%s] with value [%s] for DN [%s].", ldapAttributeName, attrValues.toString(), entryDN);
+                        }
+                        if (attrValues.size() == 1) {
+                            ldapObject.setAttribute(ldapAttributeName, attrValues.iterator().next());
+                        } else {
+                            ldapObject.setAttribute(ldapAttributeName, attrValues);
+                        }
+
+                        if (uppercasedReadOnlyAttrNames.contains(ldapAttributeName.toUpperCase())) {
+                            ldapObject.addReadOnlyAttributeName(ldapAttributeName);
+                        }
                     }
                 }
             }
@@ -487,16 +496,16 @@ public class LDAPIdentityStore implements IdentityStore {
     protected BasicAttributes extractAttributes(LDAPObject ldapObject, boolean isCreate) {
         BasicAttributes entryAttributes = new BasicAttributes();
 
-        for (Map.Entry<String, Serializable> attrEntry : ldapObject.getAttributes().entrySet()) {
+        for (Map.Entry<String, Object> attrEntry : ldapObject.getAttributes().entrySet()) {
             String attrName = attrEntry.getKey();
-            Serializable attrValue = attrEntry.getValue();
+            Object attrValue = attrEntry.getValue();
             if (!ldapObject.getReadOnlyAttributeNames().contains(attrName) && (isCreate || !ldapObject.getRdnAttributeName().equals(attrName))) {
 
                 if (String.class.isInstance(attrValue)) {
                     entryAttributes.put(attrName, attrValue);
                 } else if (Collection.class.isInstance(attrValue)) {
                     BasicAttribute attr = new BasicAttribute(attrName);
-                    Collection<String> valueCollection = (Collection<String>) attr;
+                    Collection<String> valueCollection = (Collection<String>) attrValue;
                     for (String val : valueCollection) {
                         attr.add(val);
                     }
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java
index 2442d30..65181e1 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPConfig.java
@@ -16,7 +16,7 @@ import org.keycloak.models.UserFederationProviderModel;
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  *
- * TODO: init properties at startup instead of always compute them
+ * TODO: init properties at constructor instead of always compute them
  */
 public class LDAPConfig {
 
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
index 7d3ca02..ad55bc3 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
@@ -19,7 +19,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserCredentialValueModel;
-import org.keycloak.models.UserFederationMapper;
+import org.keycloak.mappers.UserFederationMapper;
 import org.keycloak.models.UserFederationMapperModel;
 import org.keycloak.models.UserFederationProvider;
 import org.keycloak.models.UserFederationProviderModel;
@@ -114,7 +114,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
         List<UserFederationMapperModel> federationMappers = realm.getUserFederationMappers();
         for (UserFederationMapperModel mapperModel : federationMappers) {
             LDAPFederationMapper ldapMapper = getMapper(mapperModel);
-            proxied = ldapMapper.proxy(mapperModel, this, ldapObject, proxied);
+            proxied = ldapMapper.proxy(mapperModel, this, ldapObject, proxied, realm);
         }
 
         return proxied;
@@ -227,7 +227,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
 
     /**
      * @param local
-     * @return ldapObject corresponding to local user or null if user is no longer in LDAP
+     * @return ldapUser corresponding to local user or null if user is no longer in LDAP
      */
     protected LDAPObject loadAndValidateUser(RealmModel realm, UserModel local) {
         LDAPObject ldapUser = loadLDAPUserByUsername(realm, local.getUsername());
@@ -271,7 +271,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
         List<UserFederationMapperModel> federationMappers = realm.getUserFederationMappers();
         for (UserFederationMapperModel mapperModel : federationMappers) {
             LDAPFederationMapper ldapMapper = getMapper(mapperModel);
-            ldapMapper.importUserFromLDAP(mapperModel, this, ldapUser, imported, true);
+            ldapMapper.onImportUserFromLDAP(mapperModel, this, ldapUser, imported, realm, true);
         }
 
         String userDN = ldapUser.getDn().toString();
@@ -314,7 +314,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
     @Override
     public void preRemove(RealmModel realm, RoleModel role) {
         // complete I don't think we have to do anything here
-        // TODO: requires implementation... Maybe mappers callback
+        // TODO: requires implementation... Maybe mappers callback to ensure role deletion propagated to LDAP by RoleLDAPFederationMapper
     }
 
     public boolean validPassword(RealmModel realm, UserModel user, String password) {
@@ -407,7 +407,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
                     List<UserFederationMapperModel> federationMappers = realm.getUserFederationMappers();
                     for (UserFederationMapperModel mapperModel : federationMappers) {
                         LDAPFederationMapper ldapMapper = getMapper(mapperModel);
-                        ldapMapper.importUserFromLDAP(mapperModel, this, ldapUser, currentUser, false);
+                        ldapMapper.onImportUserFromLDAP(mapperModel, this, ldapUser, currentUser, realm, false);
                     }
 
                     logger.debugf("Updated user from LDAP: %s", currentUser.getUsername());
@@ -477,8 +477,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
     }
 
     public LDAPFederationMapper getMapper(UserFederationMapperModel mapperModel) {
-        LDAPFederationMapper ldapMapper = (LDAPFederationMapper) getSession().getKeycloakSessionFactory()
-                .getProviderFactory(UserFederationMapper.class, mapperModel.getFederationMapperId());
+        LDAPFederationMapper ldapMapper = (LDAPFederationMapper) getSession().getProvider(UserFederationMapper.class, mapperModel.getFederationMapperId());
         if (ldapMapper == null) {
             throw new ModelException("Can't find mapper type with ID: " + mapperModel.getFederationMapperId());
         }
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java
index 1e9918d..6c5ac93 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java
@@ -37,7 +37,7 @@ public class LDAPUtils {
         List<UserFederationMapperModel> federationMappers = realm.getUserFederationMappers();
         for (UserFederationMapperModel mapperModel : federationMappers) {
             LDAPFederationMapper ldapMapper = ldapProvider.getMapper(mapperModel);
-            ldapMapper.registerUserToLDAP(mapperModel, ldapProvider, ldapObject, user);
+            ldapMapper.onRegisterUserToLDAP(mapperModel, ldapProvider, ldapObject, user, realm);
         }
 
         LDAPUtils.computeAndSetDn(ldapConfig, ldapObject);
@@ -161,10 +161,10 @@ public class LDAPUtils {
         return fullName;
     }   */
 
-    // ldapObject has filled attributes, but doesn't have filled
+    // ldapUser has filled attributes, but doesn't have filled dn
     public static void computeAndSetDn(LDAPConfig config, LDAPObject ldapObject) {
         String rdnLdapAttrName = config.getRdnLdapAttribute();
-        String rdnLdapAttrValue = (String) ldapObject.getAttribute(rdnLdapAttrName);
+        String rdnLdapAttrValue = ldapObject.getAttributeAsString(rdnLdapAttrName);
         if (rdnLdapAttrValue == null) {
             throw new ModelException("RDN Attribute [" + rdnLdapAttrName + "] is not filled. Filled attributes: " + ldapObject.getAttributes());
         }
@@ -176,11 +176,6 @@ public class LDAPUtils {
 
     public static String getUsername(LDAPObject ldapUser, LDAPConfig config) {
         String usernameAttr = config.getUsernameLdapAttribute();
-        return (String) ldapUser.getAttribute(usernameAttr);
-    }
-
-    public static boolean parseBooleanParameter(UserFederationMapperModel mapperModel, String paramName) {
-        String readOnly = mapperModel.getConfig().get(paramName);
-        return Boolean.parseBoolean(readOnly);
+        return ldapUser.getAttributeAsString(usernameAttr);
     }
 }
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java
index 357262e..6daa011 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapper.java
@@ -1,9 +1,6 @@
 package org.keycloak.federation.ldap.mappers;
 
-import org.keycloak.Config;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
-import org.keycloak.models.UserFederationMapper;
+import org.keycloak.models.UserFederationMapperModel;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -15,18 +12,8 @@ public abstract class AbstractLDAPFederationMapper implements LDAPFederationMapp
 
     }
 
-    @Override
-    public UserFederationMapper create(KeycloakSession session) {
-        throw new RuntimeException("UNSUPPORTED METHOD");
-    }
-
-    @Override
-    public void init(Config.Scope config) {
-
-    }
-
-    @Override
-    public void postInit(KeycloakSessionFactory factory) {
-
+    protected boolean parseBooleanParameter(UserFederationMapperModel mapperModel, String paramName) {
+        String paramm = mapperModel.getConfig().get(paramName);
+        return Boolean.parseBoolean(paramm);
     }
 }
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
new file mode 100644
index 0000000..6b8f186
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractLDAPFederationMapperFactory.java
@@ -0,0 +1,25 @@
+package org.keycloak.federation.ldap.mappers;
+
+import org.keycloak.Config;
+import org.keycloak.mappers.UserFederationMapperFactory;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class AbstractLDAPFederationMapperFactory implements UserFederationMapperFactory {
+
+    @Override
+    public void init(Config.Scope config) {
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+    }
+
+    @Override
+    public void close() {
+    }
+
+
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java
index 483d825..109b0b0 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java
@@ -11,7 +11,10 @@ import org.keycloak.federation.ldap.idm.query.Condition;
 import org.keycloak.federation.ldap.idm.query.QueryParameter;
 import org.keycloak.federation.ldap.idm.query.internal.EqualCondition;
 import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+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.models.UserFederationProvider;
 import org.keycloak.models.UserModel;
@@ -28,24 +31,9 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
     public static final String READ_ONLY = "read.only";
 
     @Override
-    public String getHelpText() {
-        return "Some help text - full name mapper - TODO";
-    }
-
-    @Override
-    public List<ProviderConfigProperty> getConfigProperties() {
-        return null;
-    }
-
-    @Override
-    public String getId() {
-        return "full-name-ldap-mapper";
-    }
-
-    @Override
-    public void importUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel user, boolean isCreate) {
+    public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
         String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
-        String fullName = (String) ldapObject.getAttribute(ldapFullNameAttrName);
+        String fullName = ldapUser.getAttributeAsString(ldapFullNameAttrName);
         fullName = fullName.trim();
         if (fullName != null) {
             int lastSpaceIndex = fullName.lastIndexOf(" ");
@@ -59,22 +47,22 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
     }
 
     @Override
-    public void registerUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel localUser) {
+    public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
         String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
         String fullName = getFullName(localUser.getFirstName(), localUser.getLastName());
-        ldapObject.setAttribute(ldapFullNameAttrName, fullName);
+        ldapUser.setAttribute(ldapFullNameAttrName, fullName);
 
         if (isReadOnly(mapperModel)) {
-            ldapObject.addReadOnlyAttributeName(ldapFullNameAttrName);
+            ldapUser.addReadOnlyAttributeName(ldapFullNameAttrName);
         }
     }
 
     @Override
-    public UserModel proxy(final UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel delegate) {
+    public UserModel proxy(final UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
         if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) {
 
 
-            AbstractTxAwareLDAPUserModelDelegate txDelegate = new AbstractTxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapObject) {
+            TxAwareLDAPUserModelDelegate txDelegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) {
 
                 @Override
                 public void setFirstName(String firstName) {
@@ -97,7 +85,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
                     ensureTransactionStarted();
 
                     String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
-                    ldapObject.setAttribute(ldapFullNameAttrName, fullName);
+                    ldapUser.setAttribute(ldapFullNameAttrName, fullName);
                 }
 
             };
@@ -127,10 +115,10 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
                     lastNameCondition = (EqualCondition) condition;
                     query.getConditions().remove(condition);
                 } else if (param.getName().equals(LDAPConstants.GIVENNAME)) {
-                    // Some previous mapper already converted it
+                    // Some previous mapper already converted it to LDAP name
                     firstNameCondition = (EqualCondition) condition;
                 } else if (param.getName().equals(LDAPConstants.SN)) {
-                    // Some previous mapper already converted it
+                    // Some previous mapper already converted it to LDAP name
                     lastNameCondition = (EqualCondition) condition;
                 }
             }
@@ -169,6 +157,6 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
     }
 
     private boolean isReadOnly(UserFederationMapperModel mapperModel) {
-        return LDAPUtils.parseBooleanParameter(mapperModel, READ_ONLY);
+        return parseBooleanParameter(mapperModel, READ_ONLY);
     }
 }
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java
new file mode 100644
index 0000000..6820345
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapperFactory.java
@@ -0,0 +1,33 @@
+package org.keycloak.federation.ldap.mappers;
+
+import java.util.List;
+
+import org.keycloak.mappers.UserFederationMapper;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.provider.ProviderConfigProperty;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class FullNameLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory {
+
+    @Override
+    public String getHelpText() {
+        return "Some help text - full name mapper - TODO";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return null;
+    }
+
+    @Override
+    public String getId() {
+        return "full-name-ldap-mapper";
+    }
+
+    @Override
+    public UserFederationMapper create(KeycloakSession session) {
+        return new FullNameLDAPFederationMapper();
+    }
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java
index de89e92..4676a6b 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/LDAPFederationMapper.java
@@ -3,7 +3,8 @@ package org.keycloak.federation.ldap.mappers;
 import org.keycloak.federation.ldap.LDAPFederationProvider;
 import org.keycloak.federation.ldap.idm.model.LDAPObject;
 import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
-import org.keycloak.models.UserFederationMapper;
+import org.keycloak.models.RealmModel;
+import org.keycloak.mappers.UserFederationMapper;
 import org.keycloak.models.UserFederationMapperModel;
 import org.keycloak.models.UserModel;
 
@@ -12,17 +13,50 @@ import org.keycloak.models.UserModel;
  */
 public interface LDAPFederationMapper extends UserFederationMapper {
 
-    // TODO: rename?
-    // Called when importing user from federation provider to local keycloak DB. Flag "isCreate" means if we creating new user to Keycloak DB or just update existing user in Keycloak DB
-    void importUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel user, boolean isCreate);
 
-    // TODO: rename to beforeRegister or something?
-    // Called when register new user to federation provider
-    void registerUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel localUser);
+    /**
+     * Called when importing user from LDAP to local keycloak DB.
+     *
+     * @param mapperModel
+     * @param ldapProvider
+     * @param ldapUser
+     * @param user
+     * @param realm
+     * @param isCreate true if we importing new user from LDAP. False if user already exists in Keycloak, but we are upgrading (syncing) it from LDAP
+     */
+    void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate);
 
-    // Called when invoke proxy on federation provider
-    UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel delegate);
 
-    // Called before any LDAPIdentityQuery is executed
+    /**
+     * Called when register new user to LDAP - just after user was created in Keycloak DB
+     *
+     * @param mapperModel
+     * @param ldapProvider
+     * @param ldapUser
+     * @param localUser
+     * @param realm
+     */
+    void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm);
+
+
+    /**
+     * Called when invoke proxy on LDAP federation provider
+     *
+     * @param mapperModel
+     * @param ldapProvider
+     * @param ldapUser
+     * @param delegate
+     * @param realm
+     * @return
+     */
+    UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm);
+
+
+    /**
+     * Called before LDAP Identity query for retrieve LDAP users was executed. It allows to change query somehow (add returning attributes from LDAP, change conditions etc)
+     *
+     * @param mapperModel
+     * @param query
+     */
     void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPIdentityQuery query);
 }
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
new file mode 100644
index 0000000..47f288d
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java
@@ -0,0 +1,473 @@
+package org.keycloak.federation.ldap.mappers;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import javax.naming.directory.SearchControls;
+
+import org.jboss.logging.Logger;
+import org.keycloak.federation.ldap.LDAPFederationProvider;
+import org.keycloak.federation.ldap.idm.model.LDAPDn;
+import org.keycloak.federation.ldap.idm.model.LDAPObject;
+import org.keycloak.federation.ldap.idm.query.Condition;
+import org.keycloak.federation.ldap.idm.query.QueryParameter;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleContainerModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserFederationMapperModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.models.utils.UserModelDelegate;
+
+/**
+ * Map realm roles or roles of particular client to LDAP roles
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
+
+    private static final Logger logger = Logger.getLogger(RoleLDAPFederationMapper.class);
+
+    // LDAP DN where are roles of this tree saved.
+    public static final String ROLES_DN = "roles.dn";
+
+    // Name of LDAP attribute, which is used in role objects for name and RDN of role. Usually it will be "cn"
+    public static final String ROLE_NAME_LDAP_ATTRIBUTE = "role.name.ldap.attribute";
+
+    // Name of LDAP attribute on role, which is used for membership mappings. Usually it will be "member"
+    public static final String MEMBERSHIP_LDAP_ATTRIBUTE = "membership.ldap.attribute";
+
+    // Object classes of the role object.
+    public static final String ROLE_OBJECT_CLASSES = "role.object.classes";
+
+    // Boolean option. If true, we will map LDAP roles to realm roles. If false, we will map to client roles (client specified by option CLIENT_ID)
+    public static final String USE_REALM_ROLES_MAPPING = "use.realm.roles.mapping";
+
+    // ClientId, which we want to map roles. Applicable just if "USE_REALM_ROLES_MAPPING" is false
+    public static final String CLIENT_ID = "client.id";
+
+    // See docs for Mode enum
+    public static final String MODE = "mode";
+
+
+    // List of IDs of UserFederationMapperModels where syncRolesFromLDAP was already called in this KeycloakSession. This is to improve performance
+    // TODO: Rather address this with caching at LDAPIdentityStore level?
+    private Set<String> rolesSyncedModels = new TreeSet<String>();
+
+    @Override
+    public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
+        syncRolesFromLDAP(mapperModel, ldapProvider, realm);
+
+        Mode mode = getMode(mapperModel);
+
+        // For now, import LDAP role mappings just during create
+        if (mode == Mode.IMPORT && isCreate) {
+
+            List<LDAPObject> ldapRoles = getLDAPRoleMappings(mapperModel, ldapProvider, ldapUser);
+
+            // Import role mappings from LDAP into Keycloak DB
+            String roleNameAttr = getRoleNameLdapAttribute(mapperModel);
+            for (LDAPObject ldapRole : ldapRoles) {
+                String roleName = ldapRole.getAttributeAsString(roleNameAttr);
+
+                RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
+                RoleModel role = roleContainer.getRole(roleName);
+
+                // TODO: debug
+                logger.infof("Granting role [%s] to user [%s] during import from LDAP", roleName, user.getUsername());
+                user.grantRole(role);
+            }
+        }
+    }
+
+    @Override
+    public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
+        syncRolesFromLDAP(mapperModel, ldapProvider, realm);
+    }
+
+    // Sync roles from LDAP tree and create them in local Keycloak DB (if they don't exist here yet)
+    protected void syncRolesFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, RealmModel realm) {
+        if (!rolesSyncedModels.contains(mapperModel.getId())) {
+            LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
+
+            // Send query
+            List<LDAPObject> ldapRoles = ldapQuery.getResultList();
+
+            RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
+            String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel);
+            for (LDAPObject ldapRole : ldapRoles) {
+                String roleName = ldapRole.getAttributeAsString(rolesRdnAttr);
+
+                if (roleContainer.getRole(roleName) == null) {
+                    // TODO: rather change to debug
+                    logger.infof("Syncing role [%s] from LDAP to keycloak DB", roleName);
+                    roleContainer.addRole(roleName);
+                }
+            }
+
+            rolesSyncedModels.add(mapperModel.getId());
+        }
+    }
+
+    public LDAPIdentityQuery createRoleQuery(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider) {
+        LDAPIdentityQuery ldapQuery = new LDAPIdentityQuery(ldapProvider);
+        ldapQuery.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+
+        String rolesDn = getRolesDn(mapperModel);
+        ldapQuery.addSearchDns(Arrays.asList(rolesDn));
+
+        Collection<String> roleObjectClasses = getRoleObjectClasses(mapperModel);
+        ldapQuery.addObjectClasses(roleObjectClasses);
+
+        String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel);
+        String membershipAttr = getMembershipLdapAttribute(mapperModel);
+        ldapQuery.addReturningLdapAttribute(rolesRdnAttr);
+        ldapQuery.addReturningLdapAttribute(membershipAttr);
+
+        return ldapQuery;
+    }
+
+    protected RoleContainerModel getTargetRoleContainer(UserFederationMapperModel mapperModel, RealmModel realm) {
+        boolean realmRolesMapping = parseBooleanParameter(mapperModel, USE_REALM_ROLES_MAPPING);
+        if (realmRolesMapping) {
+            return realm;
+        } else {
+            String clientId = mapperModel.getConfig().get(CLIENT_ID);
+            if (clientId == null) {
+                throw new IllegalStateException("Using client roles mapping is requested, but parameter client.id not found!");
+            }
+            ClientModel client = realm.getClientByClientId(clientId);
+            if (client == null) {
+                throw new IllegalStateException("Can't found requested client with clientId: " + clientId);
+            }
+            return client;
+        }
+    }
+
+    protected String getRolesDn(UserFederationMapperModel mapperModel) {
+        String rolesDn = mapperModel.getConfig().get(ROLES_DN);
+        if (rolesDn == null) {
+            throw new IllegalStateException("Roles DN is null! Check your configuration");
+        }
+        return rolesDn;
+    }
+
+    protected String getRoleNameLdapAttribute(UserFederationMapperModel mapperModel) {
+        String rolesRdnAttr = mapperModel.getConfig().get(ROLE_NAME_LDAP_ATTRIBUTE);
+        return rolesRdnAttr!=null ? rolesRdnAttr : LDAPConstants.CN;
+    }
+
+    protected String getMembershipLdapAttribute(UserFederationMapperModel mapperModel) {
+        String membershipAttrName = mapperModel.getConfig().get(MEMBERSHIP_LDAP_ATTRIBUTE);
+        return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER;
+    }
+
+    protected Collection<String> getRoleObjectClasses(UserFederationMapperModel mapperModel) {
+        String objectClasses = mapperModel.getConfig().get(ROLE_OBJECT_CLASSES);
+        if (objectClasses == null) {
+            objectClasses = "groupOfNames";
+        }
+        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();
+            if (objectClass.length() > 0) {
+                trimmed.add(objectClass);
+            }
+        }
+        return trimmed;
+    }
+
+    private Mode getMode(UserFederationMapperModel mapperModel) {
+        String modeString = mapperModel.getConfig().get(MODE);
+        if (modeString == null || modeString.trim().length() == 0) {
+            return Mode.LDAP_ONLY;
+        }
+
+        return Enum.valueOf(Mode.class, modeString.toUpperCase());
+    }
+
+    protected LDAPObject createLDAPRole(UserFederationMapperModel mapperModel, String roleName, LDAPFederationProvider ldapProvider) {
+        LDAPObject ldapObject = new LDAPObject();
+        String roleNameAttribute = getRoleNameLdapAttribute(mapperModel);
+        ldapObject.setRdnAttributeName(roleNameAttribute);
+        ldapObject.setObjectClasses(getRoleObjectClasses(mapperModel));
+        ldapObject.setAttribute(roleNameAttribute, roleName);
+
+        LDAPDn roleDn = LDAPDn.fromString(getRolesDn(mapperModel));
+        roleDn.addToHead(roleNameAttribute, roleName);
+        ldapObject.setDn(roleDn);
+
+        // TODO: debug
+        logger.infof("Creating role to [%s] to LDAP with DN [%s]", roleName, roleDn.toString());
+        ldapProvider.getLdapIdentityStore().add(ldapObject);
+        return ldapObject;
+    }
+
+    public void addRoleMappingInLDAP(UserFederationMapperModel mapperModel, String roleName, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) {
+        LDAPObject ldapRole = loadLDAPRoleByName(mapperModel, ldapProvider, roleName);
+        if (ldapRole == null) {
+            ldapRole = createLDAPRole(mapperModel, roleName, ldapProvider);
+        }
+
+        Set<String> memberships = getExistingMemberships(mapperModel, ldapRole);
+        memberships.add(ldapUser.getDn().toString());
+        ldapRole.setAttribute(getMembershipLdapAttribute(mapperModel), memberships);
+
+        ldapProvider.getLdapIdentityStore().update(ldapRole);
+    }
+
+    public void deleteRoleMappingInLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, LDAPObject ldapRole) {
+        Set<String> memberships = getExistingMemberships(mapperModel, ldapRole);
+        memberships.remove(ldapUser.getDn().toString());
+
+        // Some membership placeholder needs to be always here as "member" is mandatory attribute on some LDAP servers
+        if (memberships.size() == 0) {
+            memberships.add(LDAPConstants.EMPTY_ATTRIBUTE_VALUE);
+        }
+
+        ldapRole.setAttribute(getMembershipLdapAttribute(mapperModel), memberships);
+        ldapProvider.getLdapIdentityStore().update(ldapRole);
+    }
+
+    public LDAPObject loadLDAPRoleByName(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, String roleName) {
+        LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
+        Condition roleNameCondition = new LDAPQueryConditionsBuilder().equal(new QueryParameter(getRoleNameLdapAttribute(mapperModel)), roleName);
+        ldapQuery.where(roleNameCondition);
+        return ldapQuery.getFirstResult();
+    }
+
+    protected Set<String> getExistingMemberships(UserFederationMapperModel mapperModel, LDAPObject ldapRole) {
+        String memberAttrName = getMembershipLdapAttribute(mapperModel);
+        Set<String> memberships = new TreeSet<String>();
+        Object existingMemberships = ldapRole.getAttribute(memberAttrName);
+
+        if (existingMemberships != null) {
+            if (existingMemberships instanceof String) {
+                String existingMembership = existingMemberships.toString().trim();
+                if (existingMemberships != null && existingMembership.length() > 0) {
+                    memberships.add(existingMembership);
+                }
+            } else if (existingMemberships instanceof Collection) {
+                Collection<String> exMemberships = (Collection<String>) existingMemberships;
+                for (String membership : exMemberships) {
+                    if (membership.trim().length() > 0) {
+                        memberships.add(membership);
+                    }
+                }
+            }
+        }
+        return memberships;
+    }
+
+    protected List<LDAPObject> getLDAPRoleMappings(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser) {
+        LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
+        String membershipAttr = getMembershipLdapAttribute(mapperModel);
+        Condition membershipCondition = new LDAPQueryConditionsBuilder().equal(new QueryParameter(membershipAttr), ldapUser.getDn().toString());
+        ldapQuery.where(membershipCondition);
+        return ldapQuery.getResultList();
+    }
+
+    protected Set<RoleModel> getLDAPRoleMappingsConverted(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, RoleContainerModel roleContainer) {
+        List<LDAPObject> ldapRoles = getLDAPRoleMappings(mapperModel, ldapProvider, ldapUser);
+
+        Set<RoleModel> roles = new HashSet<RoleModel>();
+        String roleNameLdapAttr = getRoleNameLdapAttribute(mapperModel);
+        for (LDAPObject role : ldapRoles) {
+            String roleName = role.getAttributeAsString(roleNameLdapAttr);
+            RoleModel modelRole = roleContainer.getRole(roleName);
+            if (modelRole == null) {
+                // Add role to local DB
+                modelRole = roleContainer.addRole(roleName);
+            }
+            roles.add(modelRole);
+        }
+        return roles;
+    }
+
+    @Override
+    public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
+        final Mode mode = getMode(mapperModel);
+
+        // For IMPORT mode, all operations are performed against local DB
+        if (mode == Mode.IMPORT) {
+            return delegate;
+        } else {
+            return new LDAPRoleMappingsUserDelegate(delegate, mapperModel, ldapProvider, ldapUser, realm, mode);
+        }
+    }
+
+    @Override
+    public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPIdentityQuery query) {
+    }
+
+
+
+    public class LDAPRoleMappingsUserDelegate extends UserModelDelegate {
+
+        private final UserFederationMapperModel mapperModel;
+        private final LDAPFederationProvider ldapProvider;
+        private final LDAPObject ldapUser;
+        private final RealmModel realm;
+        private final Mode mode;
+
+        public LDAPRoleMappingsUserDelegate(UserModel user, UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser,
+                                            RealmModel realm, Mode mode) {
+            super(user);
+            this.mapperModel = mapperModel;
+            this.ldapProvider = ldapProvider;
+            this.ldapUser = ldapUser;
+            this.realm = realm;
+            this.mode = mode;
+        }
+
+        @Override
+        public Set<RoleModel> getRealmRoleMappings() {
+            RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
+            if (roleContainer.equals(realm)) {
+                Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted(mapperModel, ldapProvider, ldapUser, roleContainer);
+
+                if (mode == Mode.LDAP_ONLY) {
+                    // Use just role mappings from LDAP
+                    return ldapRoleMappings;
+                } else {
+                    // Merge mappings from both DB and LDAP
+                    Set<RoleModel> modelRoleMappings = super.getRealmRoleMappings();
+                    ldapRoleMappings.addAll(modelRoleMappings);
+                    return ldapRoleMappings;
+                }
+            } else {
+                return super.getRealmRoleMappings();
+            }
+        }
+
+        @Override
+        public Set<RoleModel> getClientRoleMappings(ClientModel client) {
+            RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
+            if (roleContainer.equals(client)) {
+                Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted(mapperModel, ldapProvider, ldapUser, roleContainer);
+
+                if (mode == Mode.LDAP_ONLY) {
+                    // Use just role mappings from LDAP
+                    return ldapRoleMappings;
+                } else {
+                    // Merge mappings from both DB and LDAP
+                    Set<RoleModel> modelRoleMappings = super.getClientRoleMappings(client);
+                    ldapRoleMappings.addAll(modelRoleMappings);
+                    return ldapRoleMappings;
+                }
+            } else {
+                return super.getClientRoleMappings(client);
+            }
+        }
+
+        @Override
+        public boolean hasRole(RoleModel role) {
+            Set<RoleModel> roles = getRoleMappings();
+            return KeycloakModelUtils.hasRole(roles, role);
+        }
+
+        @Override
+        public void grantRole(RoleModel role) {
+            if (mode == Mode.LDAP_ONLY) {
+                RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
+
+                if (role.getContainer().equals(roleContainer)) {
+
+                    // We need to create new role mappings in LDAP
+                    addRoleMappingInLDAP(mapperModel, role.getName(), ldapProvider, ldapUser);
+                } else {
+                    super.grantRole(role);
+                }
+            } else {
+                super.grantRole(role);
+            }
+        }
+
+        @Override
+        public Set<RoleModel> getRoleMappings() {
+            Set<RoleModel> modelRoleMappings = super.getRoleMappings();
+
+            RoleContainerModel targetRoleContainer = getTargetRoleContainer(mapperModel, realm);
+            Set<RoleModel> ldapRoleMappings = getLDAPRoleMappingsConverted(mapperModel, ldapProvider, ldapUser, targetRoleContainer);
+
+            if (mode == Mode.LDAP_ONLY) {
+                // For LDAP-only we want to retrieve role mappings of target container just from LDAP
+                Set<RoleModel> modelRolesCopy = new HashSet<RoleModel>(modelRoleMappings);
+                for (RoleModel role : modelRolesCopy) {
+                    if (role.getContainer().equals(targetRoleContainer)) {
+                        modelRoleMappings.remove(role);
+                    }
+                }
+            }
+
+            modelRoleMappings.addAll(ldapRoleMappings);
+            return modelRoleMappings;
+        }
+
+        @Override
+        public void deleteRoleMapping(RoleModel role) {
+            RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
+            if (role.getContainer().equals(roleContainer)) {
+
+                LDAPIdentityQuery ldapQuery = createRoleQuery(mapperModel, ldapProvider);
+                LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
+                Condition roleNameCondition = conditionsBuilder.equal(new QueryParameter(getRoleNameLdapAttribute(mapperModel)), role.getName());
+                Condition membershipCondition = conditionsBuilder.equal(new QueryParameter(getMembershipLdapAttribute(mapperModel)), ldapUser.getDn().toString());
+                ldapQuery.where(roleNameCondition).where(membershipCondition);
+                LDAPObject ldapRole = ldapQuery.getFirstResult();
+
+                if (ldapRole == null) {
+                    // Role mapping doesn't exist in LDAP. For LDAP_ONLY mode, we don't need to do anything. For READ_ONLY, delete it in local DB.
+                    if (mode == Mode.READ_ONLY) {
+                        super.deleteRoleMapping(role);
+                    }
+                } else {
+                    // Role mappings exists in LDAP. For LDAP_ONLY mode, we can just delete it in LDAP. For READ_ONLY we can't delete it -> throw error
+                    if (mode == Mode.READ_ONLY) {
+                        throw new ModelException("Not possible to delete LDAP role mappings as mapper mode is READ_ONLY");
+                    } else {
+                        // Delete ldap role mappings
+                        deleteRoleMappingInLDAP(mapperModel, ldapProvider, ldapUser, ldapRole);
+                    }
+                }
+            } else {
+                super.deleteRoleMapping(role);
+            }
+        }
+    }
+
+    public enum Mode {
+        /**
+         * All role mappings are retrieved from LDAP and saved into LDAP
+         */
+        LDAP_ONLY,
+
+        /**
+         * Read-only LDAP mode. Role mappings are retrieved from LDAP for particular user just at the time when he is imported and then
+         * they are saved to local keycloak DB. Then all role mappings are always retrieved from keycloak DB, never from LDAP.
+         * Creating or deleting of role mapping is propagated only to DB.
+         *
+         * This is read-only mode LDAP mode and it's good for performance, but when user is put to some role directly in LDAP, it
+         * won't be seen by Keycloak
+         */
+        IMPORT,
+
+        /**
+         * Read-only LDAP mode. Role mappings are retrieved from both LDAP and DB and merged together. New role grants are not saved to LDAP but to DB.
+         * Deleting role mappings, which is mapped to LDAP, will throw an error.
+         */
+        READ_ONLY
+    }
+}
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
new file mode 100644
index 0000000..321a0d5
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapperFactory.java
@@ -0,0 +1,33 @@
+package org.keycloak.federation.ldap.mappers;
+
+import java.util.List;
+
+import org.keycloak.mappers.UserFederationMapper;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.provider.ProviderConfigProperty;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RoleLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory {
+
+    @Override
+    public String getHelpText() {
+        return "Some help text - role mapper - TODO";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return null;
+    }
+
+    @Override
+    public String getId() {
+        return "role-ldap-mapper";
+    }
+
+    @Override
+    public UserFederationMapper create(KeycloakSession session) {
+        return new RoleLDAPFederationMapper();
+    }
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java
index de0b834..c4c3029 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapper.java
@@ -11,6 +11,9 @@ import org.keycloak.federation.ldap.idm.model.LDAPObject;
 import org.keycloak.federation.ldap.idm.query.Condition;
 import org.keycloak.federation.ldap.idm.query.QueryParameter;
 import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.mappers.UserFederationMapper;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserFederationMapperModel;
 import org.keycloak.models.UserFederationProvider;
 import org.keycloak.models.UserModel;
@@ -43,31 +46,15 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
 
     public static final String USER_MODEL_ATTRIBUTE = "user.model.attribute";
     public static final String LDAP_ATTRIBUTE = "ldap.attribute";
-
-    // TODO: Merge with fullname mapper
     public static final String READ_ONLY = "read.only";
 
-    @Override
-    public String getHelpText() {
-        return "Some help text TODO";
-    }
 
     @Override
-    public List<ProviderConfigProperty> getConfigProperties() {
-        return null;
-    }
-
-    @Override
-    public String getId() {
-        return "user-attribute-ldap-mapper";
-    }
-
-    @Override
-    public void importUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel user, boolean isCreate) {
+    public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
         String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
         String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
 
-        Serializable ldapAttrValue = ldapObject.getAttribute(ldapAttrName);
+        Object ldapAttrValue = ldapUser.getAttribute(ldapAttrName);
         if (ldapAttrValue != null) {
             Property<Object> userModelProperty = userModelProperties.get(userModelAttrName);
 
@@ -82,7 +69,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
     }
 
     @Override
-    public void registerUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel localUser) {
+    public void onRegisterUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
         String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
         String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
 
@@ -97,46 +84,42 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
             attrValue = localUser.getAttribute(userModelAttrName);
         }
 
-        ldapObject.setAttribute(ldapAttrName, (Serializable) attrValue);
+        ldapUser.setAttribute(ldapAttrName, attrValue);
         if (isReadOnly(mapperModel)) {
-            ldapObject.addReadOnlyAttributeName(ldapAttrName);
+            ldapUser.addReadOnlyAttributeName(ldapAttrName);
         }
     }
 
     @Override
-    public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel delegate) {
+    public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
         if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) {
 
             final String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
             final String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
 
-            AbstractTxAwareLDAPUserModelDelegate txDelegate = new AbstractTxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapObject) {
+            TxAwareLDAPUserModelDelegate txDelegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) {
 
                 @Override
                 public void setAttribute(String name, String value) {
                     setLDAPAttribute(name, value);
-
                     super.setAttribute(name, value);
                 }
 
                 @Override
                 public void setEmail(String email) {
                     setLDAPAttribute(UserModel.EMAIL, email);
-
                     super.setEmail(email);
                 }
 
                 @Override
                 public void setLastName(String lastName) {
                     setLDAPAttribute(UserModel.LAST_NAME, lastName);
-
                     super.setLastName(lastName);
                 }
 
                 @Override
                 public void setFirstName(String firstName) {
                     setLDAPAttribute(UserModel.FIRST_NAME, firstName);
-
                     super.setFirstName(firstName);
                 }
 
@@ -148,7 +131,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
 
                         ensureTransactionStarted();
 
-                        ldapObject.setAttribute(ldapAttrName, value);
+                        ldapUser.setAttribute(ldapAttrName, value);
                     }
                 }
 
@@ -181,6 +164,6 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
     }
 
     private boolean isReadOnly(UserFederationMapperModel mapperModel) {
-        return LDAPUtils.parseBooleanParameter(mapperModel, READ_ONLY);
+        return parseBooleanParameter(mapperModel, READ_ONLY);
     }
 }
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
new file mode 100644
index 0000000..16d4879
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java
@@ -0,0 +1,33 @@
+package org.keycloak.federation.ldap.mappers;
+
+import java.util.List;
+
+import org.keycloak.mappers.UserFederationMapper;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.provider.ProviderConfigProperty;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFederationMapperFactory {
+
+    @Override
+    public String getHelpText() {
+        return "Some help text TODO";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return null;
+    }
+
+    @Override
+    public String getId() {
+        return "user-attribute-ldap-mapper";
+    }
+
+    @Override
+    public UserFederationMapper create(KeycloakSession session) {
+        return new UserAttributeLDAPFederationMapper();
+    }
+}
diff --git a/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory
new file mode 100644
index 0000000..d9e3631
--- /dev/null
+++ b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.mappers.UserFederationMapperFactory
@@ -0,0 +1,3 @@
+org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapperFactory
+org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory
+org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapperFactory
\ No newline at end of file
diff --git a/model/api/src/main/java/org/keycloak/mappers/UserFederationMapper.java b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapper.java
new file mode 100644
index 0000000..509ae64
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/mappers/UserFederationMapper.java
@@ -0,0 +1,10 @@
+package org.keycloak.mappers;
+
+import org.keycloak.provider.Provider;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface UserFederationMapper extends Provider {
+
+}
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 44f14c2..64625c5 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
@@ -252,4 +252,19 @@ public final class KeycloakModelUtils {
     public static String getMasterRealmAdminApplicationClientId(RealmModel realm) {
         return realm.getName() + "-realm";
     }
+
+    /**
+     *
+     * @param roles
+     * @param targetRole
+     * @return true if targetRole is in roles (directly or indirectly via composite role)
+     */
+    public static boolean hasRole(Set<RoleModel> roles, RoleModel targetRole) {
+        if (roles.contains(targetRole)) return true;
+
+        for (RoleModel mapping : roles) {
+            if (mapping.hasRole(targetRole)) return true;
+        }
+        return false;
+    }
 }
diff --git a/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index d830551..37844e0 100755
--- a/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -1,5 +1,5 @@
 org.keycloak.models.UserFederationSpi
-org.keycloak.models.UserFederationMapperSpi
+org.keycloak.mappers.UserFederationMapperSpi
 org.keycloak.models.RealmSpi
 org.keycloak.models.UserSessionSpi
 org.keycloak.models.UserSpi
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
index eb4277f..b39fc6d 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
@@ -33,6 +33,7 @@ import org.keycloak.models.entities.CredentialEntity;
 import org.keycloak.models.entities.FederatedIdentityEntity;
 import org.keycloak.models.entities.RoleEntity;
 import org.keycloak.models.entities.UserEntity;
+import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
 import org.keycloak.util.Time;
 
@@ -394,12 +395,7 @@ public class UserAdapter implements UserModel, Comparable {
     @Override
     public boolean hasRole(RoleModel role) {
         Set<RoleModel> roles = getRoleMappings();
-        if (roles.contains(role)) return true;
-
-        for (RoleModel mapping : roles) {
-            if (mapping.hasRole(role)) return true;
-        }
-        return false;
+        return KeycloakModelUtils.hasRole(roles, role);
     }
 
     @Override
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java
index eeeeedf..d62d8d8 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java
@@ -952,6 +952,7 @@ public class RealmAdapter implements RealmModel {
         return null;
     }
 
+    public static String LDAP_MODE = "LDAP_ONLY";
 
     @Override
     public List<UserFederationMapperModel> getUserFederationMappers() {
@@ -992,6 +993,17 @@ public class RealmAdapter implements RealmModel {
                 "user.model.attribute", LDAPConstants.MODIFY_TIMESTAMP,
                 "ldap.attribute", LDAPConstants.MODIFY_TIMESTAMP,
                 "read.only", "true"));
+
+        mappers.add(createMapperModel("realmRoleMpr", "realmRoleMapper", "role-ldap-mapper",
+                "roles.dn", "ou=RealmRoles,dc=keycloak,dc=org",
+                "use.realm.roles.mapping", "true",
+                "mode", LDAP_MODE));
+        mappers.add(createMapperModel("financeRoleMpr", "financeRoleMapper", "role-ldap-mapper",
+                "roles.dn", "ou=FinanceRoles,dc=keycloak,dc=org",
+                "use.realm.roles.mapping", "false",
+                "client.id", "finance",
+                "mode", LDAP_MODE));
+
         return mappers;
     }
 
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
index 7c9086b..977a4f5 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
@@ -404,12 +404,7 @@ public class UserAdapter implements UserModel {
     @Override
     public boolean hasRole(RoleModel role) {
         Set<RoleModel> roles = getRoleMappings();
-        if (roles.contains(role)) return true;
-
-        for (RoleModel mapping : roles) {
-            if (mapping.hasRole(role)) return true;
-        }
-        return false;
+        return KeycloakModelUtils.hasRole(roles, role);
     }
 
     protected TypedQuery<UserRoleMappingEntity> getUserRoleMappingEntityTypedQuery(RoleModel role) {
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
index f7895f2..813faab 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
@@ -23,6 +23,7 @@ import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
 import org.keycloak.models.mongo.utils.MongoModelUtils;
+import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
 import org.keycloak.util.Time;
 
@@ -374,12 +375,7 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
     @Override
     public boolean hasRole(RoleModel role) {
         Set<RoleModel> roles = getRoleMappings();
-        if (roles.contains(role)) return true;
-
-        for (RoleModel mapping : roles) {
-            if (mapping.hasRole(role)) return true;
-        }
-        return false;
+        return KeycloakModelUtils.hasRole(roles, role);
     }
 
     @Override
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java
index e680130..966260a 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java
@@ -13,6 +13,7 @@ import org.keycloak.federation.ldap.LDAPFederationProvider;
 import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
 import org.keycloak.federation.ldap.LDAPUtils;
 import org.keycloak.federation.ldap.idm.model.LDAPObject;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.LDAPConstants;
 import org.keycloak.models.ModelReadOnlyException;
@@ -65,6 +66,9 @@ public class FederationProvidersIntegrationTest {
             LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
             LDAPUtils.removeAllUsers(ldapFedProvider, appRealm);
 
+            // Add sample application TODO: REmove this!!!! It's just temporarily needed in SyncProvidersTest until model for federation mappers is implemented
+            ClientModel finance = appRealm.addClient("finance");
+
             LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", "1234");
             ldapFedProvider.getLdapIdentityStore().updatePassword(john, "Password1");
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java
new file mode 100644
index 0000000..bdc3574
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/LDAPRoleMappingsTest.java
@@ -0,0 +1,367 @@
+package org.keycloak.testsuite.federation;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+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.federation.ldap.LDAPFederationProvider;
+import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
+import org.keycloak.federation.ldap.LDAPUtils;
+import org.keycloak.federation.ldap.idm.model.LDAPObject;
+import org.keycloak.federation.ldap.idm.query.Condition;
+import org.keycloak.federation.ldap.idm.query.QueryParameter;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery;
+import org.keycloak.federation.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
+import org.keycloak.federation.ldap.mappers.RoleLDAPFederationMapper;
+import org.keycloak.models.AccountRoles;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserFederationMapperModel;
+import org.keycloak.models.UserFederationProvider;
+import org.keycloak.models.UserFederationProviderModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.cache.RealmAdapter;
+import org.keycloak.services.managers.RealmManager;
+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.openqa.selenium.WebDriver;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class LDAPRoleMappingsTest {
+
+    private static LDAPRule ldapRule = new LDAPRule();
+
+    private static UserFederationProviderModel ldapModel = null;
+
+    private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            FederationTestUtils.addLocalUser(manager.getSession(), appRealm, "mary", "mary@test.com", "password-app");
+
+            Map<String,String> ldapConfig = ldapRule.getConfig();
+            ldapConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "true");
+            ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString());
+
+            ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap", -1, -1, 0);
+
+            // Delete all LDAP users and add some new for testing
+            LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
+            LDAPUtils.removeAllUsers(ldapFedProvider, appRealm);
+
+            // Add sample application
+            ClientModel finance = appRealm.addClient("finance");
+
+            LDAPObject john = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", "1234");
+            ldapFedProvider.getLdapIdentityStore().updatePassword(john, "Password1");
+
+            LDAPObject mary = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "marykeycloak", "Mary", "Kelly", "mary@email.org", "5678");
+            ldapFedProvider.getLdapIdentityStore().updatePassword(mary, "Password1");
+
+            LDAPObject rob = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "robkeycloak", "Rob", "Brown", "rob@email.org", "8910");
+            ldapFedProvider.getLdapIdentityStore().updatePassword(rob, "Password1");
+
+        }
+    }) {
+
+        @Override
+        protected void after() {
+            // Need to cleanup some LDAP objects after the test
+            update(new KeycloakRule.KeycloakSetup() {
+
+                @Override
+                public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                    RoleLDAPFederationMapper roleMapper = new RoleLDAPFederationMapper();
+                    UserFederationMapperModel roleMapperModel = findRoleMapperModel(appRealm);
+                    LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
+
+                    LDAPObject ldapRole = roleMapper.loadLDAPRoleByName(roleMapperModel, ldapProvider, "realmRole3");
+                    if (ldapRole != null) {
+                        ldapProvider.getLdapIdentityStore().remove(ldapRole);
+                    }
+                }
+
+            });
+
+            super.after();
+        }
+
+    };
+
+    @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 test01_ldapOnlyRoleMappings() {
+        // TODO: Remove me!!!
+        RealmAdapter.LDAP_MODE = "LDAP_ONLY";
+
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+            UserModel john = session.users().getUserByUsername("johnkeycloak", appRealm);
+            UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm);
+
+            // 1 - Grant some roles in LDAP
+
+            // This role should already exists as it was imported from LDAP
+            RoleModel realmRole1 = appRealm.getRole("realmRole1");
+            john.grantRole(realmRole1);
+
+            // This role should already exists as it was imported from LDAP
+            RoleModel realmRole2 = appRealm.getRole("realmRole2");
+            mary.grantRole(realmRole2);
+
+            RoleModel realmRole3 = appRealm.addRole("realmRole3");
+            john.grantRole(realmRole3);
+            mary.grantRole(realmRole3);
+
+            ClientModel accountApp = appRealm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
+            ClientModel financeApp = appRealm.getClientByClientId("finance");
+
+            RoleModel manageAccountRole = accountApp.getRole(AccountRoles.MANAGE_ACCOUNT);
+            RoleModel financeRole1 = financeApp.getRole("financeRole1");
+            john.grantRole(financeRole1);
+
+            // 2 - Check that role mappings are not in local Keycloak DB (They are in LDAP).
+
+            UserModel johnDb = session.userStorage().getUserByUsername("johnkeycloak", appRealm);
+            Set<RoleModel> johnDbRoles = johnDb.getRoleMappings();
+            Assert.assertFalse(johnDbRoles.contains(realmRole1));
+            Assert.assertFalse(johnDbRoles.contains(realmRole2));
+            Assert.assertFalse(johnDbRoles.contains(realmRole3));
+            Assert.assertFalse(johnDbRoles.contains(financeRole1));
+            Assert.assertTrue(johnDbRoles.contains(manageAccountRole));
+
+            // 3 - Check that role mappings are in LDAP and hence available through federation
+
+            Set<RoleModel> johnRoles = john.getRoleMappings();
+            Assert.assertTrue(johnRoles.contains(realmRole1));
+            Assert.assertFalse(johnRoles.contains(realmRole2));
+            Assert.assertTrue(johnRoles.contains(realmRole3));
+            Assert.assertTrue(johnRoles.contains(financeRole1));
+            Assert.assertTrue(johnRoles.contains(manageAccountRole));
+
+            Set<RoleModel> johnRealmRoles = john.getRealmRoleMappings();
+            Assert.assertEquals(2, johnRealmRoles.size());
+            Assert.assertTrue(johnRealmRoles.contains(realmRole1));
+            Assert.assertTrue(johnRealmRoles.contains(realmRole3));
+
+            // account roles are not mapped in LDAP. Those are in Keycloak DB
+            Set<RoleModel> johnAccountRoles = john.getClientRoleMappings(accountApp);
+            Assert.assertTrue(johnAccountRoles.contains(manageAccountRole));
+
+            Set<RoleModel> johnFinanceRoles = john.getClientRoleMappings(financeApp);
+            Assert.assertEquals(1, johnFinanceRoles.size());
+            Assert.assertTrue(johnFinanceRoles.contains(financeRole1));
+
+            // 4 - Delete some role mappings and check they are deleted
+
+            john.deleteRoleMapping(realmRole3);
+            john.deleteRoleMapping(realmRole1);
+            john.deleteRoleMapping(financeRole1);
+            john.deleteRoleMapping(manageAccountRole);
+
+            johnRoles = john.getRoleMappings();
+            Assert.assertFalse(johnRoles.contains(realmRole1));
+            Assert.assertFalse(johnRoles.contains(realmRole2));
+            Assert.assertFalse(johnRoles.contains(realmRole3));
+            Assert.assertFalse(johnRoles.contains(financeRole1));
+            Assert.assertFalse(johnRoles.contains(manageAccountRole));
+
+            // Cleanup
+            mary.deleteRoleMapping(realmRole2);
+            mary.deleteRoleMapping(realmRole3);
+            john.grantRole(manageAccountRole);
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+    @Test
+    public void test02_readOnlyRoleMappings() {
+        // TODO: Remove me!!!
+        RealmAdapter.LDAP_MODE = "READ_ONLY";
+
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+            UserModel mary = session.users().getUserByUsername("marykeycloak", appRealm);
+
+            RoleModel realmRole1 = appRealm.getRole("realmRole1");
+            RoleModel realmRole2 = appRealm.getRole("realmRole2");
+            RoleModel realmRole3 = appRealm.getRole("realmRole3");
+            if (realmRole3 == null) {
+                realmRole3 = appRealm.addRole("realmRole3");
+            }
+
+            // Add some role mappings directly into LDAP
+            RoleLDAPFederationMapper roleMapper = new RoleLDAPFederationMapper();
+            UserFederationMapperModel roleMapperModel = findRoleMapperModel(appRealm);
+            LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
+            LDAPObject maryLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "marykeycloak");
+            roleMapper.addRoleMappingInLDAP(roleMapperModel, "realmRole1", ldapProvider, maryLdap);
+            roleMapper.addRoleMappingInLDAP(roleMapperModel, "realmRole2", ldapProvider, maryLdap);
+
+            // Add some role to model
+            mary.grantRole(realmRole3);
+
+            // Assert that mary has both LDAP and DB mapped roles
+            Set<RoleModel> maryRoles = mary.getRealmRoleMappings();
+            Assert.assertTrue(maryRoles.contains(realmRole1));
+            Assert.assertTrue(maryRoles.contains(realmRole2));
+            Assert.assertTrue(maryRoles.contains(realmRole3));
+
+            // Assert that access through DB will have just DB mapped role
+            UserModel maryDB = session.userStorage().getUserByUsername("marykeycloak", appRealm);
+            Set<RoleModel> maryDBRoles = maryDB.getRealmRoleMappings();
+            Assert.assertFalse(maryDBRoles.contains(realmRole1));
+            Assert.assertFalse(maryDBRoles.contains(realmRole2));
+            Assert.assertTrue(maryDBRoles.contains(realmRole3));
+
+            mary.deleteRoleMapping(realmRole3);
+            try {
+                mary.deleteRoleMapping(realmRole1);
+                Assert.fail("It wasn't expected to successfully delete LDAP role mappings in READ_ONLY mode");
+            } catch (ModelException expected) {
+            }
+
+            // Delete role mappings directly in LDAP
+            deleteRoleMappingsInLDAP(roleMapperModel, roleMapper, ldapProvider, maryLdap, "realmRole1");
+            deleteRoleMappingsInLDAP(roleMapperModel, roleMapper, ldapProvider, maryLdap, "realmRole2");
+
+            // Assert role mappings is not available
+            maryRoles = mary.getRealmRoleMappings();
+            Assert.assertFalse(maryRoles.contains(realmRole1));
+            Assert.assertFalse(maryRoles.contains(realmRole2));
+            Assert.assertFalse(maryRoles.contains(realmRole3));
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+    @Test
+    public void test03_importRoleMappings() {
+        // TODO: Remove me!!!
+        RealmAdapter.LDAP_MODE = "IMPORT";
+
+        KeycloakSession session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = session.realms().getRealmByName("test");
+
+            // Add some role mappings directly in LDAP
+            RoleLDAPFederationMapper roleMapper = new RoleLDAPFederationMapper();
+            UserFederationMapperModel roleMapperModel = findRoleMapperModel(appRealm);
+            LDAPFederationProvider ldapProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
+            LDAPObject robLdap = ldapProvider.loadLDAPUserByUsername(appRealm, "robkeycloak");
+            roleMapper.addRoleMappingInLDAP(roleMapperModel, "realmRole1", ldapProvider, robLdap);
+            roleMapper.addRoleMappingInLDAP(roleMapperModel, "realmRole2", ldapProvider, robLdap);
+
+            // Get user and check that he has requested roles from LDAP
+            UserModel rob = session.users().getUserByUsername("robkeycloak", appRealm);
+            RoleModel realmRole1 = appRealm.getRole("realmRole1");
+            RoleModel realmRole2 = appRealm.getRole("realmRole2");
+            RoleModel realmRole3 = appRealm.getRole("realmRole3");
+            if (realmRole3 == null) {
+                realmRole3 = appRealm.addRole("realmRole3");
+            }
+            Set<RoleModel> robRoles = rob.getRealmRoleMappings();
+            Assert.assertTrue(robRoles.contains(realmRole1));
+            Assert.assertTrue(robRoles.contains(realmRole2));
+            Assert.assertFalse(robRoles.contains(realmRole3));
+
+            // Add some role mappings in model and check that user has it
+            rob.grantRole(realmRole3);
+            robRoles = rob.getRealmRoleMappings();
+            Assert.assertTrue(robRoles.contains(realmRole3));
+
+            // Delete some role mappings in LDAP and check that it doesn't have any effect and user still has role
+            deleteRoleMappingsInLDAP(roleMapperModel, roleMapper, ldapProvider, robLdap, "realmRole1");
+            deleteRoleMappingsInLDAP(roleMapperModel, roleMapper, ldapProvider, robLdap, "realmRole2");
+            robRoles = rob.getRealmRoleMappings();
+            Assert.assertTrue(robRoles.contains(realmRole1));
+            Assert.assertTrue(robRoles.contains(realmRole2));
+
+            // Delete role mappings through model and verifies that user doesn't have them anymore
+            rob.deleteRoleMapping(realmRole1);
+            rob.deleteRoleMapping(realmRole2);
+            rob.deleteRoleMapping(realmRole3);
+            robRoles = rob.getRealmRoleMappings();
+            Assert.assertFalse(robRoles.contains(realmRole1));
+            Assert.assertFalse(robRoles.contains(realmRole2));
+            Assert.assertFalse(robRoles.contains(realmRole3));
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+    private static UserFederationMapperModel findRoleMapperModel(RealmModel appRealm) {
+        List<UserFederationMapperModel> fedMappers = appRealm.getUserFederationMappers();
+        for (UserFederationMapperModel mapper : fedMappers) {
+            if ("realmRoleMapper".equals(mapper.getName())) {
+                return mapper;
+            }
+        }
+
+        throw new IllegalStateException("Mapper 'realmRoleMapper' not found");
+    }
+
+    private void deleteRoleMappingsInLDAP(UserFederationMapperModel roleMapperModel, RoleLDAPFederationMapper roleMapper, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, String roleName) {
+        LDAPIdentityQuery ldapQuery = roleMapper.createRoleQuery(roleMapperModel, ldapProvider);
+        LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
+        Condition roleNameCondition = conditionsBuilder.equal(new QueryParameter(LDAPConstants.CN), roleName);
+        ldapQuery.where(roleNameCondition);
+        LDAPObject ldapRole1 = ldapQuery.getFirstResult();
+        roleMapper.deleteRoleMappingInLDAP(roleMapperModel, ldapProvider, ldapUser, ldapRole1);
+    }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java
index c44a709..71a37b7 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java
@@ -11,6 +11,7 @@ import org.keycloak.federation.ldap.LDAPFederationProvider;
 import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
 import org.keycloak.federation.ldap.LDAPUtils;
 import org.keycloak.federation.ldap.idm.model.LDAPObject;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.LDAPConstants;
@@ -60,6 +61,9 @@ public class SyncProvidersTest {
             LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
             LDAPUtils.removeAllUsers(ldapFedProvider, appRealm);
 
+            // Add sample application TODO: REmove this!!!! It's just temporarily needed in SyncProvidersTest until model for federation mappers is implemented
+            ClientModel finance = appRealm.addClient("finance");
+
             for (int i=1 ; i<=5 ; i++) {
                 LDAPObject ldapUser = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "user" + i, "User" + i + "FN", "User" + i + "LN", "user" + i + "@email.org", "12" + i);
                 ldapFedProvider.getLdapIdentityStore().updatePassword(ldapUser, "Password1");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java
index d2f0ed7..9026557 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KerberosRule.java
@@ -5,7 +5,7 @@ import java.net.URL;
 
 import org.jboss.logging.Logger;
 import org.keycloak.testsuite.ldap.EmbeddedServersFactory;
-import org.keycloak.testsuite.ldap.LDAPConfiguration;
+import org.keycloak.testsuite.ldap.LDAPTestConfiguration;
 import org.keycloak.testsuite.ldap.LDAPEmbeddedServer;
 
 /**
@@ -21,7 +21,7 @@ public class KerberosRule extends LDAPRule {
         this.configLocation = configLocation;
 
         // Global kerberos configuration
-        URL krb5ConfURL = LDAPConfiguration.class.getResource("/kerberos/test-krb5.conf");
+        URL krb5ConfURL = LDAPTestConfiguration.class.getResource("/kerberos/test-krb5.conf");
         String krb5ConfPath = new File(krb5ConfURL.getFile()).getAbsolutePath();
         log.info("Krb5.conf file location is: " + krb5ConfPath);
         System.setProperty("java.security.krb5.conf", krb5ConfPath);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java
index 290341c..1bf9b8e 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java
@@ -4,7 +4,7 @@ import java.util.Map;
 
 import org.junit.rules.ExternalResource;
 import org.keycloak.testsuite.ldap.EmbeddedServersFactory;
-import org.keycloak.testsuite.ldap.LDAPConfiguration;
+import org.keycloak.testsuite.ldap.LDAPTestConfiguration;
 import org.keycloak.testsuite.ldap.LDAPEmbeddedServer;
 
 /**
@@ -14,15 +14,15 @@ public class LDAPRule extends ExternalResource {
 
     public static final String LDAP_CONNECTION_PROPERTIES_LOCATION = "ldap/ldap-connection.properties";
 
-    protected LDAPConfiguration ldapConfiguration;
+    protected LDAPTestConfiguration ldapTestConfiguration;
     protected LDAPEmbeddedServer ldapEmbeddedServer;
 
     @Override
     protected void before() throws Throwable {
         String connectionPropsLocation = getConnectionPropertiesLocation();
-        ldapConfiguration = LDAPConfiguration.readConfiguration(connectionPropsLocation);
+        ldapTestConfiguration = LDAPTestConfiguration.readConfiguration(connectionPropsLocation);
 
-        if (ldapConfiguration.isStartEmbeddedLdapLerver()) {
+        if (ldapTestConfiguration.isStartEmbeddedLdapLerver()) {
             EmbeddedServersFactory factory = EmbeddedServersFactory.readConfiguration();
             ldapEmbeddedServer = createServer(factory);
             ldapEmbeddedServer.init();
@@ -36,7 +36,7 @@ public class LDAPRule extends ExternalResource {
             if (ldapEmbeddedServer != null) {
                 ldapEmbeddedServer.stop();
                 ldapEmbeddedServer = null;
-                ldapConfiguration = null;
+                ldapTestConfiguration = null;
             }
         } catch (Exception e) {
             throw new RuntimeException("Error tearDown Embedded LDAP server.", e);
@@ -52,6 +52,6 @@ public class LDAPRule extends ExternalResource {
     }
 
     public Map<String, String> getConfig() {
-        return ldapConfiguration.getLDAPConfig();
+        return ldapTestConfiguration.getLDAPConfig();
     }
 }
diff --git a/testsuite/integration/src/test/resources/ldap/users.ldif b/testsuite/integration/src/test/resources/ldap/users.ldif
index b41d9f5..b1777d7 100644
--- a/testsuite/integration/src/test/resources/ldap/users.ldif
+++ b/testsuite/integration/src/test/resources/ldap/users.ldif
@@ -9,13 +9,31 @@ objectclass: top
 objectclass: organizationalUnit
 ou: People
 
-dn: ou=Roles,dc=keycloak,dc=org
+dn: ou=RealmRoles,dc=keycloak,dc=org
 objectclass: top
 objectclass: organizationalUnit
-ou: Roles
+ou: RealmRoles
 
-dn: ou=Groups,dc=keycloak,dc=org
+dn: cn=realmRole1,ou=RealmRoles,dc=keycloak,dc=org
+objectclass: top
+objectclass: groupOfNames
+cn: realmRole1
+member:
+
+dn: cn=realmRole2,ou=RealmRoles,dc=keycloak,dc=org
+objectclass: top
+objectclass: groupOfNames
+cn: realmRole2
+member:
+
+dn: ou=FinanceRoles,dc=keycloak,dc=org
 objectclass: top
 objectclass: organizationalUnit
-ou: Groups
+ou: FinanceRoles
+
+dn: cn=financeRole1,ou=FinanceRoles,dc=keycloak,dc=org
+objectclass: top
+objectclass: groupOfNames
+cn: financeRole1
+member: