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 58364d3b3e..1e9918df35 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 @@ -9,7 +9,6 @@ import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore; import org.keycloak.federation.ldap.mappers.LDAPFederationMapper; import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; -import org.keycloak.models.UserFederationMapper; import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserModel; @@ -179,4 +178,9 @@ public class LDAPUtils { String usernameAttr = config.getUsernameLdapAttribute(); return (String) ldapUser.getAttribute(usernameAttr); } + + public static boolean parseBooleanParameter(UserFederationMapperModel mapperModel, String paramName) { + String readOnly = mapperModel.getConfig().get(paramName); + return Boolean.parseBoolean(readOnly); + } } 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 f51bdadd7f..1d62a3c7bc 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 @@ -186,7 +186,7 @@ public class LDAPIdentityQuery { } public Set getConditions() { - return unmodifiableSet(this.conditions); + return this.conditions; } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/TxAwareLDAPUserModelDelegate.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractTxAwareLDAPUserModelDelegate.java similarity index 56% rename from federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/TxAwareLDAPUserModelDelegate.java rename to federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractTxAwareLDAPUserModelDelegate.java index 95274e2d3f..4518f4bebf 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/TxAwareLDAPUserModelDelegate.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/AbstractTxAwareLDAPUserModelDelegate.java @@ -1,12 +1,8 @@ package org.keycloak.federation.ldap.mappers; -import java.util.HashMap; -import java.util.Map; - import org.jboss.logging.Logger; import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.idm.model.LDAPObject; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.UserModel; import org.keycloak.models.utils.UserModelDelegate; @@ -14,71 +10,57 @@ import org.keycloak.models.utils.UserModelDelegate; /** * @author Marek Posolda */ -public class TxAwareLDAPUserModelDelegate extends UserModelDelegate { +public abstract class AbstractTxAwareLDAPUserModelDelegate extends UserModelDelegate { - private static final Logger logger = Logger.getLogger(TxAwareLDAPUserModelDelegate.class); + public static final Logger logger = Logger.getLogger(AbstractTxAwareLDAPUserModelDelegate.class); protected LDAPFederationProvider provider; protected LDAPObject ldapObject; - protected LDAPTransaction transaction; + private final LDAPTransaction transaction; - // Map of allowed writable UserModel attributes to LDAP attributes. Includes UserModel properties (firstName, lastName, email, ...) - private final Map mappedAttributes = new HashMap(); - - public TxAwareLDAPUserModelDelegate(UserModel delegate, LDAPFederationProvider provider, LDAPObject ldapObject) { + public AbstractTxAwareLDAPUserModelDelegate(UserModel delegate, LDAPFederationProvider provider, LDAPObject ldapObject) { super(delegate); this.provider = provider; this.ldapObject = ldapObject; + this.transaction = findOrCreateTransaction(); } - public void addMappedAttribute(String userModelAttrName, String ldapAttrName) { - mappedAttributes.put(userModelAttrName, ldapAttrName); + public LDAPTransaction getTransaction() { + return transaction; } - @Override - public void setAttribute(String name, String value) { - setLDAPAttribute(name, value); - - super.setAttribute(name, value); - } - - @Override - public void setEmail(String email) { - setLDAPAttribute(UserModel.EMAIL, email); - - super.setEmail(email); - } - - @Override - public void setLastName(String lastName) { - setLDAPAttribute(UserModel.LAST_NAME, lastName); - - super.setLastName(lastName); - } - - @Override - public void setFirstName(String firstName) { - setLDAPAttribute(UserModel.FIRST_NAME, firstName); - - super.setFirstName(firstName); - } - - protected void setLDAPAttribute(String modelAttrName, String value) { - String ldapAttrName = mappedAttributes.get(modelAttrName); - if (ldapAttrName != null) { - if (logger.isTraceEnabled()) { - logger.tracef("Pushing user attribute to LDAP. Model attribute name: %s, LDAP attribute name: %s, Attribute value: %s", modelAttrName, ldapAttrName, value); + // Try to find transaction in any delegate. We want to enlist just single transaction per all delegates + protected LDAPTransaction findOrCreateTransaction() { + UserModelDelegate delegate = this; + while (true) { + UserModel deleg = delegate.getDelegate(); + if (!(deleg instanceof UserModelDelegate)) { + // Existing transaction not available. Need to create new + return new LDAPTransaction(); + } else { + delegate = (UserModelDelegate) deleg; } - if (transaction == null) { - transaction = new LDAPTransaction(); - provider.getSession().getTransaction().enlistAfterCompletion(transaction); + // Check if it's transaction aware delegate + if (delegate instanceof AbstractTxAwareLDAPUserModelDelegate) { + AbstractTxAwareLDAPUserModelDelegate txDelegate = (AbstractTxAwareLDAPUserModelDelegate) delegate; + return txDelegate.getTransaction(); } - - ldapObject.setAttribute(ldapAttrName, value); } } + protected void ensureTransactionStarted() { + if (transaction.state == TransactionState.NOT_STARTED) { + if (logger.isTraceEnabled()) { + logger.trace("Starting and enlisting transaction for object " + ldapObject.getDn().toString()); + } + + this.provider.getSession().getTransaction().enlistAfterCompletion(transaction); + } + } + + + protected class LDAPTransaction implements KeycloakTransaction { protected TransactionState state = TransactionState.NOT_STARTED; 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 new file mode 100644 index 0000000000..483d825f9d --- /dev/null +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/mappers/FullNameLDAPFederationMapper.java @@ -0,0 +1,174 @@ +package org.keycloak.federation.ldap.mappers; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.LDAPUtils; +import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.federation.ldap.idm.query.Condition; +import org.keycloak.federation.ldap.idm.query.QueryParameter; +import org.keycloak.federation.ldap.idm.query.internal.EqualCondition; +import org.keycloak.federation.ldap.idm.query.internal.LDAPIdentityQuery; +import org.keycloak.models.LDAPConstants; +import org.keycloak.models.UserFederationMapperModel; +import org.keycloak.models.UserFederationProvider; +import org.keycloak.models.UserModel; +import org.keycloak.provider.ProviderConfigProperty; + +/** + * Mapper useful for the LDAP deployments when some attribute (usually CN) is mapped to full name of user + * + * @author Marek Posolda + */ +public class FullNameLDAPFederationMapper extends AbstractLDAPFederationMapper { + + public static final String LDAP_FULL_NAME_ATTRIBUTE = "ldap.full.name.attribute"; + public static final String READ_ONLY = "read.only"; + + @Override + public String getHelpText() { + return "Some help text - full name mapper - TODO"; + } + + @Override + public List getConfigProperties() { + return null; + } + + @Override + public String getId() { + return "full-name-ldap-mapper"; + } + + @Override + public void importUserFromLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel user, boolean isCreate) { + String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel); + String fullName = (String) ldapObject.getAttribute(ldapFullNameAttrName); + fullName = fullName.trim(); + if (fullName != null) { + int lastSpaceIndex = fullName.lastIndexOf(" "); + if (lastSpaceIndex == -1) { + user.setLastName(fullName); + } else { + user.setFirstName(fullName.substring(0, lastSpaceIndex)); + user.setLastName(fullName.substring(lastSpaceIndex + 1)); + } + } + } + + @Override + public void registerUserToLDAP(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel localUser) { + String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel); + String fullName = getFullName(localUser.getFirstName(), localUser.getLastName()); + ldapObject.setAttribute(ldapFullNameAttrName, fullName); + + if (isReadOnly(mapperModel)) { + ldapObject.addReadOnlyAttributeName(ldapFullNameAttrName); + } + } + + @Override + public UserModel proxy(final UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel delegate) { + if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) { + + + AbstractTxAwareLDAPUserModelDelegate txDelegate = new AbstractTxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapObject) { + + @Override + public void setFirstName(String firstName) { + super.setFirstName(firstName); + setFullNameToLDAPObject(); + } + + @Override + public void setLastName(String lastName) { + super.setLastName(lastName); + setFullNameToLDAPObject(); + } + + private void setFullNameToLDAPObject() { + String fullName = getFullName(getFirstName(), getLastName()); + if (logger.isTraceEnabled()) { + logger.tracef("Pushing full name attribute to LDAP. Full name: %s", fullName); + } + + ensureTransactionStarted(); + + String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel); + ldapObject.setAttribute(ldapFullNameAttrName, fullName); + } + + }; + + return txDelegate; + } else { + return delegate; + } + } + + @Override + public void beforeLDAPQuery(UserFederationMapperModel mapperModel, LDAPIdentityQuery query) { + String ldapFullNameAttrName = getLdapFullNameAttrName(mapperModel); + query.addReturningLdapAttribute(ldapFullNameAttrName); + + // Change conditions and compute condition for fullName from the conditions for firstName and lastName. Right now just "equal" condition is supported + EqualCondition firstNameCondition = null; + EqualCondition lastNameCondition = null; + Set conditionsCopy = new HashSet(query.getConditions()); + for (Condition condition : conditionsCopy) { + QueryParameter param = condition.getParameter(); + if (param != null) { + if (param.getName().equals(UserModel.FIRST_NAME)) { + firstNameCondition = (EqualCondition) condition; + query.getConditions().remove(condition); + } else if (param.getName().equals(UserModel.LAST_NAME)) { + lastNameCondition = (EqualCondition) condition; + query.getConditions().remove(condition); + } else if (param.getName().equals(LDAPConstants.GIVENNAME)) { + // Some previous mapper already converted it + firstNameCondition = (EqualCondition) condition; + } else if (param.getName().equals(LDAPConstants.SN)) { + // Some previous mapper already converted it + lastNameCondition = (EqualCondition) condition; + } + } + } + + + String fullName = null; + if (firstNameCondition != null && lastNameCondition != null) { + fullName = firstNameCondition.getValue() + " " + lastNameCondition.getValue(); + } else if (firstNameCondition != null) { + fullName = (String) firstNameCondition.getValue(); + } else if (firstNameCondition != null) { + fullName = (String) lastNameCondition.getValue(); + } else { + return; + } + EqualCondition fullNameCondition = new EqualCondition(new QueryParameter(ldapFullNameAttrName), fullName); + query.getConditions().add(fullNameCondition); + } + + protected String getLdapFullNameAttrName(UserFederationMapperModel mapperModel) { + String ldapFullNameAttrName = mapperModel.getConfig().get(LDAP_FULL_NAME_ATTRIBUTE); + return ldapFullNameAttrName == null ? LDAPConstants.CN : ldapFullNameAttrName; + } + + protected String getFullName(String firstName, String lastName) { + if (firstName != null && lastName != null) { + return firstName + " " + lastName; + } else if (firstName != null) { + return firstName; + } else if (lastName != null) { + return lastName; + } else { + return LDAPConstants.EMPTY_ATTRIBUTE_VALUE; + } + } + + private boolean isReadOnly(UserFederationMapperModel mapperModel) { + return LDAPUtils.parseBooleanParameter(mapperModel, READ_ONLY); + } +} 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 43610046da..de0b8349a0 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 @@ -6,6 +6,7 @@ import java.util.List; import java.util.Map; import org.keycloak.federation.ldap.LDAPFederationProvider; +import org.keycloak.federation.ldap.LDAPUtils; import org.keycloak.federation.ldap.idm.model.LDAPObject; import org.keycloak.federation.ldap.idm.query.Condition; import org.keycloak.federation.ldap.idm.query.QueryParameter; @@ -42,6 +43,8 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap public static final String USER_MODEL_ATTRIBUTE = "user.model.attribute"; public static final String LDAP_ATTRIBUTE = "ldap.attribute"; + + // TODO: Merge with fullname mapper public static final String READ_ONLY = "read.only"; @Override @@ -56,7 +59,7 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap @Override public String getId() { - return "ldap-user-attribute-mapper"; + return "user-attribute-ldap-mapper"; } @Override @@ -104,18 +107,52 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap public UserModel proxy(UserFederationMapperModel mapperModel, LDAPFederationProvider ldapProvider, LDAPObject ldapObject, UserModel delegate) { if (ldapProvider.getEditMode() == UserFederationProvider.EditMode.WRITABLE && !isReadOnly(mapperModel)) { - // This assumes that mappers are sorted by type! Maybe improve... - TxAwareLDAPUserModelDelegate txDelegate; - if (delegate instanceof TxAwareLDAPUserModelDelegate) { - // We will reuse already existing delegate and just register our mapped attribute in existing transaction. - txDelegate = (TxAwareLDAPUserModelDelegate) delegate; - } else { - txDelegate = new TxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapObject); - } + final String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE); + final String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE); - String userModelAttrName = mapperModel.getConfig().get(USER_MODEL_ATTRIBUTE); - String ldapAttrName = mapperModel.getConfig().get(LDAP_ATTRIBUTE); - txDelegate.addMappedAttribute(userModelAttrName, ldapAttrName); + AbstractTxAwareLDAPUserModelDelegate txDelegate = new AbstractTxAwareLDAPUserModelDelegate(delegate, ldapProvider, ldapObject) { + + @Override + public void setAttribute(String name, String value) { + setLDAPAttribute(name, value); + + super.setAttribute(name, value); + } + + @Override + public void setEmail(String email) { + setLDAPAttribute(UserModel.EMAIL, email); + + super.setEmail(email); + } + + @Override + public void setLastName(String lastName) { + setLDAPAttribute(UserModel.LAST_NAME, lastName); + + super.setLastName(lastName); + } + + @Override + public void setFirstName(String firstName) { + setLDAPAttribute(UserModel.FIRST_NAME, firstName); + + super.setFirstName(firstName); + } + + protected void setLDAPAttribute(String modelAttrName, String value) { + if (modelAttrName.equals(userModelAttrName)) { + if (logger.isTraceEnabled()) { + logger.tracef("Pushing user attribute to LDAP. Model attribute name: %s, LDAP attribute name: %s, Attribute value: %s", modelAttrName, ldapAttrName, value); + } + + ensureTransactionStarted(); + + ldapObject.setAttribute(ldapAttrName, value); + } + } + + }; return txDelegate; } else { @@ -144,7 +181,6 @@ public class UserAttributeLDAPFederationMapper extends AbstractLDAPFederationMap } private boolean isReadOnly(UserFederationMapperModel mapperModel) { - String readOnly = mapperModel.getConfig().get(READ_ONLY); - return Boolean.parseBoolean(readOnly); + return LDAPUtils.parseBooleanParameter(mapperModel, READ_ONLY); } } diff --git a/federation/ldap/src/main/resources/META-INF/services/org.keycloak.models.UserFederationMapper b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.models.UserFederationMapper index 7e3a77b986..9d85b2075b 100644 --- a/federation/ldap/src/main/resources/META-INF/services/org.keycloak.models.UserFederationMapper +++ b/federation/ldap/src/main/resources/META-INF/services/org.keycloak.models.UserFederationMapper @@ -1 +1,2 @@ -org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper \ No newline at end of file +org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper +org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapper \ No newline at end of file diff --git a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java index f0256473f8..3f6bec44a0 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java +++ b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java @@ -227,4 +227,7 @@ public class UserModelDelegate implements UserModel { return delegate.revokeConsentForClient(clientId); } + public UserModel getDelegate() { + return delegate; + } } diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java index df9f26fdab..eeeeedfcf1 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java @@ -957,26 +957,38 @@ public class RealmAdapter implements RealmModel { public List getUserFederationMappers() { // TODO: Some hardcoded stuff... List mappers = new ArrayList(); - mappers.add(createMapperModel("usn", "usernameMapper", "ldap-user-attribute-mapper", + + mappers.add(createMapperModel("usn", "usernameMapper", "user-attribute-ldap-mapper", "user.model.attribute", UserModel.USERNAME, "ldap.attribute", LDAPConstants.UID)); - mappers.add(createMapperModel("fn", "firstNameMapper", "ldap-user-attribute-mapper", + + // Uncomment this for CN + SN config + /*mappers.add(createMapperModel("fn", "firstNameMapper", "user-attribute-ldap-mapper", "user.model.attribute", UserModel.FIRST_NAME, - "ldap.attribute", LDAPConstants.CN)); - mappers.add(createMapperModel("ln", "lastNameMapper", "ldap-user-attribute-mapper", + "ldap.attribute", LDAPConstants.CN));*/ + + // Uncomment this for CN + SN + givenname config + mappers.add(createMapperModel("fn", "firstNameMapper", "user-attribute-ldap-mapper", + "user.model.attribute", UserModel.FIRST_NAME, + "ldap.attribute", LDAPConstants.GIVENNAME)); + mappers.add(createMapperModel("fulln", "fullNameMapper", "full-name-ldap-mapper", + "ldap.full.name.attribute", LDAPConstants.CN)); + + mappers.add(createMapperModel("ln", "lastNameMapper", "user-attribute-ldap-mapper", "user.model.attribute", UserModel.LAST_NAME, "ldap.attribute", LDAPConstants.SN)); - mappers.add(createMapperModel("emailMpr", "emailMapper", "ldap-user-attribute-mapper", + + mappers.add(createMapperModel("emailMpr", "emailMapper", "user-attribute-ldap-mapper", "user.model.attribute", UserModel.EMAIL, "ldap.attribute", LDAPConstants.EMAIL)); - mappers.add(createMapperModel("postalCodeMpr", "postalCodeMapper", "ldap-user-attribute-mapper", + mappers.add(createMapperModel("postalCodeMpr", "postalCodeMapper", "user-attribute-ldap-mapper", "user.model.attribute", "postal_code", "ldap.attribute", LDAPConstants.POSTAL_CODE)); - mappers.add(createMapperModel("createdDateMpr", "createTimeStampMapper", "ldap-user-attribute-mapper", + mappers.add(createMapperModel("createdDateMpr", "createTimeStampMapper", "user-attribute-ldap-mapper", "user.model.attribute", LDAPConstants.CREATE_TIMESTAMP, "ldap.attribute", LDAPConstants.CREATE_TIMESTAMP, "read.only", "true")); - mappers.add(createMapperModel("modifyDateMpr", "modifyTimeStampMapper", "ldap-user-attribute-mapper", + mappers.add(createMapperModel("modifyDateMpr", "modifyTimeStampMapper", "user-attribute-ldap-mapper", "user.model.attribute", LDAPConstants.MODIFY_TIMESTAMP, "ldap.attribute", LDAPConstants.MODIFY_TIMESTAMP, "read.only", "true"));