KEYCLOAK-12842 Not possible to update user with multivalued LDAP RDN
This commit is contained in:
parent
9f3b847817
commit
38195ca789
12 changed files with 377 additions and 94 deletions
|
@ -21,19 +21,21 @@ import javax.naming.ldap.Rdn;
|
|||
import java.util.Collection;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class LDAPDn {
|
||||
|
||||
private final Deque<Entry> entries;
|
||||
private final Deque<RDN> entries;
|
||||
|
||||
private LDAPDn() {
|
||||
this.entries = new LinkedList<>();
|
||||
}
|
||||
|
||||
private LDAPDn(Deque<Entry> entries) {
|
||||
private LDAPDn(Deque<RDN> entries) {
|
||||
this.entries = entries;
|
||||
}
|
||||
|
||||
|
@ -46,20 +48,38 @@ public class LDAPDn {
|
|||
// Keycloak must be able to process it, properly, w/o throwing an ArrayIndexOutOfBoundsException
|
||||
if(dnString.trim().isEmpty())
|
||||
return dn;
|
||||
|
||||
|
||||
String[] rdns = dnString.split("(?<!\\\\),");
|
||||
for (String entryStr : rdns) {
|
||||
String[] rdn = entryStr.split("(?<!\\\\)=");
|
||||
if (rdn.length >1) {
|
||||
dn.addLast(rdn[0].trim(), rdn[1].trim());
|
||||
if (entryStr.indexOf('+') == -1) {
|
||||
// This is 99.9% of cases where RDN consists of single key-value pair
|
||||
SubEntry subEntry = parseSingleSubEntry(dn, entryStr);
|
||||
dn.addLast(new RDN(subEntry));
|
||||
} 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;
|
||||
}
|
||||
|
||||
// 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
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof LDAPDn)) {
|
||||
|
@ -79,52 +99,39 @@ public class LDAPDn {
|
|||
return toString(entries);
|
||||
}
|
||||
|
||||
private static String toString(Collection<Entry> entries) {
|
||||
private static String toString(Collection<RDN> entries) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
boolean first = true;
|
||||
for (Entry rdn : entries) {
|
||||
for (RDN rdn : entries) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
builder.append(",");
|
||||
}
|
||||
builder.append(rdn.attrName).append("=").append(rdn.attrValue);
|
||||
builder.append(rdn.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() {
|
||||
Entry firstEntry = entries.getFirst();
|
||||
return firstEntry.attrName + "=" + unescapeValue(firstEntry.attrValue);
|
||||
public RDN getFirstRdn() {
|
||||
return entries.getFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
private static String unescapeValue(String escaped) {
|
||||
// Something needed to handle non-String types?
|
||||
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".
|
||||
|
@ -132,7 +139,7 @@ public class LDAPDn {
|
|||
*
|
||||
*/
|
||||
public LDAPDn getParentDn() {
|
||||
LinkedList<Entry> parentDnEntries = new LinkedList<>(entries);
|
||||
LinkedList<RDN> parentDnEntries = new LinkedList<>(entries);
|
||||
parentDnEntries.remove();
|
||||
return new LDAPDn(parentDnEntries);
|
||||
}
|
||||
|
@ -140,7 +147,7 @@ public class LDAPDn {
|
|||
public boolean isDescendantOf(LDAPDn expectedParentDn) {
|
||||
int parentEntriesCount = expectedParentDn.entries.size();
|
||||
|
||||
Deque<Entry> myEntries = new LinkedList<>(this.entries);
|
||||
Deque<RDN> myEntries = new LinkedList<>(this.entries);
|
||||
boolean someRemoved = false;
|
||||
while (myEntries.size() > parentEntriesCount) {
|
||||
myEntries.removeFirst();
|
||||
|
@ -153,21 +160,137 @@ public class LDAPDn {
|
|||
}
|
||||
|
||||
public void addFirst(String rdnName, String rdnValue) {
|
||||
rdnValue = Rdn.escapeValue(rdnValue);
|
||||
entries.addFirst(new Entry(rdnName, rdnValue));
|
||||
rdnValue = escapeValue(rdnValue);
|
||||
entries.addFirst(new RDN(new SubEntry(rdnName, rdnValue)));
|
||||
}
|
||||
|
||||
private void addLast(String rdnName, String rdnValue) {
|
||||
entries.addLast(new Entry(rdnName, rdnValue));
|
||||
public void addFirst(RDN entry) {
|
||||
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 attrValue;
|
||||
private String attrValue;
|
||||
|
||||
private Entry(String attrName, String attrValue) {
|
||||
private SubEntry(String attrName, String attrValue) {
|
||||
this.attrName = attrName;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.storage.ldap.idm.model;
|
|||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
|
@ -36,7 +37,9 @@ public class LDAPObject {
|
|||
|
||||
private String uuid;
|
||||
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<>();
|
||||
|
||||
|
@ -88,12 +91,25 @@ public class LDAPObject {
|
|||
readOnlyAttributeNames.remove(readOnlyAttribute.toLowerCase());
|
||||
}
|
||||
|
||||
public String getRdnAttributeName() {
|
||||
return rdnAttributeName;
|
||||
public List<String> getRdnAttributeNames() {
|
||||
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) {
|
||||
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) {
|
||||
|
|
|
@ -57,6 +57,8 @@ import java.util.Set;
|
|||
import java.util.TreeSet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.naming.directory.AttributeInUseException;
|
||||
import javax.naming.directory.NoSuchAttributeException;
|
||||
import javax.naming.directory.SchemaViolationException;
|
||||
|
@ -157,28 +159,68 @@ public class LDAPIdentityStore implements IdentityStore {
|
|||
}
|
||||
|
||||
protected void checkRename(LDAPObject ldapObject) {
|
||||
String rdnAttrName = ldapObject.getRdnAttributeName();
|
||||
if (ldapObject.getReadOnlyAttributeNames().contains(rdnAttrName.toLowerCase())) {
|
||||
return;
|
||||
LDAPDn.RDN firstRdn = ldapObject.getDn().getFirstRdn();
|
||||
String oldDn = ldapObject.getDn().toString();
|
||||
|
||||
// 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
|
||||
if (rdnAttrVal == null) {
|
||||
return;
|
||||
// Could be the case when RDN attribute of the target object is not included in Keycloak mappers
|
||||
if (rdnAttrVal == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
changed = true;
|
||||
firstRdn.setAttrValue(attrKey, rdnAttrVal);
|
||||
}
|
||||
|
||||
String oldRdnAttrVal = ldapObject.getDn().getFirstRdnAttrValue();
|
||||
if (!oldRdnAttrVal.equals(rdnAttrVal)) {
|
||||
// Remove old keys
|
||||
for (String attrKey : toRemoveKeys) {
|
||||
changed |= firstRdn.removeAttrValue(attrKey);
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
LDAPDn newLdapDn = ldapObject.getDn().getParentDn();
|
||||
newLdapDn.addFirst(rdnAttrName, rdnAttrVal);
|
||||
newLdapDn.addFirst(firstRdn);
|
||||
|
||||
String oldDn = ldapObject.getDn().toString();
|
||||
String newDn = newLdapDn.toString();
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debugf("Renaming LDAP Object. Old DN: [%s], New DN: [%s]", oldDn, newDn);
|
||||
// TODO:mposolda
|
||||
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
|
||||
|
@ -377,7 +419,7 @@ public class LDAPIdentityStore implements IdentityStore {
|
|||
LDAPObject ldapObject = new LDAPObject();
|
||||
LDAPDn dn = LDAPDn.fromString(entryDN);
|
||||
ldapObject.setDn(dn);
|
||||
ldapObject.setRdnAttributeName(dn.getFirstRdnAttrName());
|
||||
ldapObject.setRdnAttributeNames(dn.getFirstRdn().getAllKeys());
|
||||
|
||||
NamingEnumeration<? extends Attribute> ldapAttributes = attributes.getAll();
|
||||
|
||||
|
@ -455,6 +497,10 @@ public class LDAPIdentityStore implements IdentityStore {
|
|||
protected BasicAttributes extractAttributesForSaving(LDAPObject ldapObject, boolean isCreate) {
|
||||
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()) {
|
||||
String attrName = attrEntry.getKey();
|
||||
Set<String> attrValue = attrEntry.getValue();
|
||||
|
@ -465,15 +511,16 @@ public class LDAPIdentityStore implements IdentityStore {
|
|||
attrValue = Collections.emptySet();
|
||||
}
|
||||
|
||||
String attrNameLowercased = attrName.toLowerCase();
|
||||
if (
|
||||
// Ignore empty attributes on create (changetype: add)
|
||||
!(isCreate && attrValue.isEmpty()) &&
|
||||
|
||||
// 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
|
||||
(isCreate || !ldapObject.getRdnAttributeName().equalsIgnoreCase(attrName))
|
||||
(isCreate || !rdnAttrNamesLowerCased.contains(attrNameLowercased))
|
||||
) {
|
||||
if (getConfig().getBinaryAttributeNames().contains(attrName)) {
|
||||
// Binary attribute
|
||||
|
@ -537,7 +584,7 @@ public class LDAPIdentityStore implements IdentityStore {
|
|||
// we need this to retrieve the entry's identifier from the ldap server
|
||||
String uuidAttrName = getConfig().getUuidLDAPAttributeName();
|
||||
|
||||
String rdn = ldapObject.getDn().getFirstRdn();
|
||||
String rdn = ldapObject.getDn().getFirstRdn().toString(false);
|
||||
String filter = "(" + EscapeStrategy.DEFAULT.escape(rdn) + ")";
|
||||
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());
|
||||
|
|
|
@ -45,18 +45,13 @@ import javax.naming.ldap.LdapContext;
|
|||
import javax.naming.ldap.LdapName;
|
||||
import javax.naming.ldap.PagedResultsControl;
|
||||
import javax.naming.ldap.PagedResultsResponseControl;
|
||||
import javax.naming.ldap.StartTlsRequest;
|
||||
import javax.naming.ldap.StartTlsResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
|
@ -237,8 +232,9 @@ public class LDAPOperationManager {
|
|||
|
||||
private String findNextDNForFallback(String newDn, int counter) {
|
||||
LDAPDn dn = LDAPDn.fromString(newDn);
|
||||
String rdnAttrName = dn.getFirstRdnAttrName();
|
||||
String rdnAttrVal = dn.getFirstRdnAttrValue();
|
||||
LDAPDn.RDN firstRdn = dn.getFirstRdn();
|
||||
String rdnAttrName = firstRdn.getAllKeys().get(0);
|
||||
String rdnAttrVal = firstRdn.getAttrValue(rdnAttrName);
|
||||
LDAPDn parentDn = dn.getParentDn();
|
||||
parentDn.addFirst(rdnAttrName, rdnAttrVal + counter);
|
||||
return parentDn.toString();
|
||||
|
|
|
@ -93,20 +93,21 @@ public enum MembershipType {
|
|||
LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
|
||||
if (ldapConfig.getUsernameLdapAttribute().equals(ldapConfig.getRdnLdapAttribute())) {
|
||||
for (LDAPDn userDn : dns) {
|
||||
String username = userDn.getFirstRdnAttrValue();
|
||||
String username = userDn.getFirstRdn().getAttrValue(ldapConfig.getRdnLdapAttribute());
|
||||
usernames.add(username);
|
||||
}
|
||||
} else {
|
||||
LDAPQuery query = LDAPUtils.createQueryForUserSearch(ldapProvider, realm);
|
||||
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
|
||||
Condition[] orSubconditions = new Condition[dns.size()];
|
||||
int index = 0;
|
||||
List<Condition> orSubconditions = new ArrayList<>();
|
||||
for (LDAPDn userDn : dns) {
|
||||
Condition condition = conditionsBuilder.equal(userDn.getFirstRdnAttrName(), userDn.getFirstRdnAttrValue(), EscapeStrategy.DEFAULT);
|
||||
orSubconditions[index] = condition;
|
||||
index++;
|
||||
String firstRdnAttrValue = userDn.getFirstRdn().getAttrValue(ldapConfig.getRdnLdapAttribute());
|
||||
if (firstRdnAttrValue != null) {
|
||||
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);
|
||||
List<LDAPObject> ldapUsers = query.getResultList();
|
||||
for (LDAPObject ldapUser : ldapUsers) {
|
||||
|
|
|
@ -99,10 +99,12 @@ public interface UserRolesRetrieveStrategy {
|
|||
LDAPObject role = new LDAPObject();
|
||||
role.setDn(roleDN);
|
||||
|
||||
String firstDN = roleDN.getFirstRdnAttrName();
|
||||
if (firstDN.equalsIgnoreCase(roleOrGroupMapper.getConfig().getLDAPGroupNameLdapAttribute())) {
|
||||
role.setRdnAttributeName(firstDN);
|
||||
role.setSingleAttribute(firstDN, roleDN.getFirstRdnAttrValue());
|
||||
LDAPDn.RDN firstRDN = roleDN.getFirstRdn();
|
||||
String attrKey = roleOrGroupMapper.getConfig().getLDAPGroupNameLdapAttribute();
|
||||
String attrVal = firstRDN.getAttrValue(attrKey);
|
||||
if (attrVal != null) {
|
||||
role.setRdnAttributeName(attrKey);
|
||||
role.setSingleAttribute(attrKey, attrVal);
|
||||
roles.add(role);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -238,7 +238,8 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
|
|||
if (config.isPreserveGroupsInheritance()) {
|
||||
Set<String> subgroupNames = new HashSet<>();
|
||||
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));
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
package org.keycloak.storage.ldap.idm.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Assert;
|
||||
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(dn));
|
||||
|
||||
Assert.assertEquals("uid", dn.getFirstRdnAttrName());
|
||||
Assert.assertEquals("Johny,Depp+Pepp\\Foo", dn.getFirstRdnAttrValue());
|
||||
Assert.assertEquals("uid", dn.getFirstRdn().getAllKeys().get(0));
|
||||
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
|
||||
|
@ -52,8 +56,8 @@ public class LDAPDnTest {
|
|||
LDAPDn dn = LDAPDn.fromString("dc=keycloak, dc=org");
|
||||
dn.addFirst("ou", "");
|
||||
|
||||
Assert.assertEquals("ou", dn.getFirstRdnAttrName());
|
||||
Assert.assertEquals("", dn.getFirstRdnAttrValue());
|
||||
Assert.assertEquals("ou", dn.getFirstRdn().getAllKeys().get(0));
|
||||
Assert.assertEquals("", dn.getFirstRdn().getAttrValue("ou"));
|
||||
|
||||
Assert.assertEquals("ou=,dc=keycloak,dc=org", dn.toString());
|
||||
|
||||
|
@ -71,16 +75,36 @@ public class LDAPDnTest {
|
|||
LDAPDn dn = LDAPDn.fromString("dc=keycloak, dc=org");
|
||||
dn.addFirst("cn", "Johny,Džýa Foo");
|
||||
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.addFirst("cn", "Johny,Džýa Foo ");
|
||||
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.addFirst("cn", "Johny,Džýa ");
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -490,7 +490,7 @@ public class LDAPGroupMapperTest extends AbstractLDAPTest {
|
|||
|
||||
// 3 - Add non-existing user to LDAP group
|
||||
LDAPDn nonExistentDn = LDAPDn.fromString(ldapProvider.getLdapIdentityStore().getConfig().getUsersDn());
|
||||
nonExistentDn.addFirst(jamesLdap.getRdnAttributeName(), "nonexistent");
|
||||
nonExistentDn.addFirst(jamesLdap.getRdnAttributeNames().get(0), "nonexistent");
|
||||
LDAPObject nonExistentLdapUser = new LDAPObject();
|
||||
nonExistentLdapUser.setDn(nonExistentDn);
|
||||
LDAPUtils.addMember(ldapProvider, MembershipType.DN, LDAPConstants.MEMBER, "not-used", group2, nonExistentLdapUser);
|
||||
|
|
|
@ -22,12 +22,15 @@ import org.junit.ClassRule;
|
|||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Test;
|
||||
import org.junit.runners.MethodSorters;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
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.mappers.LDAPStorageMapper;
|
||||
import org.keycloak.testsuite.util.LDAPRule;
|
||||
|
@ -36,6 +39,8 @@ import org.keycloak.testsuite.util.LDAPTestUtils;
|
|||
|
||||
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" )
|
||||
*
|
||||
|
@ -94,13 +99,16 @@ public class LDAPNoMSADTest extends AbstractLDAPTest {
|
|||
RealmModel appRealm = ctx.getRealm();
|
||||
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());
|
||||
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");
|
||||
ldapProvider.getLdapIdentityStore().update(john2);
|
||||
|
||||
// Assert DN was changed
|
||||
Assert.assertEquals("sn=Doe2", john2.getDn().getFirstRdn().toString());
|
||||
|
||||
// Remove "sn" mapper
|
||||
List<ComponentModel> components = appRealm.getComponents(ctx.getLdapModel().getId(), LDAPStorageMapper.class.getName());
|
||||
for (ComponentModel mapper : components) {
|
||||
|
@ -132,4 +140,66 @@ public class LDAPNoMSADTest extends AbstractLDAPTest {
|
|||
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)));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -420,7 +420,7 @@ public class LDAPSyncTest extends AbstractLDAPTest {
|
|||
LDAPObject group1Loaded = groupMapper.loadLDAPGroupByName("group1");
|
||||
|
||||
// update group name and description
|
||||
group1Loaded.setSingleAttribute(group1Loaded.getRdnAttributeName(), "group5");
|
||||
group1Loaded.setSingleAttribute(group1Loaded.getRdnAttributeNames().get(0), "group5");
|
||||
group1Loaded.setSingleAttribute(descriptionAttrName, "group5 - description");
|
||||
LDAPTestUtils.updateLDAPGroup(session, appRealm, ctx.getLdapModel(), group1Loaded);
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.junit.runners.MethodSorters;
|
|||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.storage.ldap.idm.model.LDAPDn;
|
||||
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.testsuite.runonserver.RunOnServerException;
|
||||
import org.keycloak.testsuite.util.LDAPRule;
|
||||
|
@ -77,7 +78,8 @@ public class LdapUsernameAttributeTest extends AbstractLDAPTest {
|
|||
Assert.assertEquals("johndow", john.getLastName());
|
||||
LDAPObject johnLdap = ctx.getLdapProvider().loadLDAPUserByUsername(appRealm, "johndow");
|
||||
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
|
||||
testingClient.server().run(session -> {
|
||||
|
@ -103,7 +105,8 @@ public class LdapUsernameAttributeTest extends AbstractLDAPTest {
|
|||
Assert.assertEquals("johndow2", john2.getLastName());
|
||||
LDAPObject johnLdap2 = ctx.getLdapProvider().loadLDAPUserByUsername(appRealm, "johndow2");
|
||||
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);
|
||||
Assert.assertNull(session.users().getUserByUsername("johndow2", appRealm));
|
||||
|
|
Loading…
Reference in a new issue