Skip to content

Commit

Permalink
Polishing.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mp911de committed Nov 18, 2024
1 parent de3b8ff commit 877c583
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,42 @@
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
*/
@AliasFor("encoder")
Class<? extends LdapEncoder> value();

/**
* {@link LdapEncoder} to instantiate to encode query parameters.
* {@link LdapEncoder} to encode query parameters.
*
* @return {@link LdapEncoder} class
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.
*/
String filterEncode(String value);
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();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
/**
* 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
*/
Expand All @@ -36,15 +40,15 @@

/**
* 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()}
*/
String base() default "";

/**
* 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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,22 @@
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> {

Expand Down Expand Up @@ -65,9 +69,10 @@ protected LdapParameters createFrom(List<LdapParameter> parameters) {
*
* @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}.
Expand All @@ -76,8 +81,29 @@ static class LdapParameter extends Parameter {
* @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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -25,10 +27,8 @@
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;
Expand All @@ -39,12 +39,11 @@
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 {
Expand Down Expand Up @@ -79,8 +78,7 @@ private ExpressionDependencies createExpressionDependencies() {

for (ParameterBinding binding : queryParameterBindings) {
if (binding.isExpression()) {
dependencies
.add(binding.getRequiredExpression().getExpressionDependencies());
dependencies.add(binding.getRequiredExpression().getExpressionDependencies());
}
}

Expand Down Expand Up @@ -256,7 +254,6 @@ private static Matcher findNextBindingOrExpression(String input, int startPositi
*/
static class ParameterBinder {


private static final String ARGUMENT_PLACEHOLDER = "?_param_?";
private static final Pattern ARGUMENT_PLACEHOLDER_PATTERN = Pattern.compile(Pattern.quote(ARGUMENT_PLACEHOLDER));

Expand Down Expand Up @@ -358,15 +355,20 @@ private Object getParameterValueForBinding(ParameterBinding binding) {
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) {
Expand Down Expand Up @@ -413,38 +415,6 @@ static ParameterBinding named(String name) {
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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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*");
}
}
Loading

0 comments on commit 877c583

Please sign in to comment.