Use jdk LdapName and Rdn to parse inside LDAPDn and RDN and avoid string conversions

Closes: https://github.com/keycloak/keycloak/issues/21797
Closes: https://github.com/keycloak/keycloak/issues/21818
This commit is contained in:
rmartinc 2023-09-27 16:49:30 +02:00 committed by Marek Posolda
parent a2dd0f31c5
commit d10ccc7245
10 changed files with 206 additions and 246 deletions

View file

@ -76,6 +76,11 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>

View file

@ -231,7 +231,7 @@ public class LDAPUtils {
*/
public static void addMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, String memberChildAttrName, LDAPObject ldapParent, LDAPObject ldapChild) {
String membership = getMemberValueOfChildObject(ldapChild, membershipType, memberChildAttrName);
ldapProvider.getLdapIdentityStore().addMemberToGroup(ldapParent.getDn().toString(), memberAttrName, membership);
ldapProvider.getLdapIdentityStore().addMemberToGroup(ldapParent.getDn().getLdapName(), memberAttrName, membership);
}
/**
@ -246,7 +246,7 @@ public class LDAPUtils {
*/
public static void deleteMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, String memberChildAttrName, LDAPObject ldapParent, LDAPObject ldapChild) {
String userMembership = getMemberValueOfChildObject(ldapChild, membershipType, memberChildAttrName);
ldapProvider.getLdapIdentityStore().removeMemberFromGroup(ldapParent.getDn().toString(), memberAttrName, userMembership);
ldapProvider.getLdapIdentityStore().removeMemberFromGroup(ldapParent.getDn().getLdapName(), memberAttrName, userMembership);
}
/**
@ -333,7 +333,7 @@ public class LDAPUtils {
private static LDAPQuery createLdapQueryForRangeAttribute(LDAPStorageProvider ldapProvider, LDAPObject ldapObject, String name) {
LDAPQuery q = new LDAPQuery(ldapProvider);
q.setSearchDn(ldapObject.getDn().toString());
q.setSearchDn(ldapObject.getDn().getLdapName());
q.setSearchScope(SearchControls.OBJECT_SCOPE);
q.addReturningLdapAttribute(name + ";range=" + (ldapObject.getCurrentRange(name) + 1) + "-*");
return q;

View file

@ -17,72 +17,53 @@
package org.keycloak.storage.ldap.idm.model;
import javax.naming.ldap.Rdn;
import java.util.Collection;
import java.util.Deque;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPDn {
private static final Pattern DN_PATTERN = Pattern.compile("(?<!\\\\),");
private static final Pattern ENTRY_PATTERN = Pattern.compile("(?<!\\\\)\\+");
private static final Pattern SUB_ENTRY_PATTERN = Pattern.compile("(?<!\\\\)=");
private final Deque<RDN> entries;
private final LdapName ldapName;
private LDAPDn() {
this.entries = new LinkedList<>();
this.ldapName = new LdapName(Collections.emptyList());
}
private LDAPDn(Deque<RDN> entries) {
this.entries = entries;
private LDAPDn(LdapName ldapName) {
this.ldapName = ldapName;
}
public static LDAPDn fromLdapName(LdapName ldapName) {
return new LDAPDn(ldapName);
}
public static LDAPDn fromString(String dnString) {
LDAPDn dn = new LDAPDn();
// In certain OpenLDAP implementations the uniqueMember attribute is mandatory
// Thus, if a new group is created, it will contain an empty uniqueMember attribute
// Later on, when adding members, this empty attribute will be kept
// Keycloak must be able to process it, properly, w/o throwing an ArrayIndexOutOfBoundsException
if(dnString.trim().isEmpty())
return dn;
String[] rdns = DN_PATTERN.split(dnString);
for (String entryStr : rdns) {
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 {
// This is 0.1% of cases where RDN consists of more key-value pairs like "uid=foo+cn=bar"
String[] subEntries = ENTRY_PATTERN.split(entryStr);
RDN entry = new RDN();
for (String subEntryStr : subEntries) {
SubEntry subEntry = parseSingleSubEntry(dn, subEntryStr);
entry.addSubEntry(subEntry);
}
dn.addLast(entry);
}
if(dnString.trim().isEmpty()) {
return new LDAPDn();
}
return dn;
try {
return new LDAPDn(new LdapName(dnString));
} catch (NamingException e) {
throw new IllegalArgumentException("Invalid DN:" + dnString, e);
}
}
// 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 = SUB_ENTRY_PATTERN.split(subEntryStr);
if (rdn.length >1) {
return new SubEntry(rdn[0].trim(), rdn[1].trim());
} else {
return new SubEntry(rdn[0].trim(), "");
}
public LdapName getLdapName() {
return ldapName;
}
@Override
@ -91,50 +72,27 @@ public class LDAPDn {
return false;
}
return toString().equals(obj.toString());
return ldapName.equals(((LDAPDn) obj).ldapName);
}
@Override
public int hashCode() {
return toString().hashCode();
return ldapName.hashCode();
}
@Override
public String toString() {
return toString(entries);
}
private static String toString(Collection<RDN> entries) {
StringBuilder builder = new StringBuilder();
boolean first = true;
for (RDN rdn : entries) {
if (first) {
first = false;
} else {
builder.append(",");
}
builder.append(rdn.toString());
}
return builder.toString();
return ldapName.toString();
}
/**
* @return first entry. Usually entry corresponding to something like "uid=joe" from the DN like "uid=joe,dc=something,dc=org"
*/
public RDN getFirstRdn() {
return entries.getFirst();
}
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);
if (ldapName.size() > 0) {
return new RDN(ldapName.getRdn(ldapName.size() - 1));
}
return null;
}
/**
@ -144,37 +102,31 @@ public class LDAPDn {
*
*/
public LDAPDn getParentDn() {
LinkedList<RDN> parentDnEntries = new LinkedList<>(entries);
parentDnEntries.remove();
return new LDAPDn(parentDnEntries);
if (ldapName.size() > 0) {
LdapName parent = (LdapName) ldapName.getPrefix(ldapName.size() - 1);
return new LDAPDn(parent);
}
return null;
}
public boolean isDescendantOf(LDAPDn expectedParentDn) {
int parentEntriesCount = expectedParentDn.entries.size();
Deque<RDN> myEntries = new LinkedList<>(this.entries);
boolean someRemoved = false;
while (myEntries.size() > parentEntriesCount) {
myEntries.removeFirst();
someRemoved = true;
LDAPDn parent = getParentDn();
if (parent == null) {
return false;
}
String myEntriesParentStr = toString(myEntries).toLowerCase();
String expectedParentDnStr = expectedParentDn.toString().toLowerCase();
return someRemoved && myEntriesParentStr.equals(expectedParentDnStr);
return parent.ldapName.startsWith(expectedParentDn.ldapName);
}
public void addFirst(String rdnName, String rdnValue) {
rdnValue = escapeValue(rdnValue);
entries.addFirst(new RDN(new SubEntry(rdnName, rdnValue)));
try {
ldapName.add(rdnName + "=" + Rdn.escapeValue(rdnValue));
} catch (NamingException e) {
throw new IllegalArgumentException("Invalid RDN name=" + rdnName + " value=" + rdnValue, e);
}
}
public void addFirst(RDN entry) {
entries.addFirst(entry);
}
private void addLast(RDN entry) {
entries.addLast(entry);
ldapName.add(entry.rdn);
}
/**
@ -183,24 +135,27 @@ public class LDAPDn {
*/
public static class RDN {
private List<SubEntry> subs = new LinkedList<>();
private Rdn rdn;
private RDN() {
}
private RDN(SubEntry subEntry) {
subs.add(subEntry);
}
private void addSubEntry(SubEntry subEntry) {
subs.add(subEntry);
private RDN(Rdn rdn) {
this.rdn = rdn;
}
/**
* @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());
try {
Attributes attrs = rdn.toAttributes();
List<String> result = new ArrayList<>(attrs.size());
NamingEnumeration<? extends Attribute> ne = attrs.getAll();
while (ne.hasMore()) {
result.add(ne.next().getID());
}
return result;
} catch (NamingException e) {
throw new IllegalStateException(e);
}
}
/**
@ -213,44 +168,52 @@ public class LDAPDn {
* @return
*/
public String getAttrValue(String attrName) {
for (SubEntry sub : subs) {
if (attrName.equalsIgnoreCase(sub.attrName)) {
return LDAPDn.unescapeValue(sub.attrValue);
try {
Attribute attr = rdn.toAttributes().get(attrName);
if (attr != null) {
Object value = attr.get();
if (value != null) {
return value.toString();
}
}
return null;
} catch (NamingException e) {
throw new IllegalStateException(e);
}
return null;
}
public void setAttrValue(String attrName, String newAttrValue) {
for (SubEntry sub : subs) {
if (attrName.equalsIgnoreCase(sub.attrName)) {
sub.attrValue = escapeValue(newAttrValue);
return;
try {
Attributes attrs = rdn.toAttributes();
Attribute attr = attrs.get(attrName);
if (attr != null) {
attr.clear();
attr.add(newAttrValue);
} else {
attrs.put(attrName, newAttrValue);
}
rdn = new Rdn(attrs);
} catch (NamingException e) {
throw new IllegalStateException(e);
}
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;
try {
Attributes attrs = rdn.toAttributes();
if (attrs.remove(attrName) != null) {
rdn = new Rdn(attrs);
return true;
}
}
if (toRemove != null) {
subs.remove(toRemove);
return true;
} else {
return false;
} catch (NamingException e) {
throw new IllegalStateException(e);
}
}
@Override
public String toString() {
return toString(true);
return rdn.toString();
}
/**
@ -259,43 +222,26 @@ public class LDAPDn {
* @return
*/
public String toString(boolean escaped) {
StringBuilder builder = new StringBuilder();
if (escaped) {
return toString();
}
boolean first = true;
for (SubEntry subEntry : subs) {
if (first) {
first = false;
} else {
builder.append('+');
StringBuilder builder = new StringBuilder();
try {
NamingEnumeration<? extends Attribute> attrs = rdn.toAttributes().getAll();
while (attrs.hasMore()) {
Attribute attr = attrs.next();
NamingEnumeration<?> values = attr.getAll();
while (values.hasMore()) {
builder.append(attr.getID()).append("=").append(values.next().toString()).append("+");
}
}
builder.append(subEntry.toString(escaped));
builder.setLength(builder.length() - 1);
} catch (NamingException e) {
throw new IllegalStateException(e);
}
return builder.toString();
}
}
private static class SubEntry {
private final String attrName;
private 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;
}
}
}

View file

@ -22,6 +22,7 @@ import org.keycloak.component.ComponentModel;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
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.query.Condition;
import org.keycloak.storage.ldap.idm.query.Sort;
@ -32,6 +33,7 @@ import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import javax.naming.NamingException;
import javax.naming.directory.SearchControls;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName;
import java.util.*;
@ -54,7 +56,7 @@ public class LDAPQuery implements AutoCloseable {
private int limit;
private PaginationContext paginationContext;
private LDAPContextManager ldapContextManager;
private String searchDn;
private LdapName searchDn;
private final Set<Condition> conditions = new LinkedHashSet<>();
private final Set<Sort> ordering = new LinkedHashSet<>();
@ -84,6 +86,11 @@ public class LDAPQuery implements AutoCloseable {
}
public LDAPQuery setSearchDn(String searchDn) {
this.searchDn = LDAPDn.fromString(searchDn).getLdapName();
return this;
}
public LDAPQuery setSearchDn(LdapName searchDn) {
this.searchDn = searchDn;
return this;
}
@ -117,7 +124,7 @@ public class LDAPQuery implements AutoCloseable {
return unmodifiableSet(this.ordering);
}
public String getSearchDn() {
public LdapName getSearchDn() {
return this.searchDn;
}

View file

@ -17,7 +17,10 @@
package org.keycloak.storage.ldap.idm.store;
import java.util.List;
import java.util.Set;
import javax.naming.AuthenticationException;
import javax.naming.ldap.LdapName;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
@ -25,9 +28,6 @@ import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
import javax.naming.AuthenticationException;
import java.util.List;
/**
* IdentityStore representation providing minimal SPI
*
@ -74,7 +74,7 @@ public interface IdentityStore {
* @param memberAttrName The member attribute name
* @param value The value (it can be uid or dn depending the group type)
*/
public void addMemberToGroup(String groupDn, String memberAttrName, String value);
public void addMemberToGroup(LdapName groupDn, String memberAttrName, String value);
/**
* Removes a member from a group.
@ -82,7 +82,7 @@ public interface IdentityStore {
* @param memberAttrName The member attribute name
* @param value The value (it can be uid or dn depending the group type)
*/
public void removeMemberFromGroup(String groupDn, String memberAttrName, String value);
public void removeMemberFromGroup(LdapName groupDn, String memberAttrName, String value);
// Identity query

View file

@ -64,6 +64,7 @@ import java.util.stream.Collectors;
import javax.naming.directory.AttributeInUseException;
import javax.naming.directory.NoSuchAttributeException;
import javax.naming.directory.SchemaViolationException;
import javax.naming.ldap.LdapName;
/**
* An IdentityStore implementation backed by an LDAP directory
@ -97,18 +98,17 @@ public class LDAPIdentityStore implements IdentityStore {
throw new ModelException("Can't add object with already assigned uuid");
}
String entryDN = ldapObject.getDn().toString();
BasicAttributes ldapAttributes = extractAttributesForSaving(ldapObject, true);
this.operationManager.createSubContext(entryDN, ldapAttributes);
this.operationManager.createSubContext(ldapObject.getDn().getLdapName(), ldapAttributes);
ldapObject.setUuid(getEntryIdentifier(ldapObject));
if (logger.isDebugEnabled()) {
logger.debugf("Type with identifier [%s] and dn [%s] successfully added to LDAP store.", ldapObject.getUuid(), entryDN);
logger.debugf("Type with identifier [%s] and dn [%s] successfully added to LDAP store.", ldapObject.getUuid(), ldapObject.getDn());
}
}
@Override
public void addMemberToGroup(String groupDn, String memberAttrName, String value) {
public void addMemberToGroup(LdapName groupDn, String memberAttrName, String value) {
// do not check EMPTY_MEMBER_ATTRIBUTE_VALUE, we save one useless query
// the value will be there forever for objectclasses that enforces the attribute as MUST
BasicAttribute attr = new BasicAttribute(memberAttrName, value);
@ -123,7 +123,7 @@ public class LDAPIdentityStore implements IdentityStore {
}
@Override
public void removeMemberFromGroup(String groupDn, String memberAttrName, String value) {
public void removeMemberFromGroup(LdapName groupDn, String memberAttrName, String value) {
BasicAttribute attr = new BasicAttribute(memberAttrName, value);
ModificationItem item = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, attr);
try {
@ -152,17 +152,16 @@ public class LDAPIdentityStore implements IdentityStore {
BasicAttributes updatedAttributes = extractAttributesForSaving(ldapObject, false);
NamingEnumeration<Attribute> attributes = updatedAttributes.getAll();
String entryDn = ldapObject.getDn().toString();
this.operationManager.modifyAttributes(entryDn, attributes);
this.operationManager.modifyAttributes(ldapObject.getDn().getLdapName(), attributes);
if (logger.isDebugEnabled()) {
logger.debugf("Type with identifier [%s] and DN [%s] successfully updated to LDAP store.", ldapObject.getUuid(), entryDn);
logger.debugf("Type with identifier [%s] and DN [%s] successfully updated to LDAP store.", ldapObject.getUuid(), ldapObject.getDn());
}
}
protected void checkRename(LDAPObject ldapObject) {
LDAPDn.RDN firstRdn = ldapObject.getDn().getFirstRdn();
String oldDn = ldapObject.getDn().toString();
LdapName oldDn = ldapObject.getDn().getLdapName();
// 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();
@ -218,20 +217,20 @@ public class LDAPIdentityStore implements IdentityStore {
LDAPDn newLdapDn = ldapObject.getDn().getParentDn();
newLdapDn.addFirst(firstRdn);
String newDn = newLdapDn.toString();
LdapName newDn = newLdapDn.getLdapName();
logger.debugf("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
newDn = this.operationManager.renameEntry(oldDn, newDn, true);
ldapObject.setDn(LDAPDn.fromString(newDn));
ldapObject.setDn(LDAPDn.fromLdapName(newDn));
}
}
@Override
public void remove(LDAPObject ldapObject) {
this.operationManager.removeEntry(ldapObject.getDn().toString());
this.operationManager.removeEntry(ldapObject.getDn().getLdapName());
if (logger.isDebugEnabled()) {
logger.debugf("Type with identifier [%s] and DN [%s] successfully removed from LDAP store.", ldapObject.getUuid(), ldapObject.getDn().toString());
@ -248,7 +247,7 @@ public class LDAPIdentityStore implements IdentityStore {
List<LDAPObject> results = new ArrayList<>();
try {
String baseDN = identityQuery.getSearchDn();
LdapName baseDN = identityQuery.getSearchDn();
for (Condition condition : identityQuery.getConditions()) {
@ -281,7 +280,7 @@ public class LDAPIdentityStore implements IdentityStore {
for (SearchResult result : search) {
// don't add the branch in subtree search
if (identityQuery.getSearchScope() != SearchControls.SUBTREE_SCOPE || !result.getNameInNamespace().equalsIgnoreCase(baseDN)) {
if (identityQuery.getSearchScope() != SearchControls.SUBTREE_SCOPE || !baseDN.equals(new LdapName(result.getNameInNamespace()))) {
results.add(populateAttributedType(result, identityQuery));
}
}
@ -314,7 +313,7 @@ public class LDAPIdentityStore implements IdentityStore {
attrs.add("supportedExtension");
attrs.add("supportedFeatures");
List<SearchResult> searchResults = operationManager
.search("", "(objectClass=*)", Collections.unmodifiableCollection(attrs), SearchControls.OBJECT_SCOPE);
.search(new LdapName(Collections.emptyList()), "(objectClass=*)", Collections.unmodifiableCollection(attrs), SearchControls.OBJECT_SCOPE);
if (searchResults.size() != 1) {
throw new ModelException("Could not query root DSE: unexpected result size");
}
@ -343,36 +342,32 @@ public class LDAPIdentityStore implements IdentityStore {
@Override
public void validatePassword(LDAPObject user, String password) throws AuthenticationException {
String userDN = user.getDn().toString();
if (logger.isTraceEnabled()) {
logger.tracef("Using DN [%s] for authentication of user", userDN);
logger.tracef("Using DN [%s] for authentication of user", user.getDn());
}
operationManager.authenticate(userDN, password);
operationManager.authenticate(user.getDn().getLdapName(), password);
}
@Override
public void updatePassword(LDAPObject user, String password, LDAPOperationDecorator passwordUpdateDecorator) {
String userDN = user.getDn().toString();
if (logger.isDebugEnabled()) {
logger.debugf("Using DN [%s] for updating LDAP password of user", userDN);
logger.debugf("Using DN [%s] for updating LDAP password of user", user.getDn());
}
if (getConfig().isActiveDirectory()) {
updateADPassword(userDN, password, passwordUpdateDecorator);
updateADPassword(user.getDn().getLdapName(), password, passwordUpdateDecorator);
return;
}
try {
if (config.useExtendedPasswordModifyOp()) {
operationManager.passwordModifyExtended(userDN, password, passwordUpdateDecorator);
operationManager.passwordModifyExtended(user.getDn().getLdapName(), password, passwordUpdateDecorator);
} else {
ModificationItem[] mods = new ModificationItem[1];
BasicAttribute mod0 = new BasicAttribute(LDAPConstants.USER_PASSWORD_ATTRIBUTE, password);
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
operationManager.modifyAttributes(userDN, mods, passwordUpdateDecorator);
operationManager.modifyAttributes(user.getDn().getLdapName(), mods, passwordUpdateDecorator);
}
} catch (ModelException me) {
throw me;
@ -381,7 +376,7 @@ public class LDAPIdentityStore implements IdentityStore {
}
}
private void updateADPassword(String userDN, String password, LDAPOperationDecorator passwordUpdateDecorator) {
private void updateADPassword(LdapName userDN, String password, LDAPOperationDecorator passwordUpdateDecorator) {
try {
// Replace the "unicdodePwd" attribute with a new value
// Password must be both Unicode and a quoted string
@ -617,7 +612,7 @@ public class LDAPIdentityStore implements IdentityStore {
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);
List<SearchResult> search = this.operationManager.search(ldapObject.getDn().getLdapName(), filter, Arrays.asList(uuidAttrName), SearchControls.OBJECT_SCOPE);
Attribute id = search.get(0).getAttributes().get(getConfig().getUuidLDAPAttributeName());
if (id == null) {

View file

@ -87,7 +87,7 @@ public class LDAPOperationManager {
* @param dn
* @param attribute
*/
public void modifyAttribute(String dn, Attribute attribute) {
public void modifyAttribute(LdapName dn, Attribute attribute) {
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attribute)};
modifyAttributes(dn, mods, null);
}
@ -101,7 +101,7 @@ public class LDAPOperationManager {
* @param dn
* @param attributes
*/
public void modifyAttributes(String dn, NamingEnumeration<Attribute> attributes) {
public void modifyAttributes(LdapName dn, NamingEnumeration<Attribute> attributes) {
try {
List<ModificationItem> modItems = new ArrayList<>();
while (attributes.hasMore()) {
@ -125,7 +125,7 @@ public class LDAPOperationManager {
* @param dn
* @param attribute
*/
public void removeAttribute(String dn, Attribute attribute) {
public void removeAttribute(LdapName dn, Attribute attribute) {
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.REMOVE_ATTRIBUTE, attribute)};
modifyAttributes(dn, mods, null);
}
@ -138,7 +138,7 @@ public class LDAPOperationManager {
* @param dn
* @param attribute
*/
public void addAttribute(String dn, Attribute attribute) {
public void addAttribute(LdapName dn, Attribute attribute) {
ModificationItem[] mods = new ModificationItem[]{new ModificationItem(DirContext.ADD_ATTRIBUTE, attribute)};
modifyAttributes(dn, mods, null);
}
@ -148,7 +148,7 @@ public class LDAPOperationManager {
* Removes the object from the LDAP tree
* </p>
*/
public void removeEntry(final String entryDn) {
public void removeEntry(final LdapName entryDn) {
try {
execute(new LdapOperation<SearchResult>() {
@ -185,25 +185,25 @@ public class LDAPOperationManager {
* attempt to rename to "CN=John Doe", but there is already existing "CN=John Doe", we will try "CN=John Doe0"
* @return the non-conflicting DN, which was used in the end
*/
public String renameEntry(String oldDn, String newDn, boolean fallback) {
public LdapName renameEntry(LdapName oldDn, LdapName newDn, boolean fallback) {
try {
String newNonConflictingDn = execute(new LdapOperation<String>() {
LdapName newNonConflictingDn = execute(new LdapOperation<LdapName>() {
@Override
public String execute(LdapContext context) throws NamingException {
String dn = newDn;
public LdapName execute(LdapContext context) throws NamingException {
LdapName dn = newDn;
// Max 5 attempts for now
int max = 5;
for (int i=0 ; i<max ; i++) {
try {
context.rename(new LdapName(oldDn), new LdapName(dn));
context.rename(oldDn, dn);
return dn;
} catch (NameAlreadyBoundException ex) {
if (!fallback) {
throw ex;
} else {
String failedDn = dn;
LdapName failedDn = dn;
if (i<max) {
dn = findNextDNForFallback(newDn, i);
logger.warnf("Failed to rename DN [%s] to [%s]. Will try to fallback to DN [%s]", oldDn, failedDn, dn);
@ -234,18 +234,18 @@ public class LDAPOperationManager {
}
}
private String findNextDNForFallback(String newDn, int counter) {
LDAPDn dn = LDAPDn.fromString(newDn);
private LdapName findNextDNForFallback(LdapName newDn, int counter) {
LDAPDn dn = LDAPDn.fromLdapName(newDn);
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();
return parentDn.getLdapName();
}
public List<SearchResult> search(final String baseDN, final String filter, Collection<String> returningAttributes, int searchScope) throws NamingException {
public List<SearchResult> search(final LdapName baseDN, final String filter, Collection<String> returningAttributes, int searchScope) throws NamingException {
final List<SearchResult> result = new ArrayList<>();
final SearchControls cons = getSearchControls(returningAttributes, searchScope);
@ -253,7 +253,7 @@ public class LDAPOperationManager {
return execute(new LdapOperation<List<SearchResult>>() {
@Override
public List<SearchResult> execute(LdapContext context) throws NamingException {
NamingEnumeration<SearchResult> search = context.search(new LdapName(baseDN), filter, cons);
NamingEnumeration<SearchResult> search = context.search(baseDN, filter, cons);
while (search.hasMoreElements()) {
result.add(search.nextElement());
@ -284,7 +284,7 @@ public class LDAPOperationManager {
}
}
public List<SearchResult> searchPaginated(final String baseDN, final String filter, final LDAPQuery identityQuery) throws NamingException {
public List<SearchResult> searchPaginated(final LdapName baseDN, final String filter, final LDAPQuery identityQuery) throws NamingException {
final List<SearchResult> result = new ArrayList<>();
final SearchControls cons = getSearchControls(identityQuery.getReturningLdapAttributes(), identityQuery.getSearchScope());
@ -303,7 +303,7 @@ public class LDAPOperationManager {
PagedResultsControl pagedControls = new PagedResultsControl(identityQuery.getLimit(), cookie, Control.CRITICAL);
context.setRequestControls(new Control[] { pagedControls });
NamingEnumeration<SearchResult> search = context.search(new LdapName(baseDN), filter, cons);
NamingEnumeration<SearchResult> search = context.search(baseDN, filter, cons);
while (search.hasMoreElements()) {
result.add(search.nextElement());
@ -401,7 +401,7 @@ public class LDAPOperationManager {
return ldapIdFilter;
}
public SearchResult lookupById(final String baseDN, final String id, final Collection<String> returningAttributes) {
public SearchResult lookupById(final LdapName baseDN, final String id, final Collection<String> returningAttributes) {
final String filter = getFilterById(id);
try {
@ -411,7 +411,7 @@ public class LDAPOperationManager {
@Override
public SearchResult execute(LdapContext context) throws NamingException {
NamingEnumeration<SearchResult> search = context.search(new LdapName(baseDN), filter, cons);
NamingEnumeration<SearchResult> search = context.search(baseDN, filter, cons);
try {
if (search.hasMoreElements()) {
@ -450,21 +450,21 @@ public class LDAPOperationManager {
*
* @param dn
*/
private void destroySubcontext(LdapContext context, final String dn) {
private void destroySubcontext(LdapContext context, final LdapName dn) {
try {
NamingEnumeration<Binding> enumeration = null;
try {
enumeration = context.listBindings(new LdapName(dn));
enumeration = context.listBindings(dn);
while (enumeration.hasMore()) {
Binding binding = enumeration.next();
String name = binding.getNameInNamespace();
destroySubcontext(context, name);
destroySubcontext(context, new LdapName(name));
}
context.unbind(new LdapName(dn));
context.unbind(dn);
} finally {
try {
enumeration.close();
@ -486,7 +486,7 @@ public class LDAPOperationManager {
* @throws AuthenticationException if authentication is not successful
*
*/
public void authenticate(String dn, String password) throws AuthenticationException {
public void authenticate(LdapName dn, String password) throws AuthenticationException {
if (password == null || password.isEmpty()) {
throw new AuthenticationException("Empty password used");
@ -503,7 +503,7 @@ public class LDAPOperationManager {
if(!this.config.isStartTls()) {
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, dn);
env.put(Context.SECURITY_PRINCIPAL, dn.toString());
env.put(Context.SECURITY_CREDENTIALS, password);
}
@ -515,7 +515,7 @@ public class LDAPOperationManager {
sslSocketFactory = provider.getSSLSocketFactory();
}
tlsResponse = LDAPContextManager.startTLS(authCtx, "simple", dn, password.toCharArray(), sslSocketFactory);
tlsResponse = LDAPContextManager.startTLS(authCtx, "simple", dn.toString(), password.toCharArray(), sslSocketFactory);
// Exception should be already thrown by LDAPContextManager.startTLS if "startTLS" could not be established, but rather do some additional check
if (tlsResponse == null) {
@ -557,7 +557,7 @@ public class LDAPOperationManager {
}
}
public void modifyAttributesNaming(final String dn, final ModificationItem[] mods, LDAPOperationDecorator decorator) throws NamingException {
public void modifyAttributesNaming(final LdapName dn, final ModificationItem[] mods, LDAPOperationDecorator decorator) throws NamingException {
if (logger.isTraceEnabled()) {
logger.tracef("Modifying attributes for entry [%s]: [", dn);
@ -585,7 +585,7 @@ public class LDAPOperationManager {
@Override
public Void execute(LdapContext context) throws NamingException {
context.modifyAttributes(new LdapName(dn), mods);
context.modifyAttributes(dn, mods);
return null;
}
@ -600,7 +600,7 @@ public class LDAPOperationManager {
}, decorator);
}
public void modifyAttributes(final String dn, final ModificationItem[] mods, LDAPOperationDecorator decorator) {
public void modifyAttributes(final LdapName dn, final ModificationItem[] mods, LDAPOperationDecorator decorator) {
try {
modifyAttributesNaming(dn, mods, decorator);
} catch (NamingException e) {
@ -608,7 +608,7 @@ public class LDAPOperationManager {
}
}
public void createSubContext(final String name, final Attributes attributes) {
public void createSubContext(final LdapName name, final Attributes attributes) {
try {
if (logger.isTraceEnabled()) {
logger.tracef("Creating entry [%s] with attributes: [", name);
@ -633,7 +633,7 @@ public class LDAPOperationManager {
execute(new LdapOperation<Void>() {
@Override
public Void execute(LdapContext context) throws NamingException {
DirContext subcontext = context.createSubcontext(new LdapName(name), attributes);
DirContext subcontext = context.createSubcontext(name, attributes);
subcontext.close();
@ -659,7 +659,7 @@ public class LDAPOperationManager {
return this.config.getUuidLDAPAttributeName();
}
public Attributes getAttributes(final String entryUUID, final String baseDN, Set<String> returningAttributes) {
public Attributes getAttributes(final String entryUUID, final LdapName baseDN, Set<String> returningAttributes) {
SearchResult search = lookupById(baseDN, entryUUID, returningAttributes);
if (search == null) {
@ -689,10 +689,10 @@ public class LDAPOperationManager {
* @param decorator A decorator to apply to the ldap operation.
*/
public void passwordModifyExtended(String dn, String password, LDAPOperationDecorator decorator) {
public void passwordModifyExtended(LdapName dn, String password, LDAPOperationDecorator decorator) {
try {
execute(context -> {
PasswordModifyRequest modifyRequest = new PasswordModifyRequest(dn, null, password);
PasswordModifyRequest modifyRequest = new PasswordModifyRequest(dn.toString(), null, password);
return context.extendedOperation(modifyRequest);
}, decorator);
} catch (NamingException e) {

View file

@ -17,8 +17,8 @@
package org.keycloak.storage.ldap.idm.model;
import java.util.List;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Test;
@ -45,7 +45,7 @@ public class LDAPDnTest {
Assert.assertFalse(dn.isDescendantOf(LDAPDn.fromString("dc=keycloakk, dc=org")));
Assert.assertFalse(dn.isDescendantOf(dn));
Assert.assertEquals("uid", dn.getFirstRdn().getAllKeys().get(0));
MatcherAssert.assertThat(dn.getFirstRdn().getAllKeys(), Matchers.containsInAnyOrder("uid"));
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"));
@ -56,7 +56,7 @@ public class LDAPDnTest {
LDAPDn dn = LDAPDn.fromString("dc=keycloak, dc=org");
dn.addFirst("ou", "");
Assert.assertEquals("ou", dn.getFirstRdn().getAllKeys().get(0));
MatcherAssert.assertThat(dn.getFirstRdn().getAllKeys(), Matchers.containsInAnyOrder("ou"));
Assert.assertEquals("", dn.getFirstRdn().getAttrValue("ou"));
Assert.assertEquals("ou=,dc=keycloak,dc=org", dn.toString());
@ -86,25 +86,32 @@ public class LDAPDnTest {
dn.addFirst("cn", "Johny,Džýa ");
Assert.assertEquals("cn=Johny\\,Džýa\\ ,dc=keycloak,dc=org", dn.toString());
Assert.assertEquals("Johny,Džýa ", dn.getFirstRdn().getAttrValue("cn"));
dn = LDAPDn.fromString("CN=Test User\\\\,OU=Users,DC=example,DC=com");
Assert.assertEquals("CN=Test User\\\\,OU=Users,DC=example,DC=com", dn.toString());
Assert.assertEquals("Test User\\", dn.getFirstRdn().getAttrValue("CN"));
dn = LDAPDn.fromString("CN=Test User\\ ,OU=Users,DC=example,DC=com");
Assert.assertEquals("CN=Test User\\ ,OU=Users,DC=example,DC=com", dn.toString());
Assert.assertEquals("Test User ", 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");
LDAPDn dn = LDAPDn.fromString("cn=John Do\\+eř+uid=john,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("cn=John Do\\+eř+uid=john", dn.getFirstRdn().toString());
MatcherAssert.assertThat(dn.getFirstRdn().getAllKeys(), Matchers.containsInAnyOrder("uid", "cn"));
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());
LDAPDn.RDN rdn = dn.getFirstRdn();
rdn.setAttrValue("UID", "john2");
Assert.assertEquals("cn=John Do\\+eř+uid=john2", rdn.toString());
dn.getFirstRdn().setAttrValue("some", "somet+hing");
Assert.assertEquals("uid=john2+cn=John Do\\+eř+some=somet\\+hing", dn.getFirstRdn().toString());
rdn.setAttrValue("some", "somet+hing");
Assert.assertEquals("cn=John Do\\+eř+some=somet\\+hing+uid=john2", rdn.toString());
}
}

View file

@ -158,7 +158,7 @@ public class LDAPNoMSADTest extends AbstractLDAPTest {
// Assert DN was changed
String rdnAttrName = ldapProvider.getLdapIdentityStore().getConfig().getRdnLdapAttribute();
Assert.assertEquals(rdnAttrName + "=johnkeycloak3+sn=Doe3", john2.getDn().getFirstRdn().toString());
Assert.assertEquals("sn=Doe3+" + rdnAttrName + "=johnkeycloak3", john2.getDn().getFirstRdn().toString());
});
// Update some user attributes not mapped to DN. DN won't be changed

View file

@ -190,7 +190,7 @@ public class UserSyncTest extends KeycloakModelTest {
LDAPObject user1LdapObject = ldapFedProvider.loadLDAPUserByUsername(realm, "user1");
LDAPOperationManager ldapOperationManager = new LDAPOperationManager(session, ldapFedProvider.getLdapIdentityStore().getConfig());
ldapOperationManager.removeAttribute(user1LdapObject.getDn().toString(), new BasicAttribute(LDAPConstants.STREET));
ldapOperationManager.removeAttribute(user1LdapObject.getDn().getLdapName(), new BasicAttribute(LDAPConstants.STREET));
return null;
});