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 dab99e081d..36be680a66 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/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java index 7ad05d34d7..cd857ff63d 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 876a96d5fc..98ee688663 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 396d997dbe..27e8df3850 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/idm/model/LDAPObject.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java index b7e6c0e36d..c449484cef 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 Marek Posolda */ public class LDAPObject { + private static final Logger logger = Logger.getLogger(LDAPObject.class); + private String uuid; private LDAPDn dn; private String rdnAttributeName; - private final List objectClasses = new LinkedList(); - private final List readOnlyAttributeNames = new LinkedList(); - private final Map attributes = new HashMap(); + private final List objectClasses = new LinkedList<>(); + + // NOTE: names of read-only attributes are lower-cased to avoid case sensitivity issues + private final List readOnlyAttributeNames = new LinkedList<>(); + + private final Map attributes = new HashMap<>(); + + // Copy of "attributes" containing lower-cased keys + private final Map 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 attrValues = (Collection) 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 b3fe0f7f27..ee292b17e7 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 returningLdapAttributes = new LinkedHashSet(); // 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 returningReadOnlyLdapAttributes = new LinkedHashSet(); private final Set objectClasses = new LinkedHashSet(); @@ -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 3dbfd0a399..64087ad139 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 ldapAttributes = attributes.getAll(); - // Exact name of attributes might be different - List 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 attrValues = new TreeSet<>(); + Set 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 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/mappers/FullNameLDAPFederationMapper.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java index 7466778b5b..483cc06b68 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 165309c1e7..bc9fb05160 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 getExistingMemberships(UserFederationMapperModel mapperModel, LDAPObject ldapRole) { String memberAttrName = getMembershipLdapAttribute(mapperModel); Set memberships = new TreeSet(); - 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 roles = new HashSet(); 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 dd139b0b1f..c372769575 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 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) { + 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); + + // For writable mode, we want to propagate writing of attribute to LDAP as well 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); - - 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 getAttributes() { + Map 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 90dd21a8e4..1b1b44d2bd 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 4ed0d28372..1643aa0743 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 @@ -262,6 +262,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(); 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 540ae64ed2..1a78875ff4 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 9450126233..de41e19420 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