KEYCLOAK-12842 Not possible to update user with multivalued LDAP RDN

This commit is contained in:
mposolda 2020-04-01 18:50:05 +02:00 committed by Stian Thorgersen
parent 9f3b847817
commit 38195ca789
12 changed files with 377 additions and 94 deletions

View file

@ -21,19 +21,21 @@ import javax.naming.ldap.Rdn;
import java.util.Collection; import java.util.Collection;
import java.util.Deque; import java.util.Deque;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */
public class LDAPDn { public class LDAPDn {
private final Deque<Entry> entries; private final Deque<RDN> entries;
private LDAPDn() { private LDAPDn() {
this.entries = new LinkedList<>(); this.entries = new LinkedList<>();
} }
private LDAPDn(Deque<Entry> entries) { private LDAPDn(Deque<RDN> entries) {
this.entries = entries; this.entries = entries;
} }
@ -49,17 +51,35 @@ public class LDAPDn {
String[] rdns = dnString.split("(?<!\\\\),"); String[] rdns = dnString.split("(?<!\\\\),");
for (String entryStr : rdns) { for (String entryStr : rdns) {
String[] rdn = entryStr.split("(?<!\\\\)="); if (entryStr.indexOf('+') == -1) {
if (rdn.length >1) { // This is 99.9% of cases where RDN consists of single key-value pair
dn.addLast(rdn[0].trim(), rdn[1].trim()); SubEntry subEntry = parseSingleSubEntry(dn, entryStr);
dn.addLast(new RDN(subEntry));
} else { } else {
dn.addLast(rdn[0].trim(), ""); // This is 0.1% of cases where RDN consists of more key-value pairs like "uid=foo+cn=bar"
String[] subEntries = entryStr.split("(?<!\\\\)\\+");
RDN entry = new RDN();
for (String subEntryStr : subEntries) {
SubEntry subEntry = parseSingleSubEntry(dn, subEntryStr);
entry.addSubEntry(subEntry);
}
dn.addLast(entry);
} }
} }
return dn; return dn;
} }
// parse single sub-entry and add it to the "dn" . Assumption is that subentry is something like "uid=bar" and does not contain + character
private static SubEntry parseSingleSubEntry(LDAPDn dn, String subEntryStr) {
String[] rdn = subEntryStr.split("(?<!\\\\)=");
if (rdn.length >1) {
return new SubEntry(rdn[0].trim(), rdn[1].trim());
} else {
return new SubEntry(rdn[0].trim(), "");
}
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (!(obj instanceof LDAPDn)) { if (!(obj instanceof LDAPDn)) {
@ -79,52 +99,39 @@ public class LDAPDn {
return toString(entries); return toString(entries);
} }
private static String toString(Collection<Entry> entries) { private static String toString(Collection<RDN> entries) {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
boolean first = true; boolean first = true;
for (Entry rdn : entries) { for (RDN rdn : entries) {
if (first) { if (first) {
first = false; first = false;
} else { } else {
builder.append(","); builder.append(",");
} }
builder.append(rdn.attrName).append("=").append(rdn.attrValue); builder.append(rdn.toString());
} }
return builder.toString(); return builder.toString();
} }
/** /**
* @return string like "uid=joe" from the DN like "uid=joe,dc=something,dc=org" * @return first entry. Usually entry corresponding to something like "uid=joe" from the DN like "uid=joe,dc=something,dc=org"
*/ */
public String getFirstRdn() { public RDN getFirstRdn() {
Entry firstEntry = entries.getFirst(); return entries.getFirst();
return firstEntry.attrName + "=" + unescapeValue(firstEntry.attrValue);
} }
/** private static String unescapeValue(String escaped) {
* @return string attribute name like "uid" from the DN like "uid=joe,dc=something,dc=org"
*/
public String getFirstRdnAttrName() {
Entry firstEntry = entries.getFirst();
return firstEntry.attrName;
}
/**
* @return string attribute value like "joe" from the DN like "uid=joe,dc=something,dc=org"
*/
public String getFirstRdnAttrValue() {
Entry firstEntry = entries.getFirst();
String dnEscaped = firstEntry.attrValue;
return unescapeValue(dnEscaped);
}
private String unescapeValue(String escaped) {
// Something needed to handle non-String types? // Something needed to handle non-String types?
return Rdn.unescapeValue(escaped).toString(); return Rdn.unescapeValue(escaped).toString();
} }
private static String escapeValue(String unescaped) {
// Something needed to handle non-String types?
return Rdn.escapeValue(unescaped);
}
/** /**
* *
* @return DN like "dc=something,dc=org" from the DN like "uid=joe,dc=something,dc=org". * @return DN like "dc=something,dc=org" from the DN like "uid=joe,dc=something,dc=org".
@ -132,7 +139,7 @@ public class LDAPDn {
* *
*/ */
public LDAPDn getParentDn() { public LDAPDn getParentDn() {
LinkedList<Entry> parentDnEntries = new LinkedList<>(entries); LinkedList<RDN> parentDnEntries = new LinkedList<>(entries);
parentDnEntries.remove(); parentDnEntries.remove();
return new LDAPDn(parentDnEntries); return new LDAPDn(parentDnEntries);
} }
@ -140,7 +147,7 @@ public class LDAPDn {
public boolean isDescendantOf(LDAPDn expectedParentDn) { public boolean isDescendantOf(LDAPDn expectedParentDn) {
int parentEntriesCount = expectedParentDn.entries.size(); int parentEntriesCount = expectedParentDn.entries.size();
Deque<Entry> myEntries = new LinkedList<>(this.entries); Deque<RDN> myEntries = new LinkedList<>(this.entries);
boolean someRemoved = false; boolean someRemoved = false;
while (myEntries.size() > parentEntriesCount) { while (myEntries.size() > parentEntriesCount) {
myEntries.removeFirst(); myEntries.removeFirst();
@ -153,21 +160,137 @@ public class LDAPDn {
} }
public void addFirst(String rdnName, String rdnValue) { public void addFirst(String rdnName, String rdnValue) {
rdnValue = Rdn.escapeValue(rdnValue); rdnValue = escapeValue(rdnValue);
entries.addFirst(new Entry(rdnName, rdnValue)); entries.addFirst(new RDN(new SubEntry(rdnName, rdnValue)));
} }
private void addLast(String rdnName, String rdnValue) { public void addFirst(RDN entry) {
entries.addLast(new Entry(rdnName, rdnValue)); entries.addFirst(entry);
} }
private static class Entry { private void addLast(RDN entry) {
entries.addLast(entry);
}
/**
* Single RDN inside the DN. RDN usually consists of single item like "uid=john" . In some rare cases, it can have multiple
* sub-entries like "uid=john+sn=Doe"
*/
public static class RDN {
private List<SubEntry> subs = new LinkedList<>();
private RDN() {
}
private RDN(SubEntry subEntry) {
subs.add(subEntry);
}
private void addSubEntry(SubEntry subEntry) {
subs.add(subEntry);
}
/**
* @return Keys in the RDN. Returned list is the copy, which is not linked to the original RDN
*/
public List<String> getAllKeys() {
return subs.stream().map(SubEntry::getAttrName).collect(Collectors.toList());
}
/**
* Assume that RDN is something like "uid=john", then this method will return "john" in case that attrName is "uid" .
* This is useful in case that RDN is multi-key - something like "uid=john+cn=John Doe" and we want to return just "john" as the value of "uid"
*
* The returned value will be unescaped
*
* @param attrName
* @return
*/
public String getAttrValue(String attrName) {
for (SubEntry sub : subs) {
if (attrName.equalsIgnoreCase(sub.attrName)) {
return LDAPDn.unescapeValue(sub.attrValue);
}
}
return null;
}
public void setAttrValue(String attrName, String newAttrValue) {
for (SubEntry sub : subs) {
if (attrName.equalsIgnoreCase(sub.attrName)) {
sub.attrValue = escapeValue(newAttrValue);
return;
}
}
addSubEntry(new SubEntry(attrName, escapeValue(newAttrValue)));
}
public boolean removeAttrValue(String attrName) {
SubEntry toRemove = null;
for (SubEntry sub : subs) {
if (attrName.equalsIgnoreCase(sub.attrName)) {
toRemove = sub;
continue;
}
}
if (toRemove != null) {
subs.remove(toRemove);
return true;
} else {
return false;
}
}
@Override
public String toString() {
return toString(true);
}
/**
*
* @param escaped indicates whether return escaped or unescaped values. EG. "uid=john,comma" VS "uid=john\,comma"
* @return
*/
public String toString(boolean escaped) {
StringBuilder builder = new StringBuilder();
boolean first = true;
for (SubEntry subEntry : subs) {
if (first) {
first = false;
} else {
builder.append('+');
}
builder.append(subEntry.toString(escaped));
}
return builder.toString();
}
}
private static class SubEntry {
private final String attrName; private final String attrName;
private final String attrValue; private String attrValue;
private Entry(String attrName, String attrValue) { private SubEntry(String attrName, String attrValue) {
this.attrName = attrName; this.attrName = attrName;
this.attrValue = attrValue; this.attrValue = attrValue;
} }
private String getAttrName() {
return attrName;
}
@Override
public String toString() {
return toString(true);
}
private String toString(boolean escaped) {
String val = escaped ? attrValue : unescapeValue(attrValue);
return attrName + '=' + val;
}
} }
} }

View file

@ -19,6 +19,7 @@ package org.keycloak.storage.ldap.idm.model;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@ -36,7 +37,9 @@ public class LDAPObject {
private String uuid; private String uuid;
private LDAPDn dn; private LDAPDn dn;
private String rdnAttributeName;
// In most cases, there is single "rdnAttributeName" . Usually "uid" or "cn"
private final List<String> rdnAttributeNames = new LinkedList<>();
private final List<String> objectClasses = new LinkedList<>(); private final List<String> objectClasses = new LinkedList<>();
@ -88,12 +91,25 @@ public class LDAPObject {
readOnlyAttributeNames.remove(readOnlyAttribute.toLowerCase()); readOnlyAttributeNames.remove(readOnlyAttribute.toLowerCase());
} }
public String getRdnAttributeName() { public List<String> getRdnAttributeNames() {
return rdnAttributeName; return rdnAttributeNames;
} }
/**
* Useful when single value will be used as the "RDN" attribute. Which will be most of the cases
*/
public void setRdnAttributeName(String rdnAttributeName) { public void setRdnAttributeName(String rdnAttributeName) {
this.rdnAttributeName = rdnAttributeName; this.rdnAttributeNames.clear();
this.rdnAttributeNames.add(rdnAttributeName);
}
public void setRdnAttributeNames(List<String> rdnAttributeNames) {
this.rdnAttributeNames.clear();
this.rdnAttributeNames.addAll(rdnAttributeNames);
}
public void addRdnAttributeName(String rdnAttributeName) {
this.rdnAttributeNames.add(rdnAttributeName);
} }
public void setSingleAttribute(String attributeName, String attributeValue) { public void setSingleAttribute(String attributeName, String attributeValue) {

View file

@ -57,6 +57,8 @@ import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.naming.directory.AttributeInUseException; import javax.naming.directory.AttributeInUseException;
import javax.naming.directory.NoSuchAttributeException; import javax.naming.directory.NoSuchAttributeException;
import javax.naming.directory.SchemaViolationException; import javax.naming.directory.SchemaViolationException;
@ -157,28 +159,68 @@ public class LDAPIdentityStore implements IdentityStore {
} }
protected void checkRename(LDAPObject ldapObject) { protected void checkRename(LDAPObject ldapObject) {
String rdnAttrName = ldapObject.getRdnAttributeName(); LDAPDn.RDN firstRdn = ldapObject.getDn().getFirstRdn();
if (ldapObject.getReadOnlyAttributeNames().contains(rdnAttrName.toLowerCase())) { String oldDn = ldapObject.getDn().toString();
return;
// Detect which keys will need to be updated in RDN, which are new keys to be added, and which are to be removed
List<String> toUpdateKeys = firstRdn.getAllKeys();
toUpdateKeys.retainAll(ldapObject.getRdnAttributeNames());
List<String> toRemoveKeys = firstRdn.getAllKeys();
toRemoveKeys.removeAll(ldapObject.getRdnAttributeNames());
List<String> toAddKeys = new ArrayList<>(ldapObject.getRdnAttributeNames());
toAddKeys.removeAll(firstRdn.getAllKeys());
// Go through all the keys in the oldRDN and doublecheck if they are changed or not
boolean changed = false;
for (String attrKey : toUpdateKeys) {
if (ldapObject.getReadOnlyAttributeNames().contains(attrKey.toLowerCase())) {
continue;
}
String rdnAttrVal = ldapObject.getAttributeAsString(attrKey);
// Could be the case when RDN attribute of the target object is not included in Keycloak mappers
if (rdnAttrVal == null) {
continue;
}
String oldRdnAttrVal = firstRdn.getAttrValue(attrKey);
if (!oldRdnAttrVal.equalsIgnoreCase(rdnAttrVal)) {
changed = true;
firstRdn.setAttrValue(attrKey, rdnAttrVal);
}
} }
String rdnAttrVal = ldapObject.getAttributeAsString(rdnAttrName); // Add new keys
for (String attrKey : toAddKeys) {
String rdnAttrVal = ldapObject.getAttributeAsString(attrKey);
// Could be the case when RDN attribute of the target object is not included in Keycloak mappers // Could be the case when RDN attribute of the target object is not included in Keycloak mappers
if (rdnAttrVal == null) { if (rdnAttrVal == null) {
return; continue;
}
changed = true;
firstRdn.setAttrValue(attrKey, rdnAttrVal);
} }
String oldRdnAttrVal = ldapObject.getDn().getFirstRdnAttrValue(); // Remove old keys
if (!oldRdnAttrVal.equals(rdnAttrVal)) { for (String attrKey : toRemoveKeys) {
changed |= firstRdn.removeAttrValue(attrKey);
}
if (changed) {
LDAPDn newLdapDn = ldapObject.getDn().getParentDn(); LDAPDn newLdapDn = ldapObject.getDn().getParentDn();
newLdapDn.addFirst(rdnAttrName, rdnAttrVal); newLdapDn.addFirst(firstRdn);
String oldDn = ldapObject.getDn().toString();
String newDn = newLdapDn.toString(); String newDn = newLdapDn.toString();
if (logger.isDebugEnabled()) { // TODO:mposolda
logger.debugf("Renaming LDAP Object. Old DN: [%s], New DN: [%s]", oldDn, newDn); if (logger.isInfoEnabled()) {
logger.infof("Renaming LDAP Object. Old DN: [%s], New DN: [%s]", oldDn, newDn);
} }
// In case, that there is conflict (For example already existing "CN=John Anthony"), the different DN is returned // In case, that there is conflict (For example already existing "CN=John Anthony"), the different DN is returned
@ -377,7 +419,7 @@ public class LDAPIdentityStore implements IdentityStore {
LDAPObject ldapObject = new LDAPObject(); LDAPObject ldapObject = new LDAPObject();
LDAPDn dn = LDAPDn.fromString(entryDN); LDAPDn dn = LDAPDn.fromString(entryDN);
ldapObject.setDn(dn); ldapObject.setDn(dn);
ldapObject.setRdnAttributeName(dn.getFirstRdnAttrName()); ldapObject.setRdnAttributeNames(dn.getFirstRdn().getAllKeys());
NamingEnumeration<? extends Attribute> ldapAttributes = attributes.getAll(); NamingEnumeration<? extends Attribute> ldapAttributes = attributes.getAll();
@ -455,6 +497,10 @@ public class LDAPIdentityStore implements IdentityStore {
protected BasicAttributes extractAttributesForSaving(LDAPObject ldapObject, boolean isCreate) { protected BasicAttributes extractAttributesForSaving(LDAPObject ldapObject, boolean isCreate) {
BasicAttributes entryAttributes = new BasicAttributes(); BasicAttributes entryAttributes = new BasicAttributes();
Set<String> rdnAttrNamesLowerCased = ldapObject.getRdnAttributeNames().stream()
.map(String::toLowerCase)
.collect(Collectors.toSet());
for (Map.Entry<String, Set<String>> attrEntry : ldapObject.getAttributes().entrySet()) { for (Map.Entry<String, Set<String>> attrEntry : ldapObject.getAttributes().entrySet()) {
String attrName = attrEntry.getKey(); String attrName = attrEntry.getKey();
Set<String> attrValue = attrEntry.getValue(); Set<String> attrValue = attrEntry.getValue();
@ -465,15 +511,16 @@ public class LDAPIdentityStore implements IdentityStore {
attrValue = Collections.emptySet(); attrValue = Collections.emptySet();
} }
String attrNameLowercased = attrName.toLowerCase();
if ( if (
// Ignore empty attributes on create (changetype: add) // Ignore empty attributes on create (changetype: add)
!(isCreate && attrValue.isEmpty()) && !(isCreate && attrValue.isEmpty()) &&
// Since we're extracting for saving, skip read-only attributes. ldapObject.getReadOnlyAttributeNames() are lower-cased // Since we're extracting for saving, skip read-only attributes. ldapObject.getReadOnlyAttributeNames() are lower-cased
!ldapObject.getReadOnlyAttributeNames().contains(attrName.toLowerCase()) && !ldapObject.getReadOnlyAttributeNames().contains(attrNameLowercased) &&
// Only extract RDN for create since it can't be changed on update // Only extract RDN for create since it can't be changed on update
(isCreate || !ldapObject.getRdnAttributeName().equalsIgnoreCase(attrName)) (isCreate || !rdnAttrNamesLowerCased.contains(attrNameLowercased))
) { ) {
if (getConfig().getBinaryAttributeNames().contains(attrName)) { if (getConfig().getBinaryAttributeNames().contains(attrName)) {
// Binary attribute // Binary attribute
@ -537,7 +584,7 @@ public class LDAPIdentityStore implements IdentityStore {
// we need this to retrieve the entry's identifier from the ldap server // we need this to retrieve the entry's identifier from the ldap server
String uuidAttrName = getConfig().getUuidLDAPAttributeName(); String uuidAttrName = getConfig().getUuidLDAPAttributeName();
String rdn = ldapObject.getDn().getFirstRdn(); String rdn = ldapObject.getDn().getFirstRdn().toString(false);
String filter = "(" + EscapeStrategy.DEFAULT.escape(rdn) + ")"; String filter = "(" + EscapeStrategy.DEFAULT.escape(rdn) + ")";
List<SearchResult> search = this.operationManager.search(ldapObject.getDn().toString(), filter, Arrays.asList(uuidAttrName), SearchControls.OBJECT_SCOPE); List<SearchResult> search = this.operationManager.search(ldapObject.getDn().toString(), filter, Arrays.asList(uuidAttrName), SearchControls.OBJECT_SCOPE);
Attribute id = search.get(0).getAttributes().get(getConfig().getUuidLDAPAttributeName()); Attribute id = search.get(0).getAttributes().get(getConfig().getUuidLDAPAttributeName());

View file

@ -45,18 +45,13 @@ import javax.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName; import javax.naming.ldap.LdapName;
import javax.naming.ldap.PagedResultsControl; import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl; import javax.naming.ldap.PagedResultsResponseControl;
import javax.naming.ldap.StartTlsRequest;
import javax.naming.ldap.StartTlsResponse; import javax.naming.ldap.StartTlsResponse;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set; import java.util.Set;
/** /**
@ -237,8 +232,9 @@ public class LDAPOperationManager {
private String findNextDNForFallback(String newDn, int counter) { private String findNextDNForFallback(String newDn, int counter) {
LDAPDn dn = LDAPDn.fromString(newDn); LDAPDn dn = LDAPDn.fromString(newDn);
String rdnAttrName = dn.getFirstRdnAttrName(); LDAPDn.RDN firstRdn = dn.getFirstRdn();
String rdnAttrVal = dn.getFirstRdnAttrValue(); String rdnAttrName = firstRdn.getAllKeys().get(0);
String rdnAttrVal = firstRdn.getAttrValue(rdnAttrName);
LDAPDn parentDn = dn.getParentDn(); LDAPDn parentDn = dn.getParentDn();
parentDn.addFirst(rdnAttrName, rdnAttrVal + counter); parentDn.addFirst(rdnAttrName, rdnAttrVal + counter);
return parentDn.toString(); return parentDn.toString();

View file

@ -93,20 +93,21 @@ public enum MembershipType {
LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig(); LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
if (ldapConfig.getUsernameLdapAttribute().equals(ldapConfig.getRdnLdapAttribute())) { if (ldapConfig.getUsernameLdapAttribute().equals(ldapConfig.getRdnLdapAttribute())) {
for (LDAPDn userDn : dns) { for (LDAPDn userDn : dns) {
String username = userDn.getFirstRdnAttrValue(); String username = userDn.getFirstRdn().getAttrValue(ldapConfig.getRdnLdapAttribute());
usernames.add(username); usernames.add(username);
} }
} else { } else {
LDAPQuery query = LDAPUtils.createQueryForUserSearch(ldapProvider, realm); LDAPQuery query = LDAPUtils.createQueryForUserSearch(ldapProvider, realm);
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder(); LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition[] orSubconditions = new Condition[dns.size()]; List<Condition> orSubconditions = new ArrayList<>();
int index = 0;
for (LDAPDn userDn : dns) { for (LDAPDn userDn : dns) {
Condition condition = conditionsBuilder.equal(userDn.getFirstRdnAttrName(), userDn.getFirstRdnAttrValue(), EscapeStrategy.DEFAULT); String firstRdnAttrValue = userDn.getFirstRdn().getAttrValue(ldapConfig.getRdnLdapAttribute());
orSubconditions[index] = condition; if (firstRdnAttrValue != null) {
index++; Condition condition = conditionsBuilder.equal(ldapConfig.getRdnLdapAttribute(), firstRdnAttrValue, EscapeStrategy.DEFAULT);
orSubconditions.add(condition);
}
} }
Condition orCondition = conditionsBuilder.orCondition(orSubconditions); Condition orCondition = conditionsBuilder.orCondition(orSubconditions.toArray(new Condition[] {}));
query.addWhereCondition(orCondition); query.addWhereCondition(orCondition);
List<LDAPObject> ldapUsers = query.getResultList(); List<LDAPObject> ldapUsers = query.getResultList();
for (LDAPObject ldapUser : ldapUsers) { for (LDAPObject ldapUser : ldapUsers) {

View file

@ -99,10 +99,12 @@ public interface UserRolesRetrieveStrategy {
LDAPObject role = new LDAPObject(); LDAPObject role = new LDAPObject();
role.setDn(roleDN); role.setDn(roleDN);
String firstDN = roleDN.getFirstRdnAttrName(); LDAPDn.RDN firstRDN = roleDN.getFirstRdn();
if (firstDN.equalsIgnoreCase(roleOrGroupMapper.getConfig().getLDAPGroupNameLdapAttribute())) { String attrKey = roleOrGroupMapper.getConfig().getLDAPGroupNameLdapAttribute();
role.setRdnAttributeName(firstDN); String attrVal = firstRDN.getAttrValue(attrKey);
role.setSingleAttribute(firstDN, roleDN.getFirstRdnAttrValue()); if (attrVal != null) {
role.setRdnAttributeName(attrKey);
role.setSingleAttribute(attrKey, attrVal);
roles.add(role); roles.add(role);
} }
} }

View file

@ -238,7 +238,8 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
if (config.isPreserveGroupsInheritance()) { if (config.isPreserveGroupsInheritance()) {
Set<String> subgroupNames = new HashSet<>(); Set<String> subgroupNames = new HashSet<>();
for (LDAPDn groupDn : getLDAPSubgroups(ldapGroup)) { for (LDAPDn groupDn : getLDAPSubgroups(ldapGroup)) {
subgroupNames.add(groupDn.getFirstRdnAttrValue()); String subGroupName = groupDn.getFirstRdn().getAttrValue(groupsRdnAttr);
subgroupNames.add(subGroupName);
} }
ldapGroupsRep.add(new GroupTreeResolver.Group(groupName, subgroupNames)); ldapGroupsRep.add(new GroupTreeResolver.Group(groupName, subgroupNames));

View file

@ -17,6 +17,8 @@
package org.keycloak.storage.ldap.idm.model; package org.keycloak.storage.ldap.idm.model;
import java.util.List;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
@ -43,8 +45,10 @@ public class LDAPDnTest {
Assert.assertFalse(dn.isDescendantOf(LDAPDn.fromString("dc=keycloakk, dc=org"))); Assert.assertFalse(dn.isDescendantOf(LDAPDn.fromString("dc=keycloakk, dc=org")));
Assert.assertFalse(dn.isDescendantOf(dn)); Assert.assertFalse(dn.isDescendantOf(dn));
Assert.assertEquals("uid", dn.getFirstRdnAttrName()); Assert.assertEquals("uid", dn.getFirstRdn().getAllKeys().get(0));
Assert.assertEquals("Johny,Depp+Pepp\\Foo", dn.getFirstRdnAttrValue()); Assert.assertEquals("uid=Johny\\,Depp\\+Pepp\\\\Foo", dn.getFirstRdn().toString());
Assert.assertEquals("uid=Johny,Depp+Pepp\\Foo", dn.getFirstRdn().toString(false));
Assert.assertEquals("Johny,Depp+Pepp\\Foo", dn.getFirstRdn().getAttrValue("uid"));
} }
@Test @Test
@ -52,8 +56,8 @@ public class LDAPDnTest {
LDAPDn dn = LDAPDn.fromString("dc=keycloak, dc=org"); LDAPDn dn = LDAPDn.fromString("dc=keycloak, dc=org");
dn.addFirst("ou", ""); dn.addFirst("ou", "");
Assert.assertEquals("ou", dn.getFirstRdnAttrName()); Assert.assertEquals("ou", dn.getFirstRdn().getAllKeys().get(0));
Assert.assertEquals("", dn.getFirstRdnAttrValue()); Assert.assertEquals("", dn.getFirstRdn().getAttrValue("ou"));
Assert.assertEquals("ou=,dc=keycloak,dc=org", dn.toString()); Assert.assertEquals("ou=,dc=keycloak,dc=org", dn.toString());
@ -71,16 +75,36 @@ public class LDAPDnTest {
LDAPDn dn = LDAPDn.fromString("dc=keycloak, dc=org"); LDAPDn dn = LDAPDn.fromString("dc=keycloak, dc=org");
dn.addFirst("cn", "Johny,Džýa Foo"); dn.addFirst("cn", "Johny,Džýa Foo");
Assert.assertEquals("cn=Johny\\,Džýa Foo,dc=keycloak,dc=org", dn.toString()); Assert.assertEquals("cn=Johny\\,Džýa Foo,dc=keycloak,dc=org", dn.toString());
Assert.assertEquals("Johny,Džýa Foo", dn.getFirstRdnAttrValue()); Assert.assertEquals("Johny,Džýa Foo", dn.getFirstRdn().getAttrValue("cn"));
dn = LDAPDn.fromString("dc=keycloak, dc=org"); dn = LDAPDn.fromString("dc=keycloak, dc=org");
dn.addFirst("cn", "Johny,Džýa Foo "); dn.addFirst("cn", "Johny,Džýa Foo ");
Assert.assertEquals("cn=Johny\\,Džýa Foo\\ ,dc=keycloak,dc=org", dn.toString()); Assert.assertEquals("cn=Johny\\,Džýa Foo\\ ,dc=keycloak,dc=org", dn.toString());
Assert.assertEquals("Johny,Džýa Foo ", dn.getFirstRdnAttrValue()); Assert.assertEquals("Johny,Džýa Foo ", dn.getFirstRdn().getAttrValue("cn"));
dn = LDAPDn.fromString("dc=keycloak, dc=org"); dn = LDAPDn.fromString("dc=keycloak, dc=org");
dn.addFirst("cn", "Johny,Džýa "); dn.addFirst("cn", "Johny,Džýa ");
Assert.assertEquals("cn=Johny\\,Džýa\\ ,dc=keycloak,dc=org", dn.toString()); Assert.assertEquals("cn=Johny\\,Džýa\\ ,dc=keycloak,dc=org", dn.toString());
Assert.assertEquals("Johny,Džýa ", dn.getFirstRdnAttrValue()); Assert.assertEquals("Johny,Džýa ", dn.getFirstRdn().getAttrValue("cn"));
}
@Test
public void testDNWithMultivaluedRDN() throws Exception {
LDAPDn dn = LDAPDn.fromString("uid=john+cn=John Do\\+eř,dc=keycloak+ou=foo, dc=org");
Assert.assertEquals("uid=john+cn=John Do\\+eř", dn.getFirstRdn().toString());
List<String> keys = dn.getFirstRdn().getAllKeys();
Assert.assertEquals("uid", keys.get(0));
Assert.assertEquals("cn", keys.get(1));
Assert.assertEquals("john", dn.getFirstRdn().getAttrValue("UiD"));
Assert.assertEquals("John Do+eř", dn.getFirstRdn().getAttrValue("CN"));
Assert.assertEquals("dc=keycloak+ou=foo,dc=org", dn.getParentDn().toString());
dn.getFirstRdn().setAttrValue("UID", "john2");
Assert.assertEquals("uid=john2+cn=John Do\\+eř", dn.getFirstRdn().toString());
dn.getFirstRdn().setAttrValue("some", "somet+hing");
Assert.assertEquals("uid=john2+cn=John Do\\+eř+some=somet\\+hing", dn.getFirstRdn().toString());
} }
} }

View file

@ -490,7 +490,7 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
// 3 - Add non-existing user to LDAP group // 3 - Add non-existing user to LDAP group
LDAPDn nonExistentDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn()); LDAPDn nonExistentDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn());
nonExistentDn.addFirst(jamesLdap.getRdnAttributeName(), "nonexistent"); nonExistentDn.addFirst(jamesLdap.getRdnAttributeNames().get(0), "nonexistent");
LDAPObject nonExistentLdapUser = new LDAPObject(); LDAPObject nonExistentLdapUser = new LDAPObject();
nonExistentLdapUser.setDn(nonExistentDn); nonExistentLdapUser.setDn(nonExistentDn);
LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group2, nonExistentLdapUser); LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group2, nonExistentLdapUser);

View file

@ -22,12 +22,15 @@ import org.junit.ClassRule;
import org.junit.FixMethodOrder; import org.junit.FixMethodOrder;
import org.junit.Test; import org.junit.Test;
import org.junit.runners.MethodSorters; import org.junit.runners.MethodSorters;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentModel;
import org.keycloak.models.LDAPConstants; import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.model.LDAPObject; import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper; import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import org.keycloak.testsuite.util.LDAPRule; import org.keycloak.testsuite.util.LDAPRule;
@ -36,6 +39,8 @@ import org.keycloak.testsuite.util.LDAPTestUtils;
import java.util.List; import java.util.List;
import static org.hamcrest.Matchers.equalToIgnoringCase;
/** /**
* Test for special scenarios, which don't work on MSAD (eg. renaming user RDN to "sn=john2" ) * Test for special scenarios, which don't work on MSAD (eg. renaming user RDN to "sn=john2" )
* *
@ -94,13 +99,16 @@ public class LDAPNoMSADTest extends AbstractLDAPTest {
RealmModel appRealm = ctx.getRealm(); RealmModel appRealm = ctx.getRealm();
ComponentModel snMapper = null; ComponentModel snMapper = null;
// Create LDAP user with "sn" attribute in RDN like "sn=johnkeycloak2,ou=People,dc=domain,dc=com" // Create LDAP user with "sn" attribute in RDN like "sn=Doe2,ou=People,dc=domain,dc=com"
LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ctx.getLdapModel()); LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ctx.getLdapModel());
LDAPObject john2 = LDAPTestUtils.addLDAPUser(ldapProvider, appRealm, "johnkeycloak2", "john2", "Doe2", "john2@email.org", null, "4321"); LDAPObject john2 = LDAPTestUtils.addLDAPUser(ldapProvider, appRealm, "johnkeycloak2", "John2", "Doe2", "john2@email.org", null, "4321");
john2.setRdnAttributeName("sn"); john2.setRdnAttributeName("sn");
ldapProvider.getLdapIdentityStore().update(john2); ldapProvider.getLdapIdentityStore().update(john2);
// Assert DN was changed
Assert.assertEquals("sn=Doe2", john2.getDn().getFirstRdn().toString());
// Remove "sn" mapper // Remove "sn" mapper
List<ComponentModel> components = appRealm.getComponents(ctx.getLdapModel().getId(), LDAPStorageMapper.class.getName()); List<ComponentModel> components = appRealm.getComponents(ctx.getLdapModel().getId(), LDAPStorageMapper.class.getName());
for (ComponentModel mapper : components) { for (ComponentModel mapper : components) {
@ -132,4 +140,66 @@ public class LDAPNoMSADTest extends AbstractLDAPTest {
testRealm().components().add(snMapperRep); testRealm().components().add(snMapperRep);
} }
// KEYCLOAK-12842
@Test
public void testMultivaluedRDN() {
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
ComponentModel snMapper = null;
// Create LDAP user with both "uid" and "sn" attribute in RDN. Something like "uid=johnkeycloak3+sn=Doe3,ou=People,dc=domain,dc=com"
LDAPStorageProvider ldapProvider = LDAPTestUtils.getLdapProvider(session, ctx.getLdapModel());
LDAPObject john2 = LDAPTestUtils.addLDAPUser(ldapProvider, appRealm, "johnkeycloak3", "John3", "Doe3", "john3@email.org", null, "4321");
john2.addRdnAttributeName("sn");
ldapProvider.getLdapIdentityStore().update(john2);
// Assert DN was changed
String rdnAttrName = ldapProvider.getLdapIdentityStore().getConfig().getRdnLdapAttribute();
Assert.assertEquals(rdnAttrName + "=johnkeycloak3+sn=Doe3", john2.getDn().getFirstRdn().toString());
});
// Update some user attributes not mapped to DN. DN won't be changed
String userId = testRealm().users().search("johnkeycloak3").get(0).getId();
UserResource user = testRealm().users().get(userId);
UserRepresentation userRep = user.toRepresentation();
assertFirstRDNEndsWith(userRep, "johnkeycloak3", "Doe3");
userRep.setEmail("newemail@email.cz");
user.update(userRep);
userRep = user.toRepresentation();
Assert.assertEquals("newemail@email.cz", userRep.getEmail());
assertFirstRDNEndsWith(userRep, "johnkeycloak3", "Doe3");
// Update some user attributes mapped to DN. DN will be changed
userRep.setLastName("Doe3Changed");
user.update(userRep);
userRep = user.toRepresentation();
// ApacheDS bug causes that attribute, which was added to DN, is lowercased. Works for other LDAPs (RHDS, OpenLDAP)
Assert.assertThat("Doe3Changed", equalToIgnoringCase(userRep.getLastName()));
assertFirstRDNEndsWith(userRep, "johnkeycloak3", "Doe3Changed");
// Remove user
user.remove();
}
private void assertFirstRDNEndsWith(UserRepresentation user, String expectedUsernameInDN, String expectedLastNameInDN) {
String currentDN = user.getAttributes().get(LDAPConstants.LDAP_ENTRY_DN).get(0);
LDAPDn.RDN firstRDN = LDAPDn.fromString(currentDN).getFirstRdn();
// Order is not guaranteed and can be dependent on LDAP server, so can't test simple string
List<String> rdnKeys = firstRDN.getAllKeys();
Assert.assertEquals(2, rdnKeys.size());
Assert.assertEquals(expectedLastNameInDN, firstRDN.getAttrValue("sn"));
rdnKeys.remove("sn");
Assert.assertEquals(expectedUsernameInDN, firstRDN.getAttrValue(rdnKeys.get(0)));
}
} }

View file

@ -420,7 +420,7 @@ public class LDAPSyncTest extends AbstractLDAPTest {
LDAPObject group1Loaded = groupMapper.loadLDAPGroupByName("group1"); LDAPObject group1Loaded = groupMapper.loadLDAPGroupByName("group1");
// update group name and description // update group name and description
group1Loaded.setSingleAttribute(group1Loaded.getRdnAttributeName(), "group5"); group1Loaded.setSingleAttribute(group1Loaded.getRdnAttributeNames().get(0), "group5");
group1Loaded.setSingleAttribute(descriptionAttrName, "group5 - description"); group1Loaded.setSingleAttribute(descriptionAttrName, "group5 - description");
LDAPTestUtils.updateLDAPGroup(session, appRealm, ctx.getLdapModel(), group1Loaded); LDAPTestUtils.updateLDAPGroup(session, appRealm, ctx.getLdapModel(), group1Loaded);

View file

@ -25,6 +25,7 @@ import org.junit.runners.MethodSorters;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.model.LDAPObject; import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.testsuite.runonserver.RunOnServerException; import org.keycloak.testsuite.runonserver.RunOnServerException;
import org.keycloak.testsuite.util.LDAPRule; import org.keycloak.testsuite.util.LDAPRule;
@ -77,7 +78,8 @@ public class LdapUsernameAttributeTest extends AbstractLDAPTest {
Assert.assertEquals("johndow", john.getLastName()); Assert.assertEquals("johndow", john.getLastName());
LDAPObject johnLdap = ctx.getLdapProvider().loadLDAPUserByUsername(appRealm, "johndow"); LDAPObject johnLdap = ctx.getLdapProvider().loadLDAPUserByUsername(appRealm, "johndow");
Assert.assertNotNull(johnLdap); Assert.assertNotNull(johnLdap);
Assert.assertEquals("johndow", johnLdap.getDn().getFirstRdnAttrValue()); LDAPDn.RDN firstRdnEntry = johnLdap.getDn().getFirstRdn();
Assert.assertEquals("johndow", firstRdnEntry.getAttrValue(firstRdnEntry.getAllKeys().get(0)));
}); });
// rename to johndow2 // rename to johndow2
testingClient.server().run(session -> { testingClient.server().run(session -> {
@ -103,7 +105,8 @@ public class LdapUsernameAttributeTest extends AbstractLDAPTest {
Assert.assertEquals("johndow2", john2.getLastName()); Assert.assertEquals("johndow2", john2.getLastName());
LDAPObject johnLdap2 = ctx.getLdapProvider().loadLDAPUserByUsername(appRealm, "johndow2"); LDAPObject johnLdap2 = ctx.getLdapProvider().loadLDAPUserByUsername(appRealm, "johndow2");
Assert.assertNotNull(johnLdap2); Assert.assertNotNull(johnLdap2);
Assert.assertEquals("johndow2", johnLdap2.getDn().getFirstRdnAttrValue()); LDAPDn.RDN firstRdnEntry = johnLdap2.getDn().getFirstRdn();
Assert.assertEquals("johndow2", firstRdnEntry.getAttrValue(firstRdnEntry.getAllKeys().get(0)));
session.users().removeUser(appRealm, john2); session.users().removeUser(appRealm, john2);
Assert.assertNull(session.users().getUserByUsername("johndow2", appRealm)); Assert.assertNull(session.users().getUserByUsername("johndow2", appRealm));