commit
d4819886a6
14 changed files with 261 additions and 56 deletions
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(" ");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
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<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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue