Polishing.

Refine Javadoc. Move LdapEncoder lookup into LdapParameters. Eagerly instantiate LdapEncoder. Refine method naming.

Add NameEncoder and LikeEncoder for simplified usage.

See #509
Original pull request: #518
This commit is contained in:
Mark Paluch
2024-10-24 15:13:10 +02:00
parent de3b8ffc5f
commit 877c58387e
8 changed files with 177 additions and 65 deletions

View File

@@ -24,18 +24,34 @@ import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
* Allows passing of custom {@link LdapEncoder}.
* Annotation which indicates that a method parameter should be encoded using a specific {@link LdapEncoder} for a
* repository query method invocation.
* <p>
* If no {@link LdapEncoder} is configured, bound method parameters are encoded using
* {@link org.springframework.ldap.support.LdapEncoder#filterEncode(String)}. The default encoder considers chars such
* as {@code *} (asterisk) to be encoded which might interfere with the intent of running a Like query. Since Spring
* Data LDAP doesn't parse queries it is up to you to decide which encoder to use.
* <p>
* {@link LdapEncoder} implementations must declare a no-args constructor so they can be instantiated during repository
* initialization.
* <p>
* Note that parameter encoding applies only to parameters that are directly bound to a query. Parameters used in Value
* Expressions (SpEL, Configuration Properties) are not considered for encoding and must be encoded properly by using
* SpEL Method invocations or a SpEL Extension.
*
* @author Marcin Grzejszczak
* @since 3.5.0
* @author Mark Paluch
* @since 3.5
* @see LdapEncoder.LikeEncoder
* @see LdapEncoder.NameEncoder
*/
@Target(ElementType.PARAMETER)
@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LdapEncode {
/**
* {@link LdapEncoder} to instantiate to encode query parameters.
* {@link LdapEncoder} to encode query parameters.
*
* @return {@link LdapEncoder} class
*/
@@ -43,7 +59,7 @@ public @interface LdapEncode {
Class<? extends LdapEncoder> value();
/**
* {@link LdapEncoder} to instantiate to encode query parameters.
* {@link LdapEncoder} to encode query parameters.
*
* @return {@link LdapEncoder} class
*/

View File

@@ -15,18 +15,70 @@
*/
package org.springframework.data.ldap.repository;
import org.springframework.util.ObjectUtils;
/**
* Allows plugging in custom encoding for {@link LdapEncode}.
* Strategy interface to escape values for use in LDAP filters.
* <p>
* Accepts an LDAP filter value to be encoded (escaped) for String-based LDAP query usage as LDAP queries do not feature
* an out-of-band parameter binding mechanism.
* <p>
* Make sure that your implementation escapes special characters in the value adequately to prevent injection attacks.
*
* @author Marcin Grzejszczak
* @since 3.5.0
* @author Mark Paluch
* @since 3.5
*/
public interface LdapEncoder {
/**
* Escape a value for use in a filter.
* @param value the value to escape.
* @return a properly escaped representation of the supplied value.
* Encode a value for use in a filter.
*
* @param value the value to encode.
* @return a properly encoded representation of the supplied value.
*/
String filterEncode(String value);
String encode(String value);
/**
* {@link LdapEncoder} using {@link org.springframework.ldap.support.LdapEncoder#nameEncode(String)}. Encodes a value
* for use with a DN. Escapes for LDAP, not JNDI!
*/
class NameEncoder implements LdapEncoder {
@Override
public String encode(String value) {
return org.springframework.ldap.support.LdapEncoder.nameEncode(value);
}
}
/**
* Escape a value for use in a filter retaining asterisks ({@code *}) for like/contains searches.
*/
class LikeEncoder implements LdapEncoder {
@Override
public String encode(String value) {
if (ObjectUtils.isEmpty(value)) {
return value;
}
String[] substrings = value.split("\\*", -2);
if (substrings.length == 1) {
return org.springframework.ldap.support.LdapEncoder.filterEncode(substrings[0]);
}
StringBuilder buff = new StringBuilder();
for (int i = 0; i < substrings.length; i++) {
buff.append(org.springframework.ldap.support.LdapEncoder.filterEncode(substrings[i]));
if (i < substrings.length - 1) {
buff.append("*");
}
}
return buff.toString();
}
}
}

View File

@@ -26,6 +26,10 @@ import org.springframework.ldap.query.SearchScope;
/**
* Annotation for use in {@link org.springframework.data.ldap.repository.LdapRepository} declarations to create
* automatic query methods based on statically defined queries.
* <p>
* LDAP queries support parameter bindings (by-index using ?0, by-name using :someParam), SpEL expressions, and
* Configuration Properties to create dynamic queries. All non-String arguments are converted to their
* {@link Object#toString() toString} representation to render the query.
*
* @author Mattias Hellborg Arthursson
*/
@@ -36,7 +40,7 @@ public @interface Query {
/**
* Search base, to be used as input to {@link org.springframework.ldap.query.LdapQueryBuilder#base(javax.naming.Name)}
* .
* . Supports parameter binding.
*
* @return the search base, default is {@link org.springframework.ldap.support.LdapUtils#emptyLdapName()}
*/
@@ -44,7 +48,7 @@ public @interface Query {
/**
* The filter format string, to be used as input to
* {@link org.springframework.ldap.query.LdapQueryBuilder#filter(String, Object...)}.
* {@link org.springframework.ldap.query.LdapQueryBuilder#filter(String, Object...)}. Supports parameter binding.
*
* @return search filter, must be specified.
*/

View File

@@ -18,18 +18,22 @@ package org.springframework.data.ldap.repository.query;
import java.lang.reflect.Method;
import java.util.List;
import org.springframework.beans.BeanUtils;
import org.springframework.core.MethodParameter;
import org.springframework.data.geo.Distance;
import org.springframework.data.ldap.repository.LdapEncode;
import org.springframework.data.ldap.repository.LdapEncoder;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersSource;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
/**
* Custom extension of {@link Parameters} discovering additional
*
* @author Marcin Grzejszczak
* @since 3.5.0
* @since 3.5
*/
public class LdapParameters extends Parameters<LdapParameters, LdapParameters.LdapParameter> {
@@ -65,9 +69,10 @@ public class LdapParameters extends Parameters<LdapParameters, LdapParameters.Ld
*
* @author Marcin Grzejszczak
*/
static class LdapParameter extends Parameter {
protected static class LdapParameter extends Parameter {
final MethodParameter parameter;
private final @Nullable LdapEncoder ldapEncoder;
private final MethodParameter parameter;
/**
* Creates a new {@link LdapParameter}.
@@ -76,8 +81,29 @@ public class LdapParameters extends Parameters<LdapParameters, LdapParameters.Ld
* @param domainType must not be {@literal null}.
*/
LdapParameter(MethodParameter parameter, TypeInformation<?> domainType) {
super(parameter, domainType);
this.parameter = parameter;
LdapEncode encode = parameter.getParameterAnnotation(LdapEncode.class);
if (encode != null) {
this.ldapEncoder = BeanUtils.instantiateClass(encode.value());
} else {
this.ldapEncoder = null;
}
}
public boolean hasLdapEncoder() {
return ldapEncoder != null;
}
public LdapEncoder getLdapEncoder() {
if (ldapEncoder == null) {
throw new IllegalStateException("No LdapEncoder found for parameter " + parameter);
}
return ldapEncoder;
}
}

View File

@@ -52,7 +52,7 @@ class LdapQueryCreator extends AbstractQueryCreator<LdapQuery, ContainerCriteria
* @param tree must not be {@literal null}.
* @param entityType must not be {@literal null}.
* @param mapper must not be {@literal null}.
* @param values must not be {@literal null}.
* @param parameterAccessor must not be {@literal null}.
* @param inputProperties must not be {@literal null}.
*/
LdapQueryCreator(PartTree tree, Class<?> entityType, ObjectDirectoryMapper mapper,

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2024 the original author or authors.
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,6 +15,8 @@
*/
package org.springframework.data.ldap.repository.query;
import static org.springframework.data.ldap.repository.query.StringBasedQuery.BindingContext.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -25,10 +27,8 @@ import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.beans.BeanUtils;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.ldap.repository.LdapEncode;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.Parameters;
@@ -39,12 +39,11 @@ import org.springframework.ldap.support.LdapEncoder;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import static org.springframework.data.ldap.repository.query.StringBasedQuery.BindingContext.ParameterBinding;
/**
* String-based Query abstracting a query with parameter bindings.
*
* @author Marcin Grzejszczak
* @author Mark Paluch
* @since 3.5
*/
class StringBasedQuery {
@@ -79,8 +78,7 @@ class StringBasedQuery {
for (ParameterBinding binding : queryParameterBindings) {
if (binding.isExpression()) {
dependencies
.add(binding.getRequiredExpression().getExpressionDependencies());
dependencies.add(binding.getRequiredExpression().getExpressionDependencies());
}
}
@@ -256,7 +254,6 @@ class StringBasedQuery {
*/
static class ParameterBinder {
private static final String ARGUMENT_PLACEHOLDER = "?_param_?";
private static final Pattern ARGUMENT_PLACEHOLDER_PATTERN = Pattern.compile(Pattern.quote(ARGUMENT_PLACEHOLDER));
@@ -358,15 +355,20 @@ class StringBasedQuery {
if (binding.isExpression()) {
return evaluator.apply(binding.getRequiredExpression());
}
Object value = binding.isNamed()
? parameterAccessor.getBindableValue(getParameterIndex(parameters, binding.getRequiredParameterName()))
: parameterAccessor.getBindableValue(binding.getParameterIndex());
int index = binding.isNamed() ? getParameterIndex(parameters, binding.getRequiredParameterName())
: binding.getParameterIndex();
Object value = parameterAccessor.getBindableValue(index);
if (value == null) {
return null;
}
return binding.getEncodedValue(parameters, value);
String toString = value.toString();
LdapParameters.LdapParameter parameter = parameters.getBindableParameter(index);
return parameter.hasLdapEncoder() ? parameter.getLdapEncoder().encode(toString)
: LdapEncoder.filterEncode(toString);
}
private int getParameterIndex(Parameters<?, ?> parameters, String parameterName) {
@@ -413,38 +415,6 @@ class StringBasedQuery {
return new ParameterBinding(-1, null, name);
}
Object getEncodedValue(LdapParameters ldapParameters, Object value) {
org.springframework.data.ldap.repository.LdapEncoder encoder = encoderForParameter(ldapParameters);
if (encoder == null) {
return LdapEncoder.filterEncode(value.toString());
}
return encoder.filterEncode(value.toString());
}
@Nullable
org.springframework.data.ldap.repository.LdapEncoder encoderForParameter(LdapParameters ldapParameters) {
for (LdapParameters.LdapParameter parameter : ldapParameters) {
if (isByName(parameter) || isByIndex(parameter)) {
LdapEncode ldapEncode = parameter.parameter.getParameterAnnotation(LdapEncode.class);
if (ldapEncode == null) {
return null;
}
Class<? extends org.springframework.data.ldap.repository.LdapEncoder> encoder = ldapEncode.value();
return BeanUtils.instantiateClass(encoder);
}
}
return null;
}
private boolean isByIndex(LdapParameters.LdapParameter parameter) {
return parameterIndex != -1 && parameter.getIndex() == parameterIndex;
}
private boolean isByName(LdapParameters.LdapParameter parameter) {
return parameterName != null && parameterName.equals(parameter.getName().orElse(null));
}
boolean isNamed() {
return (parameterName != null);
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2024 the original author or authors.
*
* 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
*
* https://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.springframework.data.ldap.repository;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link LdapEncoder}.
*
* @author Mark Paluch
*/
class LdapEncoderUnitTests {
@Test // GH-509
void shouldEncodeName() {
String result = new LdapEncoder.NameEncoder().encode("# foo ,+\"\\<>; ");
assertThat(result).isEqualTo("\\# foo \\,\\+\\\"\\\\\\<\\>\\;\\ ");
}
@Test // GH-509
void shouldEncodeLikeFilter() {
String result = new LdapEncoder.LikeEncoder().encode("*hugo*ern(o)*");
assertThat(result).isEqualTo("*hugo*ern\\28o\\29*");
}
}

View File

@@ -15,14 +15,16 @@
*/
package org.springframework.data.ldap.repository.query;
import static org.assertj.core.api.Assertions.*;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.data.ldap.core.mapping.LdapMappingContext;
import org.springframework.data.ldap.repository.LdapEncoder;
import org.springframework.data.ldap.repository.LdapEncode;
import org.springframework.data.ldap.repository.LdapEncoder;
import org.springframework.data.ldap.repository.LdapRepository;
import org.springframework.data.ldap.repository.Query;
import org.springframework.data.mapping.model.EntityInstantiators;
@@ -32,8 +34,6 @@ import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.ldap.core.LdapOperations;
import org.springframework.ldap.query.LdapQuery;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link AnnotatedLdapRepositoryQuery}
*
@@ -122,7 +122,7 @@ class AnnotatedLdapRepositoryQueryUnitTests {
static class MyEncoder implements LdapEncoder {
@Override
public String filterEncode(String value) {
public String encode(String value) {
return value + "bar";
}
}