Use conditions instead of String for filters and just use default escape strategy

Closes https://github.com/keycloak/keycloak/issues/24767

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2023-11-15 11:25:12 +01:00 committed by Bruno Oliveira da Silva
parent c8009b4627
commit 4e7bd76954
15 changed files with 319 additions and 140 deletions

View file

@ -18,6 +18,7 @@
package org.keycloak.storage.ldap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@ -78,7 +79,6 @@ import org.keycloak.storage.adapter.UpdateOnlyChangeUserModelDelegate;
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.EscapeStrategy;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
@ -289,7 +289,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
try (LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm)) {
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition attrCondition = conditionsBuilder.equal(attrName, attrValue, EscapeStrategy.DEFAULT);
Condition attrCondition = conditionsBuilder.equal(attrName, attrValue);
ldapQuery.addWhereCondition(attrCondition);
ldapObjects = ldapQuery.getResultList();
@ -476,6 +476,34 @@ public class LDAPStorageProvider implements UserStorageProvider,
.map(ldapUser -> importUserFromLDAP(session, realm, ldapUser));
}
private Condition createSearchCondition(LDAPQueryConditionsBuilder conditionsBuilder, String name, boolean equals, String value) {
if (equals) {
return conditionsBuilder.equal(name, value);
}
// perform a substring search based on *
String[] values = value.split("\\Q*\\E+", -1); // split by *
String start = null, end = null;
String[] middle = null;
if (!values[0].isEmpty()) {
start = values[0];
}
if (values.length > 1 && !values[values.length -1].isEmpty()) {
end = values[values.length - 1];
}
if (values.length > 2) {
middle = Arrays.copyOfRange(values, 1, values.length - 1);
}
if (start == null && middle == null && end == null) {
// just searching using empty string or *
return conditionsBuilder.present(name);
}
// return proper substring search
return conditionsBuilder.substring(name, start, middle, end);
}
/**
* Searches LDAP using logical conjunction of params. It supports
* <ul>
@ -495,16 +523,16 @@ public class LDAPStorageProvider implements UserStorageProvider,
// Mapper should replace parameter with correct LDAP mapped attributes
if (attributes.containsKey(UserModel.USERNAME)) {
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.USERNAME, attributes.get(UserModel.USERNAME), EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.USERNAME, attributes.get(UserModel.USERNAME)));
}
if (attributes.containsKey(UserModel.EMAIL)) {
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.EMAIL, attributes.get(UserModel.EMAIL), EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.EMAIL, attributes.get(UserModel.EMAIL)));
}
if (attributes.containsKey(UserModel.FIRST_NAME)) {
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.FIRST_NAME, attributes.get(UserModel.FIRST_NAME), EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.FIRST_NAME, attributes.get(UserModel.FIRST_NAME)));
}
if (attributes.containsKey(UserModel.LAST_NAME)) {
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.LAST_NAME, attributes.get(UserModel.LAST_NAME), EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.LAST_NAME, attributes.get(UserModel.LAST_NAME)));
}
// for all other searchable fields: Ignoring is the fallback option, since it may overestimate the results but does not ignore matches.
// for empty params: all users are returned (pagination applies)
@ -531,19 +559,21 @@ public class LDAPStorageProvider implements UserStorageProvider,
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
for (String s : search.split("\\s+")) {
boolean equals = false;
List<Condition> conditions = new LinkedList<>();
if (s.startsWith("\"") && s.endsWith("\"")) {
// exact search
s = s.substring(1, s.length() - 1);
equals = true;
} else if (!s.endsWith("*")) {
// default to prefix search
s += "*";
}
conditions.add(conditionsBuilder.equal(UserModel.USERNAME, s.trim().toLowerCase(), EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
conditions.add(conditionsBuilder.equal(UserModel.EMAIL, s.trim().toLowerCase(), EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
conditions.add(conditionsBuilder.equal(UserModel.FIRST_NAME, s, EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
conditions.add(conditionsBuilder.equal(UserModel.LAST_NAME, s, EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
conditions.add(createSearchCondition(conditionsBuilder, UserModel.USERNAME, equals, s));
conditions.add(createSearchCondition(conditionsBuilder, UserModel.EMAIL, equals, s));
conditions.add(createSearchCondition(conditionsBuilder, UserModel.FIRST_NAME, equals, s));
conditions.add(createSearchCondition(conditionsBuilder, UserModel.LAST_NAME, equals, s));
ldapQuery.addWhereCondition(conditionsBuilder.orCondition(conditions.toArray(Condition[]::new)));
}
@ -653,7 +683,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
// Mapper should replace "email" in parameter name with correct LDAP mapped attribute
Condition emailCondition = conditionsBuilder.equal(UserModel.EMAIL, email, EscapeStrategy.DEFAULT);
Condition emailCondition = conditionsBuilder.equal(UserModel.EMAIL, email);
ldapQuery.addWhereCondition(emailCondition);
return ldapQuery.getFirstResult();
@ -913,7 +943,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
logger.debugf("Trying to find kerberos authenticated user [%s] in LDAP. Kerberos principal attribute is [%s]", kerberosPrincipal.toString(), kerberosPrincipalAttrName);
try (LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm)) {
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition krbPrincipalCondition = conditionsBuilder.equal(kerberosPrincipalAttrName, kerberosPrincipal.toString(), EscapeStrategy.DEFAULT);
Condition krbPrincipalCondition = conditionsBuilder.equal(kerberosPrincipalAttrName, kerberosPrincipal.toString());
ldapQuery.addWhereCondition(krbPrincipalCondition);
LDAPObject ldapUser = ldapQuery.getFirstResult();
@ -936,7 +966,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
String usernameMappedAttribute = this.ldapIdentityStore.getConfig().getUsernameLdapAttribute();
Condition usernameCondition = conditionsBuilder.equal(usernameMappedAttribute, username, EscapeStrategy.DEFAULT);
Condition usernameCondition = conditionsBuilder.equal(usernameMappedAttribute, username);
ldapQuery.addWhereCondition(usernameCondition);
LDAPObject ldapUser = ldapQuery.getFirstResult();
@ -956,7 +986,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
String uuidLDAPAttributeName = this.ldapIdentityStore.getConfig().getUuidLDAPAttributeName();
Condition usernameCondition = conditionsBuilder.equal(uuidLDAPAttributeName, uuid, EscapeStrategy.DEFAULT);
Condition usernameCondition = conditionsBuilder.equal(uuidLDAPAttributeName, uuid);
ldapQuery.addWhereCondition(usernameCondition);
return ldapQuery.getFirstResult();

View file

@ -36,11 +36,15 @@ public interface Condition {
*/
void updateParameterName(String modelParamName, String ldapParamName);
void applyCondition(StringBuilder filter);
void setBinary(boolean binary);
boolean isBinary();
default String toFilter() {
StringBuilder sb = new StringBuilder();
applyCondition(sb);
return sb.toString();
}
}

