diff --git a/core/src/main/java/org/springframework/ldap/query/LdapQueryBuilder.java b/core/src/main/java/org/springframework/ldap/query/LdapQueryBuilder.java index b74cc9ed..8933c164 100644 --- a/core/src/main/java/org/springframework/ldap/query/LdapQueryBuilder.java +++ b/core/src/main/java/org/springframework/ldap/query/LdapQueryBuilder.java @@ -17,9 +17,12 @@ package org.springframework.ldap.query; import org.springframework.ldap.filter.Filter; +import org.springframework.ldap.filter.HardcodedFilter; +import org.springframework.ldap.support.LdapEncoder; import org.springframework.ldap.support.LdapUtils; import javax.naming.Name; +import java.text.MessageFormat; /** * Builder of LdapQueries. Start with a call to {@link #query()}, proceed with specifying the @@ -150,6 +153,7 @@ public class LdapQueryBuilder implements LdapQuery { * * @param attribute The attribute that the first part of the filter should test against. * @return A ConditionCriteria instance for specifying the compare operation. + * @throws IllegalStateException if a filter has already been specified. */ public ConditionCriteria where(String attribute) { assertFilterNotStarted(); @@ -157,6 +161,44 @@ public class LdapQueryBuilder implements LdapQuery { return new DefaultConditionCriteria(rootContainer, attribute); } + /** + * Specify a hardcoded filter. Please note that using this method, the filter string will not be + * validated or escaped in any way. Never use direct user input and use it concatenating strings + * to use as LDAP filters. Doing so opens up for "LDAP injection", where malicious user + * may inject specifically constructed data to form filters at their convenience. When user input is used + * consider using {@link #where(String)} or {@link #filter(String, String...)} instead. + * + * @param hardcodedFilter The hardcoded filter string to use in the search. + * @return this instance. + * @throws IllegalStateException if a filter has already been specified. + */ + public LdapQuery filter(String hardcodedFilter) { + assertFilterNotStarted(); + rootContainer = new DefaultContainerCriteria(this); + rootContainer.append(new HardcodedFilter(hardcodedFilter)); + return this; + } + + /** + * Specify a hardcoded filter using the specified parameters. The parameters will be properly encoded using + * {@link LdapEncoder#filterEncode(String)} to make sure no malicious data gets through. The filterFormat + * String should be formatted for input to {@link MessageFormat#format(String, Object...)}. + * + * @param filterFormat the filter format string, formatted for input to {@link MessageFormat#format(String, Object...)}. + * @param params the parameters that will be used for building the final filter. All parameters will be properly encoded. + * @return this instance. + * @throws IllegalStateException if a filter has already been specified. + */ + public LdapQuery filter(String filterFormat, String... params) { + Object[] encodedParams = new String[params.length]; + + for (int i=0; i < params.length; i++) { + encodedParams[i] = LdapEncoder.filterEncode(params[i]); + } + + return filter(MessageFormat.format(filterFormat, encodedParams)); + } + private void assertFilterNotStarted() { if(rootContainer != null) { throw new IllegalStateException("Invalid operation - filter condition specification already started"); diff --git a/core/src/test/java/org/springframework/ldap/query/LdapQueryBuilderTest.java b/core/src/test/java/org/springframework/ldap/query/LdapQueryBuilderTest.java index 33646101..e9410958 100644 --- a/core/src/test/java/org/springframework/ldap/query/LdapQueryBuilderTest.java +++ b/core/src/test/java/org/springframework/ldap/query/LdapQueryBuilderTest.java @@ -59,6 +59,34 @@ public class LdapQueryBuilderTest { assertEquals("(cn=*)", result.filter().encode()); } + @Test + public void buildHardcodedFilter() { + LdapQuery result = query().filter("(cn=Person*)"); + + assertEquals("(cn=Person*)", result.filter().encode()); + } + + @Test(expected = IllegalStateException.class) + public void verifyThatHardcodedFilterFailsIfFilterAlreadySpecified() { + LdapQueryBuilder query = query(); + query.where("sn").is("Doe"); + query.filter("(cn=Person*)"); + } + + @Test(expected = IllegalStateException.class) + public void verifyThatFilterFormatFailsIfFilterAlreadySpecified() { + LdapQueryBuilder query = query(); + query.where("sn").is("Doe"); + query.filter("(|(cn={0})(cn={1}))", "Person*", "Parson*"); + } + + @Test + public void buildFilterFormat() { + LdapQuery result = query().filter("(|(cn={0})(cn={1}))", "Person*", "Parson*"); + + assertEquals("(|(cn=Person\\2a)(cn=Parson\\2a))", result.filter().encode()); + } + @Test public void testBuildSimpleAnd() { LdapQuery query = query() diff --git a/odm/src/main/java/org/springframework/ldap/odm/core/OdmManager.java b/odm/src/main/java/org/springframework/ldap/odm/core/OdmManager.java index ab2b512a..f146d66a 100755 --- a/odm/src/main/java/org/springframework/ldap/odm/core/OdmManager.java +++ b/odm/src/main/java/org/springframework/ldap/odm/core/OdmManager.java @@ -1,9 +1,26 @@ +/* + * Copyright 2005-2013 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 + * + * 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.springframework.ldap.odm.core; -import java.util.List; +import org.springframework.ldap.query.LdapQuery; import javax.naming.Name; import javax.naming.directory.SearchControls; +import java.util.List; /** * The OdmManager interface provides generic CRUD (create/read/update/delete) @@ -13,6 +30,7 @@ import javax.naming.directory.SearchControls; * {@link org.springframework.ldap.odm.annotations}. * * @author Paul Harvey <paul.at.pauls-place.me.uk> + * @author Mattias Hellborg Arthursson * * @see org.springframework.ldap.odm.annotations.Entry * @see org.springframework.ldap.odm.annotations.Attribute @@ -94,4 +112,19 @@ public interface OdmManager { * @see LDAP: String Representation of Search Filters RFC. */ List search(Class clazz, Name base, String filter, SearchControls searchControls); + + /** + * Search for entries in the LDAP directory. + *

+ * Only those entries that both match the query search filter and + * are represented by the given Java class are returned. + * + * @param The Java type to return + * @param clazz The Java type to return + * @param query the LDAP query specification + * @return All matching entries. + * + * @exception org.springframework.ldap.NamingException on error. + */ + List search(Class clazz, LdapQuery query); } diff --git a/odm/src/main/java/org/springframework/ldap/odm/core/impl/OdmManagerImpl.java b/odm/src/main/java/org/springframework/ldap/odm/core/impl/OdmManagerImpl.java index d4993c19..bc2811f4 100755 --- a/odm/src/main/java/org/springframework/ldap/odm/core/impl/OdmManagerImpl.java +++ b/odm/src/main/java/org/springframework/ldap/odm/core/impl/OdmManagerImpl.java @@ -29,6 +29,8 @@ import org.springframework.ldap.filter.EqualsFilter; import org.springframework.ldap.odm.core.OdmException; import org.springframework.ldap.odm.core.OdmManager; import org.springframework.ldap.odm.typeconversion.ConverterManager; +import org.springframework.ldap.query.LdapQuery; +import org.springframework.ldap.query.SearchScope; import org.springframework.ldap.support.LdapUtils; import javax.naming.Name; @@ -52,6 +54,7 @@ import java.util.Set; * convert between Java and LDAP representations of attribute values. * * @author Paul Harvey <paul.at.pauls-place.me.uk> + * @author Mattias Hellborg Arthursson * */ public final class OdmManagerImpl implements OdmManager { @@ -272,11 +275,32 @@ public final class OdmManagerImpl implements OdmManager { return result; } - - /* - * (non-Javadoc) - * @see org.springframework.ldap.odm.core.OdmManager#findAll(javax.naming.Name, javax.naming.directory.SearchControls) - */ + @Override + public List search(Class clazz, LdapQuery query) { + SearchControls searchControls = new SearchControls(); + SearchScope searchScope = query.searchScope(); + if(searchScope == null) { + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + } else { + searchControls.setSearchScope(searchScope.getId()); + } + + Integer countLimit = query.countLimit(); + if(countLimit != null) { + searchControls.setCountLimit(countLimit); + } + + Integer timeLimit = query.timeLimit(); + if(timeLimit != null) { + searchControls.setCountLimit(timeLimit); + } + + // Defaults to null which means 'all', so if it's not set we're still good. + searchControls.setReturningAttributes(query.attributes()); + + return search(clazz, query.base(), query.filter().encode(), searchControls); + } + public List findAll(Class managedClass, Name base, SearchControls scope) { if (LOG.isDebugEnabled()) { LOG.debug(String.format("Searching for all Entries with objectClass=%1$s, with base=%2$s, scope=%3$s", diff --git a/odm/src/test/java/org/springframework/ldap/odm/test/TestLdap.java b/odm/src/test/java/org/springframework/ldap/odm/test/TestLdap.java index 346cc345..7286385c 100755 --- a/odm/src/test/java/org/springframework/ldap/odm/test/TestLdap.java +++ b/odm/src/test/java/org/springframework/ldap/odm/test/TestLdap.java @@ -67,6 +67,7 @@ import java.util.Set; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.springframework.ldap.query.LdapQueryBuilder.query; // Tests all OdmManager functions public final class TestLdap { @@ -361,6 +362,16 @@ public final class TestLdap { assertEquals("Hartnell", foundPerson.getSurname()); } + @Test + public void verifySearchWithLdapQuery() { + List result = odmManager.search(Person.class, query().base(baseName).where("cn").is("William Hartnell")); + assertEquals(1, result.size()); + + Person foundPerson = result.get(0); + assertEquals("William Hartnell", foundPerson.getCn()); + assertEquals("Hartnell", foundPerson.getSurname()); + } + @Test public void updatePlainPerson() { List result = odmManager.search(PlainPerson.class, baseName, "(cn=William Hartnell)", searchControls); diff --git a/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/SearchForPeople.java b/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/SearchForPeople.java index f23a211a..05c5b135 100755 --- a/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/SearchForPeople.java +++ b/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/SearchForPeople.java @@ -1,3 +1,19 @@ +/* + * Copyright 2005-2013 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 + * + * 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.springframework.ldap.odm.sample; import org.springframework.context.ApplicationContext; @@ -5,17 +21,14 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.ldap.odm.core.OdmManager; import org.springframework.ldap.support.LdapUtils; -import javax.naming.directory.SearchControls; import javax.naming.ldap.LdapName; import java.util.List; +import static org.springframework.ldap.query.LdapQueryBuilder.query; + // A very simple example - just showing how little code you actually need to write // when using Spring LDAP ODM public class SearchForPeople { - private static final SearchControls searchControls = - new SearchControls(SearchControls.SUBTREE_SCOPE, 100, 10000, - null, true, false); - private static final LdapName baseDn = LdapUtils.newLdapName("o=Whoniverse"); private static void print(List personList) { @@ -31,7 +44,7 @@ public class SearchForPeople { OdmManager odmManager = (OdmManager)context.getBean("odmManager"); // Find people with a surname of Harvey - List searchResults = odmManager.search(SimplePerson.class, baseDn, "sn=Harvey", searchControls); + List searchResults = odmManager.search(SimplePerson.class, query().base(baseDn).where("sn").is("Harvey")); // Print the results print(searchResults); diff --git a/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/SimplePerson.java b/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/SimplePerson.java index 91ac26fb..e1b55a57 100755 --- a/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/SimplePerson.java +++ b/samples/simple-odm/src/main/java/org/springframework/ldap/odm/sample/SimplePerson.java @@ -1,19 +1,34 @@ +/* + * Copyright 2005-2013 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 + * + * 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.springframework.ldap.odm.sample; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.HashSet; -import java.util.ArrayList; - -import javax.naming.Name; - -import static org.springframework.ldap.odm.annotations.Attribute.*; - import org.springframework.ldap.odm.annotations.Attribute; import org.springframework.ldap.odm.annotations.Entry; import org.springframework.ldap.odm.annotations.Id; +import javax.naming.Name; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; + +import static org.springframework.ldap.odm.annotations.Attribute.Type; + /** * Automatically generated to represent the LDAP object classes * "person", "top". diff --git a/src/docbkx/advancedqueries.xml b/src/docbkx/advancedqueries.xml index 08e35fff..33163341 100644 --- a/src/docbkx/advancedqueries.xml +++ b/src/docbkx/advancedqueries.xml @@ -108,4 +108,28 @@ List<Person> persons = ldapTemplate.search( + + Hardcoded Filters + + There are occasions when you will want to specify a hardcoded filter as input to an LdapQuery. + LdapQueryBuilder has two methods for this purpose: + + + filter(String hardcodedFilter) - uses the specified string as filter. + Note that the specified input string will not be touched in any way, meaning that this method is not + particularly well suited if you are building filters from user input. + + + filter(String filterFormat, String... params) - uses the specified string as input + to MessageFormat, properly encoding the parameters and inserting them at the + specified places in the filter string. + + + + + You cannot mix the hardcoded filter methods with the where approach described above; it's + either one or the other. What this means is that if you specified a filter using filter() + you will get an exception if you try to call where afterwards. + + \ No newline at end of file diff --git a/src/docbkx/odm.xml b/src/docbkx/odm.xml index 6da56fb8..3eb30d57 100644 --- a/src/docbkx/odm.xml +++ b/src/docbkx/odm.xml @@ -50,6 +50,9 @@ <T> List<T> search(Class<T> clazz, Name base, String filter, SearchControls searchControls) + + <T> List<T> search(Class<T> clazz, LdapQuery query) + A reference to an implementation of this interface can be obtained