From 8f48118990f8a9e455c8d62617e3af65b9598869 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 16 Mar 2023 12:21:36 -0600 Subject: [PATCH] Add LdapClient Closes gh-675 --- .../springframework/ldap/core/LdapClient.java | 569 ++++++++++++++ .../ldap/core/SpringLdapClient.java | 682 +++++++++++++++++ .../ldap/core/SpringLdapClientBuilder.java | 93 +++ .../ldap/query/LdapQueryBuilder.java | 20 +- .../ldap/core/SpringLdapClientListTest.java | 388 ++++++++++ .../ldap/core/SpringLdapClientLookupTest.java | 239 ++++++ .../ldap/core/SpringLdapClientRenameTest.java | 124 +++ .../ldap/core/SpringLdapClientTest.java | 709 ++++++++++++++++++ src/docs/asciidoc/index.adoc | 247 +++--- .../ldap/itest/PersonAttributesMapper.java | 4 +- .../ldap/itest/PersonContextMapper.java | 4 +- .../SpringLdapClientAuthenticationITest.java | 179 +++++ .../SpringLdapClientBindUnbindITest.java | 167 +++++ .../ldap/itest/SpringLdapClientListITest.java | 176 +++++ .../itest/SpringLdapClientLookupITest.java | 189 +++++ .../SpringLdapClientLookupMultiRdnITest.java | 84 +++ .../itest/SpringLdapClientModifyITest.java | 251 +++++++ .../SpringLdapClientRecursiveDeleteITest.java | 129 ++++ .../itest/SpringLdapClientRenameITest.java | 102 +++ .../SpringLdapClientSearchResultITest.java | 543 ++++++++++++++ .../resources/conf/ldapClientTestContext.xml | 13 + 21 files changed, 4784 insertions(+), 128 deletions(-) create mode 100644 core/src/main/java/org/springframework/ldap/core/LdapClient.java create mode 100644 core/src/main/java/org/springframework/ldap/core/SpringLdapClient.java create mode 100644 core/src/main/java/org/springframework/ldap/core/SpringLdapClientBuilder.java create mode 100644 core/src/test/java/org/springframework/ldap/core/SpringLdapClientListTest.java create mode 100644 core/src/test/java/org/springframework/ldap/core/SpringLdapClientLookupTest.java create mode 100644 core/src/test/java/org/springframework/ldap/core/SpringLdapClientRenameTest.java create mode 100644 core/src/test/java/org/springframework/ldap/core/SpringLdapClientTest.java create mode 100644 test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientAuthenticationITest.java create mode 100644 test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientBindUnbindITest.java create mode 100644 test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientListITest.java create mode 100644 test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientLookupITest.java create mode 100644 test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientLookupMultiRdnITest.java create mode 100644 test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientModifyITest.java create mode 100644 test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientRecursiveDeleteITest.java create mode 100644 test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientRenameITest.java create mode 100644 test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientSearchResultITest.java create mode 100644 test/integration-tests/src/test/resources/conf/ldapClientTestContext.xml diff --git a/core/src/main/java/org/springframework/ldap/core/LdapClient.java b/core/src/main/java/org/springframework/ldap/core/LdapClient.java new file mode 100644 index 000000000..1f6063838 --- /dev/null +++ b/core/src/main/java/org/springframework/ldap/core/LdapClient.java @@ -0,0 +1,569 @@ +/* + * Copyright 2005-2023 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.ldap.core; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import javax.naming.Name; +import javax.naming.NameNotFoundException; +import javax.naming.SizeLimitExceededException; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.ModificationItem; +import javax.naming.directory.SearchControls; + +import org.springframework.LdapDataEntry; +import org.springframework.ldap.NameAlreadyBoundException; +import org.springframework.ldap.PartialResultException; +import org.springframework.ldap.query.LdapQuery; +import org.springframework.ldap.query.LdapQueryBuilder; + +/** + * An LDAP Client + * + * @author Josh Cummings + * @since 3.1 + * @see LdapMapperClient + */ +public interface LdapClient { + + /** + * Start building a request for all children of the + * given {@code name}. + * + * @param name the distinguished name to find children for + * @return a spec for specifying the list parameters + */ + ListSpec list(String name); + + /** + * Start building a request for all children of the + * given {@code name}. + * + * @param name the distinguished name to find children for + * @return a spec for specifying the list parameters + */ + ListSpec list(Name name); + + /** + * Start building a request for all children of the + * given {@code name}. The result will include the object bound to + * the name. + * + * @param name the distinguished name to find children for + * @return a spec for specifying the list parameters + */ + ListBindingsSpec listBindings(String name); + + /** + * Start building a request for all children of the + * given {@code name}. The result will include the object bound to + * the name. + * + * @param name the distinguished name to find children for + * @return a spec for specifying the list parameters + */ + ListBindingsSpec listBindings(Name name); + + /** + * Start building a search request. + * + * @return a spec for specifying the search parameters + */ + SearchSpec search(); + + /** + * Start building an authentication request. + * + * @return a spec for specifying the authentication parameters + */ + AuthenticateSpec authenticate(); + + /** + * Start building a bind request, using the given {@code name} + * as the identifier. + * + * @return a spec for specifying the bind parameters + */ + BindSpec bind(String name); + + /** + * Start building a bind or rebind request, using the given {@code name} + * as the identifier. + * + * @return a spec for specifying the bind parameters + */ + BindSpec bind(Name name); + + /** + * Start building a request to modify name or attributes of an entry, using the given {@code name} + * as the identifier. + * + *

+ * Note that a {@link #modify(Name)} is different from a rebind in that + * entries are changed instead of removed and recreated. + * + *

+ * A change in name uses LDAP's {@link DirContext#rename} function. + * A change in attributes uses LDAP's {@link DirContext#modifyAttributes} function. + * The {@code rename} action is optimistically performed before the {@code modify} function. + * A rollback of the name is attempted in the event that attribute modification fails. + * + * @param name the name of the entry to modify + * @return a spec for specifying the modify parameters + */ + ModifySpec modify(String name); + + /** + * Start building a request to modify name or attributes of an entry, using the given {@code name} + * as the identifier. + * + *

+ * Note that a {@link #modify(Name)} is different from a rebind in that + * entries are changed instead of removed and recreated. + * + *

+ * A change in name uses LDAP's {@link DirContext#rename} function. + * A change in attributes uses LDAP's {@link DirContext#modifyAttributes} function. + * The {@code rename} action is optimistically performed before the {@code modify} function. + * A rollback of the name is attempted in the event that attribute modification fails. + * + * @param name the name of the entry to modify + * @return a spec for specifying the modify parameters + */ + ModifySpec modify(Name name); + + /** + * Start building a request to remove the {@code name} entry. + * + * @param name the name of the entry to remove + * @return a spec for specifying the unbind parameters + */ + UnbindSpec unbind(String name); + + /** + * Start building a request to remove the {@code name} entry. + * + * @param name the name of the entry to remove + * @return a spec for specifying the unbind parameters + */ + UnbindSpec unbind(Name name); + + /** + * Return a builder to create a new {@code LdapClient} whose settings are + * replicated from the current {@code LdapClient}. + */ + Builder mutate(); + + + // Static, factory methods + + /** + * Create an instance of {@link LdapClient} + * @param contextSource the {@link ContextSource} for all requests + * @see #builder() + */ + static LdapClient create(ContextSource contextSource) { + return new SpringLdapClientBuilder().contextSource(contextSource).build(); + } + + /** + * Obtain a {@code LdapClient} builder. + */ + static LdapClient.Builder builder() { + return new SpringLdapClientBuilder(); + } + + + /** + * A mutable builder for creating an {@link LdapClient}. + */ + interface Builder { + + /** + * Use this {@link ContextSource} + * @return the {@link Builder} for further customizations + */ + Builder contextSource(ContextSource contextSource); + + /** + * Use this {@link Supplier} to generate a {@link SearchControls}. + * It should generate a new {@link SearchControls} on each call. + * @param searchControlsSupplier the {@link Supplier} to use + * @return the {@link Builder} for further customizations + */ + Builder defaultSearchControls(Supplier searchControlsSupplier); + + /** + * Whether to ignore the {@link org.springframework.ldap.PartialResultException}. + * Defaults to {@code true}. + * + * @param ignore whether to ignore the {@link PartialResultException} + * @return the {@link LdapClient.Builder} for further customizations + */ + Builder ignorePartialResultException(boolean ignore); + + /** + * Whether to ignore the {@link org.springframework.ldap.NameNotFoundException}. + * Defaults to {@code true}. + * + * @param ignore whether to ignore the {@link NameNotFoundException} + * @return the {@link LdapClient.Builder} for further customizations + */ + Builder ignoreNameNotFoundException(boolean ignore); + + /** + * Whether to ignore the {@link org.springframework.ldap.SizeLimitExceededException}. + * Defaults to {@code true}. + * + * @param ignore whether to ignore the {@link SizeLimitExceededException} + * @return the {@link LdapClient.Builder} for further customizations + */ + Builder ignoreSizeLimitExceededException(boolean ignore); + + /** + * Apply the given {@code Consumer} to this builder instance. + *

This can be useful for applying pre-packaged customizations. + * @param builderConsumer the consumer to apply + */ + Builder apply(Consumer builderConsumer); + + /** + * Clone this {@code LdapClient.Builder}. + */ + Builder clone(); + + /** + * Build the {@link LdapClient} instance. + */ + LdapClient build(); + } + + /** + * The specifications for the {@link #list} request. + */ + interface ListSpec { + /** + * Return the entry's children as a list of mapped results + * + * @param mapper the {@link NameClassPairMapper} strategy to mapping each search result + * @return the entry's children or an empty list + */ + List toList(NameClassPairMapper mapper); + + /** + * Return the entry's children as a stream of mapped results. Note that + * the {@link Stream} must be closed when done reading from it. + * + * @param mapper the {@link NameClassPairMapper} strategy to mapping each search result + * @return the entry's children or an empty stream + */ + Stream toStream(NameClassPairMapper mapper); + } + + /** + * The specifications for the {@link #listBindings} request. + */ + interface ListBindingsSpec { + /** + * Return the entry's children as a list of mapped results + * + * @param mapper the {@link NameClassPairMapper} strategy to mapping each search result + * @return the entry's children or an empty list + */ + List toList(NameClassPairMapper mapper); + + /** + * Return the entry's children as a list of mapped results + * + * @param mapper the {@link ContextMapper} strategy to mapping each search result + * @return the entry's children or an empty list + */ + List toList(ContextMapper mapper); + + /** + * Return the entry's children as a stream of mapped results. Note that + * the {@link Stream} must be closed when done reading from it. + * + * @param mapper the {@link NameClassPairMapper} strategy to mapping each search result + * @return the entry's children or an empty stream + */ + Stream toStream(NameClassPairMapper mapper); + + /** + * Return the entry's children as a stream of mapped results. Note that + * the {@link Stream} must be closed when done reading from it. + * + * @param mapper the {@link ContextMapper} strategy to mapping each search result + * @return the entry's children or an empty stream + */ + Stream toStream(ContextMapper mapper); + } + + /** + * The specifications for the {@link #search} request. + */ + interface SearchSpec { + /** + * The name to search for. This is a convenience method for + * creating an {@link LdapQuery} based only on the {@code name}. + * + * @param name the name to search for + * @return the {@link SearchSpec} for further configuration + */ + SearchSpec name(String name); + + /** + * The name to search for. This is a convenience method for + * creating an {@link LdapQuery} based only on the {@code name}. + * + * @param name the name to search for + * @return the {@link SearchSpec} for further configuration + */ + SearchSpec name(Name name); + + /** + * The no-filter query to execute. Or, that is, the filter is {@code (objectclass=*)}. + * + *

This is helpful when searching by name and needing to customize the {@link SearchControls} or the + * returned attribute set. + * + * @param consumer the consumer to alter a default query + * @return the {@link SearchSpec} for further configuration + */ + SearchSpec query(Consumer consumer); + + /** + * The query to execute. + * + * @param query the query to execute + * @return the {@link SearchSpec} for further configuration + */ + SearchSpec query(LdapQuery query); + + default O toEntry() { + ContextMapper cast = (ctx) -> (O) ctx; + return toObject(cast); + } + + /** + * Expect at most one search result, mapped by the given strategy. + * + *

Returns {@code null} if no result is found. + * + * @param mapper the {@link ContextMapper} strategy to use to map the result + * @return the single search result, or {@code null} if none was found + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if the result + * set contains more than one result + */ + O toObject(ContextMapper mapper); + + /** + * Expect at most one search result, mapped by the given strategy. + * + * @param mapper the {@link AttributesMapper} strategy to use to map the result + * @return the single search result, or {@code null} if none was found + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if the result + * set contains more than one result + */ + O toObject(AttributesMapper mapper); + + default List toEntryList() { + ContextMapper cast = (ctx) -> (O) ctx; + return toList(cast); + } + + /** + * Return a list of search results, each mapped by the given strategy. + * + * @param mapper the {@link ContextMapper} strategy to use to map the result + * @return the single search result, or empty list if none was found + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if the result + * set contains more than one result + */ + List toList(ContextMapper mapper); + + /** + * Return a list of search results, each mapped by the given strategy. + * + * @param mapper the {@link AttributesMapper} strategy to use to map the result + * @return the single search result, or empty list if none was found + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if the result + * set contains more than one result + */ + List toList(AttributesMapper mapper); + + default Stream toEntryStream() { + ContextMapper cast = (ctx) -> (O) ctx; + return toStream(cast); + } + + /** + * Return a stream of search results, each mapped by the given strategy. + * + * @param mapper the {@link ContextMapper} strategy to use to map the result + * @return the single search result, or empty stream if none was found + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if the result + * set contains more than one result + */ + Stream toStream(ContextMapper mapper); + + /** + * Return a stream of search results, each mapped by the given strategy. + * + * @param mapper the {@link AttributesMapper} strategy to use to map the result + * @return the single search result, or empty stream if none was found + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if the result + * set contains more than one result + */ + Stream toStream(AttributesMapper mapper); + } + + /** + * The specifications for the {@link #authenticate} request. + */ + interface AuthenticateSpec { + /** + * The query to authenticate + * + * @param query the query to authenticate + * @return the {@link AuthenticateSpec} for further configuration + */ + AuthenticateSpec query(LdapQuery query); + + /** + * The password to use + * + * @param password the password to use + * @return the {@link AuthenticateSpec} for further configuration + */ + AuthenticateSpec password(String password); + + /** + * Authenticate the query against the provided password + * + * @throws org.springframework.ldap.AuthenticationException if authentication fails or the query returns no results + */ + void execute(); + + /** + * Authenticate the query against the provided password. + * + * @param mapper a strategy for mapping the query results against another datasource + * @throws org.springframework.ldap.AuthenticationException if authentication fails or the query returns no results + */ + T execute(AuthenticatedLdapEntryContextMapper mapper); + } + + /** + * The specifications for the {@link #bind} request. + */ + interface BindSpec { + /** + * The object to associate with this binding. + * + *

+ * Note that this object is encoded into a set of attributes. If the object is + * of type {@link DirContext}, then it will be converted into attributes via + * {@link DirContext#getAttributes}. + * + * @param object the object to associate + * @return the {@link BindSpec} for further configuration + */ + BindSpec object(Object object); + + /** + * The attributes to associate with this binding. + * @param attributes the attributes + * @return the {@link BindSpec} for further configuration + */ + BindSpec attributes(Attributes attributes); + + /** + * Replace any existing binding with this one (equivalent to "rebind"). + * + *

+ * If {@code false}, then bind will throw a {@link NameAlreadyBoundException} if the entry + * already exists. + * + * @param replaceExisting whether to replace any existing entry + * @return the {@link BindSpec} for further configuration + */ + BindSpec replaceExisting(boolean replaceExisting); + + /** + * Bind the name, object, and attributes together + * + * @throws NameAlreadyBoundException if {@code name} is already bound and {@link #replaceExisting} is {@code false} + */ + void execute(); + } + + /** + * The specifications for the {@link #modify} request. + */ + interface ModifySpec { + /** + * The new name for this entry. + * + * @param name the new name + * @return the {@link ModifySpec} for further configuration + */ + ModifySpec name(String name); + + /** + * The new name for this entry. + * + * @param name the new name + * @return the {@link ModifySpec} for further configuration + */ + ModifySpec name(Name name); + + /** + * The attribute modifications to apply to this entry + * + * @param modifications the attribute modifications + * @return the {@link ModifySpec} for further configuration + */ + ModifySpec attributes(ModificationItem... modifications); + + /** + * Modify the name and attributes for this entry + */ + void execute(); + } + + /** + * The specifications for the {@link #unbind} request. + */ + interface UnbindSpec { + /** + * Delete all children related to this entry + * + * @param recursive whether to delete all children as well + * @return the {@link UnbindSpec} for further configuration + */ + UnbindSpec recursive(boolean recursive); + + /** + * Delete the entry + */ + void execute(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/springframework/ldap/core/SpringLdapClient.java b/core/src/main/java/org/springframework/ldap/core/SpringLdapClient.java new file mode 100644 index 000000000..5a91a803c --- /dev/null +++ b/core/src/main/java/org/springframework/ldap/core/SpringLdapClient.java @@ -0,0 +1,682 @@ +/* + * Copyright 2002-2023 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.ldap.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import javax.naming.Binding; +import javax.naming.Name; +import javax.naming.NameClassPair; +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.PartialResultException; +import javax.naming.SizeLimitExceededException; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.ModificationItem; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.LdapName; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.ldap.filter.HardcodedFilter; +import org.springframework.ldap.query.LdapQuery; +import org.springframework.ldap.query.LdapQueryBuilder; +import org.springframework.ldap.query.SearchScope; +import org.springframework.ldap.support.LdapUtils; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link LdapClient}. + * + * @author Josh Cummings + * @since 3.1 + */ +class SpringLdapClient implements LdapClient { + private final Logger logger = LoggerFactory.getLogger(SpringLdapClient.class); + + private static final boolean DONT_RETURN_OBJ_FLAG = false; + + private static final boolean RETURN_OBJ_FLAG = true; + + private final ContextSource contextSource; + + private final Supplier searchControlsSupplier; + + private boolean ignorePartialResultException = false; + + private boolean ignoreNameNotFoundException = false; + + private boolean ignoreSizeLimitExceededException = true; + + SpringLdapClient(ContextSource contextSource, Supplier searchControlsSupplier) { + this.contextSource = contextSource; + this.searchControlsSupplier = searchControlsSupplier; + } + + @Override + public ListSpec list(String name) { + return new SpringListSpec(LdapUtils.newLdapName(name)); + } + + @Override + public ListSpec list(Name name) { + return new SpringListSpec(LdapUtils.newLdapName(name)); + } + + @Override + public ListBindingsSpec listBindings(String name) { + return new SpringListBindingsSpec(LdapUtils.newLdapName(name)); + } + + @Override + public ListBindingsSpec listBindings(Name name) { + return new SpringListBindingsSpec(LdapUtils.newLdapName(name)); + } + + @Override + public LdapClient.SearchSpec search() { + return new SpringSearchSpec(); + } + + @Override + public LdapClient.AuthenticateSpec authenticate() { + return new SpringAuthenticateSpec(); + } + + @Override + public LdapClient.BindSpec bind(String name) { + return new SpringBindSpec(LdapUtils.newLdapName(name)); + } + + @Override + public LdapClient.BindSpec bind(Name name) { + return new SpringBindSpec(LdapUtils.newLdapName(name)); + } + + @Override + public ModifySpec modify(String name) { + return new SpringModifySpec(new DirContextAdapter(LdapUtils.newLdapName(name))); + } + + @Override + public ModifySpec modify(Name name) { + return new SpringModifySpec(new DirContextAdapter(LdapUtils.newLdapName(name))); + } + + @Override + public LdapClient.UnbindSpec unbind(String name) { + return new SpringUnbindSpec(LdapUtils.newLdapName(name)); + } + + /** + * {@inheritDoc} + */ + @Override + public LdapClient.UnbindSpec unbind(Name name) { + return new SpringUnbindSpec(LdapUtils.newLdapName(name)); + } + + /** + * {@inheritDoc} + */ + @Override + public LdapClient.Builder mutate() { + return new SpringLdapClientBuilder(this.contextSource, this.searchControlsSupplier); + } + + /** + * Ignore {@link PartialResultException}s. + * + * @param ignorePartialResultException whether to ignore {@link PartialResultException}s + */ + void setIgnorePartialResultException(boolean ignorePartialResultException) { + this.ignorePartialResultException = ignorePartialResultException; + } + + /** + * Ignore {@link NameNotFoundException}s. + * + * @param ignoreNameNotFoundException whether to ignore {@link NameNotFoundException}s + */ + void setIgnoreNameNotFoundException(boolean ignoreNameNotFoundException) { + this.ignoreNameNotFoundException = ignoreNameNotFoundException; + } + + + /** + * Ignore {@link SizeLimitExceededException}s. + * + * @param ignoreSizeLimitExceededException whether to ignore {@link SizeLimitExceededException}s + */ + void setIgnoreSizeLimitExceededException(boolean ignoreSizeLimitExceededException) { + this.ignoreSizeLimitExceededException = ignoreSizeLimitExceededException; + } + + private final class SpringListSpec implements LdapClient.ListSpec { + private final Name name; + + private SpringListSpec(Name name) { + this.name = name; + } + + @Override + public List toList(NameClassPairMapper mapper) { + ContextExecutor> executor = (ctx) -> ctx.list(this.name); + NamingEnumeration results = computeWithReadOnlyContext(executor); + return SpringLdapClient.this.toList(results, mapper::mapFromNameClassPair); + } + + @Override + public Stream toStream(NameClassPairMapper mapper) { + ContextExecutor> executor = (ctx) -> ctx.list(this.name); + NamingEnumeration results = computeWithReadOnlyContext(executor); + return SpringLdapClient.this.toStream(results, mapper::mapFromNameClassPair); + } + } + + private final class SpringListBindingsSpec implements LdapClient.ListBindingsSpec { + private final Name name; + + private SpringListBindingsSpec(Name name) { + this.name = name; + } + + @Override + public List toList(NameClassPairMapper mapper) { + ContextExecutor> executor = (ctx) -> ctx.listBindings(this.name); + NamingEnumeration results = computeWithReadOnlyContext(executor); + return SpringLdapClient.this.toList(results, mapper::mapFromNameClassPair); + } + + @Override + public List toList(ContextMapper mapper) { + ContextExecutor> executor = (ctx) -> ctx.listBindings(this.name); + NamingEnumeration results = computeWithReadOnlyContext(executor); + return SpringLdapClient.this.toList(results, function(mapper)); + } + + @Override + public Stream toStream(NameClassPairMapper mapper) { + ContextExecutor> executor = (ctx) -> ctx.listBindings(this.name); + NamingEnumeration results = computeWithReadOnlyContext(executor); + return SpringLdapClient.this.toStream(results, mapper::mapFromNameClassPair); + } + + @Override + public Stream toStream(ContextMapper mapper) { + ContextExecutor> executor = (ctx) -> ctx.listBindings(this.name); + NamingEnumeration results = computeWithReadOnlyContext(executor); + return SpringLdapClient.this.toStream(results, function(mapper)); + } + } + + private final class SpringAuthenticateSpec implements LdapClient.AuthenticateSpec { + LdapClient.SearchSpec search = new SpringSearchSpec(); + + char[] password; + + @Override + public LdapClient.AuthenticateSpec query(LdapQuery query) { + this.search.query(query); + return this; + } + + @Override + public LdapClient.AuthenticateSpec password(String password) { + this.password = password.toCharArray(); + return this; + } + + @Override + public void execute() { + execute((ctx, identification) -> ctx); + } + + @Override + public T execute(AuthenticatedLdapEntryContextMapper mapper) { + LdapEntryIdentificationContextMapper m = new LdapEntryIdentificationContextMapper(); + List identification = this.search.toList(m); + if (identification.size() == 0) { + throw new EmptyResultDataAccessException(1); + } + else if (identification.size() != 1) { + throw new IncorrectResultSizeDataAccessException(1, identification.size()); + } + DirContext ctx = null; + try { + String password = new String(this.password); + ctx = contextSource.getContext(identification.get(0).getAbsoluteName().toString(), password); + return mapper.mapWithContext(ctx, identification.get(0)); + } finally { + this.password = null; + closeContext(ctx); + } + } + } + + private final class SpringSearchSpec implements LdapClient.SearchSpec { + LdapQuery query = LdapQueryBuilder.query().filter("(objectClass=*)"); + + SearchControls controls; + + @Override + public SpringSearchSpec name(String name) { + return query((builder) -> builder.base(name).searchScope(SearchScope.OBJECT)); + } + + @Override + public SpringSearchSpec name(Name name) { + return query((builder) -> builder.base(name).searchScope(SearchScope.OBJECT)); + } + + public SpringSearchSpec query(Consumer consumer) { + LdapQueryBuilder builder = LdapQueryBuilder.fromQuery(this.query); + consumer.accept(builder); + this.query = builder; + return this; + } + + @Override + public SpringSearchSpec query(LdapQuery query) { + this.query = query; + return this; + } + + @Override + public T toObject(ContextMapper mapper) { + this.controls = searchControlsForQuery(RETURN_OBJ_FLAG); + NamingEnumeration results = computeWithReadOnlyContext(this::search); + return SpringLdapClient.this.toObject(results, function(mapper)); + } + + @Override + public T toObject(AttributesMapper mapper) { + this.controls = searchControlsForQuery(DONT_RETURN_OBJ_FLAG); + NamingEnumeration results = computeWithReadOnlyContext(this::search); + return SpringLdapClient.this.toObject(results, function(mapper)); + } + + @Override + public List toList(ContextMapper mapper) { + this.controls = searchControlsForQuery(RETURN_OBJ_FLAG); + NamingEnumeration results = computeWithReadOnlyContext(this::search); + return SpringLdapClient.this.toList(results, function(mapper)); + } + + @Override + public List toList(AttributesMapper mapper) { + this.controls = searchControlsForQuery(DONT_RETURN_OBJ_FLAG); + NamingEnumeration results = computeWithReadOnlyContext(this::search); + return SpringLdapClient.this.toList(results, function(mapper)); + } + + @Override + public Stream toStream(ContextMapper mapper) { + this.controls = searchControlsForQuery(RETURN_OBJ_FLAG); + NamingEnumeration results = computeWithReadOnlyContext(this::search); + return SpringLdapClient.this.toStream(results, function(mapper)); + } + + @Override + public Stream toStream(AttributesMapper mapper) { + this.controls = searchControlsForQuery(DONT_RETURN_OBJ_FLAG); + NamingEnumeration results = computeWithReadOnlyContext(this::search); + return SpringLdapClient.this.toStream(results, function(mapper)); + } + + private NamingEnumeration search(DirContext ctx) throws NamingException { + return ctx.search(this.query.base(), this.query.filter().encode(), this.controls); + } + + private SearchControls searchControlsForQuery(boolean returnObjFlag) { + SearchControls controls = SpringLdapClient.this.searchControlsSupplier.get(); + controls.setReturningObjFlag(returnObjFlag); + controls.setReturningAttributes(this.query.attributes()); + if (this.query.searchScope() != null) { + controls.setSearchScope(query.searchScope().getId()); + } + if (this.query.countLimit() != null) { + controls.setCountLimit(query.countLimit()); + } + if (this.query.timeLimit() != null) { + controls.setTimeLimit(query.timeLimit()); + } + return controls; + } + } + + private final class SpringBindSpec implements LdapClient.BindSpec { + private final Name name; + private Object obj; + private Attributes attributes; + private boolean rebind = false; + + private SpringBindSpec(Name name) { + this.name = name; + } + + public LdapClient.BindSpec object(Object obj) { + if (obj instanceof DirContextOperations) { + boolean updateMode = ((DirContextOperations) obj).isUpdateMode(); + Assert.isTrue(!updateMode, "DirContextOperations must not be in update mode"); + } + this.obj = obj; + return this; + } + + public LdapClient.BindSpec attributes(Attributes attributes) { + this.attributes = attributes; + return this; + } + + @Override + public BindSpec replaceExisting(boolean replaceExisting) { + this.rebind = replaceExisting; + return this; + } + + @Override + public void execute() { + if (this.rebind) { + runWithReadWriteContext((ctx) -> ctx.rebind(this.name, this.obj, this.attributes)); + } else { + runWithReadWriteContext((ctx) -> ctx.bind(this.name, this.obj, this.attributes)); + } + } + } + + private final class SpringModifySpec implements ModifySpec { + private final DirContextOperations entry; + private Name name; + private ModificationItem[] items; + + private SpringModifySpec(DirContextOperations entry) { + this.entry = entry; + this.name = entry.getDn(); + this.items = entry.getModificationItems(); + } + + @Override + public ModifySpec name(String name) { + this.name = LdapUtils.newLdapName(name); + return this; + } + + @Override + public ModifySpec name(Name name) { + this.name = LdapUtils.newLdapName(name); + return this; + } + + @Override + public ModifySpec attributes(ModificationItem... modifications) { + this.items = modifications; + return this; + } + + @Override + public void execute() { + boolean renamed = false; + if (!this.entry.getDn().equals(this.name)) { + runWithReadWriteContext((ctx) -> ctx.rename(this.entry.getDn(), this.name)); + renamed = true; + } + try { + if (this.items.length > 0) { + runWithReadWriteContext((ctx) -> ctx.modifyAttributes(this.name, this.items)); + } + } catch (Throwable t) { + if (renamed) { + // attempt to change the name back + runWithReadWriteContext((ctx) -> ctx.rename(this.name, this.entry.getDn())); + } + throw t; + } + } + } + + private final class SpringUnbindSpec implements LdapClient.UnbindSpec { + private final Name name; + private boolean recursive = false; + + private SpringUnbindSpec(Name name) { + this.name = name; + } + + @Override + public LdapClient.UnbindSpec recursive(boolean recursive) { + this.recursive = recursive; + return this; + } + + @Override + public void execute() { + if (this.recursive) { + runWithReadWriteContext((ctx) -> unbindRecursive(ctx, this.name)); + return; + } + runWithReadWriteContext((ctx) -> ctx.unbind(this.name)); + } + + void unbindRecursive(DirContext ctx, Name name) throws NamingException { + NamingEnumeration bindings = null; + try { + bindings = ctx.listBindings(name); + while (bindings.hasMore()) { + Binding binding = bindings.next(); + LdapName childName = LdapUtils.newLdapName(binding.getName()); + childName.addAll(0, name); + unbindRecursive(ctx, childName); + } + ctx.unbind(name); + if (SpringLdapClient.this.logger.isDebugEnabled()) { + SpringLdapClient.this.logger.debug("Entry " + name + " deleted"); + } + } finally { + closeNamingEnumeration(bindings); + } + } + } + + T computeWithReadOnlyContext(ContextExecutor executor) { + DirContext context = this.contextSource.getReadOnlyContext(); + try { + return executor.executeWithContext(context); + } catch (NamingException ex) { + this.namingExceptionHandler.accept(ex); + return null; + } finally { + closeContext(context); + } + } + + void runWithReadWriteContext(ContextRunnable runnable) { + DirContext context = this.contextSource.getReadWriteContext(); + try { + runnable.run(context); + } catch (NamingException ex) { + this.namingExceptionHandler.accept(ex); + } finally { + closeContext(context); + } + } + + private NamingExceptionFunction function(ContextMapper mapper) { + return (result) -> mapper.mapFromContext(result.getObject()); + } + + private NamingExceptionFunction function(AttributesMapper mapper) { + return (result) -> mapper.mapFromAttributes(result.getAttributes()); + } + + private Enumeration enumeration(NamingEnumeration enumeration) { + return new Enumeration<>() { + @Override + public boolean hasMoreElements() { + try { + return enumeration.hasMore(); + } catch (NamingException ex) { + namingExceptionHandler.accept(ex); + return false; + } + } + + @Override + public T nextElement() { + try { + return enumeration.next(); + } catch (NamingException ex) { + namingExceptionHandler.accept(ex); + throw new NoSuchElementException("no such element", ex); + } + } + }; + } + + private final Consumer namingExceptionHandler = (ex) -> { + if (ex instanceof NameNotFoundException) { + if (!this.ignoreNameNotFoundException) { + throw LdapUtils.convertLdapException(ex); + } + this.logger.warn("Base context not found, ignoring: " + ex.getMessage()); + return; + } + if (ex instanceof PartialResultException) { + // Workaround for AD servers not handling referrals correctly. + if (!this.ignorePartialResultException) { + throw LdapUtils.convertLdapException(ex); + } + this.logger.debug("PartialResultException encountered and ignored", ex); + return; + } + if (ex instanceof SizeLimitExceededException) { + if (!this.ignoreSizeLimitExceededException) { + throw LdapUtils.convertLdapException(ex); + } + this.logger.debug("SizeLimitExceededException encountered and ignored", ex); + return; + } + throw LdapUtils.convertLdapException(ex); + }; + + private T toObject(NamingEnumeration results, NamingExceptionFunction mapper) { + try { + Enumeration enumeration = enumeration(results); + Function function = mapper.wrap(this.namingExceptionHandler); + if (!enumeration.hasMoreElements()) { + return null; + } + T result = function.apply(enumeration.nextElement()); + if (enumeration.hasMoreElements()) { + throw new IncorrectResultSizeDataAccessException(1); + } + return result; + } finally { + closeNamingEnumeration(results); + } + } + + private List toList(NamingEnumeration results, NamingExceptionFunction mapper) { + if (results == null) { + return Collections.emptyList(); + } + try { + Enumeration enumeration = enumeration(results); + Function function = mapper.wrap(this.namingExceptionHandler); + List mapped = new ArrayList<>(); + while (enumeration.hasMoreElements()) { + T result = function.apply(enumeration.nextElement()); + if (result != null) { + mapped.add(result); + } + } + return mapped; + } finally { + closeNamingEnumeration(results); + } + } + + private Stream toStream(NamingEnumeration results, NamingExceptionFunction mapper) { + if (results == null) { + return Stream.empty(); + } + Enumeration enumeration = enumeration(results); + Function function = mapper.wrap(this.namingExceptionHandler); + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(enumeration.asIterator(), Spliterator.ORDERED), false) + .map(function::apply).filter(Objects::nonNull) + .onClose(() -> closeNamingEnumeration(results)); + } + + private void closeContext(DirContext ctx) { + if (ctx != null) { + try { + ctx.close(); + } + catch (Exception e) { + // Never mind this. + } + } + } + + private void closeNamingEnumeration(NamingEnumeration results) { + if (results != null) { + try { + results.close(); + } + catch (Exception e) { + // Never mind this. + } + } + } + + interface ContextRunnable { + void run(DirContext ctx) throws NamingException; + } + + interface NamingExceptionFunction { + T apply(S element) throws NamingException; + + default Function wrap(Consumer handler) { + return (s) -> { + try { + return apply(s); + } catch (NamingException ex) { + handler.accept(ex); + return null; + } + }; + } + } +} + diff --git a/core/src/main/java/org/springframework/ldap/core/SpringLdapClientBuilder.java b/core/src/main/java/org/springframework/ldap/core/SpringLdapClientBuilder.java new file mode 100644 index 000000000..17f0600d6 --- /dev/null +++ b/core/src/main/java/org/springframework/ldap/core/SpringLdapClientBuilder.java @@ -0,0 +1,93 @@ +package org.springframework.ldap.core; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import javax.naming.NameNotFoundException; +import javax.naming.SizeLimitExceededException; +import javax.naming.directory.SearchControls; + +class SpringLdapClientBuilder implements LdapClient.Builder { + private ContextSource contextSource; + + private Supplier searchControlsSupplier = () -> { + SearchControls controls = new SearchControls(); + controls.setSearchScope(SearchControls.SUBTREE_SCOPE); + controls.setCountLimit(0); + controls.setTimeLimit(0); + return controls; + }; + + private boolean ignorePartialResultException = false; + + private boolean ignoreNameNotFoundException = false; + + private boolean ignoreSizeLimitExceededException = true; + + SpringLdapClientBuilder() {} + + SpringLdapClientBuilder(ContextSource contextSource, + Supplier searchControlsSupplier) { + this.contextSource = contextSource; + this.searchControlsSupplier = searchControlsSupplier; + } + + @Override + public SpringLdapClientBuilder contextSource(ContextSource contextSource) { + this.contextSource = contextSource; + return this; + } + + @Override + public SpringLdapClientBuilder defaultSearchControls(Supplier searchControlsSupplier) { + this.searchControlsSupplier = searchControlsSupplier; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public LdapClient.Builder ignorePartialResultException(boolean ignore) { + this.ignorePartialResultException = ignore; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public LdapClient.Builder ignoreNameNotFoundException(boolean ignore) { + this.ignoreNameNotFoundException = ignore; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public LdapClient.Builder ignoreSizeLimitExceededException(boolean ignore) { + this.ignoreSizeLimitExceededException = ignore; + return this; + } + + @Override + public SpringLdapClientBuilder apply(Consumer builderConsumer) { + builderConsumer.accept(this); + return this; + } + + @Override + public SpringLdapClientBuilder clone() { + return new SpringLdapClientBuilder(this.contextSource, this.searchControlsSupplier); + } + + @Override + public LdapClient build() { + SpringLdapClient client = new SpringLdapClient(this.contextSource, this.searchControlsSupplier); + client.setIgnorePartialResultException(this.ignorePartialResultException); + client.setIgnoreSizeLimitExceededException(this.ignoreSizeLimitExceededException); + client.setIgnoreNameNotFoundException(this.ignoreNameNotFoundException); + return client; + } +} 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 e4ffa56de..f0821e9e5 100644 --- a/core/src/main/java/org/springframework/ldap/query/LdapQueryBuilder.java +++ b/core/src/main/java/org/springframework/ldap/query/LdapQueryBuilder.java @@ -62,6 +62,8 @@ public final class LdapQueryBuilder implements LdapQuery { private DefaultContainerCriteria rootContainer = null; + private boolean isFilterStarted = false; + /** * Not to be instantiated directly - use static query() method. */ @@ -79,15 +81,20 @@ public static LdapQueryBuilder query() { } /** - * Construct a new LdapQueryBuilder based on an existing {@link LdapQuery} - * All non-filter fields are copied. + * Construct a new {@link LdapQueryBuilder} based on an existing {@link LdapQuery} + * All fields are copied, including giving the query a default filter. + * + *

+ * Note that all filter invariants are still enforced; an application cannot specify + * any non-filter values after it specifies a filter. + * * @return a new instance. * @since 3.0 */ public static LdapQueryBuilder fromQuery(LdapQuery query) { - LdapQueryBuilder builder = LdapQueryBuilder.query() - .attributes(query.attributes()) - .base(query.base()); + LdapQueryBuilder builder = new LdapQueryBuilder(); + builder.rootContainer = new DefaultContainerCriteria(builder).append(query.filter()); + builder.attributes(query.attributes()).base(query.base()); if (query.countLimit() != null) { builder.countLimit(query.countLimit()); } @@ -184,6 +191,7 @@ public ConditionCriteria where(String attribute) { private void initRootContainer() { assertFilterNotStarted(); rootContainer = new DefaultContainerCriteria(this); + isFilterStarted = true; } /** @@ -237,7 +245,7 @@ public LdapQuery filter(String filterFormat, Object... params) { } private void assertFilterNotStarted() { - if(rootContainer != null) { + if (isFilterStarted) { throw new IllegalStateException("Invalid operation - filter condition specification already started"); } } diff --git a/core/src/test/java/org/springframework/ldap/core/SpringLdapClientListTest.java b/core/src/test/java/org/springframework/ldap/core/SpringLdapClientListTest.java new file mode 100644 index 000000000..259f4b237 --- /dev/null +++ b/core/src/test/java/org/springframework/ldap/core/SpringLdapClientListTest.java @@ -0,0 +1,388 @@ +/* + * Copyright 2005-2022 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.ldap.core; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.naming.Binding; +import javax.naming.Name; +import javax.naming.NameClassPair; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.DirContext; +import javax.naming.ldap.LdapContext; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.ldap.LimitExceededException; +import org.springframework.ldap.PartialResultException; +import org.springframework.ldap.support.LdapUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for the list operations in {@link LdapTemplate}. + * + * @author Ulrik Sandberg + */ +public class SpringLdapClientListTest { + + private static final String NAME = "o=example.com"; + + private static final String CLASS = "com.example.SomeClass"; + + private ContextSource contextSourceMock; + + private DirContext dirContextMock; + + private NamingEnumeration namingEnumerationMock; + + private Name nameMock = LdapUtils.newLdapName(NAME); + + private ContextMapper contextMapperMock; + + private SpringLdapClient tested; + + @Before + public void setUp() throws Exception { + // Setup ContextSource mock + contextSourceMock = mock(ContextSource.class); + + // Setup LdapContext mock + dirContextMock = mock(LdapContext.class); + + // Setup NamingEnumeration mock + namingEnumerationMock = mock(NamingEnumeration.class); + + contextMapperMock = mock(ContextMapper.class); + + tested = (SpringLdapClient) LdapClient.create(contextSourceMock); + } + + private void expectGetReadOnlyContext() { + when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock); + } + + private void setupListAndNamingEnumeration(NameClassPair listResult) + throws NamingException { + when(dirContextMock.list(nameMock)).thenReturn(namingEnumerationMock); + + setupNamingEnumeration(listResult); + } + + private void setupListBindingsAndNamingEnumeration(NameClassPair listResult) + throws NamingException { + when(dirContextMock.listBindings(nameMock)).thenReturn(namingEnumerationMock); + + setupNamingEnumeration(listResult); + } + + private void setupNamingEnumeration(NameClassPair listResult) + throws NamingException { + when(namingEnumerationMock.hasMore()).thenReturn(true, false); + when(namingEnumerationMock.next()).thenReturn(listResult); + } + + @Test + public void testList_Name() throws NamingException { + expectGetReadOnlyContext(); + + NameClassPair listResult = new NameClassPair(NAME, CLASS); + + setupListAndNamingEnumeration(listResult); + + List list = tested.list(nameMock).toList(NameClassPair::getName); + + verify(dirContextMock).close(); + verify(namingEnumerationMock).close(); + + assertThat(list).isNotNull(); + assertThat(list).hasSize(1); + assertThat(list.get(0)).isSameAs(NAME); + } + + + @Test + public void testList_String() throws NamingException { + expectGetReadOnlyContext(); + + NameClassPair listResult = new NameClassPair(NAME, CLASS); + + setupListAndNamingEnumeration(listResult); + + List list = tested.list(NAME).toList(NameClassPair::getName); + + verify(dirContextMock).close(); + verify(namingEnumerationMock).close(); + + assertThat(list).isNotNull(); + assertThat(list).hasSize(1); + assertThat(list.get(0)).isSameAs(NAME); + } + + @Test + public void testList_PartialResultException() throws NamingException { + expectGetReadOnlyContext(); + javax.naming.PartialResultException pre = new javax.naming.PartialResultException(); + when(dirContextMock.list(nameMock)).thenThrow(pre); + + assertThatExceptionOfType(PartialResultException.class).isThrownBy(() -> + tested.list(NAME).toList(NameClassPair::getName)); + + verify(dirContextMock).close(); + } + + @Test + public void testList_Stream_PartialResultException() throws NamingException { + expectGetReadOnlyContext(); + javax.naming.PartialResultException pre = new javax.naming.PartialResultException(); + when(dirContextMock.list(nameMock)).thenThrow(pre); + + assertThatExceptionOfType(PartialResultException.class).isThrownBy(() -> + tested.list(NAME).toStream(NameClassPair::getName).collect(Collectors.toList())); + + verify(dirContextMock).close(); + } + + @Test + public void testList_PartialResultException_Ignore() throws NamingException { + expectGetReadOnlyContext(); + + javax.naming.PartialResultException pre = new javax.naming.PartialResultException(); + when(dirContextMock.list(this.nameMock)).thenThrow(pre); + + tested.setIgnorePartialResultException(true); + + List list = tested.list(NAME).toList(NameClassPair::getName); + + verify(dirContextMock).close(); + + assertThat(list).isNotNull(); + assertThat(list).isEmpty(); + } + + @Test + public void testList_AsStream_PartialResultException_Ignore() throws NamingException { + expectGetReadOnlyContext(); + + javax.naming.PartialResultException pre = new javax.naming.PartialResultException(); + when(dirContextMock.list(this.nameMock)).thenThrow(pre); + + tested.setIgnorePartialResultException(true); + + try (Stream results = tested.list(NAME).toStream(NameClassPair::getName)) { + List list = results.collect(Collectors.toList()); + assertThat(list).isNotNull(); + assertThat(list).isEmpty(); + } + + verify(dirContextMock).close(); + } + + @Test + public void testList_NamingException() throws NamingException { + expectGetReadOnlyContext(); + javax.naming.LimitExceededException ne = new javax.naming.LimitExceededException(); + when(dirContextMock.list(nameMock)).thenThrow(ne); + assertThatExceptionOfType(LimitExceededException.class).isThrownBy(() -> + tested.list(NAME).toList(NameClassPair::getName)); + verify(dirContextMock).close(); + } + + @Test + public void testList_AsStream_NamingException() throws NamingException { + expectGetReadOnlyContext(); + javax.naming.LimitExceededException ne = new javax.naming.LimitExceededException(); + when(dirContextMock.list(nameMock)).thenThrow(ne); + assertThatExceptionOfType(LimitExceededException.class).isThrownBy(() -> + tested.list(NAME).toStream(NameClassPair::getName).collect(Collectors.toList())); + verify(dirContextMock).close(); + } + + // Tests for listBindings + + @Test + public void testListBindings_String() throws NamingException { + expectGetReadOnlyContext(); + + Binding listResult = new Binding(NAME, CLASS, null); + + setupListBindingsAndNamingEnumeration(listResult); + + List list = tested.listBindings(NAME).toList(NameClassPair::getName); + + verify(dirContextMock).close(); + verify(namingEnumerationMock).close(); + + assertThat(list).isNotNull(); + assertThat(list).hasSize(1); + assertThat(list.get(0)).isSameAs(NAME); + } + + @Test + public void testListBindings_AsStream_String() throws NamingException { + expectGetReadOnlyContext(); + + Binding listResult = new Binding(NAME, CLASS, null); + + setupListBindingsAndNamingEnumeration(listResult); + + try (Stream results = tested.listBindings(NAME).toStream(NameClassPair::getName)) { + List list = results.collect(Collectors.toList()); + assertThat(list).isNotNull(); + assertThat(list).hasSize(1); + assertThat(list.get(0)).isSameAs(NAME); + } + + verify(dirContextMock).close(); + verify(namingEnumerationMock).close(); + } + + @Test + public void testListBindings_Name() throws NamingException { + expectGetReadOnlyContext(); + + Binding listResult = new Binding(NAME, CLASS, null); + + setupListBindingsAndNamingEnumeration(listResult); + + List list = tested.listBindings(nameMock).toList(NameClassPair::getName); + + verify(dirContextMock).close(); + verify(namingEnumerationMock).close(); + + assertThat(list).isNotNull(); + assertThat(list).hasSize(1); + assertThat(list.get(0)).isSameAs(NAME); + } + + @Test + public void testListBindings_Name_AsStream() throws NamingException { + expectGetReadOnlyContext(); + + Binding listResult = new Binding(NAME, CLASS, null); + + setupListBindingsAndNamingEnumeration(listResult); + + try (Stream results = tested.listBindings(nameMock).toStream(NameClassPair::getName)) { + List list = results.collect(Collectors.toList()); + assertThat(list).isNotNull(); + assertThat(list).hasSize(1); + assertThat(list.get(0)).isSameAs(NAME); + } + + verify(dirContextMock).close(); + verify(namingEnumerationMock).close(); + } + + @Test + public void testListBindings_ContextMapper() throws NamingException { + expectGetReadOnlyContext(); + + Object expectedObject = new Object(); + Binding listResult = new Binding("", expectedObject); + + setupListBindingsAndNamingEnumeration(listResult); + + Object expectedResult = expectedObject; + when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult); + + List list = tested.listBindings(NAME).toList(contextMapperMock); + + verify(dirContextMock).close(); + verify(namingEnumerationMock).close(); + + assertThat(list).isNotNull(); + assertThat(list).hasSize(1); + assertThat(list.get(0)).isSameAs(expectedResult); + } + + @Test + public void testListBindings_AsStream_ContextMapper() throws NamingException { + expectGetReadOnlyContext(); + + Object expectedObject = new Object(); + Binding listResult = new Binding("", expectedObject); + + setupListBindingsAndNamingEnumeration(listResult); + + Object expectedResult = expectedObject; + when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult); + + try (Stream results = tested.listBindings(NAME).toStream(contextMapperMock)) { + List list = results.collect(Collectors.toList()); + assertThat(list).isNotNull(); + assertThat(list).hasSize(1); + assertThat(list.get(0)).isSameAs(expectedResult); + } + + verify(dirContextMock).close(); + verify(namingEnumerationMock).close(); + } + + @Test + public void testListBindings_Name_ContextMapper() throws NamingException { + expectGetReadOnlyContext(); + + Object expectedObject = new Object(); + Binding listResult = new Binding("", expectedObject); + + setupListBindingsAndNamingEnumeration(listResult); + + Object expectedResult = expectedObject; + when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult); + + List list = tested.listBindings(nameMock).toList(contextMapperMock); + + verify(dirContextMock).close(); + verify(namingEnumerationMock).close(); + + assertThat(list).isNotNull(); + assertThat(list).hasSize(1); + assertThat(list.get(0)).isSameAs(expectedResult); + } + + @Test + public void testListBindings_Name_AsStream_ContextMapper() throws NamingException { + expectGetReadOnlyContext(); + + Object expectedObject = new Object(); + Binding listResult = new Binding("", expectedObject); + + setupListBindingsAndNamingEnumeration(listResult); + + Object expectedResult = expectedObject; + when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult); + + try (Stream results = tested.listBindings(nameMock).toStream(contextMapperMock)) { + List list = results.collect(Collectors.toList()); + assertThat(list).isNotNull(); + assertThat(list).hasSize(1); + assertThat(list.get(0)).isSameAs(expectedResult); + } + + verify(dirContextMock).close(); + verify(namingEnumerationMock).close(); + } +} diff --git a/core/src/test/java/org/springframework/ldap/core/SpringLdapClientLookupTest.java b/core/src/test/java/org/springframework/ldap/core/SpringLdapClientLookupTest.java new file mode 100644 index 000000000..90a98c61e --- /dev/null +++ b/core/src/test/java/org/springframework/ldap/core/SpringLdapClientLookupTest.java @@ -0,0 +1,239 @@ +/* + * Copyright 2005-2022 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.ldap.core; + +import java.util.Arrays; +import java.util.Iterator; + +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.LdapContext; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.stubbing.OngoingStubbing; + +import org.springframework.LdapDataEntry; +import org.springframework.ldap.NameNotFoundException; +import org.springframework.ldap.support.LdapUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class SpringLdapClientLookupTest { + + private static final Name DEFAULT_BASE = LdapUtils.newLdapName("o=example.com"); + + private ContextSource contextSourceMock; + + private DirContext dirContextMock; + + private final Name name = LdapUtils.newLdapName("ou=name"); + + private LdapClient tested; + + @Before + public void setUp() throws Exception { + contextSourceMock = mock(ContextSource.class); + dirContextMock = mock(LdapContext.class); + tested = LdapClient.create(contextSourceMock); + } + + private void expectGetReadOnlyContext() { + when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock); + } + + @Test + public void testLookup() throws Exception { + expectGetReadOnlyContext(); + + LdapDataEntry expected = new DirContextAdapter(); + whenSearching(name).thenReturn(result(expected, null)); + + LdapDataEntry actual = tested.search().name(name).toEntry(); + + verify(dirContextMock).close(); + assertThat(actual).isSameAs(expected); + } + + @Test + public void testLookup_String() throws Exception { + expectGetReadOnlyContext(); + + LdapDataEntry expected = new DirContextAdapter(); + whenSearching(DEFAULT_BASE).thenReturn(result(expected, null)); + + LdapDataEntry actual = tested.search().name(DEFAULT_BASE.toString()).toEntry(); + + verify(dirContextMock).close(); + assertThat(actual).isSameAs(expected); + } + + @Test + public void testLookup_NamingException() throws Exception { + expectGetReadOnlyContext(); + + javax.naming.NameNotFoundException ne = new javax.naming.NameNotFoundException(); + whenSearching(name).thenThrow(ne); + + assertThatExceptionOfType(NameNotFoundException.class) + .describedAs("NameNotFoundException expected") + .isThrownBy(() -> tested.search().name(name).toEntry()); + verify(dirContextMock).close(); + } + + @Test + public void testLookup_AttributesMapper() throws Exception { + expectGetReadOnlyContext(); + + Attributes expected = new BasicAttributes(); + whenSearching(name).thenReturn(result(null, expected)); + + AttributesMapper mapper = (attributes) -> attributes; + Attributes actual = tested.search().name(name).toObject(mapper); + + verify(dirContextMock).close(); + assertThat(actual).isSameAs(expected); + } + + @Test + public void testLookup_String_AttributesMapper() throws Exception { + expectGetReadOnlyContext(); + + Attributes expected = new BasicAttributes(); + whenSearching(DEFAULT_BASE).thenReturn(result(null, expected)); + + AttributesMapper mapper = (attributes) -> attributes; + Attributes actual = tested.search().name(DEFAULT_BASE.toString()).toObject(mapper); + + verify(dirContextMock).close(); + assertThat(actual).isSameAs(expected); + } + + @Test + public void testLookup_AttributesMapper_NamingException() throws Exception { + expectGetReadOnlyContext(); + + javax.naming.NameNotFoundException ne = new javax.naming.NameNotFoundException(); + whenSearching(name).thenThrow(ne); + + AttributesMapper mapper = (attributes) -> attributes; + assertThatExceptionOfType(NameNotFoundException.class) + .describedAs("NameNotFoundException expected") + .isThrownBy(() -> tested.search().name(name).toObject(mapper)); + verify(dirContextMock).close(); + } + + // Tests for lookup(name, ContextMapper) + + @Test + public void testLookup_ContextMapper() throws Exception { + expectGetReadOnlyContext(); + + Object expected = new Object(); + whenSearching(name).thenReturn(result(expected, null)); + + ContextMapper mapper = (ctx) -> ctx; + Object actual = tested.search().name(name).toObject(mapper); + + verify(dirContextMock).close(); + assertThat(actual).isSameAs(expected); + } + + @Test + public void testLookup_String_ContextMapper() throws Exception { + expectGetReadOnlyContext(); + + Object expected = new Object(); + whenSearching(DEFAULT_BASE).thenReturn(result(expected, null)); + + ContextMapper mapper = (ctx) -> ctx; + Object actual = tested.search().name(DEFAULT_BASE.toString()).toObject(mapper); + + verify(dirContextMock).close(); + assertThat(actual).isSameAs(expected); + } + + @Test + public void testLookup_ContextMapper_NamingException() throws Exception { + expectGetReadOnlyContext(); + + javax.naming.NameNotFoundException ne = new javax.naming.NameNotFoundException(); + whenSearching(name).thenThrow(ne); + + ContextMapper mapper = (ctx) -> ctx; + assertThatExceptionOfType(NameNotFoundException.class) + .describedAs("NameNotFoundException expected") + .isThrownBy(() -> tested.search().name(name).toObject(mapper)); + verify(dirContextMock).close(); + } + + private static NamingEnumeration result(Object object, Attributes attributes) { + return results(new SearchResult("ou=name", object, attributes)); + } + + private static NamingEnumeration results(SearchResult... results) { + return new NamingEnumeration(results); + } + + private OngoingStubbing> whenSearching(Name name) throws Exception { + return when(dirContextMock.search(eq(name), anyString(), any())); + } + + private static class NamingEnumeration implements javax.naming.NamingEnumeration { + private final Iterator names; + + public NamingEnumeration(SearchResult... results) { + names = Arrays.asList(results).iterator(); + } + + @Override + public SearchResult next() { + return this.names.next(); + } + + @Override + public boolean hasMore() { + return this.names.hasNext(); + } + + @Override + public void close() throws NamingException { + + } + + @Override + public boolean hasMoreElements() { + return this.names.hasNext(); + } + + @Override + public SearchResult nextElement() { + return this.names.next(); + } + } +} diff --git a/core/src/test/java/org/springframework/ldap/core/SpringLdapClientRenameTest.java b/core/src/test/java/org/springframework/ldap/core/SpringLdapClientRenameTest.java new file mode 100644 index 000000000..8d8623839 --- /dev/null +++ b/core/src/test/java/org/springframework/ldap/core/SpringLdapClientRenameTest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2005-2016 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.ldap.core; + +import javax.naming.Name; +import javax.naming.directory.DirContext; +import javax.naming.ldap.LdapContext; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.ldap.NameAlreadyBoundException; +import org.springframework.ldap.UncategorizedLdapException; +import org.springframework.ldap.support.LdapUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for the rename operations in the LdapTemplate class. + * + * @author Josh Cummings + */ +public class SpringLdapClientRenameTest { + + private ContextSource contextSourceMock; + + private DirContext dirContextMock; + + private final Name oldName = LdapUtils.newLdapName("ou=old"); + + private final Name newName = LdapUtils.newLdapName("ou=new"); + + private LdapClient tested; + + @Before + public void setUp() throws Exception { + // Setup ContextSource mock + contextSourceMock = mock(ContextSource.class); + + // Setup LdapContext mock + dirContextMock = mock(LdapContext.class); + + tested = LdapClient.create(contextSourceMock); + } + + private void expectGetReadWriteContext() { + when(contextSourceMock.getReadWriteContext()).thenReturn(dirContextMock); + } + + @Test + public void testRename() throws Exception { + expectGetReadWriteContext(); + + tested.modify(oldName).name(newName).execute(); + + verify(dirContextMock).rename(oldName, newName); + verify(dirContextMock).close(); + } + + @Test + public void testRename_NameAlreadyBoundException() throws Exception { + expectGetReadWriteContext(); + + javax.naming.NameAlreadyBoundException ne = new javax.naming.NameAlreadyBoundException(); + doThrow(ne).when(dirContextMock).rename(oldName, newName); + + try { + tested.modify(oldName).name(newName).execute(); + fail("NameAlreadyBoundException expected"); + } catch (NameAlreadyBoundException expected) { + assertThat(true).isTrue(); + } + + verify(dirContextMock).close(); + } + + @Test + public void testRename_NamingException() throws Exception { + expectGetReadWriteContext(); + + javax.naming.NamingException ne = new javax.naming.NamingException(); + + doThrow(ne).when(dirContextMock).rename(oldName, newName); + + try { + tested.modify(oldName).name(newName).execute(); + fail("UncategorizedLdapException expected"); + } catch (UncategorizedLdapException expected) { + assertThat(true).isTrue(); + } + + verify(dirContextMock).close(); + } + + @Test + public void testRename_String() throws Exception { + expectGetReadWriteContext(); + + tested.modify("o=example.com").name("o=somethingelse.com").execute(); + + verify(dirContextMock).rename(LdapUtils.newLdapName("o=example.com"), + LdapUtils.newLdapName("o=somethingelse.com")); + verify(dirContextMock).close(); + } +} diff --git a/core/src/test/java/org/springframework/ldap/core/SpringLdapClientTest.java b/core/src/test/java/org/springframework/ldap/core/SpringLdapClientTest.java new file mode 100644 index 000000000..8e0b56c4c --- /dev/null +++ b/core/src/test/java/org/springframework/ldap/core/SpringLdapClientTest.java @@ -0,0 +1,709 @@ +/* + * Copyright 2005-2023 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.ldap.core; + +import java.util.function.Supplier; + +import javax.naming.Binding; +import javax.naming.CompositeName; +import javax.naming.Name; +import javax.naming.NamingEnumeration; +import javax.naming.directory.BasicAttributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.ModificationItem; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.LdapContext; +import javax.naming.ldap.LdapName; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatcher; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.ldap.LimitExceededException; +import org.springframework.ldap.NameNotFoundException; +import org.springframework.ldap.PartialResultException; +import org.springframework.ldap.UncategorizedLdapException; +import org.springframework.ldap.odm.core.ObjectDirectoryMapper; +import org.springframework.ldap.query.LdapQuery; +import org.springframework.ldap.query.LdapQueryBuilder; +import org.springframework.ldap.query.SearchScope; +import org.springframework.ldap.support.LdapUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link LdapClient} + * + * @author Josh Cummings + */ +public class SpringLdapClientTest { + + private static final Name DEFAULT_BASE = LdapUtils.newLdapName("o=example.com"); + + private ContextSource contextSourceMock; + + private DirContext dirContextMock; + + private AttributesMapper attributesMapperMock; + + private NamingEnumeration namingEnumerationMock; + + private Name nameMock; + + private NameClassPairCallbackHandler handlerMock; + + private ContextMapper contextMapperMock; + + private ContextExecutor contextExecutorMock; + + private SearchExecutor searchExecutorMock; + + private LdapClient tested; + + private DirContextProcessor dirContextProcessorMock; + + private DirContextOperations dirContextOperationsMock; + + private DirContext authenticatedContextMock; + + private AuthenticatedLdapEntryContextCallback entryContextCallbackMock; + private ObjectDirectoryMapper odmMock; + + private LdapQuery query; + private AuthenticatedLdapEntryContextMapper authContextMapperMock; + + @Before + public void setUp() throws Exception { + + // Setup ContextSource mock + contextSourceMock = mock(ContextSource.class); + // Setup LdapContext mock + dirContextMock = mock(LdapContext.class); + // Setup NamingEnumeration mock + namingEnumerationMock = mock(NamingEnumeration.class); + // Setup Name mock + nameMock = LdapUtils.emptyLdapName(); + // Setup Handler mock + handlerMock = mock(NameClassPairCallbackHandler.class); + contextMapperMock = mock(ContextMapper.class); + attributesMapperMock = mock(AttributesMapper.class); + contextExecutorMock = mock(ContextExecutor.class); + searchExecutorMock = mock(SearchExecutor.class); + dirContextProcessorMock = mock(DirContextProcessor.class); + dirContextOperationsMock = mock(DirContextOperations.class); + authenticatedContextMock = mock(DirContext.class); + entryContextCallbackMock = mock(AuthenticatedLdapEntryContextCallback.class); + odmMock = mock(ObjectDirectoryMapper.class); + query = LdapQueryBuilder.query().base("ou=spring").filter("ou=user"); + authContextMapperMock = mock(AuthenticatedLdapEntryContextMapper.class); + + tested = LdapClient.create(contextSourceMock); + } + + private void expectGetReadWriteContext() { + when(contextSourceMock.getReadWriteContext()).thenReturn(dirContextMock); + } + + private void expectGetReadOnlyContext() { + when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock); + } + + @Test + public void testSearchContextMapper() throws Exception { + expectGetReadOnlyContext(); + + SearchResult searchResult = new SearchResult("", new Object(), new BasicAttributes()); + + singleSearchResult(searchControlsOneLevel(), searchResult); + + tested.search().query((builder) -> builder.base(nameMock) + .searchScope(SearchScope.ONELEVEL) + .filter("(ou=somevalue)")).toObject(contextMapperMock); + + verify(contextMapperMock).mapFromContext(any()); + verify(dirContextMock).close(); + } + + @Test + public void testSearch_StringBase_CallbackHandler() throws Exception { + expectGetReadOnlyContext(); + + SearchControls controls = searchControlsOneLevel(); + + SearchResult searchResult = new SearchResult("", new Object(), new BasicAttributes()); + + singleSearchResultWithStringBase(controls, searchResult); + + tested.search().query((builder) -> builder.base(DEFAULT_BASE.toString()) + .searchScope(SearchScope.ONELEVEL) + .filter("(ou=somevalue)")).toObject(contextMapperMock); + + verify(contextMapperMock).mapFromContext(any()); + verify(dirContextMock).close(); + } + + @Test + public void testSearch_AttributeMapper_Defaults() throws Exception { + expectGetReadOnlyContext(); + + SearchControls controls = searchControlsRecursive(); + controls.setReturningObjFlag(false); + + SearchResult searchResult = new SearchResult("", new Object(), new BasicAttributes()); + + singleSearchResult(controls, searchResult); + + tested.search().query((builder) -> builder.base(nameMock) + .searchScope(SearchScope.SUBTREE) + .filter("(ou=somevalue)")).toObject(attributesMapperMock); + + verify(attributesMapperMock).mapFromAttributes(any()); + verify(dirContextMock).close(); + } + + @Test + public void testSearch_String_AttributeMapper_Defaults() throws Exception { + expectGetReadOnlyContext(); + + SearchControls controls = searchControlsRecursive(); + controls.setReturningObjFlag(false); + + SearchResult searchResult = new SearchResult("", new Object(), new BasicAttributes()); + + singleSearchResultWithStringBase(controls, searchResult); + + tested.search().query((builder) -> builder.base(DEFAULT_BASE.toString()) + .searchScope(SearchScope.SUBTREE) + .filter("(ou=somevalue)")).toObject(attributesMapperMock); + + verify(attributesMapperMock).mapFromAttributes(any()); + verify(dirContextMock).close(); + } + + @Test + public void testSearch_NameNotFoundException() throws Exception { + expectGetReadOnlyContext(); + + final SearchControls controls = searchControlsRecursive(); + controls.setReturningObjFlag(false); + + javax.naming.NameNotFoundException ne = new javax.naming.NameNotFoundException("some text"); + when(dirContextMock.search( + eq(nameMock), + eq("(ou=somevalue)"), + argThat(new SearchControlsMatcher(controls)))).thenThrow(ne); + + try { + tested.search().query((builder) -> builder.base(nameMock) + .searchScope(SearchScope.SUBTREE) + .filter("(ou=somevalue)")).toObject(attributesMapperMock); + fail("NameNotFoundException expected"); + } + catch (NameNotFoundException expected) { + assertThat(true).isTrue(); + } + verify(dirContextMock).close(); + } + + @Test + public void testSearch_NamingException() throws Exception { + expectGetReadOnlyContext(); + + SearchControls controls = searchControlsRecursive(); + controls.setReturningObjFlag(false); + + javax.naming.LimitExceededException ne = new javax.naming.LimitExceededException(); + when(dirContextMock.search( + eq(nameMock), + eq("(ou=somevalue)"), + argThat(new SearchControlsMatcher(controls)))).thenThrow(ne); + + try { + tested.search().query((builder) -> builder.base(nameMock) + .filter("(ou=somevalue)")).toObject(attributesMapperMock); + fail("LimitExceededException expected"); + } + catch (LimitExceededException expected) { + // expected + } + + verify(dirContextMock).close(); + } + + @Test + public void verifyThatDefaultSearchControlParametersAreAutomaticallyAppliedInSearch() throws Exception { + Supplier defaults = mock(Supplier.class); + when(defaults.get()).thenReturn(new SearchControls()); + LdapClient tested = LdapClient.builder() + .contextSource(contextSourceMock) + .defaultSearchControls(defaults).build(); + + expectGetReadOnlyContext(); + + when(dirContextMock.search(eq(nameMock), anyString(), any())).thenReturn(namingEnumerationMock); + tested.search().name(nameMock).toEntry(); + + verify(defaults).get(); + verify(namingEnumerationMock).close(); + verify(dirContextMock).close(); + } + + @Test + public void testModifyAttributes() throws Exception { + expectGetReadWriteContext(); + + ModificationItem[] mods = new ModificationItem[1]; + + tested.modify(nameMock).attributes(mods).execute(); + + verify(dirContextMock).modifyAttributes(nameMock, mods); + verify(dirContextMock).close(); + } + + @Test + public void testModifyAttributes_String() throws Exception { + expectGetReadWriteContext(); + + ModificationItem[] mods = new ModificationItem[1]; + + tested.modify(DEFAULT_BASE.toString()).attributes(mods).execute(); + + verify(dirContextMock).modifyAttributes(DEFAULT_BASE, mods); + verify(dirContextMock).close(); + } + + @Test + public void testModifyAttributes_NamingException() throws Exception { + expectGetReadWriteContext(); + + ModificationItem[] mods = new ModificationItem[1]; + + javax.naming.LimitExceededException ne = new javax.naming.LimitExceededException(); + doThrow(ne).when(dirContextMock).modifyAttributes(nameMock, mods); + + try { + tested.modify(nameMock).attributes(mods).execute(); + fail("LimitExceededException expected"); + } + catch (LimitExceededException expected) { + assertThat(true).isTrue(); + } + + verify(dirContextMock).close(); + } + + @Test + public void testBind() throws Exception { + expectGetReadWriteContext(); + + Object expectedObject = new Object(); + BasicAttributes expectedAttributes = new BasicAttributes(); + + tested.bind(nameMock).object(expectedObject).attributes(expectedAttributes).execute(); + + verify(dirContextMock).bind(nameMock, expectedObject, expectedAttributes); + verify(dirContextMock).close(); + + } + + @Test + public void testBind_String() throws Exception { + expectGetReadWriteContext(); + + Object expectedObject = new Object(); + BasicAttributes expectedAttributes = new BasicAttributes(); + + tested.bind(DEFAULT_BASE.toString()).object(expectedObject).attributes(expectedAttributes).execute(); + + verify(dirContextMock).bind(DEFAULT_BASE, expectedObject, expectedAttributes); + verify(dirContextMock).close(); + } + + @Test + public void testBind_NamingException() throws Exception { + expectGetReadWriteContext(); + + Object expectedObject = new Object(); + BasicAttributes expectedAttributes = new BasicAttributes(); + javax.naming.NameNotFoundException ne = new javax.naming.NameNotFoundException(); + doThrow(ne).when(dirContextMock).bind(nameMock, expectedObject, expectedAttributes); + + try { + tested.bind(nameMock).object(expectedObject).attributes(expectedAttributes).execute(); + fail("NameNotFoundException expected"); + } + catch (NameNotFoundException expected) { + assertThat(true).isTrue(); + } + + verify(dirContextMock).close(); + } + + @Test + public void testBindWithContext() throws Exception { + expectGetReadWriteContext(); + + when(dirContextOperationsMock.getDn()).thenReturn(nameMock); + when(dirContextOperationsMock.isUpdateMode()).thenReturn(false); + + tested.bind(nameMock).object(dirContextOperationsMock).execute(); + + verify(dirContextMock).bind(nameMock, dirContextOperationsMock, null); + verify(dirContextMock).close(); + } + + + @Test + public void testRebindWithContext() throws Exception { + expectGetReadWriteContext(); + + when(dirContextOperationsMock.getDn()).thenReturn(nameMock); + when(dirContextOperationsMock.isUpdateMode()).thenReturn(false); + + tested.bind(nameMock).object(dirContextOperationsMock).replaceExisting(true).execute(); + + verify(dirContextMock).rebind(nameMock, dirContextOperationsMock, null); + verify(dirContextMock).close(); + } + + @Test + public void testRebind() throws Exception { + expectGetReadWriteContext(); + + Object expectedObject = new Object(); + BasicAttributes expectedAttributes = new BasicAttributes(); + + tested.bind(nameMock).object(expectedObject).attributes(expectedAttributes) + .replaceExisting(true).execute(); + + verify(dirContextMock).rebind(nameMock, expectedObject, expectedAttributes); + verify(dirContextMock).close(); + } + + @Test + public void testRebind_String() throws Exception { + expectGetReadWriteContext(); + + Object expectedObject = new Object(); + BasicAttributes expectedAttributes = new BasicAttributes(); + + tested.bind(DEFAULT_BASE.toString()).object(expectedObject).attributes(expectedAttributes) + .replaceExisting(true).execute(); + + verify(dirContextMock).rebind(DEFAULT_BASE, expectedObject, expectedAttributes); + verify(dirContextMock).close(); + } + + @Test + public void testUnbind() throws Exception { + expectGetReadWriteContext(); + + tested.unbind(nameMock).execute(); + + verify(dirContextMock).unbind(nameMock); + verify(dirContextMock).close(); + } + + @Test + public void testUnbind_String() throws Exception { + expectGetReadWriteContext(); + + tested.unbind(DEFAULT_BASE.toString()).execute(); + + verify(dirContextMock).unbind(DEFAULT_BASE); + verify(dirContextMock).close(); + } + + @Test + public void testUnbindRecursive() throws Exception { + expectGetReadWriteContext(); + + when(namingEnumerationMock.hasMore()).thenReturn(true, false, false); + Binding binding = new Binding("cn=Some name", null); + when(namingEnumerationMock.next()).thenReturn(binding); + + LdapName listDn = LdapUtils.newLdapName(DEFAULT_BASE); + when(dirContextMock.listBindings(listDn)).thenReturn(namingEnumerationMock); + LdapName subListDn = LdapUtils.newLdapName("cn=Some name, o=example.com"); + when(dirContextMock.listBindings(subListDn)).thenReturn(namingEnumerationMock); + + tested.unbind(new CompositeName(DEFAULT_BASE.toString())).recursive(true).execute(); + + verify(dirContextMock).unbind(subListDn); + verify(dirContextMock).unbind(listDn); + verify(namingEnumerationMock, times(2)).close(); + verify(dirContextMock).close(); + } + + @Test + public void testUnbindRecursive_String() throws Exception { + expectGetReadWriteContext(); + + when(namingEnumerationMock.hasMore()).thenReturn(true, false, false); + Binding binding = new Binding("cn=Some name", null); + when(namingEnumerationMock.next()).thenReturn(binding); + + LdapName listDn = LdapUtils.newLdapName(DEFAULT_BASE); + when(dirContextMock.listBindings(listDn)).thenReturn(namingEnumerationMock); + LdapName subListDn = LdapUtils.newLdapName("cn=Some name, o=example.com"); + when(dirContextMock.listBindings(subListDn)).thenReturn(namingEnumerationMock); + + tested.unbind(DEFAULT_BASE.toString()).recursive(true).execute(); + + verify(dirContextMock).unbind(subListDn); + verify(dirContextMock).unbind(listDn); + verify(namingEnumerationMock, times(2)).close(); + verify(dirContextMock).close(); + } + + @Test + public void testUnbind_NamingException() throws Exception { + expectGetReadWriteContext(); + + javax.naming.NameNotFoundException ne = new javax.naming.NameNotFoundException(); + doThrow(ne).when(dirContextMock).unbind(nameMock); + + try { + tested.unbind(nameMock).execute(); + fail("NameNotFoundException expected"); + } + catch (NameNotFoundException expected) { + assertThat(true).isTrue(); + } + + verify(dirContextMock).close(); + } + + @Test + public void testSearch_PartialResult_IgnoreNotSet() throws Exception { + expectGetReadOnlyContext(); + + javax.naming.PartialResultException ex = new javax.naming.PartialResultException(); + when(dirContextMock.search(eq(nameMock), anyString(), any())).thenThrow(ex); + + try { + tested.search().name(nameMock).toEntryList(); + fail("PartialResultException expected"); + } + catch (PartialResultException expected) { + assertThat(true).isTrue(); + } + + verify(dirContextMock).close(); + } + + @Test + public void testSearch_PartialResult_IgnoreSet() throws Exception { + LdapClient tested = LdapClient.builder() + .contextSource(contextSourceMock) + .ignorePartialResultException(true).build(); + + expectGetReadOnlyContext(); + + when(dirContextMock.search(eq(nameMock), anyString(), any())).thenThrow(javax.naming.PartialResultException.class); + + tested.search().name(nameMock).toEntryStream(); + + verify(dirContextMock).close(); + } + + @Test + public void testAuthenticateWithSingleUserFoundShouldBeSuccessful() throws Exception { + AuthenticatedLdapEntryContextMapper entryContextMapper = mock(AuthenticatedLdapEntryContextMapper.class); + + when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock); + + Object expectedObject = new DirContextAdapter(new BasicAttributes(), LdapUtils.newLdapName("cn=john doe"), + LdapUtils.newLdapName("dc=jayway, dc=se")); + SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes()); + singleSearchResult(searchControlsRecursive(), searchResult); + + when(contextSourceMock.getContext("cn=john doe,dc=jayway,dc=se", "password")) + .thenReturn(authenticatedContextMock); + when(entryContextMapper.mapWithContext(any(), any())).thenReturn(new Object()); + + LdapQuery query = LdapQueryBuilder.query().base(nameMock).filter("(ou=somevalue)"); + Object result = tested.authenticate().query(query).password("password").execute(entryContextMapper); + + verify(authenticatedContextMock).close(); + verify(dirContextMock).close(); + assertThat(result).isNotNull(); + } + + @Test + public void testAuthenticateWithTwoUsersFoundShouldThrowException() throws Exception { + when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock); + + Object expectedObject = new DirContextAdapter(new BasicAttributes(), LdapUtils.newLdapName("cn=john doe"), + LdapUtils.newLdapName("dc=jayway, dc=se")); + SearchResult searchResult1 = new SearchResult("", expectedObject, new BasicAttributes()); + SearchResult searchResult2 = new SearchResult("", expectedObject, new BasicAttributes()); + + setupSearchResults(searchControlsRecursive(), new SearchResult[] { searchResult1, searchResult2 }); + + try { + LdapQuery query = LdapQueryBuilder.query().base(nameMock).filter("(ou=somevalue)"); + tested.authenticate().query(query).password("password").execute(); + fail("IncorrectResultSizeDataAccessException expected"); + } + catch (IncorrectResultSizeDataAccessException expected) { + // expected + } + + verify(dirContextMock).close(); + } + + @Test + public void testAuthenticateWhenNoUserWasFoundShouldFail() throws Exception { + when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock); + + noSearchResults(searchControlsRecursive()); + + LdapQuery query = LdapQueryBuilder.query().base(nameMock).filter("(ou=somevalue)"); + assertThatExceptionOfType(EmptyResultDataAccessException.class).isThrownBy(() -> + tested.authenticate().query(query).password("password").execute()); + + verify(dirContextMock).close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testAuthenticateQueryPasswordWhenNoUserWasFoundShouldThrowEmptyResult() throws Exception { + when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock); + + noSearchResults(searchControlsRecursive()); + + LdapQuery query = LdapQueryBuilder.query().base(nameMock).filter("(ou=somevalue)"); + assertThatExceptionOfType(EmptyResultDataAccessException.class).isThrownBy(() -> + tested.authenticate().query(query).password("password").execute((ctx, entry) -> new Object())); + + verify(dirContextMock).close(); + } + + @Test + public void testAuthenticateWithFailedAuthenticationShouldFail() throws Exception { + when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock); + + Object expectedObject = new DirContextAdapter(new BasicAttributes(), LdapUtils.newLdapName("cn=john doe"), + LdapUtils.newLdapName("dc=jayway, dc=se")); + SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes()); + + singleSearchResult(searchControlsRecursive(), searchResult); + + when(contextSourceMock.getContext("cn=john doe,dc=jayway,dc=se", "password")) + .thenThrow(new UncategorizedLdapException("Authentication failed")); + + LdapQuery query = LdapQueryBuilder.query().base(nameMock).filter("(ou=somevalue)"); + assertThatExceptionOfType(UncategorizedLdapException.class).isThrownBy(() -> + tested.authenticate().query(query).password("password").execute()); + verify(dirContextMock).close(); + } + + private void noSearchResults(SearchControls controls) throws Exception { + when(dirContextMock.search( + eq(nameMock), + eq("(ou=somevalue)"), + argThat(new SearchControlsMatcher(controls)))).thenReturn(namingEnumerationMock); + + when(namingEnumerationMock.hasMore()).thenReturn(false); + } + + private void singleSearchResult(SearchControls controls, SearchResult searchResult) throws Exception { + setupSearchResults(controls, new SearchResult[] { searchResult }); + } + + private void setupSearchResults(SearchControls controls, SearchResult... searchResults) throws Exception { + when(dirContextMock.search( + eq(nameMock), + eq("(ou=somevalue)"), + argThat(new SearchControlsMatcher(controls)))).thenReturn(namingEnumerationMock); + + if(searchResults.length == 1) { + when(namingEnumerationMock.hasMore()).thenReturn(true, false); + when(namingEnumerationMock.next()).thenReturn(searchResults[0]); + } else if(searchResults.length ==2) { + when(namingEnumerationMock.hasMore()).thenReturn(true, true, false); + when(namingEnumerationMock.next()).thenReturn(searchResults[0], searchResults[1]); + } else { + throw new IllegalArgumentException("Cannot handle " + searchResults.length + " search results"); + } + } + + private void singleSearchResultWithStringBase(SearchControls controls, SearchResult searchResult) + throws Exception { + when(dirContextMock.search( + eq(DEFAULT_BASE), + eq("(ou=somevalue)"), + argThat(new SearchControlsMatcher(controls)))).thenReturn(namingEnumerationMock); + + when(namingEnumerationMock.hasMore()).thenReturn(true, false); + when(namingEnumerationMock.next()).thenReturn(searchResult); + } + + private SearchControls searchControlsRecursive() { + SearchControls controls = new SearchControls(); + controls.setSearchScope(SearchControls.SUBTREE_SCOPE); + controls.setReturningObjFlag(true); + return controls; + } + + private SearchControls searchControlsOneLevel() { + SearchControls controls = new SearchControls(); + controls.setSearchScope(SearchControls.ONELEVEL_SCOPE); + controls.setReturningObjFlag(true); + return controls; + } + + private static class SearchControlsMatcher implements ArgumentMatcher { + private final SearchControls controls; + + public SearchControlsMatcher(SearchControls controls) { + this.controls = controls; + } + + @Override + public boolean matches(SearchControls item) { + if (item instanceof SearchControls) { + SearchControls s1 = item; + + return controls.getSearchScope() == s1.getSearchScope() + && controls.getReturningObjFlag() == s1.getReturningObjFlag() + && controls.getDerefLinkFlag() == s1.getDerefLinkFlag() + && controls.getCountLimit() == s1.getCountLimit() + && controls.getTimeLimit() == s1.getTimeLimit() + && controls.getReturningAttributes() == s1.getReturningAttributes(); + } + else { + throw new IllegalArgumentException(); + } + } + } +} diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 35a0acc66..07edcd2aa 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -43,8 +43,9 @@ Spring LDAP is designed to simplify LDAP programming in Java. Some of the featur * Proper LDAP connection pooling. * Client-side LDAP compensating transaction support. +[[spring-ldap-traditional-ldap-vs-ldapclient]] [[spring-ldap-traditional-ldap-vs-ldaptemplate]] -=== Traditional Java LDAP versus `LdapTemplate` +=== Traditional Java LDAP versus `LdapClient` Consider a method that should search some storage for all persons and return their names in a list. By using JDBC, we would create a _connection_ and run a _query_ by using a _statement_. We would then loop over the _result set_ and retrieve the _column_ we want, adding it to a list. @@ -114,7 +115,7 @@ public class TraditionalPersonRepoImpl implements PersonRepo { ---- ==== -By using the Spring LDAP `AttributesMapper` and `LdapTemplate` classes, we get the exact same functionality with the following code: +By using the Spring LDAP `AttributesMapper` and `LdapClient` classes, we get the exact same functionality with the following code: ==== [source,java] @@ -124,28 +125,25 @@ package com.example.repo; import static org.springframework.ldap.query.LdapQueryBuilder.query; public class PersonRepoImpl implements PersonRepo { - private LdapTemplate ldapTemplate; + private LdapClient ldapClient; - public void setLdapTemplate(LdapTemplate ldapTemplate) { - this.ldapTemplate = ldapTemplate; + public void setLdapClient(LdapClient ldapClient) { + this.ldapClient = ldapClient; } public List getAllPersonNames() { - return ldapTemplate.search( - **query().where("objectclass").is("person")**, - new AttributesMapper() { - public String mapFromAttributes(Attributes attrs) - throws NamingException { - **return attrs.get("cn").get().toString();** - } - }); + return ldapClient.search().query( + **query().where("objectclass").is("person")** + ).toObject((Attributes attrs) -> + **attrs.get("cn").get().toString();** + ); } } ---- ==== The amount of boilerplate code is significantly less than in the traditional example. -The `LdapTemplate` search method makes sure a `DirContext` instance is created, performs the search, maps the attributes to a string by using the given `AttributesMapper`, +The `LdapClient` search method makes sure a `DirContext` instance is created, performs the search, maps the attributes to a string by using the given `AttributesMapper`, collects the strings in an internal list, and, finally, returns the list. It also makes sure that the `NamingEnumeration` and `DirContext` are properly closed and takes care of any exceptions that might happen. @@ -167,10 +165,12 @@ Naturally, this being a Spring Framework sub-project, we use Spring to configure username="cn=Manager" password="secret" /> - + + + - + ---- @@ -300,27 +300,22 @@ package com.example.repo; import static org.springframework.ldap.query.LdapQueryBuilder.query; public class PersonRepoImpl implements PersonRepo { - private LdapTemplate ldapTemplate; + private LdapClient ldapClient; - public void setLdapTemplate(LdapTemplate ldapTemplate) { - this.ldapTemplate = ldapTemplate; + public void setLdapClient(LdapClient ldapClient) { + this.ldapClient = ldapClient; } public List getAllPersonNames() { - return ldapTemplate.search( - query().where("objectclass").is("person"), - **new AttributesMapper() { - public String mapFromAttributes(Attributes attrs) - throws NamingException { - return (String) attrs.get("cn").get(); - } - }); - }** + return ldapClient.search() + .query(query().where("objectclass").is("person")) + .toList(**(Attributes attrs) -> (String) attrs.get("cn").get()**); + } } ---- ==== -The inline implementation of `AttributesMapper` gets the desired attribute value from the `Attributes` object and returns it. Internally, `LdapTemplate` iterates over all entries found, calls the given `AttributesMapper` for each entry, and collects the results in a list. The list is then returned by the `search` method. +The inline implementation of `AttributesMapper` gets the desired attribute value from the `Attributes` object and returns it. Internally, `LdapClient` iterates over all entries found, calls the given `AttributesMapper` for each entry, and collects the results in a list. The list is then returned by the `search` method. Note that the `AttributesMapper` implementation could easily be modified to return a full `Person` object, as follows: @@ -333,7 +328,7 @@ package com.example.repo; import static org.springframework.ldap.query.LdapQueryBuilder.query; public class PersonRepoImpl implements PersonRepo { - private LdapTemplate ldapTemplate; + private LdapClient ldapClient; ... **private class PersonAttributesMapper implements AttributesMapper { public Person mapFromAttributes(Attributes attrs) throws NamingException { @@ -346,15 +341,16 @@ public class PersonRepoImpl implements PersonRepo { }** public List getAllPersons() { - return ldapTemplate.search(query() - .where("objectclass").is("person"), **new PersonAttributesMapper()**); + return ldapClient.search() + .query(query().where("objectclass").is("person")) + .toList(new PersonAttributesMapper()); } } ---- ==== Entries in LDAP are uniquely identified by their distinguished name (DN). -If you have the DN of an entry, you can retrieve the entry directly without searching for it. +If you have the DN of an entry, you can retrieve the entry directly without querying for it. This is called a "`lookup`" in Java LDAP. The following example shows a lookup for a `Person` object: .A lookup resulting in a Person object @@ -364,10 +360,10 @@ This is called a "`lookup`" in Java LDAP. The following example shows a lookup f package com.example.repo; public class PersonRepoImpl implements PersonRepo { - private LdapTemplate ldapTemplate; + private LdapClient ldapClient; ... public Person findPerson(String dn) { - return ldapTemplate.lookup(dn, new PersonAttributesMapper()); + return ldapClient.search().name(dn).toObject(new PersonAttributesMapper()); } } ---- @@ -400,7 +396,7 @@ package com.example.repo; **import static org.springframework.ldap.query.LdapQueryBuilder.query;** public class PersonRepoImpl implements PersonRepo { - private LdapTemplate ldapTemplate; + private LdapClient ldapClient; ... public List getPersonNamesByLastName(String lastName) { @@ -410,14 +406,8 @@ public class PersonRepoImpl implements PersonRepo { .where("objectclass").is("person") .and("sn").is(lastName);** - return ldapTemplate.search(**query**, - new AttributesMapper() { - public String mapFromAttributes(Attributes attrs) - throws NamingException { - - return (String) attrs.get("cn").get(); - } - }); + return ldapClient.search().query(**query**) + .toObject((Attributes attrs) -> (String) attrs.get("cn").get()); } } ---- @@ -425,7 +415,7 @@ public class PersonRepoImpl implements PersonRepo { NOTE: In addition to simplifying building of complex search parameters, the `LdapQueryBuilder` and its associated classes also provide proper escaping of any unsafe characters in search filters. This prevents "`LDAP injection`", where a user might use such characters to inject unwanted operations into your LDAP operations. -NOTE: `LdapTemplate` includes many overloaded methods for performing LDAP searches. This is in order to accommodate as many different use cases and programming style preferences as possible. For the vast majority of use cases, the methods that take an `LdapQuery` as input are the recommended methods to use. +NOTE: `LdapClient` includes many overloaded methods for performing LDAP searches. This is in order to accommodate as many different use cases and programming style preferences as possible. For the vast majority of use cases, the methods that take an `LdapQuery` as input are the recommended methods to use. NOTE: The `AttributesMapper` is only one of the available callback interfaces you can use when handling search and lookup data. See <> for alternatives. @@ -536,7 +526,7 @@ This section describes how to add and remove data. Updating is covered in the << Inserting data in Java LDAP is called binding. This is somewhat confusing, because in LDAP terminology, "`bind`" means something completely different. A JNDI bind performs an LDAP Add operation, associating a new entry that has a specified distinguished name with a set of attributes. -The following example adds data by using `LdapTemplate`: +The following example adds data by using `LdapClient`: .Adding data using Attributes ==== @@ -546,11 +536,11 @@ The following example adds data by using `LdapTemplate`: package com.example.repo; public class PersonRepoImpl implements PersonRepo { - private LdapTemplate ldapTemplate; + private LdapClient ldapClient; ... public void create(Person p) { Name dn = buildDn(p); - **ldapTemplate.bind(dn, null, buildAttributes(p));** + **ldapClient.bind(dn).attributes(buildAttributes(p)).execute();** } private Attributes buildAttributes(Person p) { @@ -574,7 +564,7 @@ Manual attributes building is -- while dull and verbose -- sufficient for many p Removing data in Java LDAP is called unbinding. A JNDI unbind performs an LDAP Delete operation, removing the entry associated with the specified distinguished name from the LDAP tree. -The following example removes data by using `LdapTemplate`: +The following example removes data by using `LdapClient`: .Removing data ==== @@ -584,11 +574,11 @@ The following example removes data by using `LdapTemplate`: package com.example.repo; public class PersonRepoImpl implements PersonRepo { - private LdapTemplate ldapTemplate; + private LdapClient ldapClient; ... public void delete(Person p) { Name dn = buildDn(p); - **ldapTemplate.unbind(dn);** + **ldapClient.unbind(dn).execute();** } } ---- @@ -603,7 +593,7 @@ In Java LDAP, data can be modified in two ways: either by using `rebind` or by u ==== Updating by Using Rebind A `rebind` is a crude way to modify data. It is basically an `unbind` followed by a `bind`. -The following example uses `rebind`: +The following example invokes LDAP's `rebind`: .Modifying using rebind ==== @@ -613,11 +603,11 @@ The following example uses `rebind`: package com.example.repo; public class PersonRepoImpl implements PersonRepo { - private LdapTemplate ldapTemplate; + private LdapClient ldapClient; ... public void update(Person p) { Name dn = buildDn(p); - **ldapTemplate.rebind(dn, null, buildAttributes(p));** + **ldapTemplate.bind(dn).attributes(buildAttributes(p)).replaceExisting(true).execute();** } } ---- @@ -637,13 +627,13 @@ and performs them on a specific entry, as follows: package com.example.repo; public class PersonRepoImpl implements PersonRepo { - private LdapTemplate ldapTemplate; + private LdapClient ldapClient; ... public void updateDescription(Person p) { Name dn = buildDn(p); Attribute attr = new BasicAttribute("description", p.getDescription()) ModificationItem item = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attr); - **ldapTemplate.modifyAttributes(dn, new ModificationItem[] {item});** + **ldapTemplate.modify().name(dn).attributes(item).execute();** } } ---- @@ -689,7 +679,7 @@ public class PersonRepoImpl implements PersonRepo { public Person findByPrimaryKey( String name, String company, String country) { Name dn = buildDn(name, company, country); - return ldapTemplate.lookup(dn, **new PersonContextMapper()**); + return ldapClient.search().name(dn).toObject(**new PersonContextMapper()**); } } ---- @@ -776,7 +766,7 @@ public class PersonRepoImpl implements PersonRepo { context.setAttributeValue("sn", p.getLastname()); context.setAttributeValue("description", p.getDescription());** - ldapTemplate.bind(context); + ldapClient.bind(dn).object(context).execute(); } } ---- @@ -807,23 +797,23 @@ public class PersonRepoImpl implements PersonRepo { ... public void update(Person p) { Name dn = buildDn(p); - **DirContextOperations context = ldapTemplate.lookupContext(dn);** + **DirContextOperations context = ldapClient.search().name(dn).toEntry();** context.setAttributeValue("cn", p.getFullname()); context.setAttributeValue("sn", p.getLastname()); context.setAttributeValue("description", p.getDescription()); - **ldapTemplate.modifyAttributes(context);** + **ldapClient.modify(dn).attributes(context.getModificationItems()).execute();** } } ---- ==== -When no mapper is passed to a `ldapTemplate.lookup()`, the result is a `DirContextAdapter` instance. -While the `lookup` method returns an `Object`, the `lookupContext` convenience method method automatically casts the return value to a `DirContextOperations` +When calling `SearchSpec#toEntry`, the result is a `DirContextAdapter` instance by default. +While the `lookup` method returns an `Object`, `toEntry` automatically casts the return value to a `DirContextOperations` (the interface that `DirContextAdapter` implements). -Notice that we have duplicate code in the `create` and `update` methods. This code maps from a domain object to a context. It can be extracted to a separate method, as follows: +Notice that we have duplicate code in the `LdapTemplate#create` and `LdapTemplate#update` methods. This code maps from a domain object to a context. It can be extracted to a separate method, as follows: .Adding and modifying using DirContextAdapter ==== @@ -833,7 +823,7 @@ Notice that we have duplicate code in the `create` and `update` methods. This co package com.example.repo; public class PersonRepoImpl implements PersonRepo { - private LdapTemplate ldapTemplate; + private LdapClient ldapClient; ... public void create(Person p) { @@ -842,14 +832,14 @@ public class PersonRepoImpl implements PersonRepo { context.setAttributeValues("objectclass", new String[] {"top", "person"}); mapToContext(p, context); - ldapTemplate.bind(context); + ldapClient.bind(dn).object(context).execute(); } public void update(Person p) { Name dn = buildDn(p); - DirContextOperations context = ldapTemplate.lookupContext(dn); + DirContextOperations context = ldapClient.search().name(dn).toEntry(); mapToContext(person, context); - ldapTemplate.modifyAttributes(context); + ldapClient.modify(dn).object(context).execute(); } protected void mapToContext (Person p, DirContextOperations context) { @@ -882,11 +872,11 @@ use distinguished name equality when calculating attribute modifications. If we [subs="verbatim,quotes"] ---- public class GroupRepo implements BaseLdapNameAware { - private LdapTemplate ldapTemplate; + private LdapClient ldapClient; private LdapName baseLdapPath; - public void setLdapTemplate(LdapTemplate ldapTemplate) { - this.ldapTemplate = ldapTemplate; + public void setLdapClient(LdapClient ldapClient) { + this.ldapClient = ldapClient; } public void setBaseLdapPath(LdapName baseLdapPath) { @@ -900,10 +890,10 @@ public class GroupRepo implements BaseLdapNameAware { person.getCompany(), person.getCountry()); - DirContextOperation ctx = ldapTemplate.lookupContext(groupDn); + DirContextOperation ctx = ldapClient.search().name(groupDn).toEntry(); ctx.addAttributeValue("member", userDn); - ldapTemplate.update(ctx); + ldapClient.modify(groupDn).attributes(ctx.getModificationItems()).execute(); } public void removeMemberFromGroup(String groupName, Person p) { @@ -913,10 +903,10 @@ public class GroupRepo implements BaseLdapNameAware { person.getCompany(), person.getCountry()); - DirContextOperation ctx = ldapTemplate.lookupContext(groupDn); + DirContextOperation ctx = ldapClient.search().name(groupDn).toEntry(); ctx.removeAttributeValue("member", userDn); - ldapTemplate.update(ctx); + ldapClient.modify(groupDn).attributes(ctx.getModificationItems()).execute(); } private Name buildGroupDn(String groupName) { @@ -965,45 +955,45 @@ import org.springframework.ldap.filter.WhitespaceWildcardsFilter; import static org.springframework.ldap.query.LdapQueryBuilder.query; public class PersonRepoImpl implements PersonRepo { - private LdapTemplate ldapTemplate; + private LdapClient ldapClient; - public void setLdapTemplate(LdapTemplate ldapTemplate) { - this.ldapTemplate = ldapTemplate; + public void setLdapClient(LdapClient ldapClient) { + this.ldapClient = ldapClient; } public void create(Person person) { DirContextAdapter context = new DirContextAdapter(buildDn(person)); mapToContext(person, context); - ldapTemplate.bind(context); + ldapClient.bind(context.getDn()).object(context).execute(); } public void update(Person person) { Name dn = buildDn(person); - DirContextOperations context = ldapTemplate.lookupContext(dn); + DirContextOperations context = ldapClient.lookupContext(dn); mapToContext(person, context); - ldapTemplate.modifyAttributes(context); + ldapClient.modify(dn).attributes(context.getModificationItems()); } public void delete(Person person) { - ldapTemplate.unbind(buildDn(person)); + ldapClient.unbind(buildDn(person)).execute(); } public Person findByPrimaryKey(String name, String company, String country) { Name dn = buildDn(name, company, country); - return ldapTemplate.lookup(dn, getContextMapper()); + return ldapClient.search().name(dn).toObject(getContextMapper()); } - public List findByName(String name) { + public List findByName(String name) { LdapQuery query = query() .where("objectclass").is("person") .and("cn").whitespaceWildcardsLike("name"); - return ldapTemplate.search(query, getContextMapper()); + return ldapClient.search().query(query).toList(getContextMapper()); } - public List findAll() { + public List findAll() { EqualsFilter filter = new EqualsFilter("objectclass", "person"); - return ldapTemplate.search(LdapUtils.emptyPath(), filter.encode(), getContextMapper()); + return ldapClient.search().query((query) -> query.filter(filter)).toList(getContextMapper()); } protected ContextMapper getContextMapper() { @@ -1259,9 +1249,9 @@ The following query searches for all entries with an object class of `Person`: import static org.springframework.ldap.query.LdapQueryBuilder.query; ... -List persons = ldapTemplate.search( - query().where("objectclass").is("person"), - new PersonAttributesMapper()); +List persons = ldapClient.search() + .query(query().where("objectclass").is("person")) + .toList(new PersonAttributesMapper()); ---- ==== @@ -1276,10 +1266,9 @@ The following query searches for all entries with an object class of `person` an import static org.springframework.ldap.query.LdapQueryBuilder.query; ... -List persons = ldapTemplate.search( - query().where("objectclass").is("person") - .and("cn").is("John Doe"), - new PersonAttributesMapper()); +List persons = ldapClient.search() + .query(query().where("objectclass").is("person").and("cn").is("John Doe")) + .toList(new PersonAttributesMapper()); ---- ==== @@ -1293,10 +1282,9 @@ The following query searches for all entries with an object class of `person` an import static org.springframework.ldap.query.LdapQueryBuilder.query; ... -List persons = ldapTemplate.search( - query().base("dc=261consulting,dc=com") - .where("objectclass").is("person"), - new PersonAttributesMapper()); +List persons = ldapClient.search() + .query(query().base("dc=261consulting,dc=com").where("objectclass").is("person")) + .toList(new PersonAttributesMapper()); ---- ==== @@ -1311,11 +1299,11 @@ The following query returns the `cn` (common name) attribute for all entries wit import static org.springframework.ldap.query.LdapQueryBuilder.query; ... -List persons = ldapTemplate.search( - query().base("dc=261consulting,dc=com") +Stream persons = ldapClient.search() + .query(query().base("dc=261consulting,dc=com") .attributes("cn") - .where("objectclass").is("person"), - new PersonAttributesMapper()); + .where("objectclass").is("person")), + .toStream(new PersonAttributesMapper()); ---- ==== @@ -1328,10 +1316,10 @@ The following query uses `or` to search for multiple spellings of a common name ---- import static org.springframework.ldap.query.LdapQueryBuilder.query; ... -List persons = ldapTemplate.search( - query().where("objectclass").is("person"), - .and(query().where("cn").is("Doe").or("cn").is("Doo")); - new PersonAttributesMapper()); +Stream persons = ldapClient.search() + .query(query().where("objectclass").is("person"), + .and(query().where("cn").is("Doe").or("cn").is("Doo")) + .toStream(new PersonAttributesMapper()); ---- ==== @@ -1463,7 +1451,7 @@ a| Defines the strategy with which to handle referrals, as described https://do When `DirContext` instances are created to be used for performing operations on an LDAP server, these contexts often need to be authenticated. Spring LDAP offers various options for configuring this. -NOTE: This section refers to authenticating contexts in the core functionality of the `ContextSource`, to construct `DirContext` instances for use by `LdapTemplate`. LDAP is commonly used for the sole purpose of user authentication, and the `ContextSource` may be used for that as well. That process is discussed in <>. +NOTE: This section refers to authenticating contexts in the core functionality of the `ContextSource`, to construct `DirContext` instances for use by `LdapClient` and `LdapTemplate`. LDAP is commonly used for the sole purpose of user authentication, and the `ContextSource` may be used for that as well. That process is discussed in <>. By default, authenticated contexts are created for both read-only and read-write operations. You should specify the `username` and `password` of the LDAP user to be used for authentication on the `context-source` element. @@ -1536,6 +1524,33 @@ This section covers more advanced ways to configure a `ContextSource`. In some cases, you might want to specify additional environment setup properties, in addition to the ones directly configurable on `context-source`. You should set such properties in a `Map` and reference them in the `base-env-props-ref` attribute. +=== `LdapClient` Configuration + +`LdapClient` is the new interface for calling an LDAP backend. It improves upon `LdapTemplate` in the following ways: + +* Provides built-in `Stream` support +* Provides a simplified API centered around bind (C), search (R), modify (U), unbind (D), and authenticate. + +[NOTE] +`LdapClient` does not yet support ODM. +If this is something you need, `LdapTemplate` has this capacity. +Both `LdapClient` and `LdapTemplate` can coexist quite nicely in the same application, if needed. + +An `LdapClient` is defined by using the `LdapClient#create` factory method like so: + +.Simplest possible LdapClient declaration +==== +[source,xml] +---- + + + +---- +==== + +This element references the default `ContextSource`, which is expected to have an ID of `contextSource` (the default for the `context-source` element). + +Your `LdapClient` instance can be configured for how to handle certain checked exceptions and what any default `SearchControls` should be used for queries. === `LdapTemplate` Configuration @@ -2400,7 +2415,7 @@ This section covers user authentication with Spring LDAP. It contains the follow [[spring-ldap-user-authentication-basic]] === Basic Authentication -While the core functionality of the `ContextSource` is to provide `DirContext` instances for use by `LdapTemplate`, you can also use it for authenticating users against an LDAP server. The `getContext(principal, credentials)` method of `ContextSource` does exactly that. It constructs a `DirContext` instance according to the `ContextSource` configuration and authenticates the context by using the supplied principal and credentials. A custom authenticate method could look like the following example: +While the core functionality of the `ContextSource` is to provide `DirContext` instances for use by `LdapClient` and `LdapTemplate`, you can also use it for authenticating users against an LDAP server. The `getContext(principal, credentials)` method of `ContextSource` does exactly that. It constructs a `DirContext` instance according to the `ContextSource` configuration and authenticates the context by using the supplied principal and credentials. A custom authenticate method could look like the following example: ==== [source,java] @@ -2430,13 +2445,9 @@ The `userDn` supplied to the `authenticate` method needs to be the full DN of th [subs="verbatim,quotes"] ---- private String getDnForUser(String uid) { - List result = ldapTemplate.search( - query().where("uid").is(uid), - new AbstractContextMapper() { - protected String doMapFromContext(DirContextOperations ctx) { - return ctx.getNameInNamespace(); - } - }); + List result = ldapClient.search() + .query(query().where("uid").is(uid)) + .toList((Object ctx) -> ((DirContextOperations) ctx).getNameInNamespace()); if(result.size() != 1) { throw new RuntimeException("User not found or not unique"); @@ -2447,7 +2458,7 @@ private String getDnForUser(String uid) { ---- ==== -There are some drawbacks to this approach. You are forced to concern yourself with the DN of the user, you can search only for the user's uid, and the search always starts at the root of the tree (the empty path). A more flexible method would let you specify the search base, the search filter, and the credentials. Spring LDAP includes an authenticate method in `LdapTemplate` that provides this functionality: `boolean authenticate(LdapQuery query, String password);`. +There are some drawbacks to this approach. You are forced to concern yourself with the DN of the user, you can search only for the user's uid, and the search always starts at the root of the tree (the empty path). A more flexible method would let you specify the search base, the search filter, and the credentials. Spring LDAP includes an authenticate method in `LdapClient` that provides this functionality. When you use this method, authentication becomes as simple as follows: @@ -2456,7 +2467,7 @@ When you use this method, authentication becomes as simple as follows: [source,java] [subs="verbatim,quotes"] ---- -ldapTemplate.authenticate(query().where("uid").is("john.doe"), "secret"); +ldapClient.authenticate().query(query().where("uid").is("john.doe")).password("secret").execute(); ---- ==== @@ -2493,7 +2504,7 @@ public boolean myAuthenticate(String userDn, String credentials) { ---- ==== -It would be better if the operation could be provided as an implementation of a callback interface, rather than limiting the operation to always be a `lookup`. Spring LDAP includes the `AuthenticatedLdapEntryContextMapper` callback interface and a corresponding `authenticate` method: ` T authenticate(LdapQuery query, String password, AuthenticatedLdapEntryContextMapper mapper);` +It would be better if the operation could be provided as an implementation of a callback interface, rather than limiting the operation to always be a `lookup`. Spring LDAP includes the `AuthenticatedLdapEntryContextMapper` callback interface and a corresponding `authenticate` method. This method lets any operation be performed on the authenticated context, as follows: @@ -2513,7 +2524,7 @@ AuthenticatedLdapEntryContextMapper mapper = new Authentic } }; -ldapTemplate.authenticate(query().where("uid").is("john.doe"), "secret", mapper); +ldapClient.authenticate().query(query().where("uid").is("john.doe")).password("secret").execute(mapper); ---- ==== diff --git a/test/integration-tests/src/main/java/org/springframework/ldap/itest/PersonAttributesMapper.java b/test/integration-tests/src/main/java/org/springframework/ldap/itest/PersonAttributesMapper.java index e7de9dbac..b0894f4ed 100644 --- a/test/integration-tests/src/main/java/org/springframework/ldap/itest/PersonAttributesMapper.java +++ b/test/integration-tests/src/main/java/org/springframework/ldap/itest/PersonAttributesMapper.java @@ -28,14 +28,14 @@ * @author Mattias Hellborg Arthursson * */ -public class PersonAttributesMapper implements AttributesMapper { +public class PersonAttributesMapper implements AttributesMapper { /** * Maps the given attributes into a {@link Person} object. * * @see org.springframework.ldap.core.AttributesMapper#mapFromAttributes(javax.naming.directory.Attributes) */ - public Object mapFromAttributes(Attributes attributes) + public Person mapFromAttributes(Attributes attributes) throws NamingException { Person person = new Person(); person.setFullname((String) attributes.get("cn").get()); diff --git a/test/integration-tests/src/main/java/org/springframework/ldap/itest/PersonContextMapper.java b/test/integration-tests/src/main/java/org/springframework/ldap/itest/PersonContextMapper.java index c273b683c..f2c07d5b3 100644 --- a/test/integration-tests/src/main/java/org/springframework/ldap/itest/PersonContextMapper.java +++ b/test/integration-tests/src/main/java/org/springframework/ldap/itest/PersonContextMapper.java @@ -25,9 +25,9 @@ * * @author Mattias Hellborg Arthursson */ -public class PersonContextMapper extends AbstractContextMapper { +public class PersonContextMapper extends AbstractContextMapper { - protected Object doMapFromContext(DirContextOperations ctx) { + protected Person doMapFromContext(DirContextOperations ctx) { Person person = new Person(); person.setFullname(ctx.getStringAttribute("cn")); person.setLastname(ctx.getStringAttribute("sn")); diff --git a/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientAuthenticationITest.java b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientAuthenticationITest.java new file mode 100644 index 000000000..a2a23e672 --- /dev/null +++ b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientAuthenticationITest.java @@ -0,0 +1,179 @@ +/* + * Copyright 2005-2023 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.ldap.itest; + +import javax.naming.NamingException; + +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.ldap.AuthenticationException; +import org.springframework.ldap.core.AuthenticatedLdapEntryContextCallback; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.LdapClient; +import org.springframework.ldap.core.support.LookupAttemptingCallback; +import org.springframework.ldap.filter.AndFilter; +import org.springframework.ldap.filter.EqualsFilter; +import org.springframework.ldap.query.LdapQuery; +import org.springframework.ldap.query.LdapQueryBuilder; +import org.springframework.ldap.support.LdapUtils; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link LdapClient}'s authenticate methods. + * + * @author Josh Cummings + */ +@ContextConfiguration(locations = {"/conf/ldapClientTestContext.xml"}) +public class SpringLdapClientAuthenticationITest extends AbstractLdapTemplateIntegrationTest { + + @Autowired + private LdapClient tested; + + @Test + @Category(NoAdTest.class) + public void testAuthenticate() { + AndFilter filter = new AndFilter(); + filter.and(new EqualsFilter("objectclass", "person")).and(new EqualsFilter("uid", "some.person3")); + LdapQuery query = LdapQueryBuilder.query().base(LdapUtils.emptyLdapName()).filter(filter); + tested.authenticate().query(query).password("password").execute(); + } + + @Test + @Category(NoAdTest.class) + public void testAuthenticateWithLdapQuery() { + AndFilter filter = new AndFilter(); + filter.and(new EqualsFilter("objectclass", "person")).and(new EqualsFilter("uid", "some.person3")); + LdapQuery query = LdapQueryBuilder.query() + .where("objectclass").is("person") + .and("uid").is("some.person3"); + tested.authenticate().query(query).password("password").execute(); + } + + @Test + @Category(NoAdTest.class) + public void testAuthenticateWithInvalidPassword() { + AndFilter filter = new AndFilter(); + filter.and(new EqualsFilter("objectclass", "person")).and(new EqualsFilter("uid", "some.person3")); + LdapQuery query = LdapQueryBuilder.query().filter(filter); + assertThatExceptionOfType(AuthenticationException.class).isThrownBy(() -> + tested.authenticate().query(query).password("invalidpassword").execute()); + } + + @Test + @Category(NoAdTest.class) + public void testAuthenticateWithLdapQueryAndInvalidPassword() { + AndFilter filter = new AndFilter(); + filter.and(new EqualsFilter("objectclass", "person")).and(new EqualsFilter("uid", "some.person3")); + LdapQuery query = LdapQueryBuilder.query() + .where("objectclass").is("person") + .and("uid").is("some.person3"); + assertThatExceptionOfType(AuthenticationException.class).isThrownBy(() -> + tested.authenticate().query(query).password("invalidpassword").execute()); + } + + @Test + @Category(NoAdTest.class) + public void testAuthenticateWithLookupOperationPerformedOnAuthenticatedContext() { + AndFilter filter = new AndFilter(); + filter.and(new EqualsFilter("objectclass", "person")).and(new EqualsFilter("uid", "some.person3")); + LdapQuery query = LdapQueryBuilder.query().filter(filter); + AuthenticatedLdapEntryContextCallback contextCallback = (ctx, entry) -> { + try { + DirContextAdapter adapter = (DirContextAdapter) ctx.lookup(entry.getRelativeDn()); + assertThat(adapter.getStringAttribute("cn")).isEqualTo("Some Person3"); + } catch (NamingException e) { + throw new RuntimeException("Failed to lookup " + entry.getRelativeDn(), e); + } + }; + tested.authenticate().query(query).password("password").execute((ctx, entry) -> { + contextCallback.executeWithContext(ctx, entry); + return null; + }); + } + + @Test + @Category(NoAdTest.class) + public void testAuthenticateWithLdapQueryAndMapper() { + LdapQuery query = LdapQueryBuilder.query() + .where("objectclass").is("person") + .and("uid").is("some.person3"); + DirContextOperations ctx = tested.authenticate().query(query).password("password") + .execute(new LookupAttemptingCallback()); + + assertThat(ctx).isNotNull(); + assertThat(ctx.getStringAttribute("uid")).isEqualTo("some.person3"); + } + + @Test + @Category(NoAdTest.class) + public void testAuthenticateWithLdapQueryAndMapperAndInvalidPassword() { + LdapQuery query = LdapQueryBuilder.query() + .where("objectclass").is("person") + .and("uid").is("some.person3"); + assertThatExceptionOfType(AuthenticationException.class).isThrownBy(() -> + tested.authenticate().query(query).password("invalidpassword").execute(new LookupAttemptingCallback())); + } + + @Test + @Category(NoAdTest.class) + public void testAuthenticateWithInvalidPasswordAndCollectedException() { + AndFilter filter = new AndFilter(); + filter.and(new EqualsFilter("objectclass", "person")).and(new EqualsFilter("uid", "some.person3")); + LdapQuery query = LdapQueryBuilder.query().filter(filter); + assertThatExceptionOfType(AuthenticationException.class).isThrownBy(() -> + tested.authenticate().query(query).password("invalidpassword").execute()); + } + + @Test + @Category(NoAdTest.class) + public void testAuthenticateWithFilterThatDoesNotMatchAnything() { + AndFilter filter = new AndFilter(); + filter.and(new EqualsFilter("objectclass", "person")).and( + new EqualsFilter("uid", "some.person.that.isnt.there")); + LdapQuery query = LdapQueryBuilder.query().filter(filter); + assertThatExceptionOfType(EmptyResultDataAccessException.class).isThrownBy(() -> + tested.authenticate().query(query).password("password").execute()); + } + + @Test + @Category(NoAdTest.class) + public void testAuthenticateWithFilterThatMatchesSeveralEntries() { + AndFilter filter = new AndFilter(); + filter.and(new EqualsFilter("objectclass", "person")).and(new EqualsFilter("cn", "Some Person")); + LdapQuery query = LdapQueryBuilder.query().filter(filter); + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy(() -> + tested.authenticate().query(query).password("password").execute()); + } + + @Test + @Category(NoAdTest.class) + public void testLookupAttemptingCallback() { + AndFilter filter = new AndFilter(); + filter.and(new EqualsFilter("objectclass", "person")).and(new EqualsFilter("uid", "some.person3")); + LdapQuery query = LdapQueryBuilder.query().filter(filter); + LookupAttemptingCallback callback = new LookupAttemptingCallback(); + tested.authenticate().query(query).password("password").execute(callback); + } +} diff --git a/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientBindUnbindITest.java b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientBindUnbindITest.java new file mode 100644 index 000000000..832289e64 --- /dev/null +++ b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientBindUnbindITest.java @@ -0,0 +1,167 @@ +/* + * Copyright 2005-2023 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.ldap.itest; + +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttribute; +import javax.naming.directory.BasicAttributes; + +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ldap.NameNotFoundException; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.LdapClient; +import org.springframework.ldap.support.LdapUtils; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests the bind and unbind methods of LdapTemplate. The test methods in this + * class tests a little too much, but we need to clean up after binding, so the + * most efficient way to test is to do it all in one test method. Also, the + * methods in this class relies on that the lookup method works as it should - + * that should be ok, since that is verified in a separate test class. + * + * @author Mattias Hellborg Arthursson + */ +@ContextConfiguration(locations = {"/conf/ldapClientTestContext.xml"}) +public class SpringLdapClientBindUnbindITest extends AbstractLdapTemplateIntegrationTest { + @Autowired + private LdapClient tested; + + private static final String DN = "cn=Some Person4,ou=company1,ou=Sweden"; + + @Test + public void testBindAndUnbindWithAttributes() { + Attributes attributes = setupAttributes(); + tested.bind(DN).attributes(attributes).execute(); + verifyBoundCorrectData(); + tested.unbind(DN).execute(); + verifyCleanup(); + } + + @Test + public void testBindGroupOfUniqueNamesWithNameValues() { + DirContextAdapter ctx = new DirContextAdapter(LdapUtils.newLdapName("cn=TEST,ou=groups")); + ctx.addAttributeValue("cn", "TEST"); + ctx.addAttributeValue("objectclass", "top"); + ctx.addAttributeValue("objectclass", "groupOfUniqueNames"); + ctx.addAttributeValue("uniqueMember", LdapUtils.newLdapName("cn=Some Person,ou=company1,ou=Sweden," + base)); + tested.bind(ctx.getDn()).object(ctx).execute(); + } + + @Test + public void testBindAndUnbindWithAttributesUsingLdapName() { + Attributes attributes = setupAttributes(); + tested.bind(LdapUtils.newLdapName(DN)).attributes(attributes).execute(); + verifyBoundCorrectData(); + tested.unbind(LdapUtils.newLdapName(DN)).execute(); + verifyCleanup(); + } + + @Test + public void testBindAndUnbindWithDirContextAdapter() { + DirContextAdapter adapter = new DirContextAdapter(); + adapter.setAttributeValues("objectclass", new String[] { "top", + "person" }); + adapter.setAttributeValue("cn", "Some Person4"); + adapter.setAttributeValue("sn", "Person4"); + + tested.bind(DN).object(adapter).execute(); + verifyBoundCorrectData(); + tested.unbind(DN).execute(); + verifyCleanup(); + } + + @Test + public void testBindAndUnbindWithDirContextAdapterUsingLdapName() { + DirContextAdapter adapter = new DirContextAdapter(); + adapter.setAttributeValues("objectclass", new String[] { "top", + "person" }); + adapter.setAttributeValue("cn", "Some Person4"); + adapter.setAttributeValue("sn", "Person4"); + + tested.bind(LdapUtils.newLdapName(DN)).object(adapter).execute(); + verifyBoundCorrectData(); + tested.unbind(LdapUtils.newLdapName(DN)).execute(); + verifyCleanup(); + } + + @Test + public void testBindAndUnbindWithDirContextAdapterOnly() { + DirContextAdapter adapter = new DirContextAdapter(LdapUtils.newLdapName(DN)); + adapter.setAttributeValues("objectclass", new String[] { "top", + "person" }); + adapter.setAttributeValue("cn", "Some Person4"); + adapter.setAttributeValue("sn", "Person4"); + + tested.bind(DN).object(adapter).execute(); + verifyBoundCorrectData(); + tested.unbind(DN).execute(); + verifyCleanup(); + } + + @Test + public void testBindAndRebindWithDirContextAdapterOnly() { + DirContextAdapter adapter = new DirContextAdapter(LdapUtils.newLdapName(DN)); + adapter.setAttributeValues("objectclass", new String[] { "top", + "person" }); + adapter.setAttributeValue("cn", "Some Person4"); + adapter.setAttributeValue("sn", "Person4"); + + tested.bind(DN).object(adapter).execute(); + verifyBoundCorrectData(); + adapter.setAttributeValue("sn", "Person4.Changed"); + tested.bind(DN).object(adapter).replaceExisting(true).execute(); + verifyReboundCorrectData(); + tested.unbind(DN).execute(); + verifyCleanup(); + } + + private Attributes setupAttributes() { + Attributes attributes = new BasicAttributes(); + BasicAttribute ocattr = new BasicAttribute("objectclass"); + ocattr.add("top"); + ocattr.add("person"); + attributes.put(ocattr); + attributes.put("cn", "Some Person4"); + attributes.put("sn", "Person4"); + return attributes; + } + + private void verifyBoundCorrectData() { + DirContextOperations result = tested.search().name(DN).toEntry(); + assertThat(result.getStringAttribute("cn")).isEqualTo("Some Person4"); + assertThat(result.getStringAttribute("sn")).isEqualTo("Person4"); + } + + private void verifyReboundCorrectData() { + DirContextOperations result = tested.search().name(DN).toEntry(); + assertThat(result.getStringAttribute("cn")).isEqualTo("Some Person4"); + assertThat(result.getStringAttribute("sn")).isEqualTo("Person4.Changed"); + } + + private void verifyCleanup() { + assertThatExceptionOfType(NameNotFoundException.class) + .describedAs("NameNotFoundException expected") + .isThrownBy(() -> tested.search().name(DN).toEntry()); + } +} diff --git a/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientListITest.java b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientListITest.java new file mode 100644 index 000000000..d9efac816 --- /dev/null +++ b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientListITest.java @@ -0,0 +1,176 @@ +/* + * Copyright 2005-2023 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.ldap.itest; + +import java.util.LinkedList; +import java.util.List; + +import javax.naming.NameClassPair; +import javax.naming.ldap.LdapName; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.LdapClient; +import org.springframework.ldap.core.support.CountNameClassPairCallbackHandler; +import org.springframework.ldap.support.LdapUtils; +import org.springframework.ldap.test.AttributeCheckContextMapper; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LdapClient}'s list methods. + * + * @author Josh Cummings + */ +@ContextConfiguration(locations = {"/conf/ldapClientTestContext.xml"}) +public class SpringLdapClientListITest extends AbstractLdapTemplateIntegrationTest { + + @Autowired + private LdapClient tested; + + private AttributeCheckContextMapper contextMapper; + + private static final String BASE_STRING = ""; + + private static final LdapName BASE_NAME = LdapUtils.newLdapName(BASE_STRING); + + private static final String[] ALL_ATTRIBUTES = { "cn", "sn", "description", "telephoneNumber" }; + + private static final String[] ALL_VALUES = { "Some Person", "Person", "Sweden, Company2, Some Person", + "+46 555-456321" }; + + @Before + public void prepareTestedInstance() throws Exception { + contextMapper = new AttributeCheckContextMapper(); + } + + @After + public void tearDown() { + contextMapper = null; + } + + @Test + public void testListBindings_ContextMapper() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.listBindings("ou=company2,ou=Sweden" + BASE_STRING).toList(contextMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testListBindings_ContextMapper_Name() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + LdapName dn = LdapUtils.newLdapName("ou=company2,ou=Sweden"); + List list = tested.listBindings(dn).toList(contextMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testListBindings_ContextMapper_MapToPersons() { + LdapName dn = LdapUtils.newLdapName("ou=company1,ou=Sweden"); + List list = tested.listBindings(dn).toList(new PersonContextMapper()); + assertThat(list).hasSize(3); + String personClass = "org.springframework.ldap.itest.Person"; + assertThat(list.get(0).getClass().getName()).isEqualTo(personClass); + assertThat(list.get(1).getClass().getName()).isEqualTo(personClass); + assertThat(list.get(2).getClass().getName()).isEqualTo(personClass); + } + + @Test + public void testList() { + List list = tested.list(BASE_STRING).toList(NameClassPair::getName); + assertThat(list).hasSize(3); + verifyBindings(list); + } + + private void verifyBindings(List list) { + LinkedList transformed = new LinkedList<>(); + + for (String s : list) { + transformed.add(LdapUtils.newLdapName(s)); + } + + assertThat(transformed.contains(LdapUtils.newLdapName("ou=groups"))).isTrue(); + assertThat(transformed.contains(LdapUtils.newLdapName("ou=Norway"))).isTrue(); + assertThat(transformed.contains(LdapUtils.newLdapName("ou=Sweden"))).isTrue(); + } + + @Test + public void testList_Name() { + List list = tested.list(BASE_NAME).toList(NameClassPair::getName); + assertThat(list).hasSize(3); + verifyBindings(list); + } + + @Test + public void testList_Handler() { + CountNameClassPairCallbackHandler handler = new CountNameClassPairCallbackHandler(); + tested.list(BASE_STRING).toList((result) -> { + handler.handleNameClassPair(result); + return result; + }); + assertThat(handler.getNoOfRows()).isEqualTo(3); + } + + @Test + public void testList_Name_Handler() { + CountNameClassPairCallbackHandler handler = new CountNameClassPairCallbackHandler(); + tested.list(BASE_NAME).toList((result) -> { + handler.handleNameClassPair(result); + return result; + }); + assertThat(handler.getNoOfRows()).isEqualTo(3); + } + + @Test + public void testListBindings() { + List list = tested.listBindings(BASE_STRING).toList(NameClassPair::getName); + assertThat(list).hasSize(3); + verifyBindings(list); + } + + @Test + public void testListBindings_Name() { + List list = tested.listBindings(BASE_NAME).toList(NameClassPair::getName); + assertThat(list).hasSize(3); + } + + @Test + public void testListBindings_Handler() { + CountNameClassPairCallbackHandler handler = new CountNameClassPairCallbackHandler(); + tested.list(BASE_STRING).toList((result) -> { + handler.handleNameClassPair(result); + return result; + }); + assertThat(handler.getNoOfRows()).isEqualTo(3); + } + + @Test + public void testListBindings_Name_Handler() { + CountNameClassPairCallbackHandler handler = new CountNameClassPairCallbackHandler(); + tested.list(BASE_NAME).toList((result) -> { + handler.handleNameClassPair(result); + return result; + }); + assertThat(handler.getNoOfRows()).isEqualTo(3); + } +} diff --git a/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientLookupITest.java b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientLookupITest.java new file mode 100644 index 000000000..a49ac0d70 --- /dev/null +++ b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientLookupITest.java @@ -0,0 +1,189 @@ +/* + * Copyright 2005-2023 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.ldap.itest; + +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; + +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ldap.core.AttributesMapper; +import org.springframework.ldap.core.ContextMapper; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.LdapClient; +import org.springframework.ldap.support.LdapUtils; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LdapClient}'s lookup methods. + * + * @author Josh Cummings + */ +@ContextConfiguration(locations = {"/conf/ldapClientTestContext.xml"}) +public class SpringLdapClientLookupITest extends AbstractLdapTemplateIntegrationTest { + + @Autowired + private LdapClient tested; + + /** + * This method depends on a DirObjectFactory ( + * {@link org.springframework.ldap.core.support.DefaultDirObjectFactory}) + * being set in the ContextSource. + */ + @Test + public void testLookup_Plain() { + DirContextOperations result = tested.search().name("cn=Some Person2, ou=company1,ou=Sweden").toEntry(); + + assertThat(result.getStringAttribute("cn")).isEqualTo("Some Person2"); + assertThat(result.getStringAttribute("sn")).isEqualTo("Person2"); + assertThat(result.getStringAttribute("description")).isEqualTo("Sweden, Company1, Some Person2"); + } + + /** + * This method depends on a DirObjectFactory ( + * {@link org.springframework.ldap.core.support.DefaultDirObjectFactory}) + * being set in the ContextSource. + */ + @Test + public void testLookupContextRoot() { + DirContextOperations result = tested.search().name("").toEntry(); + + assertThat(result.getDn().toString()).isEqualTo(""); + assertThat(result.getNameInNamespace()).isEqualTo(base); + } + + @Test + public void testLookup_AttributesMapper() { + AttributesMapper mapper = new PersonAttributesMapper(); + Person person = tested.search().name("cn=Some Person2, ou=company1,ou=Sweden").toObject(mapper); + assertThat(person.getFullname()).isEqualTo("Some Person2"); + assertThat(person.getLastname()).isEqualTo("Person2"); + assertThat(person.getDescription()).isEqualTo("Sweden, Company1, Some Person2"); + } + + @Test + public void testLookup_AttributesMapper_LdapName() { + AttributesMapper mapper = new PersonAttributesMapper(); + Person person = tested.search().name(LdapUtils.newLdapName("cn=Some Person2, ou=company1,ou=Sweden")).toObject(mapper); + + assertThat(person.getFullname()).isEqualTo("Some Person2"); + assertThat(person.getLastname()).isEqualTo("Person2"); + assertThat(person.getDescription()).isEqualTo("Sweden, Company1, Some Person2"); + } + + /** + * An {@link AttributesMapper} that only maps a subset of the full + * attributes list. Used in tests where the return attributes list has been + * limited. + * + * @author Ulrik Sandberg + */ + private static final class SubsetPersonAttributesMapper implements AttributesMapper { + /** + * Maps the cn attribute into a {@link Person} object. Also + * verifies that the other attributes haven't been set. + * + * @see AttributesMapper#mapFromAttributes(Attributes) + */ + public Person mapFromAttributes(Attributes attributes) throws NamingException { + Person person = new Person(); + person.setFullname((String) attributes.get("cn").get()); + assertThat(attributes.get("sn")).as("sn should be null").isNull(); + assertThat(attributes.get("description")).as("description should be null").isNull(); + return person; + } + } + + /** + * Verifies that only the subset is used when specifying a subset of the + * available attributes as return attributes. + */ + @Test + public void testLookup_ReturnAttributes_AttributesMapper() { + AttributesMapper mapper = new SubsetPersonAttributesMapper(); + + Person person = tested.search().query((builder) -> builder + .base("cn=Some Person2, ou=company1,ou=Sweden") + .attributes("cn")).toObject(mapper); + + assertThat(person.getFullname()).isEqualTo("Some Person2"); + assertThat(person.getLastname()).as("lastName should not be set").isNull(); + assertThat(person.getDescription()).as("description should not be set").isNull(); + } + + /** + * Verifies that only the subset is used when specifying a subset of the + * available attributes as return attributes. Uses LdapName instead + * of plain string as name. + */ + @Test + public void testLookup_ReturnAttributes_AttributesMapper_LdapName() { + AttributesMapper mapper = new SubsetPersonAttributesMapper(); + Person person = tested.search().query((builder) -> builder + .base(LdapUtils.newLdapName("cn=Some Person2, ou=company1,ou=Sweden")) + .attributes("cn")).toObject(mapper); + + assertThat(person.getFullname()).isEqualTo("Some Person2"); + assertThat(person.getLastname()).as("lastName should not be set").isNull(); + assertThat(person.getDescription()).as("description should not be set").isNull(); + } + + /** + * This method depends on a DirObjectFactory ( + * {@link org.springframework.ldap.core.support.DefaultDirObjectFactory}) + * being set in the ContextSource. + */ + @Test + public void testLookup_ContextMapper() { + ContextMapper mapper = new PersonContextMapper(); + Person person = tested.search().name("cn=Some Person2, ou=company1,ou=Sweden").toObject(mapper); + + assertThat(person.getFullname()).isEqualTo("Some Person2"); + assertThat(person.getLastname()).isEqualTo("Person2"); + assertThat(person.getDescription()).isEqualTo("Sweden, Company1, Some Person2"); + } + + /** + * Verifies that only the subset is used when specifying a subset of the + * available attributes as return attributes. + */ + @Test + public void testLookup_ReturnAttributes_ContextMapper() { + ContextMapper mapper = new PersonContextMapper(); + + Person person = tested.search().query((builder) -> builder + .base("cn=Some Person2, ou=company1,ou=Sweden").attributes("cn")).toObject(mapper); + + assertThat(person.getFullname()).isEqualTo("Some Person2"); + assertThat(person.getLastname()).as("lastName should not be set").isNull(); + assertThat(person.getDescription()).as("description should not be set").isNull(); + } + + @Test + public void testLookup_GetNameInNamespace_Plain() { + String expectedDn = "cn=Some Person2, ou=company1,ou=Sweden"; + DirContextOperations result = tested.search().name(expectedDn).toEntry(); + + LdapName expectedName = LdapUtils.newLdapName(expectedDn); + assertThat(result.getDn()).isEqualTo(expectedName); + assertThat(result.getNameInNamespace()).isEqualTo("cn=Some Person2,ou=company1,ou=Sweden," + base); + } +} diff --git a/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientLookupMultiRdnITest.java b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientLookupMultiRdnITest.java new file mode 100644 index 000000000..a94bf0c88 --- /dev/null +++ b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientLookupMultiRdnITest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2005-2023 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.ldap.itest; + +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import org.springframework.LdapDataEntry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.ldap.core.AttributesMapper; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.LdapClient; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests {@link LdapClient}'s lookup methods. + * + * @author Josh Cummings + */ +@ContextConfiguration(locations = {"/conf/ldapClientTestContext.xml"}) +public class SpringLdapClientLookupMultiRdnITest extends AbstractLdapTemplateIntegrationTest { + + @Autowired + private LdapClient tested; + + protected Resource getLdifFileResource() { + return new ClassPathResource("/setup_data_multi_rdn.ldif"); + } + + + /** + * Verifies that we can lookup an entry that has a multi-valued rdn, which + * means more than one attribute is part of the relative DN for the entry. + */ + @Test + @Category(NoAdTest.class) + public void testLookup_MultiValuedRdn() { + AttributesMapper mapper = new PersonAttributesMapper(); + Person person = tested.search().name("cn=Some Person+sn=Person, ou=company1,ou=Norway").toObject(mapper); + assertThat(person.getFullname()).isEqualTo("Some Person"); + assertThat(person.getLastname()).isEqualTo("Person"); + assertThat(person.getDescription()).isEqualTo("Norway, Company1, Some Person+Person"); + } + + /** + * Verifies that we can lookup an entry that has a multi-valued rdn, which + * means more than one attribute is part of the relative DN for the entry. + * + */ + @Test + @Category(NoAdTest.class) + public void testLookup_MultiValuedRdn_DirContextAdapter() { + LdapDataEntry result = tested.search().name("cn=Some Person+sn=Person, ou=company1,ou=Norway").toEntry(); + assertThat(result.getStringAttribute("cn")).isEqualTo("Some Person"); + assertThat(result.getStringAttribute("sn")).isEqualTo("Person"); + assertThat(result.getStringAttribute("description")).isEqualTo("Norway, Company1, Some Person+Person"); + } + + @Test + @Category(NoAdTest.class) + public void testLookup_GetNameInNamespace_MultiRdn() { + DirContextOperations result = tested.search().name("cn=Some Person+sn=Person,ou=company1,ou=Norway").toEntry(); + assertThat(result.getDn().toString()).isEqualTo("cn=Some Person+sn=Person,ou=company1,ou=Norway"); + assertThat(result.getNameInNamespace()).isEqualTo("cn=Some Person+sn=Person,ou=company1,ou=Norway," + base); + } +} diff --git a/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientModifyITest.java b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientModifyITest.java new file mode 100644 index 000000000..345c8fb8e --- /dev/null +++ b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientModifyITest.java @@ -0,0 +1,251 @@ +/* + * Copyright 2005-2023 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.ldap.itest; + +import java.util.Arrays; +import java.util.List; + +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttribute; +import javax.naming.directory.BasicAttributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.ModificationItem; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.LdapDataEntry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ldap.AttributeInUseException; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.LdapClient; +import org.springframework.ldap.support.LdapUtils; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests {@link LdapClient}'s modification methods (rebind and modifyAttributes) + * + *

It also illustrates the use of DirContextAdapter as a means of getting + * {@code ModificationItems}, in order to avoid doing a full rebind and use + * {@code modify()} instead. + * + * @author Josh Cummings + */ +@ContextConfiguration(locations = {"/conf/ldapClientTestContext.xml"}) +public class SpringLdapClientModifyITest extends AbstractLdapTemplateIntegrationTest { + + @Autowired + private LdapClient tested; + + private static final String PERSON4_DN = "cn=Some Person4,ou=company1,ou=Sweden"; + + private static final String PERSON5_DN = "cn=Some Person5,ou=company1,ou=Sweden"; + + @Before + public void prepareTestedInstance() throws Exception { + DirContextAdapter adapter = new DirContextAdapter(); + adapter.setAttributeValues("objectclass", new String[] { "top", "person" }); + adapter.setAttributeValue("cn", "Some Person4"); + adapter.setAttributeValue("sn", "Person4"); + adapter.setAttributeValue("description", "Some description"); + + tested.bind(PERSON4_DN).object(adapter).execute(); + + adapter = new DirContextAdapter(); + adapter.setAttributeValues("objectclass", new String[] { "top", "person" }); + adapter.setAttributeValue("cn", "Some Person5"); + adapter.setAttributeValue("sn", "Person5"); + adapter.setAttributeValues("description", new String[] { "qwe", "123", "rty", "uio" }); + + tested.bind(PERSON5_DN).object(adapter).execute(); + + } + + @After + public void cleanup() throws Exception { + tested.unbind(PERSON4_DN).execute(); + tested.unbind(PERSON5_DN).execute(); + } + + @Test + public void testRebind_Attributes_Plain() { + Attributes attributes = setupAttributes(); + tested.bind(PERSON4_DN).attributes(attributes).replaceExisting(true).execute(); + verifyBoundCorrectData(); + } + + @Test + public void testRebind_Attributes_LdapName() { + Attributes attributes = setupAttributes(); + tested.bind(LdapUtils.newLdapName(PERSON4_DN)).attributes(attributes).replaceExisting(true).execute(); + verifyBoundCorrectData(); + } + + @Test + public void testModifyAttributes_MultiValueReplace() { + BasicAttribute attr = new BasicAttribute("description", "Some other description"); + attr.add("Another description"); + ModificationItem[] mods = new ModificationItem[1]; + mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attr); + tested.modify(PERSON4_DN).attributes(mods).execute(); + + DirContextOperations result = tested.search().name(PERSON4_DN).toEntry(); + List attributes = Arrays.asList(result.getStringAttributes("description")); + assertThat(attributes).hasSize(2); + assertThat(attributes.contains("Some other description")).isTrue(); + assertThat(attributes.contains("Another description")).isTrue(); + } + + @Test + public void testModifyAttributes_MultiValueAdd() { + BasicAttribute attr = new BasicAttribute("description", "Some other description"); + attr.add("Another description"); + ModificationItem[] mods = new ModificationItem[1]; + mods[0] = new ModificationItem(DirContext.ADD_ATTRIBUTE, attr); + tested.modify(PERSON4_DN).attributes(mods).execute(); + LdapDataEntry result = tested.search().name(PERSON4_DN).toEntry(); + List attributes = Arrays.asList(result.getStringAttributes("description")); + assertThat(attributes).hasSize(3); + assertThat(attributes.contains("Some other description")).isTrue(); + assertThat(attributes.contains("Another description")).isTrue(); + assertThat(attributes.contains("Some description")).isTrue(); + } + + @Test + public void testModifyAttributes_AddAttributeValueWithExistingValue() { + DirContextOperations ctx = tested.search().name("cn=ROLE_USER,ou=groups").toEntry(); + String[] existing = ctx.getStringAttributes("uniqueMember"); + ctx.addAttributeValue("uniqueMember", "cn=Some Person,ou=company1,ou=Norway," + base); + tested.modify(ctx.getDn()).attributes(ctx.getModificationItems()).execute(); + ctx = tested.search().name("cn=ROLE_USER,ou=groups").toEntry(); + assertThat(ctx.getStringAttributes("uniqueMember")).hasSize(existing.length + 1); + assertThat(ctx.getStringAttributes("uniqueMember")).contains("cn=Some Person,ou=company1,ou=Norway," + base); + } + + @Test + public void testModifyAttributes_MultiValueAddDuplicateToUnordered() { + BasicAttribute attr = new BasicAttribute("description", "Some description"); + ModificationItem[] mods = new ModificationItem[1]; + mods[0] = new ModificationItem(DirContext.ADD_ATTRIBUTE, attr); + + try { + tested.modify(PERSON4_DN).attributes(mods).execute(); + fail("AttributeInUseException expected"); + } + catch (AttributeInUseException expected) { + // expected + } + } + + /** + * Test written originally to verify that duplicates are allowed on ordered + * attributes, but had to be changed since Apache DS seems to disallow + * duplicates even for ordered attributes. + */ + @Test + public void testModifyAttributes_MultiValueAddDuplicateToOrdered() { + BasicAttribute attr = new BasicAttribute("description", "Some other description", true); // ordered + attr.add("Another description"); + // Commented out duplicate to make test work for Apache DS + // attr.add("Some description"); + ModificationItem[] mods = new ModificationItem[1]; + mods[0] = new ModificationItem(DirContext.ADD_ATTRIBUTE, attr); + tested.modify(PERSON4_DN).attributes(mods).execute(); + LdapDataEntry result = tested.search().name(PERSON4_DN).toEntry(); + List attributes = Arrays.asList(result.getStringAttributes("description")); + assertThat(attributes).hasSize(3); + assertThat(attributes.contains("Some other description")).isTrue(); + assertThat(attributes.contains("Another description")).isTrue(); + assertThat(attributes.contains("Some description")).isTrue(); + } + + @Test + public void testModifyAttributes_Plain() { + ModificationItem item = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("description", + "Some other description")); + tested.modify(PERSON4_DN).attributes(item).execute(); + verifyBoundCorrectData(); + } + + @Test + public void testModifyAttributes_LdapName() { + ModificationItem item = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("description", + "Some other description")); + tested.modify(LdapUtils.newLdapName(PERSON4_DN)).attributes(item).execute(); + verifyBoundCorrectData(); + } + + @Test + public void testModifyAttributes_DirContextAdapter_MultiAttributes() { + DirContextOperations adapter = tested.search().name(PERSON5_DN).toEntry(); + adapter.setAttributeValues("description", new String[] { "qwe", "123", "klytt", "kalle" }); + tested.modify(adapter.getDn()).attributes(adapter.getModificationItems()).execute(); + + // Verify + adapter = tested.search().name(PERSON5_DN).toEntry(); + List attributes = Arrays.asList(adapter.getStringAttributes("description")); + assertThat(attributes).hasSize(4); + assertThat(attributes.contains("qwe")).isTrue(); + assertThat(attributes.contains("123")).isTrue(); + assertThat(attributes.contains("klytt")).isTrue(); + assertThat(attributes.contains("kalle")).isTrue(); + } + + /** + * Demonstrates how the DirContextAdapter can be used to automatically keep + * track of changes of the attributes and deliver ModificationItems to use + * in moifyAttributes(). + */ + @Test + public void testModifyAttributes_DirContextAdapter() { + DirContextOperations adapter = tested.search().name(PERSON4_DN).toEntry(); + adapter.setAttributeValue("description", "Some other description"); + tested.modify(adapter.getDn()).attributes(adapter.getModificationItems()).execute(); + verifyBoundCorrectData(); + } + + @Test + public void verifyCompleteReplacementOfUniqueMemberAttribute_Ldap119() { + DirContextOperations ctx = tested.search().name("cn=ROLE_USER,ou=groups").toEntry(); + ctx.setAttributeValues("uniqueMember", new String[] { "cn=Some Person,ou=company1,ou=Norway," + base }, true); + tested.modify(ctx.getDn()).attributes(ctx.getModificationItems()).execute(); + } + + private Attributes setupAttributes() { + Attributes attributes = new BasicAttributes(); + BasicAttribute ocattr = new BasicAttribute("objectclass"); + ocattr.add("top"); + ocattr.add("person"); + attributes.put(ocattr); + attributes.put("cn", "Some Person4"); + attributes.put("sn", "Person4"); + attributes.put("description", "Some other description"); + return attributes; + } + + private void verifyBoundCorrectData() { + DirContextOperations result = tested.search().name(PERSON4_DN).toEntry(); + assertThat(result.getStringAttribute("cn")).isEqualTo("Some Person4"); + assertThat(result.getStringAttribute("sn")).isEqualTo("Person4"); + assertThat(result.getStringAttribute("description")).isEqualTo("Some other description"); + } +} diff --git a/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientRecursiveDeleteITest.java b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientRecursiveDeleteITest.java new file mode 100644 index 000000000..7df7eb248 --- /dev/null +++ b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientRecursiveDeleteITest.java @@ -0,0 +1,129 @@ +/* + * Copyright 2005-2023 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.ldap.itest; + +import javax.naming.Name; +import javax.naming.NameClassPair; +import javax.naming.ldap.LdapName; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ldap.NameNotFoundException; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.LdapClient; +import org.springframework.ldap.support.LdapUtils; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests {@code LdapClient}'s recursive modification methods (unbind and the protected delete + * methods). + * + * @author Josh Cummings + */ +@ContextConfiguration(locations = {"/conf/ldapClientTestContext.xml"}) +public class SpringLdapClientRecursiveDeleteITest extends AbstractLdapTemplateIntegrationTest { + + @Autowired + private LdapClient tested; + + private static final LdapName DN = LdapUtils.newLdapName("cn=Some Person5,ou=company1,ou=Sweden"); + + private LdapName firstSubDn; + + private LdapName secondSubDn; + + private LdapName leafDn; + + @Before + public void prepareTestedInstance() throws Exception { + DirContextAdapter adapter = new DirContextAdapter(); + adapter.setAttributeValues("objectclass", new String[] { "top", "person" }); + adapter.setAttributeValue("cn", "Some Person5"); + adapter.setAttributeValue("sn", "Person5"); + adapter.setAttributeValue("description", "Some description"); + tested.bind(DN).object(adapter).execute(); + + firstSubDn = LdapUtils.newLdapName("cn=subPerson"); + firstSubDn = LdapUtils.prepend(firstSubDn, DN); + + adapter = new DirContextAdapter(); + adapter.setAttributeValues("objectclass", new String[] { "top", "person" }); + adapter.setAttributeValue("cn", "subPerson"); + adapter.setAttributeValue("sn", "subPerson"); + adapter.setAttributeValue("description", "Should be recursively deleted"); + tested.bind(firstSubDn).object(adapter).execute(); + secondSubDn = LdapUtils.newLdapName("cn=subPerson2"); + secondSubDn = LdapUtils.prepend(secondSubDn, DN); + + adapter = new DirContextAdapter(); + adapter.setAttributeValues("objectclass", new String[] { "top", "person" }); + adapter.setAttributeValue("cn", "subPerson2"); + adapter.setAttributeValue("sn", "subPerson2"); + adapter.setAttributeValue("description", "Should be recursively deleted"); + tested.bind(secondSubDn).object(adapter).execute(); + + leafDn = LdapUtils.newLdapName("cn=subSubPerson"); + leafDn = LdapUtils.prepend(leafDn, DN); + + adapter = new DirContextAdapter(); + adapter.setAttributeValues("objectclass", new String[] { "top", "person" }); + adapter.setAttributeValue("cn", "subSubPerson"); + adapter.setAttributeValue("sn", "subSubPerson"); + adapter.setAttributeValue("description", "Should be recursively deleted"); + tested.bind(leafDn).object(adapter).execute(); + } + + @After + public void cleanup() throws Exception { + try { + tested.unbind(DN).recursive(true).execute(); + } + catch (NameNotFoundException ignore) { + // ignore + } + } + + @Test + @Category(NoAdTest.class) + public void testRecursiveUnbind() { + tested.unbind(DN).recursive(true).execute(); + + verifyDeleted(DN); + verifyDeleted(firstSubDn); + verifyDeleted(secondSubDn); + verifyDeleted(leafDn); + } + + @Test + @Category(NoAdTest.class) + public void testRecursiveUnbindOnLeaf() { + tested.unbind(leafDn).recursive(true).execute(); + verifyDeleted(leafDn); + } + + private void verifyDeleted(Name dn) { + assertThatExceptionOfType(NameNotFoundException.class) + .describedAs("Expected entry '" + dn + "' to be non-existent") + .isThrownBy(() -> tested.list(dn).toList(NameClassPair::getName)); + } +} diff --git a/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientRenameITest.java b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientRenameITest.java new file mode 100644 index 000000000..cf182b1ae --- /dev/null +++ b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientRenameITest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2005-2023 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.ldap.itest; + +import javax.naming.Name; +import javax.naming.NameClassPair; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.LdapDataEntry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ldap.NameNotFoundException; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.LdapClient; +import org.springframework.ldap.support.LdapUtils; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests {@link LdapClient}'s rename methods. + * + * @author Josh Cummings + */ +@ContextConfiguration(locations = {"/conf/ldapClientTestContext.xml"}) +public class SpringLdapClientRenameITest extends AbstractLdapTemplateIntegrationTest { + + @Autowired + private LdapClient tested; + + private static final String DN = "cn=Some Person6,ou=company1,ou=Sweden"; + + private static final String NEWDN = "cn=Some Person6,ou=company2,ou=Sweden"; + + @Before + public void prepareTestedInstance() throws Exception { + DirContextAdapter adapter = new DirContextAdapter(); + adapter.setAttributeValues("objectclass", new String[] { "top", "person" }); + adapter.setAttributeValue("cn", "Some Person6"); + adapter.setAttributeValue("sn", "Person6"); + adapter.setAttributeValue("description", "Some description"); + + tested.bind(DN).object(adapter).execute(); + } + + @After + public void cleanup() throws Exception { + tested.unbind(NEWDN).execute(); + tested.unbind(DN).execute(); + } + + @Test + public void testRename() { + tested.modify(DN).name(NEWDN).execute(); + verifyDeleted(LdapUtils.newLdapName(DN)); + verifyBoundCorrectData(); + } + + @Test + public void testRename_LdapName() { + Name oldDn = LdapUtils.newLdapName(DN); + Name newDn = LdapUtils.newLdapName(NEWDN); + tested.modify(oldDn).name(newDn).execute(); + verifyDeleted(oldDn); + verifyBoundCorrectData(); + } + + private void verifyDeleted(Name dn) { + try { + tested.list(dn).toList(NameClassPair::getName); + fail("Expected entry '" + dn + "' to be non-existent"); + } + catch (NameNotFoundException expected) { + // expected + } + } + + private void verifyBoundCorrectData() { + LdapDataEntry result = tested.search().name(NEWDN).toEntry(); + assertThat(result.getStringAttribute("cn")).isEqualTo("Some Person6"); + assertThat(result.getStringAttribute("sn")).isEqualTo("Person6"); + assertThat(result.getStringAttribute("description")).isEqualTo("Some description"); + } +} diff --git a/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientSearchResultITest.java b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientSearchResultITest.java new file mode 100644 index 000000000..32a37b842 --- /dev/null +++ b/test/integration-tests/src/test/java/org/springframework/ldap/itest/SpringLdapClientSearchResultITest.java @@ -0,0 +1,543 @@ +/* + * Copyright 2005-2023 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.ldap.itest; + +import java.util.List; +import java.util.stream.Collectors; + +import javax.naming.Name; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.ldap.NameNotFoundException; +import org.springframework.ldap.SizeLimitExceededException; +import org.springframework.ldap.core.ContextMapper; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.LdapClient; +import org.springframework.ldap.query.LdapQuery; +import org.springframework.ldap.query.LdapQueryBuilder; +import org.springframework.ldap.query.SearchScope; +import org.springframework.ldap.support.LdapUtils; +import org.springframework.ldap.test.AttributeCheckAttributesMapper; +import org.springframework.ldap.test.AttributeCheckContextMapper; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.springframework.ldap.query.LdapQueryBuilder.query; + +/** + * Tests for {@link LdapClient}'s search methods. + * + * @author Josh Cummings + */ +@ContextConfiguration(locations = {"/conf/ldapClientTestContext.xml"}) +@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) +public class SpringLdapClientSearchResultITest extends AbstractLdapTemplateIntegrationTest { + + @Autowired + private LdapClient tested; + + private AttributeCheckAttributesMapper attributesMapper; + + private AttributeCheckContextMapper contextMapper; + + private static final String[] ALL_ATTRIBUTES = { "cn", "sn", "description", "telephoneNumber" }; + + private static final String[] CN_SN_ATTRS = { "cn", "sn" }; + + private static final String[] ABSENT_ATTRIBUTES = { "description", "telephoneNumber" }; + + private static final String[] CN_SN_VALUES = { "Some Person2", "Person2" }; + + private static final String[] ALL_VALUES = { "Some Person2", "Person2", "Sweden, Company1, Some Person2", + "+46 555-654321" }; + + private static final String BASE_STRING = ""; + + private static final String FILTER_STRING = "(&(objectclass=person)(sn=Person2))"; + + private static final Name BASE_NAME = LdapUtils.newLdapName(BASE_STRING); + + @Before + public void prepareTestedInstance() throws Exception { + attributesMapper = new AttributeCheckAttributesMapper(); + contextMapper = new AttributeCheckContextMapper(); + } + + @After + public void cleanup() throws Exception { + attributesMapper = null; + contextMapper = null; + } + + @Test + public void testSearch_AttributesMapper() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + LdapQuery query = LdapQueryBuilder.query().base(BASE_STRING).filter(FILTER_STRING); + List list = tested.search().query(query).toList(attributesMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearch_LdapQuery_AttributesMapper() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + + List list = tested.search().query(query() + .base(BASE_STRING) + .where("objectclass").is("person").and("sn").is("Person2")) + .toList(attributesMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearchForStream_LdapQuery_AttributesMapper() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + + List list = tested.search().query(query() + .base(BASE_STRING) + .where("objectclass").is("person").and("sn").is("Person2")) + .toStream(attributesMapper).collect(Collectors.toList()); + assertThat(list).hasSize(1); + } + + @Test + public void testSearch_LdapQuery_AttributesMapper_FewerAttributes() { + attributesMapper.setExpectedAttributes(new String[] {"cn"}); + attributesMapper.setExpectedValues(new String[]{"Some Person2"}); + + List list = tested.search().query(query() + .base(BASE_STRING) + .attributes("cn") + .where("objectclass").is("person").and("sn").is("Person2")) + .toList(attributesMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearchForStream_LdapQuery_AttributesMapper_FewerAttributes() { + attributesMapper.setExpectedAttributes(new String[] {"cn"}); + attributesMapper.setExpectedValues(new String[]{"Some Person2"}); + + List list = tested.search().query(query() + .base(BASE_STRING) + .attributes("cn") + .where("objectclass").is("person").and("sn").is("Person2")) + .toStream(attributesMapper).collect(Collectors.toList()); + assertThat(list).hasSize(1); + } + + @Test + public void testSearch_LdapQuery_AttributesMapper_SearchScope() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + + List list = tested.search().query(query() + .base(BASE_STRING) + .searchScope(SearchScope.ONELEVEL) + .where("objectclass").is("person").and("sn").is("Person2")) + .toList(attributesMapper); + assertThat(list).isEmpty(); + } + + @Test + public void testSearchForStream_LdapQuery_AttributesMapper_SearchScope() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + + List list = tested.search().query(query() + .base(BASE_STRING) + .searchScope(SearchScope.ONELEVEL) + .where("objectclass").is("person").and("sn").is("Person2")) + .toStream(attributesMapper).collect(Collectors.toList()); + assertThat(list).isEmpty(); + } + + @Test + public void testSearch_LdapQuery_AttributesMapper_SearchScope_CorrectBase() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + + List list = tested.search().query(query() + .base("ou=company1,ou=Sweden") + .searchScope(SearchScope.ONELEVEL) + .where("objectclass").is("person").and("sn").is("Person2")) + .toList(attributesMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearchForStream_LdapQuery_AttributesMapper_SearchScope_CorrectBase() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + + List list = tested.search().query(query() + .base("ou=company1,ou=Sweden") + .searchScope(SearchScope.ONELEVEL) + .where("objectclass").is("person").and("sn").is("Person2")) + .toStream(attributesMapper).collect(Collectors.toList()); + assertThat(list).hasSize(1); + } + + @Test + public void testSearch_LdapQuery_AttributesMapper_NoBase() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + + List list = tested.search().query(query() + .where("objectclass").is("person").and("sn").is("Person2")) + .toList(attributesMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearchForStream_LdapQuery_AttributesMapper_NoBase() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + + List list = tested.search().query(query() + .where("objectclass").is("person").and("sn").is("Person2")) + .toStream(attributesMapper).collect(Collectors.toList()); + assertThat(list).hasSize(1); + } + + @Test + public void testSearch_LdapQuery_AttributesMapper_DifferentBase() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + + List list = tested.search().query(query() + .base("ou=Norway") + .where("objectclass").is("person").and("sn").is("Person2")) + .toList(attributesMapper); + assertThat(list).isEmpty(); + } + + @Test + public void testSearchForStream_LdapQuery_AttributesMapper_DifferentBase() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + + List list = tested.search().query(query() + .base("ou=Norway") + .where("objectclass").is("person").and("sn").is("Person2")) + .toStream(attributesMapper).collect(Collectors.toList()); + assertThat(list).isEmpty(); + } + + @Test + public void testSearch_SearchScope_AttributesMapper() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + List list = tested.search().query(query().base(BASE_STRING) + .searchScope(SearchScope.SUBTREE).filter(FILTER_STRING)) + .toList(attributesMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearch_SearchScope_LimitedAttrs_AttributesMapper() { + attributesMapper.setExpectedAttributes(CN_SN_ATTRS); + attributesMapper.setExpectedValues(CN_SN_VALUES); + attributesMapper.setAbsentAttributes(ABSENT_ATTRIBUTES); + List list = tested.search().query(query().base(BASE_STRING) + .searchScope(SearchScope.SUBTREE) + .attributes(CN_SN_ATTRS) + .filter(FILTER_STRING)) + .toList(attributesMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearch_AttributesMapper_Name() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + List list = tested.search().query(query().base(BASE_NAME).filter(FILTER_STRING)) + .toList(attributesMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearch_SearchScope_AttributesMapper_Name() { + attributesMapper.setExpectedAttributes(ALL_ATTRIBUTES); + attributesMapper.setExpectedValues(ALL_VALUES); + List list = tested.search().query(query().base(BASE_NAME).searchScope(SearchScope.SUBTREE) + .filter(FILTER_STRING)).toList(attributesMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearch_SearchScope_LimitedAttrs_AttributesMapper_Name() { + attributesMapper.setExpectedAttributes(CN_SN_ATTRS); + attributesMapper.setExpectedValues(CN_SN_VALUES); + attributesMapper.setAbsentAttributes(ABSENT_ATTRIBUTES); + List list = tested.search().query(query().base(BASE_NAME).searchScope(SearchScope.SUBTREE) + .attributes(CN_SN_ATTRS).filter(FILTER_STRING)).toList(attributesMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearch_ContextMapper() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.search().query(query().base(BASE_STRING).filter(FILTER_STRING)) + .toList(contextMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearchForObject() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + DirContextAdapter result = tested.search().query(query().base(BASE_STRING).filter(FILTER_STRING)) + .toObject(contextMapper); + assertThat(result).isNotNull(); + } + + @Test(expected = IncorrectResultSizeDataAccessException.class) + public void testSearchForObjectWithMultipleHits() { + tested.search().query(query().base(BASE_STRING).filter("(&(objectclass=person)(sn=*))")) + .toObject((Object ctx) -> ctx); + } + + @Test//(expected = EmptyResultDataAccessException.class) + public void testSearchForObjectNoHits() { + Object result = tested.search().query(query().base(BASE_STRING) + .filter("(&(objectclass=person)(sn=Person does not exist))")) + .toObject((Object ctx) -> ctx); + assertThat(result).isNull(); + } + + @Test + public void testSearch_SearchScope_ContextMapper() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.search().query(query().base(BASE_STRING).searchScope(SearchScope.SUBTREE) + .filter(FILTER_STRING)).toList(contextMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearch_SearchScope_LimitedAttrs_ContextMapper() { + contextMapper.setExpectedAttributes(CN_SN_ATTRS); + contextMapper.setExpectedValues(CN_SN_VALUES); + contextMapper.setAbsentAttributes(ABSENT_ATTRIBUTES); + List list = tested.search().query(query().base(BASE_STRING).searchScope(SearchScope.SUBTREE) + .attributes(CN_SN_ATTRS).filter(FILTER_STRING)).toList(contextMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearch_ContextMapper_Name() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.search().query(query().base(BASE_NAME).filter(FILTER_STRING)) + .toList(contextMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearch_ContextMapper_LdapQuery() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.search().query(query() + .base(BASE_NAME) + .where("objectclass").is("person").and("sn").is("Person2")) + .toList(contextMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearchForStream_ContextMapper_LdapQuery() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.search().query(query() + .base(BASE_NAME) + .where("objectclass").is("person").and("sn").is("Person2")) + .toStream(contextMapper).collect(Collectors.toList()); + assertThat(list).hasSize(1); + } + + @Test + public void testSearch_ContextMapper_LdapQuery_NoBase() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.search().query(query() + .where("objectclass").is("person").and("sn").is("Person2")) + .toList(contextMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearchForStream_ContextMapper_LdapQuery_NoBase() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.search().query(query() + .where("objectclass").is("person").and("sn").is("Person2")) + .toStream(contextMapper).collect(Collectors.toList()); + assertThat(list).hasSize(1); + } + + @Test + public void testSearch_ContextMapper_LdapQuery_SearchScope() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.search().query(query() + .base(BASE_NAME) + .searchScope(SearchScope.ONELEVEL) + .where("objectclass").is("person").and("sn").is("Person2")) + .toList(contextMapper); + assertThat(list).isEmpty(); + } + + @Test + public void testSearchForStream_ContextMapper_LdapQuery_SearchScope() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.search().query(query() + .base(BASE_NAME) + .searchScope(SearchScope.ONELEVEL) + .where("objectclass").is("person").and("sn").is("Person2")) + .toStream(contextMapper).collect(Collectors.toList()); + assertThat(list).isEmpty(); + } + + @Test + public void testSearch_ContextMapper_LdapQuery_SearchScope_CorrectBase() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.search().query(query() + .base("ou=company1,ou=Sweden") + .searchScope(SearchScope.ONELEVEL) + .where("objectclass").is("person").and("sn").is("Person2")) + .toList(contextMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearchForStream_ContextMapper_LdapQuery_SearchScope_CorrectBase() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.search().query(query() + .base("ou=company1,ou=Sweden") + .searchScope(SearchScope.ONELEVEL) + .where("objectclass").is("person").and("sn").is("Person2")) + .toStream(contextMapper).collect(Collectors.toList()); + assertThat(list).hasSize(1); + } + + @Test + public void testSearchForContext_LdapQuery() { + ContextMapper mapper = (result) -> (DirContextOperations) result; + DirContextOperations result = tested.search().query(query() + .where("objectclass").is("person").and("sn").is("Person2")).toObject(mapper); + assertThat(result).isNotNull(); + assertThat(result.getStringAttribute("sn")).isEqualTo("Person2"); + } + + @Test//(expected = EmptyResultDataAccessException.class) + public void testSearchForContext_LdapQuery_SearchScopeNotFound() { + Object result = tested.search().query(query() + .searchScope(SearchScope.ONELEVEL) + .where("objectclass").is("person").and("sn").is("Person2")).toObject(attributesMapper); + assertThat(result).isNull(); + } + + @Test + public void testSearchForContext_LdapQuery_SearchScope_CorrectBase() { + ContextMapper mapper = (result) -> (DirContextOperations) result; + DirContextOperations result = + tested.search().query(query() + .searchScope(SearchScope.ONELEVEL) + .base("ou=company1,ou=Sweden") + .where("objectclass").is("person").and("sn").is("Person2")).toObject(mapper); + + assertThat(result).isNotNull(); + assertThat(result.getStringAttribute("sn")).isEqualTo("Person2"); + } + + @Test + public void testSearch_SearchScope_ContextMapper_Name() { + contextMapper.setExpectedAttributes(ALL_ATTRIBUTES); + contextMapper.setExpectedValues(ALL_VALUES); + List list = tested.search().query(query().base(BASE_NAME).searchScope(SearchScope.SUBTREE) + .filter(FILTER_STRING)).toList(contextMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearch_SearchScope_LimitedAttrs_ContextMapper_Name() { + contextMapper.setExpectedAttributes(CN_SN_ATTRS); + contextMapper.setExpectedValues(CN_SN_VALUES); + contextMapper.setAbsentAttributes(ABSENT_ATTRIBUTES); + List list = tested.search().query(query().base(BASE_NAME).searchScope(SearchScope.SUBTREE) + .attributes(CN_SN_ATTRS).filter(FILTER_STRING)).toList(contextMapper); + assertThat(list).hasSize(1); + } + + @Test + public void testSearchWithInvalidSearchBaseShouldByDefaultThrowException() { + try { + tested.search().query(query().base(BASE_NAME + "ou=unknown").searchScope(SearchScope.SUBTREE) + .attributes(CN_SN_ATTRS).filter(FILTER_STRING)) + .toObject(contextMapper); + fail("NameNotFoundException expected"); + } + catch (NameNotFoundException expected) { + assertThat(true).isTrue(); + } + } + + @Test + public void testSearchWithInvalidSearchBaseCanBeConfiguredToSwallowException() { + ReflectionTestUtils.setField(tested, "ignoreNameNotFoundException", true); + contextMapper.setExpectedAttributes(CN_SN_ATTRS); + contextMapper.setExpectedValues(CN_SN_VALUES); + contextMapper.setAbsentAttributes(ABSENT_ATTRIBUTES); + List list = tested.search().query(query().base(BASE_NAME + "ou=unknown") + .searchScope(SearchScope.SUBTREE).attributes(CN_SN_ATTRS).filter(FILTER_STRING)) + .toList(contextMapper); + assertThat(list).isEmpty(); + } + + @Test + public void verifyThatSearchWithCountLimitReturnsTheEntriesFoundSoFar() { + List result = tested.search().query(query() + .countLimit(3) + .where("objectclass").is("person")).toList((Object ctx) -> new Object()); + + assertThat(result).hasSize(3); + } + + @Test(expected = SizeLimitExceededException.class) + public void verifyThatSearchWithCountLimitWithFlagToFalseThrowsException() { + ReflectionTestUtils.setField(tested, "ignoreSizeLimitExceededException", false); + tested.search().query(query() + .countLimit(3) + .where("objectclass").is("person")).toList((Object ctx) -> ctx); + } +} diff --git a/test/integration-tests/src/test/resources/conf/ldapClientTestContext.xml b/test/integration-tests/src/test/resources/conf/ldapClientTestContext.xml new file mode 100644 index 000000000..525fced7b --- /dev/null +++ b/test/integration-tests/src/test/resources/conf/ldapClientTestContext.xml @@ -0,0 +1,13 @@ + + + + + + + + +