View file

@ -24,42 +24,6 @@ import java.nio.charset.StandardCharsets;
*/
public enum EscapeStrategy {
/**
* LDAP special character * is not escaped, other special characters are escaped. Non-ASCII characters like é are escaped.
* Use it for searches where wildcards are allowed.
*/
DEFAULT_EXCEPT_ASTERISK {
@Override
public String escape(String input) {
StringBuilder output = new StringBuilder();
for (byte b : input.getBytes(StandardCharsets.UTF_8)) {
switch (b) {
case 0x5c:
output.append("\\5c"); // \
break;
case 0x28:
output.append("\\28"); // (
break;
case 0x29:
output.append("\\29"); // )
break;
case 0x00:
output.append("\\00"); // \u0000
break;
default: {
appendByte(b, output);
}
}
}
return output.toString();
}
},
/**
* Escaping of LDAP special characters including non-ASCII characters like é.
*/

View file

@ -0,0 +1,69 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.query.internal;
import org.keycloak.storage.ldap.idm.query.Condition;
/**
* <p>And condition for filters.</p>
*
* @author rmartinc
*/
public class AndCondition implements Condition {
private final Condition[] innerConditions;
public AndCondition(Condition... innerConditions) {
this.innerConditions = innerConditions;
}
@Override
public String getParameterName() {
return null;
}
@Override
public void setParameterName(String parameterName) {
}
@Override
public void updateParameterName(String modelParamName, String ldapParamName) {
for (Condition innerCondition : innerConditions) {
innerCondition.updateParameterName(modelParamName, ldapParamName);
}
}
@Override
public void applyCondition(StringBuilder filter) {
filter.append("(&");
for (Condition innerCondition : innerConditions) {
innerCondition.applyCondition(filter);
}
filter.append(")");
}
@Override
public void setBinary(boolean binary) {
}
@Override
public boolean isBinary() {
return false;
}
}

View file

@ -18,20 +18,17 @@
package org.keycloak.storage.ldap.idm.query.internal;
import org.keycloak.models.LDAPConstants;
import org.keycloak.storage.ldap.idm.query.EscapeStrategy;
/**
* @author Pedro Igor
*/
public class EqualCondition extends NamedParameterCondition {
private final EscapeStrategy escapeStrategy;
private Object value;
public EqualCondition(String name, Object value, EscapeStrategy escapeStrategy) {
public EqualCondition(String name, Object value) {
super(name);
this.value = value;
this.escapeStrategy = escapeStrategy;
}
public Object getValue() {
@ -42,13 +39,9 @@ public class EqualCondition extends NamedParameterCondition {
this.value = value;
}
public EscapeStrategy getEscapeStrategy() {
return escapeStrategy;
}
@Override
public void applyCondition(StringBuilder filter) {
filter.append("(").append(getParameterName()).append(LDAPConstants.EQUAL).append(escapeValue(value, escapeStrategy)).append(")");
filter.append("(").append(getParameterName()).append(LDAPConstants.EQUAL).append(escapeValue(value)).append(")");
}
@Override
@ -56,7 +49,6 @@ public class EqualCondition extends NamedParameterCondition {
return "EqualCondition{" +
"paramName=" + getParameterName() +
", value=" + value +
", escapeStrategy=" + escapeStrategy +
'}';
}
}

View file

@ -17,9 +17,9 @@
package org.keycloak.storage.ldap.idm.query.internal;
import java.util.Arrays;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.EscapeStrategy;
import org.keycloak.storage.ldap.idm.query.Sort;
/**
@ -28,11 +28,7 @@ import org.keycloak.storage.ldap.idm.query.Sort;
public class LDAPQueryConditionsBuilder {
public Condition equal(String parameter, Object value) {
return new EqualCondition(parameter, value, EscapeStrategy.DEFAULT);
}
public Condition equal(String parameter, Object value, EscapeStrategy escapeStrategy) {
return new EqualCondition(parameter, value, escapeStrategy);
return new EqualCondition(parameter, value);
}
public Condition greaterThan(String paramName, Object x) {
@ -64,6 +60,13 @@ public class LDAPQueryConditionsBuilder {
return new OrCondition(conditions);
}
public Condition andCondition(Condition... conditions) {
if (conditions == null || conditions.length == 0) {
throw new ModelException("At least one condition should be provided to AND query");
}
return new AndCondition(conditions);
}
public Condition addCustomLDAPFilter(String filter) {
filter = filter.trim();
return new CustomLDAPFilter(filter);
@ -73,6 +76,23 @@ public class LDAPQueryConditionsBuilder {
return new InCondition(paramName, x);
}
public Condition present(String paramName) {
return new PresentCondition(paramName);
}
public Condition substring(String paramName, String start, String[] middle, String end) {
if ((start == null || start.isEmpty())
&& (end == null || end.isEmpty())
&& (middle == null || middle.length == 0)) {
throw new ModelException("Invalid substring filter with no start, middle or end");
}
if (middle != null && middle.length > 0 && Arrays.stream(middle).filter(s -> s == null || s.isEmpty()).findAny().isPresent()) {
throw new ModelException("Invalid substring filter with an empty string in the middle array");
}
return new SubstringCondition(paramName, start, middle, end);
}
public Sort asc(String paramName) {
return new Sort(paramName, true);
}

View file

@ -63,13 +63,9 @@ public abstract class NamedParameterCondition implements Condition {
}
public String escapeValue(Object value) {
return escapeValue(value, EscapeStrategy.DEFAULT);
}
public String escapeValue(Object value, EscapeStrategy strategy) {
if (Date.class.isInstance(value)) {
value = LDAPUtil.formatDate((Date) value);
}
return new OctetStringEncoder(strategy).encode(value, isBinary());
return new OctetStringEncoder(EscapeStrategy.DEFAULT).encode(value, isBinary());
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.query.internal;
/**
* <p>Present LDAP condition <em>attrname=*</em> for filters</p>
*
* @author rmartinc
*/
public class PresentCondition extends NamedParameterCondition {
public PresentCondition(String name) {
super(name);
}
@Override
public void applyCondition(StringBuilder filter) {
filter.append("(").append(getParameterName()).append("=*)");
}
@Override
public String toString() {
return "PresentCondition{"
+ "paramName=" + getParameterName()
+ '}';
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright 2023 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.storage.ldap.idm.query.internal;
import java.util.Arrays;
/**
* <p>Substring condition for ldap filters, <em>attrname=*some*thing*</em> for
* example. The filter is created <em>attrname=[start]*[middle1]*[middle2]*[middleN]*[end]</em>.
* At least one property (start, middle or end) should contain a non-empty
* string. The middle array should not contain any null or empty string.</p>
*
* @author rmartinc
*/
public class SubstringCondition extends NamedParameterCondition {
private final String start;
private final String[] middle;
private final String end;
public SubstringCondition(String name, String start, String[] middle, String end) {
super(name);
this.start = start;
this.middle = middle;
this.end = end;
}
@Override
public void applyCondition(StringBuilder filter) {
filter.append("(").append(getParameterName()).append("=");
if (start != null && !start.isEmpty()) {
filter.append(escapeValue(start));
}
filter.append("*");
if (middle != null && middle.length > 0) {
Arrays.stream(middle).forEach(s -> filter.append(escapeValue(s)).append("*"));
}
if (end != null && !end.isEmpty()) {
filter.append(escapeValue(end));
}
filter.append(")");
}
@Override
public String toString() {
return "PresentCondition{"
+ "paramName=" + getParameterName()
+ ", start=" + start
+ ", middle=" + (middle == null? "null" : Arrays.asList(middle))
+ ", end=" + end
+ '}';
}
}

View file

@ -28,9 +28,9 @@ import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.EscapeStrategy;
import org.keycloak.storage.ldap.idm.query.internal.EqualCondition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.storage.ldap.idm.store.IdentityStore;
import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
@ -270,13 +270,13 @@ public class LDAPIdentityStore implements IdentityStore {
}
StringBuilder filter = createIdentityTypeSearchFilter(identityQuery);
Condition condition = createIdentityTypeSearchFilter(identityQuery);
List<SearchResult> search;
if (getConfig().isPagination() && identityQuery.getLimit() > 0) {
search = this.operationManager.searchPaginated(baseDN, filter.toString(), identityQuery);
search = this.operationManager.searchPaginated(baseDN, condition, identityQuery);
} else {
search = this.operationManager.search(baseDN, filter.toString(), identityQuery.getReturningLdapAttributes(), identityQuery.getSearchScope());
search = this.operationManager.search(baseDN, condition, identityQuery.getReturningLdapAttributes(), identityQuery.getSearchScope());
}
for (SearchResult result : search) {
@ -320,7 +320,9 @@ public class LDAPIdentityStore implements IdentityStore {
attrs.add("supportedExtension");
attrs.add("supportedFeatures");
List<SearchResult> searchResults = operationManager
.search(new LdapName(Collections.emptyList()), "(objectClass=*)", Collections.unmodifiableCollection(attrs), SearchControls.OBJECT_SCOPE);
.search(new LdapName(Collections.emptyList()),
new LDAPQueryConditionsBuilder().present(LDAPConstants.OBJECT_CLASS),
Collections.unmodifiableCollection(attrs), SearchControls.OBJECT_SCOPE);
if (searchResults.size() != 1) {
throw new ModelException("Could not query root DSE: unexpected result size");
}
@ -405,36 +407,25 @@ public class LDAPIdentityStore implements IdentityStore {
// ************ END CREDENTIALS AND USER SPECIFIC STUFF
protected StringBuilder createIdentityTypeSearchFilter(final LDAPQuery identityQuery) {
StringBuilder filter = new StringBuilder();
for (Condition condition : identityQuery.getConditions()) {
condition.applyCondition(filter);
}
filter.insert(0, "(&");
filter.append(getObjectClassesFilter(identityQuery.getObjectClasses()));
filter.append(")");
if (logger.isTraceEnabled()) {
logger.tracef("Using filter for LDAP search: %s . Searching in DN: %s", filter, identityQuery.getSearchDn());
}
return filter;
protected Condition createIdentityTypeSearchFilter(final LDAPQuery identityQuery) {
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Set<Condition> conditions = new LinkedHashSet<>(identityQuery.getConditions());
addObjectClassesConditions(conditionsBuilder, identityQuery.getObjectClasses(), conditions);
return conditionsBuilder.andCondition(conditions.toArray(Condition[]::new));
}
private StringBuilder getObjectClassesFilter(Collection<String> objectClasses) {
StringBuilder builder = new StringBuilder();
private Set<Condition> addObjectClassesConditions(LDAPQueryConditionsBuilder conditionsBuilder,
Collection<String> objectClasses, Set<Condition> conditions) {
if (!objectClasses.isEmpty()) {
for (String objectClass : objectClasses) {
builder.append("(").append(LDAPConstants.OBJECT_CLASS).append(LDAPConstants.EQUAL).append(objectClass).append(")");
conditions.add(conditionsBuilder.equal(LDAPConstants.OBJECT_CLASS, objectClass));
}
} else {
builder.append("(").append(LDAPConstants.OBJECT_CLASS).append(LDAPConstants.EQUAL).append("*").append(")");
conditions.add(conditionsBuilder.present(LDAPConstants.OBJECT_CLASS));
}
return builder;
return conditions;
}
@ -617,9 +608,9 @@ 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().toString(false);
String filter = "(" + EscapeStrategy.DEFAULT.escape(rdn) + ")";
List<SearchResult> search = this.operationManager.search(ldapObject.getDn().getLdapName(), filter, Arrays.asList(uuidAttrName), SearchControls.OBJECT_SCOPE);
List<SearchResult> search = this.operationManager.search(ldapObject.getDn().getLdapName(),
new LDAPQueryConditionsBuilder().present(LDAPConstants.OBJECT_CLASS),
Arrays.asList(uuidAttrName), SearchControls.OBJECT_SCOPE);
Attribute id = search.get(0).getAttributes().get(getConfig().getUuidLDAPAttributeName());
if (id == null) {

View file

@ -24,8 +24,9 @@ import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.query.EscapeStrategy;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.storage.ldap.idm.store.ldap.extended.PasswordModifyRequest;
import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
import org.keycloak.truststore.TruststoreProvider;
@ -245,10 +246,10 @@ public class LDAPOperationManager {
return parentDn.getLdapName();
}
public List<SearchResult> search(final LdapName baseDN, final String filter, Collection<String> returningAttributes, int searchScope) throws NamingException {
public List<SearchResult> search(final LdapName baseDN, final Condition condition, Collection<String> returningAttributes, int searchScope) throws NamingException {
final List<SearchResult> result = new ArrayList<>();
final SearchControls cons = getSearchControls(returningAttributes, searchScope);
final String filter = condition.toFilter();
try {
return execute(new LdapOperation<List<SearchResult>>() {
@ -285,9 +286,10 @@ public class LDAPOperationManager {
}
}
public List<SearchResult> searchPaginated(final LdapName baseDN, final String filter, final LDAPQuery identityQuery) throws NamingException {
public List<SearchResult> searchPaginated(final LdapName baseDN, final Condition condition, final LDAPQuery identityQuery) throws NamingException {
final List<SearchResult> result = new ArrayList<>();
final SearchControls cons = getSearchControls(identityQuery.getReturningLdapAttributes(), identityQuery.getSearchScope());
final String filter = condition.toFilter();
// Very 1st page. Pagination context is not yet present
if (identityQuery.getPaginationContext() == null) {
@ -370,40 +372,29 @@ public class LDAPOperationManager {
return cons;
}
public String getFilterById(String id) {
StringBuilder filter = new StringBuilder();
filter.insert(0, "(&");
public Condition getFilterById(String id) {
LDAPQueryConditionsBuilder builder = new LDAPQueryConditionsBuilder();
Condition conditionId;
if (this.config.isObjectGUID()) {
byte[] objectGUID = LDAPUtil.encodeObjectGUID(id);
filter.append("(objectClass=*)(").append(
getUuidAttributeName()).append(LDAPConstants.EQUAL)
.append(LDAPUtil.convertObjectGUIDToByteString(
objectGUID)).append(")");
conditionId = builder.equal(getUuidAttributeName(), objectGUID);
} else if (this.config.isEdirectoryGUID()) {
filter.append("(objectClass=*)(").append(getUuidAttributeName().toUpperCase())
.append(LDAPConstants.EQUAL
).append(LDAPUtil.convertGUIDToEdirectoryHexString(id)).append(")");
byte[] objectGUID = LDAPUtil.encodeObjectEDirectoryGUID(id);
conditionId = builder.equal(getUuidAttributeName(), objectGUID);
} else {
filter.append("(objectClass=*)(").append(getUuidAttributeName()).append(LDAPConstants.EQUAL)
.append(EscapeStrategy.DEFAULT.escape(id)).append(")");
conditionId = builder.equal(getUuidAttributeName(), id);
}
if (config.getCustomUserSearchFilter() != null) {
filter.append(config.getCustomUserSearchFilter());
return builder.andCondition(new Condition[]{conditionId, builder.addCustomLDAPFilter(config.getCustomUserSearchFilter())});
} else {
return conditionId;
}
filter.append(")");
String ldapIdFilter = filter.toString();
logger.tracef("Using filter for lookup user by LDAP ID: %s", ldapIdFilter);
return ldapIdFilter;
}
public SearchResult lookupById(final LdapName baseDN, final String id, final Collection<String> returningAttributes) {
final String filter = getFilterById(id);
final String filter = getFilterById(id).toFilter();
try {
final SearchControls cons = getSearchControls(returningAttributes, this.config.getSearchScope());

View file

@ -17,6 +17,7 @@
package org.keycloak.storage.ldap.idm.store.ldap;
import java.io.ByteArrayOutputStream;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig;
@ -131,6 +132,23 @@ public class LDAPUtil {
return result.toString().toUpperCase();
}
/**
* Converts the EDirectory GUID string into the byte array.
* @param guid
* @return
*/
public static byte[] encodeObjectEDirectoryGUID(String guid) {
String withoutDash = guid.replace("-", "");
ByteArrayOutputStream result = new ByteArrayOutputStream();
for (int i = 0; i < withoutDash.length(); i++) {
String byteStr = new StringBuilder().append(withoutDash.charAt(i)).append(withoutDash.charAt(++i)).toString();
result.write(Integer.parseInt(byteStr, 16));
}
return result.toByteArray();
}
/**
* <p>Encode a string representing the display value of the <code>objectGUID</code> attribute retrieved from Active
* Directory.</p>

View file

@ -25,7 +25,6 @@ import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.EscapeStrategy;
import org.keycloak.storage.ldap.idm.query.internal.EqualCondition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
@ -229,9 +228,7 @@ public class FullNameLDAPStorageMapper extends AbstractLDAPStorageMapper {
return;
}
EscapeStrategy escapeStrategy = firstNameCondition != null ? firstNameCondition.getEscapeStrategy() : lastNameCondition.getEscapeStrategy();
EqualCondition fullNameCondition = new EqualCondition(ldapFullNameAttrName, fullName, escapeStrategy);
EqualCondition fullNameCondition = new EqualCondition(ldapFullNameAttrName, fullName);
query.addWhereCondition(fullNameCondition);
}

View file

@ -26,15 +26,6 @@ import org.keycloak.storage.ldap.idm.query.EscapeStrategy;
*/
public class EscapeTest {
@Test
public void testEscapingExceptAsterisk() {
String text = "Véronique* Martin(john)second\\fff//eee\u0000";
Assert.assertEquals(EscapeStrategy.DEFAULT_EXCEPT_ASTERISK.escape(text), "V\\c3\\a9ronique* Martin\\28john\\29second\\5cfff//eee\\00");
text = "Hi This is a test #çà";
Assert.assertEquals(EscapeStrategy.DEFAULT_EXCEPT_ASTERISK.escape(text), "Hi This is a test #\\c3\\a7\\c3\\a0");
}
@Test
public void testEscaping() {
String text = "Véronique* Martin(john)second\\fff//eee\u0000";

View file

@ -30,4 +30,12 @@ public class LDAPUtilTest {
String decodeObjectGUID = LDAPUtil.decodeObjectGUID(bytes);
Assert.assertEquals(displayGUID, decodeObjectGUID);
}
@Test
public void testEncodeEDirectoryGUID() {
String guid = "bcdf4a91-ccb1-ae49-a18f-bcdf4a91ccff";
byte[] bytes = LDAPUtil.encodeObjectEDirectoryGUID(guid);
String decodeObjectGUID = LDAPUtil.decodeGuid(bytes);
Assert.assertEquals(guid, decodeObjectGUID);
}
}