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.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,20 +48,38 @@ public class LDAPDn {
|
||||||
// Keycloak must be able to process it, properly, w/o throwing an ArrayIndexOutOfBoundsException
|
// Keycloak must be able to process it, properly, w/o throwing an ArrayIndexOutOfBoundsException
|
||||||
if(dnString.trim().isEmpty())
|
if(dnString.trim().isEmpty())
|
||||||
return dn;
|
return dn;
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
|
Loading…
Reference in a new issue