keycloak-aplcache

Details

diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
index dab99e0..36be680 100755
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
@@ -46,7 +46,8 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
             "org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity",
             "org.keycloak.models.entities.AuthenticationExecutionEntity",
             "org.keycloak.models.entities.AuthenticationFlowEntity",
-            "org.keycloak.models.entities.AuthenticatorEntity",
+            "org.keycloak.models.entities.AuthenticatorConfigEntity",
+            "org.keycloak.models.entities.RequiredActionProviderEntity",
     };
 
     private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class);
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 b7e6c0e..c449484 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
@@ -6,18 +6,29 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
+import org.jboss.logging.Logger;
+
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 public class LDAPObject {
 
+    private static final Logger logger = Logger.getLogger(LDAPObject.class);
+
     private String uuid;
     private LDAPDn dn;
     private String rdnAttributeName;
 
-    private final List<String> objectClasses = new LinkedList<String>();
-    private final List<String> readOnlyAttributeNames = new LinkedList<String>();
-    private final Map<String, Object> attributes = new HashMap<String, Object>();
+    private final List<String> objectClasses = new LinkedList<>();
+
+    // NOTE: names of read-only attributes are lower-cased to avoid case sensitivity issues
+    private final List<String> readOnlyAttributeNames = new LinkedList<>();
+
+    private final Map<String, Object> attributes = new HashMap<>();
+
+    // Copy of "attributes" containing lower-cased keys
+    private final Map<String, Object> lowerCasedAttributes = new HashMap<>();
+
 
     public String getUuid() {
         return uuid;
@@ -49,7 +60,7 @@ public class LDAPObject {
     }
 
     public void addReadOnlyAttributeName(String readOnlyAttribute) {
-        readOnlyAttributeNames.add(readOnlyAttribute);
+        readOnlyAttributeNames.add(readOnlyAttribute.toLowerCase());
     }
 
     public String getRdnAttributeName() {
@@ -62,21 +73,23 @@ public class LDAPObject {
 
     public void setAttribute(String attributeName, Object attributeValue) {
         attributes.put(attributeName, attributeValue);
+        lowerCasedAttributes.put(attributeName.toLowerCase(), attributeValue);
     }
 
-    public void removeAttribute(String name) {
-        attributes.remove(name);
+    public Object getAttributeCaseInsensitive(String name) {
+        return lowerCasedAttributes.get(name.toLowerCase());
     }
 
-
-    public Object getAttribute(String name) {
-        return attributes.get(name);
-    }
-
-    public String getAttributeAsString(String name) {
-        Object attrValue = attributes.get(name);
+    public String getAttributeAsStringCaseInsensitive(String name) {
+        Object attrValue = lowerCasedAttributes.get(name.toLowerCase());
         if (attrValue != null && !(attrValue instanceof String)) {
-            throw new IllegalStateException("Expected String but attribute was " + attrValue + " of type " + attrValue.getClass().getName());
+            logger.warnf("Expected String but attribute '%s' has value '%s' of type '%s' ", name, attrValue, attrValue.getClass().getName());
+
+            if (attrValue instanceof Collection) {
+                Collection<String> attrValues = (Collection<String>) attrValue;
+                attrValue = attrValues.iterator().next();
+                logger.warnf("Returning just first founded value '%s' from the collection", attrValue);
+            }
         }
 
         return (String) attrValue;
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPIdentityQuery.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPIdentityQuery.java
index b3fe0f7..ee292b1 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPIdentityQuery.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/query/internal/LDAPIdentityQuery.java
@@ -40,6 +40,7 @@ public class LDAPIdentityQuery {
     private final Set<String> returningLdapAttributes = new LinkedHashSet<String>();
 
     // Contains just those returningLdapAttributes, which are read-only. They will be marked as read-only in returned LDAPObject instances as well
+    // NOTE: names of attributes are lower-cased to avoid case sensitivity issues (LDAP searching is usually case-insensitive, so we want to be as well)
     private final Set<String> returningReadOnlyLdapAttributes = new LinkedHashSet<String>();
     private final Set<String> objectClasses = new LinkedHashSet<String>();
 
@@ -77,7 +78,7 @@ public class LDAPIdentityQuery {
     }
 
     public LDAPIdentityQuery addReturningReadOnlyLdapAttribute(String ldapAttributeName) {
-        this.returningReadOnlyLdapAttributes.add(ldapAttributeName);
+        this.returningReadOnlyLdapAttributes.add(ldapAttributeName.toLowerCase());
         return this;
     }
 
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 3dbfd0a..64087ad 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
@@ -4,11 +4,11 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Date;
+import java.util.LinkedHashSet;
 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;
@@ -382,12 +382,6 @@ public class LDAPIdentityStore implements IdentityStore {
 
             NamingEnumeration<? extends Attribute> ldapAttributes = attributes.getAll();
 
-            // Exact name of attributes might be different
-            List<String> uppercasedReadOnlyAttrNames = new ArrayList<>();
-            for (String readonlyAttr : readOnlyAttrNames) {
-                uppercasedReadOnlyAttrNames.add(readonlyAttr.toUpperCase());
-            }
-
             while (ldapAttributes.hasMore()) {
                 Attribute ldapAttribute = ldapAttributes.next();
 
@@ -403,7 +397,7 @@ public class LDAPIdentityStore implements IdentityStore {
                     Object uuidValue = ldapAttribute.get();
                     ldapObject.setUuid(this.operationManager.decodeEntryUUID(uuidValue));
                 } else {
-                    Set<String> attrValues = new TreeSet<>();
+                    Set<String> attrValues = new LinkedHashSet<>();
                     NamingEnumeration<?> enumm = ldapAttribute.getAll();
                     while (enumm.hasMoreElements()) {
                         String attrVal = enumm.next().toString();
@@ -419,7 +413,8 @@ public class LDAPIdentityStore implements IdentityStore {
                             ldapObject.setAttribute(ldapAttributeName, attrValues);
                         }
 
-                        if (uppercasedReadOnlyAttrNames.contains(ldapAttributeName.toUpperCase())) {
+                        // readOnlyAttrNames are lower-cased
+                        if (readOnlyAttrNames.contains(ldapAttributeName.toLowerCase())) {
                             ldapObject.addReadOnlyAttributeName(ldapAttributeName);
                         }
                     }
@@ -443,7 +438,9 @@ public class LDAPIdentityStore implements IdentityStore {
         for (Map.Entry<String, Object> attrEntry : ldapObject.getAttributes().entrySet()) {
             String attrName = attrEntry.getKey();
             Object attrValue = attrEntry.getValue();
-            if (!ldapObject.getReadOnlyAttributeNames().contains(attrName) && (isCreate || !ldapObject.getRdnAttributeName().equalsIgnoreCase(attrName))) {
+
+            // ldapObject.getReadOnlyAttributeNames() are lower-cased
+            if (!ldapObject.getReadOnlyAttributeNames().contains(attrName.toLowerCase()) && (isCreate || !ldapObject.getRdnAttributeName().equalsIgnoreCase(attrName))) {
 
                 if (String.class.isInstance(attrValue)) {
                     if (attrValue.toString().trim().length() == 0) {
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 7ad05d3..cd857ff 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
@@ -232,7 +232,7 @@ public class LDAPFederationProvider implements UserFederationProvider {
         if (ldapUser.getUuid().equals(local.getAttribute(LDAPConstants.LDAP_ID))) {
             return ldapUser;
         } else {
-            logger.warnf("LDAP User invalid. ID doesn't match. ID from LDAP [%s], ID from local DB: [%s]", ldapUser.getUuid(), local.getAttribute(LDAPConstants.LDAP_ID));
+            logger.warnf("LDAP User invalid. ID doesn't match. ID from LDAP [%s], LDAP ID from local DB: [%s]", ldapUser.getUuid(), local.getAttribute(LDAPConstants.LDAP_ID));
             return null;
         }
     }
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
index 876a96d..98ee688 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java
@@ -84,14 +84,17 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
 
         boolean activeDirectory = ldapConfig.isActiveDirectory();
         UserFederationProvider.EditMode editMode = ldapConfig.getEditMode();
-        String readOnly = String.valueOf(editMode==UserFederationProvider.EditMode.READ_ONLY || editMode== UserFederationProvider.EditMode.UNSYNCED);
+        String readOnly = String.valueOf(editMode == UserFederationProvider.EditMode.READ_ONLY || editMode == UserFederationProvider.EditMode.UNSYNCED);
         String usernameLdapAttribute = ldapConfig.getUsernameLdapAttribute();
 
+        String alwaysReadValueFromLDAP = String.valueOf(editMode==UserFederationProvider.EditMode.READ_ONLY || editMode== UserFederationProvider.EditMode.WRITABLE);
+
         UserFederationMapperModel mapperModel;
         mapperModel = KeycloakModelUtils.createUserFederationMapperModel("username", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                 UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME,
                 UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, usernameLdapAttribute,
-                UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
+                UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
+                UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false");
         realm.addUserFederationMapper(mapperModel);
 
         // CN is typically used as RDN for Active Directory deployments
@@ -103,7 +106,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
                 mapperModel = KeycloakModelUtils.createUserFederationMapperModel("first name", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                         UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
                         UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.GIVENNAME,
-                        UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
+                        UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
+                        UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
                 realm.addUserFederationMapper(mapperModel);
 
             } else {
@@ -113,13 +117,15 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
                     mapperModel = KeycloakModelUtils.createUserFederationMapperModel("first name", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                             UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
                             UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.GIVENNAME,
-                            UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
+                            UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
+                            UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
                     realm.addUserFederationMapper(mapperModel);
 
                     mapperModel = KeycloakModelUtils.createUserFederationMapperModel("username-cn", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                             UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.USERNAME,
                             UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.CN,
-                            UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
+                            UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
+                            UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false");
                     realm.addUserFederationMapper(mapperModel);
                 } else {
 
@@ -134,20 +140,23 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
             mapperModel = KeycloakModelUtils.createUserFederationMapperModel("first name", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                     UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.FIRST_NAME,
                     UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.CN,
-                    UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
+                    UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
+                    UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
             realm.addUserFederationMapper(mapperModel);
         }
 
         mapperModel = KeycloakModelUtils.createUserFederationMapperModel("last name", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                 UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.LAST_NAME,
                 UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.SN,
-                UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
+                UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
+                UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
         realm.addUserFederationMapper(mapperModel);
 
         mapperModel = KeycloakModelUtils.createUserFederationMapperModel("email", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                 UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, UserModel.EMAIL,
                 UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, LDAPConstants.EMAIL,
-                UserAttributeLDAPFederationMapper.READ_ONLY, readOnly);
+                UserAttributeLDAPFederationMapper.READ_ONLY, readOnly,
+                UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
         realm.addUserFederationMapper(mapperModel);
 
         String createTimestampLdapAttrName = activeDirectory ? "whenCreated" : LDAPConstants.CREATE_TIMESTAMP;
@@ -157,14 +166,16 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi
         mapperModel = KeycloakModelUtils.createUserFederationMapperModel("creation date", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                 UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.CREATE_TIMESTAMP,
                 UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, createTimestampLdapAttrName,
-                UserAttributeLDAPFederationMapper.READ_ONLY, "true");
+                UserAttributeLDAPFederationMapper.READ_ONLY, "true",
+                UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
         realm.addUserFederationMapper(mapperModel);
 
         // map modifyTimeStamp as read-only
         mapperModel = KeycloakModelUtils.createUserFederationMapperModel("modify date", newProviderModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                 UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, LDAPConstants.MODIFY_TIMESTAMP,
                 UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, modifyTimestampLdapAttrName,
-                UserAttributeLDAPFederationMapper.READ_ONLY, "true");
+                UserAttributeLDAPFederationMapper.READ_ONLY, "true",
+                UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, alwaysReadValueFromLDAP);
         realm.addUserFederationMapper(mapperModel);
     }
 
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 396d997..27e8df3 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
@@ -1,6 +1,5 @@
 package org.keycloak.federation.ldap;
 
-import java.util.List;
 import java.util.Set;
 
 import org.keycloak.federation.ldap.idm.model.LDAPDn;
@@ -61,7 +60,7 @@ public class LDAPUtils {
     // ldapUser has filled attributes, but doesn't have filled dn.
     private static void computeAndSetDn(LDAPConfig config, LDAPObject ldapUser) {
         String rdnLdapAttrName = config.getRdnLdapAttribute();
-        String rdnLdapAttrValue = ldapUser.getAttributeAsString(rdnLdapAttrName);
+        String rdnLdapAttrValue = ldapUser.getAttributeAsStringCaseInsensitive(rdnLdapAttrName);
         if (rdnLdapAttrValue == null) {
             throw new ModelException("RDN Attribute [" + rdnLdapAttrName + "] is not filled. Filled attributes: " + ldapUser.getAttributes());
         }
@@ -73,6 +72,6 @@ public class LDAPUtils {
 
     public static String getUsername(LDAPObject ldapUser, LDAPConfig config) {
         String usernameAttr = config.getUsernameLdapAttribute();
-        return ldapUser.getAttributeAsString(usernameAttr);
+        return ldapUser.getAttributeAsStringCaseInsensitive(usernameAttr);
     }
 }
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 7466778..483cc06 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
@@ -28,7 +28,7 @@ public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper {
     @Override
     public void onImportUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
         String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel);
-        String fullName = ldapUser.getAttributeAsString(ldapFullNameAttrName);
+        String fullName = ldapUser.getAttributeAsStringCaseInsensitive(ldapFullNameAttrName);
         fullName = fullName.trim();
         if (fullName != null && !fullName.trim().isEmpty()) {
             int lastSpaceIndex = fullName.lastIndexOf(" ");
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java
index 165309c..bc9fb05 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/RoleLDAPFederationMapper.java
@@ -74,7 +74,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
             // Import role mappings from LDAP into Keycloak DB
             String roleNameAttr = getRoleNameLdapAttribute(mapperModel);
             for (LDAPObject ldapRole : ldapRoles) {
-                String roleName = ldapRole.getAttributeAsString(roleNameAttr);
+                String roleName = ldapRole.getAttributeAsStringCaseInsensitive(roleNameAttr);
 
                 RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
                 RoleModel role = roleContainer.getRole(roleName);
@@ -103,7 +103,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
             RoleContainerModel roleContainer = getTargetRoleContainer(mapperModel, realm);
             String rolesRdnAttr = getRoleNameLdapAttribute(mapperModel);
             for (LDAPObject ldapRole : ldapRoles) {
-                String roleName = ldapRole.getAttributeAsString(rolesRdnAttr);
+                String roleName = ldapRole.getAttributeAsStringCaseInsensitive(rolesRdnAttr);
 
                 if (roleContainer.getRole(roleName) == null) {
                     logger.infof("Syncing role [%s] from LDAP to keycloak DB", roleName);
@@ -249,7 +249,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
     protected Set<String> getExistingMemberships(UserFederationMapperModel mapperModel, LDAPObject ldapRole) {
         String memberAttrName = getMembershipLdapAttribute(mapperModel);
         Set<String> memberships = new TreeSet<String>();
-        Object existingMemberships = ldapRole.getAttribute(memberAttrName);
+        Object existingMemberships = ldapRole.getAttributeCaseInsensitive(memberAttrName);
 
         if (existingMemberships != null) {
             if (existingMemberships instanceof String) {
@@ -411,7 +411,7 @@ public class RoleLDAPFederationMapper extends AbstractLDAPFederationMapper {
             Set<RoleModel> roles = new HashSet<RoleModel>();
             String roleNameLdapAttr = getRoleNameLdapAttribute(mapperModel);
             for (LDAPObject role : ldapRoles) {
-                String roleName = role.getAttributeAsString(roleNameLdapAttr);
+                String roleName = role.getAttributeAsStringCaseInsensitive(roleNameLdapAttr);
                 RoleModel modelRole = roleContainer.getRole(roleName);
                 if (modelRole == null) {
                     // Add role to local DB
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 dd139b0..c372769 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
@@ -1,6 +1,7 @@
 package org.keycloak.federation.ldap.mappers;
 
 import java.lang.reflect.Method;
+import java.util.HashMap;
 import java.util.Map;
 
 import org.keycloak.federation.ldap.LDAPFederationProvider;
@@ -12,6 +13,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserFederationMapperModel;
 import org.keycloak.models.UserFederationProvider;
 import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.UserModelDelegate;
 import org.keycloak.models.utils.reflection.Property;
 import org.keycloak.models.utils.reflection.PropertyCriteria;
 import org.keycloak.models.utils.reflection.PropertyQueries;
@@ -41,6 +43,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
     public static final String USER_MODEL_ATTRIBUTE = "user.model.attribute";
     public static final String LDAP_ATTRIBUTE = "ldap.attribute";
     public static final String READ_ONLY = "read.only";
+    public static final String ALWAYS_READ_VALUE_FROM_LDAP = "always.read.value.from.ldap";
 
 
     @Override
@@ -48,7 +51,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
         String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
         String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
 
-        Object ldapAttrValue = ldapUser.getAttribute(ldapAttrName);
+        Object ldapAttrValue = ldapUser.getAttributeCaseInsensitive(ldapAttrName);
         if (ldapAttrValue != null && !ldapAttrValue.toString().trim().isEmpty()) {
             Property<Object> userModelProperty = userModelProperties.get(userModelAttrName);
 
@@ -85,13 +88,15 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
     }
 
     @Override
-    public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
-        if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) {
+    public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, final LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
+        final String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
+        final String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
+        boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP);
 
-            final String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE);
-            final String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE);
+        // For writable mode, we want to propagate writing of attribute to LDAP as well
+        if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) {
 
-            TxAwareLDAPUserModelDelegate txDelegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) {
+            delegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapUser) {
 
                 @Override
                 public void setAttribute(String name, String value) {
@@ -131,10 +136,67 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap
 
             };
 
-            return txDelegate;
-        } else {
-            return delegate;
         }
+
+        // We prefer to read attribute value from LDAP instead of from local Keycloak DB
+        if (isAlwaysReadValueFromLDAP) {
+
+            delegate = new UserModelDelegate(delegate) {
+
+                @Override
+                public String getAttribute(String name) {
+                    if (name.equalsIgnoreCase(userModelAttrName)) {
+                        // TODO: Support different types than strings as well...
+                        return ldapUser.getAttributeAsStringCaseInsensitive(ldapAttrName);
+                    } else {
+                        return super.getAttribute(name);
+                    }
+                }
+
+                @Override
+                public Map<String, String> getAttributes() {
+                    Map<String, String> attrs = new HashMap<>(super.getAttributes());
+
+                    // Ignore properties
+                    if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName) || UserModel.FIRST_NAME.equalsIgnoreCase(userModelAttrName) || UserModel.LAST_NAME.equalsIgnoreCase(userModelAttrName)) {
+                        return attrs;
+                    }
+
+                    attrs.put(userModelAttrName, ldapUser.getAttributeAsStringCaseInsensitive(ldapAttrName));
+                    return attrs;
+                }
+
+                @Override
+                public String getEmail() {
+                    if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName)) {
+                        return ldapUser.getAttributeAsStringCaseInsensitive(ldapAttrName);
+                    } else {
+                        return super.getEmail();
+                    }
+                }
+
+                @Override
+                public String getLastName() {
+                    if (UserModel.LAST_NAME.equalsIgnoreCase(userModelAttrName)) {
+                        return ldapUser.getAttributeAsStringCaseInsensitive(ldapAttrName);
+                    } else {
+                        return super.getLastName();
+                    }
+                }
+
+                @Override
+                public String getFirstName() {
+                    if (UserModel.FIRST_NAME.equalsIgnoreCase(userModelAttrName)) {
+                        return ldapUser.getAttributeAsStringCaseInsensitive(ldapAttrName);
+                    } else {
+                        return super.getFirstName();
+                    }
+                }
+
+            };
+        }
+
+        return delegate;
     }
 
     @Override
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java
index 90dd21a..1b1b44d 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/UserAttributeLDAPFederationMapperFactory.java
@@ -30,6 +30,10 @@ public class UserAttributeLDAPFederationMapperFactory extends AbstractLDAPFedera
         ProviderConfigProperty readOnly = createConfigProperty(UserAttributeLDAPFederationMapper.READ_ONLY, "Read Only",
                 "Read-only attribute is imported from LDAP to Keycloak DB, but it's not saved back to LDAP when user is updated in Keycloak.", ProviderConfigProperty.BOOLEAN_TYPE, "false");
         configProperties.add(readOnly);
+
+        ProviderConfigProperty alwaysReadValueFromLDAP = createConfigProperty(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "Always read value from LDAP",
+                "If on, then during reading of the user will be value of attribute from LDAP always used instead of the value from Keycloak DB", ProviderConfigProperty.BOOLEAN_TYPE, "false");
+        configProperties.add(alwaysReadValueFromLDAP);
     }
 
     @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 4ed0d28..1643aa0 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
@@ -263,6 +263,101 @@ public class FederationProvidersIntegrationTest {
     }
 
     @Test
+    public void testCaseSensitiveAttributeName() {
+        KeycloakSession session = keycloakRule.startSession();
+
+        try {
+            RealmModel appRealm = new RealmManager(session).getRealmByName("test");
+
+            LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
+            LDAPObject johnZip = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnzip", "John", "Zip", "johnzip@email.org", "12398");
+
+            // Remove default zipcode mapper and add the mapper for "POstalCode" to test case sensitivity
+            UserFederationMapperModel currentZipMapper = appRealm.getUserFederationMapperByName(ldapModel.getId(), "zipCodeMapper");
+            appRealm.removeUserFederationMapper(currentZipMapper);
+            FederationTestUtils.addUserAttributeMapper(appRealm, ldapModel, "zipCodeMapper-cs", "postal_code", "POstalCode");
+
+            // Fetch user from LDAP and check that postalCode is filled
+            UserModel user = session.users().getUserByUsername("johnzip", appRealm);
+            String postalCode = user.getAttribute("postal_code");
+            Assert.assertEquals("12398", postalCode);
+
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+    @Test
+    public void testDirectLDAPUpdate() {
+        KeycloakSession session = keycloakRule.startSession();
+
+        try {
+            RealmModel appRealm = new RealmManager(session).getRealmByName("test");
+
+            LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
+            LDAPObject johnDirect = FederationTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johndirect", "John", "Direct", "johndirect@email.org", "12399");
+
+            // Fetch user from LDAP and check that postalCode is filled
+            UserModel user = session.users().getUserByUsername("johndirect", appRealm);
+            String postalCode = user.getAttribute("postal_code");
+            Assert.assertEquals("12399", postalCode);
+
+            // Directly update user in LDAP
+            johnDirect.setAttribute(LDAPConstants.POSTAL_CODE, "12400");
+            johnDirect.setAttribute(LDAPConstants.SN, "DirectLDAPUpdated");
+            ldapFedProvider.getLdapIdentityStore().update(johnDirect);
+
+            // Verify that postalCode is still the same as we read it's value from Keycloak DB
+            user = session.users().getUserByUsername("johndirect", appRealm);
+            postalCode = user.getAttribute("postal_code");
+            Assert.assertEquals("12399", postalCode);
+
+            // Check user.getAttributes()
+            postalCode = user.getAttributes().get("postal_code");
+            Assert.assertEquals("12399", postalCode);
+
+            // LastName is new as lastName mapper will read the value from LDAP
+            String lastName = user.getLastName();
+            Assert.assertEquals("DirectLDAPUpdated", lastName);
+        } finally {
+            keycloakRule.stopSession(session, true);
+        }
+
+        session = keycloakRule.startSession();
+        try {
+            RealmModel appRealm = new RealmManager(session).getRealmByName("test");
+
+            // Update postalCode mapper to always read the value from LDAP
+            UserFederationMapperModel zipMapper = appRealm.getUserFederationMapperByName(ldapModel.getId(), "zipCodeMapper");
+            zipMapper.getConfig().put(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "true");
+            appRealm.updateUserFederationMapper(zipMapper);
+
+            // Update lastName mapper to read the value from Keycloak DB
+            UserFederationMapperModel lastNameMapper = appRealm.getUserFederationMapperByName(ldapModel.getId(), "last name");
+            lastNameMapper.getConfig().put(UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false");
+            appRealm.updateUserFederationMapper(lastNameMapper);
+
+            // Verify that postalCode is read from LDAP now
+            UserModel user = session.users().getUserByUsername("johndirect", appRealm);
+            String postalCode = user.getAttribute("postal_code");
+            Assert.assertEquals("12400", postalCode);
+
+            // Check user.getAttributes()
+            postalCode = user.getAttributes().get("postal_code");
+            Assert.assertEquals("12400", postalCode);
+
+            Assert.assertFalse(user.getAttributes().containsKey(UserModel.LAST_NAME));
+
+            // lastName is read from Keycloak DB now
+            String lastName = user.getLastName();
+            Assert.assertEquals("Direct", lastName);
+
+        } finally {
+            keycloakRule.stopSession(session, false);
+        }
+    }
+
+    @Test
     public void testFullNameMapper() {
         KeycloakSession session = keycloakRule.startSession();
         UserFederationMapperModel firstNameMapper = null;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java
index 540ae64..1a78875 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java
@@ -95,14 +95,15 @@ class FederationTestUtils {
     }
 
     public static void addZipCodeLDAPMapper(RealmModel realm, UserFederationProviderModel providerModel) {
-        addUserAttributeMapper(realm, providerModel, "zipCodeMapper", "postal_code", LDAPConstants.POSTAL_CODE); 
+        addUserAttributeMapper(realm, providerModel, "zipCodeMapper", "postal_code", LDAPConstants.POSTAL_CODE);
     }
 
     public static void addUserAttributeMapper(RealmModel realm, UserFederationProviderModel providerModel, String mapperName, String userModelAttributeName, String ldapAttributeName) {
         UserFederationMapperModel mapperModel = KeycloakModelUtils.createUserFederationMapperModel(mapperName, providerModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID,
                 UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, userModelAttributeName,
                 UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, ldapAttributeName,
-                UserAttributeLDAPFederationMapper.READ_ONLY, "false");
+                UserAttributeLDAPFederationMapper.READ_ONLY, "false",
+                UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false");
         realm.addUserFederationMapper(mapperModel);
     }
 
diff --git a/testsuite/integration/src/test/resources/ldap/users.ldif b/testsuite/integration/src/test/resources/ldap/users.ldif
index 9450126..de41e19 100644
--- a/testsuite/integration/src/test/resources/ldap/users.ldif
+++ b/testsuite/integration/src/test/resources/ldap/users.ldif
@@ -19,4 +19,25 @@ objectclass: top
 objectclass: organizationalUnit
 ou: FinanceRoles
 
+dn: uid=jbrown,ou=People,dc=keycloak,dc=org
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+uid: jbrown
+cn: James
+sn: Brown
+mail: jbrown@keycloak.org
+postalCode: 88441
 
+dn: uid=bwilson,ou=People,dc=keycloak,dc=org
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+uid: bwilson
+cn: Bruce
+sn: Wilson
+mail: bwilson@keycloak.org
+postalCode: 88441
+postalCode: 77332