Add LdapClient

Closes gh-675
This commit is contained in:
Josh Cummings
2023-03-16 12:21:36 -06:00
parent 19f0eccd90
commit de86a00e8c
21 changed files with 4781 additions and 130 deletions

View File

@@ -0,0 +1,681 @@
/*
* 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.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 DefaultLdapClient implements LdapClient {
private final Logger logger = LoggerFactory.getLogger(DefaultLdapClient.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<SearchControls> searchControlsSupplier;
private boolean ignorePartialResultException = false;
private boolean ignoreNameNotFoundException = false;
private boolean ignoreSizeLimitExceededException = true;
DefaultLdapClient(ContextSource contextSource, Supplier<SearchControls> searchControlsSupplier) {
this.contextSource = contextSource;
this.searchControlsSupplier = searchControlsSupplier;
}
@Override
public ListSpec list(String name) {
return new DefaultListSpec(LdapUtils.newLdapName(name));
}
@Override
public ListSpec list(Name name) {
return new DefaultListSpec(LdapUtils.newLdapName(name));
}
@Override
public ListBindingsSpec listBindings(String name) {
return new DefaultListBindingsSpec(LdapUtils.newLdapName(name));
}
@Override
public ListBindingsSpec listBindings(Name name) {
return new DefaultListBindingsSpec(LdapUtils.newLdapName(name));
}
@Override
public SearchSpec search() {
return new DefaultSearchSpec();
}
@Override
public AuthenticateSpec authenticate() {
return new DefaultAuthenticateSpec();
}
@Override
public BindSpec bind(String name) {
return new DefaultBindSpec(LdapUtils.newLdapName(name));
}
@Override
public BindSpec bind(Name name) {
return new DefaultBindSpec(LdapUtils.newLdapName(name));
}
@Override
public ModifySpec modify(String name) {
return new DefaultModifySpec(new DirContextAdapter(LdapUtils.newLdapName(name)));
}
@Override
public ModifySpec modify(Name name) {
return new DefaultModifySpec(new DirContextAdapter(LdapUtils.newLdapName(name)));
}
@Override
public UnbindSpec unbind(String name) {
return new DefaultUnbindSpec(LdapUtils.newLdapName(name));
}
/**
* {@inheritDoc}
*/
@Override
public UnbindSpec unbind(Name name) {
return new DefaultUnbindSpec(LdapUtils.newLdapName(name));
}
/**
* {@inheritDoc}
*/
@Override
public Builder mutate() {
return new DefaultLdapClientBuilder(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 DefaultListSpec implements ListSpec {
private final Name name;
private DefaultListSpec(Name name) {
this.name = name;
}
@Override
public <T> List<T> toList(NameClassPairMapper<T> mapper) {
ContextExecutor<NamingEnumeration<NameClassPair>> executor = (ctx) -> ctx.list(this.name);
NamingEnumeration<NameClassPair> results = computeWithReadOnlyContext(executor);
return DefaultLdapClient.this.toList(results, mapper::mapFromNameClassPair);
}
@Override
public <T> Stream<T> toStream(NameClassPairMapper<T> mapper) {
ContextExecutor<NamingEnumeration<NameClassPair>> executor = (ctx) -> ctx.list(this.name);
NamingEnumeration<NameClassPair> results = computeWithReadOnlyContext(executor);
return DefaultLdapClient.this.toStream(results, mapper::mapFromNameClassPair);
}
}
private final class DefaultListBindingsSpec implements ListBindingsSpec {
private final Name name;
private DefaultListBindingsSpec(Name name) {
this.name = name;
}
@Override
public <T> List<T> toList(NameClassPairMapper<T> mapper) {
ContextExecutor<NamingEnumeration<Binding>> executor = (ctx) -> ctx.listBindings(this.name);
NamingEnumeration<Binding> results = computeWithReadOnlyContext(executor);
return DefaultLdapClient.this.toList(results, mapper::mapFromNameClassPair);
}
@Override
public <T> List<T> toList(ContextMapper<T> mapper) {
ContextExecutor<NamingEnumeration<Binding>> executor = (ctx) -> ctx.listBindings(this.name);
NamingEnumeration<Binding> results = computeWithReadOnlyContext(executor);
return DefaultLdapClient.this.toList(results, function(mapper));
}
@Override
public <T> Stream<T> toStream(NameClassPairMapper<T> mapper) {
ContextExecutor<NamingEnumeration<Binding>> executor = (ctx) -> ctx.listBindings(this.name);
NamingEnumeration<Binding> results = computeWithReadOnlyContext(executor);
return DefaultLdapClient.this.toStream(results, mapper::mapFromNameClassPair);
}
@Override
public <T> Stream<T> toStream(ContextMapper<T> mapper) {
ContextExecutor<NamingEnumeration<Binding>> executor = (ctx) -> ctx.listBindings(this.name);
NamingEnumeration<Binding> results = computeWithReadOnlyContext(executor);
return DefaultLdapClient.this.toStream(results, function(mapper));
}
}
private final class DefaultAuthenticateSpec implements AuthenticateSpec {
LdapClient.SearchSpec search = new DefaultSearchSpec();
char[] password;
@Override
public AuthenticateSpec query(LdapQuery query) {
this.search.query(query);
return this;
}
@Override
public AuthenticateSpec password(String password) {
this.password = password.toCharArray();
return this;
}
@Override
public void execute() {
execute((ctx, identification) -> ctx);
}
@Override
public <T> T execute(AuthenticatedLdapEntryContextMapper<T> mapper) {
LdapEntryIdentificationContextMapper m = new LdapEntryIdentificationContextMapper();
List<LdapEntryIdentification> 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 = (this.password != null) ? new String(this.password) : null;
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 DefaultSearchSpec implements SearchSpec {
LdapQuery query = LdapQueryBuilder.query().filter("(objectClass=*)");
SearchControls controls;
@Override
public SearchSpec name(String name) {
return query((builder) -> builder.base(name).searchScope(SearchScope.OBJECT));
}
@Override
public SearchSpec name(Name name) {
return query((builder) -> builder.base(name).searchScope(SearchScope.OBJECT));
}
public SearchSpec query(Consumer<LdapQueryBuilder> consumer) {
LdapQueryBuilder builder = LdapQueryBuilder.fromQuery(this.query);
consumer.accept(builder);
this.query = builder;
return this;
}
@Override
public SearchSpec query(LdapQuery query) {
this.query = query;
return this;
}
@Override
public <T> T toObject(ContextMapper<T> mapper) {
this.controls = searchControlsForQuery(RETURN_OBJ_FLAG);
NamingEnumeration<SearchResult> results = computeWithReadOnlyContext(this::search);
return DefaultLdapClient.this.toObject(results, function(mapper));
}
@Override
public <T> T toObject(AttributesMapper<T> mapper) {
this.controls = searchControlsForQuery(DONT_RETURN_OBJ_FLAG);
NamingEnumeration<SearchResult> results = computeWithReadOnlyContext(this::search);
return DefaultLdapClient.this.toObject(results, function(mapper));
}
@Override
public <T> List<T> toList(ContextMapper<T> mapper) {
this.controls = searchControlsForQuery(RETURN_OBJ_FLAG);
NamingEnumeration<SearchResult> results = computeWithReadOnlyContext(this::search);
return DefaultLdapClient.this.toList(results, function(mapper));
}
@Override
public <T> List<T> toList(AttributesMapper<T> mapper) {
this.controls = searchControlsForQuery(DONT_RETURN_OBJ_FLAG);
NamingEnumeration<SearchResult> results = computeWithReadOnlyContext(this::search);
return DefaultLdapClient.this.toList(results, function(mapper));
}
@Override
public <T> Stream<T> toStream(ContextMapper<T> mapper) {
this.controls = searchControlsForQuery(RETURN_OBJ_FLAG);
NamingEnumeration<SearchResult> results = computeWithReadOnlyContext(this::search);
return DefaultLdapClient.this.toStream(results, function(mapper));
}
@Override
public <T> Stream<T> toStream(AttributesMapper<T> mapper) {
this.controls = searchControlsForQuery(DONT_RETURN_OBJ_FLAG);
NamingEnumeration<SearchResult> results = computeWithReadOnlyContext(this::search);
return DefaultLdapClient.this.toStream(results, function(mapper));
}
private NamingEnumeration<SearchResult> search(DirContext ctx) throws NamingException {
return ctx.search(this.query.base(), this.query.filter().encode(), this.controls);
}
private SearchControls searchControlsForQuery(boolean returnObjFlag) {
SearchControls controls = DefaultLdapClient.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 DefaultBindSpec implements BindSpec {
private final Name name;
private Object obj;
private Attributes attributes;
private boolean rebind = false;
private DefaultBindSpec(Name name) {
this.name = name;
}
public 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 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 DefaultModifySpec implements ModifySpec {
private final DirContextOperations entry;
private Name name;
private ModificationItem[] items;
private DefaultModifySpec(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 DefaultUnbindSpec implements UnbindSpec {
private final Name name;
private boolean recursive = false;
private DefaultUnbindSpec(Name name) {
this.name = name;
}
@Override
public 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<Binding> 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 (DefaultLdapClient.this.logger.isDebugEnabled()) {
DefaultLdapClient.this.logger.debug("Entry " + name + " deleted");
}
} finally {
closeNamingEnumeration(bindings);
}
}
}
<T> T computeWithReadOnlyContext(ContextExecutor<T> 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 <T> NamingExceptionFunction<? extends Binding, T> function(ContextMapper<T> mapper) {
return (result) -> mapper.mapFromContext(result.getObject());
}
private <T> NamingExceptionFunction<? extends SearchResult, T> function(AttributesMapper<T> mapper) {
return (result) -> mapper.mapFromAttributes(result.getAttributes());
}
private <T> Enumeration<T> enumeration(NamingEnumeration<T> 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<NamingException> 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 <S extends NameClassPair, T> T toObject(NamingEnumeration<S> results, NamingExceptionFunction<? super S, T> mapper) {
try {
Enumeration<S> enumeration = enumeration(results);
Function<? super S, T> 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 <S extends NameClassPair, T> List<T> toList(NamingEnumeration<S> results, NamingExceptionFunction<? super S, T> mapper) {
if (results == null) {
return Collections.emptyList();
}
try {
Enumeration<S> enumeration = enumeration(results);
Function<? super S, T> function = mapper.wrap(this.namingExceptionHandler);
List<T> mapped = new ArrayList<>();
while (enumeration.hasMoreElements()) {
T result = function.apply(enumeration.nextElement());
if (result != null) {
mapped.add(result);
}
}
return mapped;
} finally {
closeNamingEnumeration(results);
}
}
private <S extends NameClassPair, T> Stream<T> toStream(NamingEnumeration<S> results, NamingExceptionFunction<? super S, T> mapper) {
if (results == null) {
return Stream.empty();
}
Enumeration<S> enumeration = enumeration(results);
Function<? super S, T> 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 <T> void closeNamingEnumeration(NamingEnumeration<T> results) {
if (results != null) {
try {
results.close();
}
catch (Exception e) {
// Never mind this.
}
}
}
interface ContextRunnable {
void run(DirContext ctx) throws NamingException;
}
interface NamingExceptionFunction<S, T> {
T apply(S element) throws NamingException;
default Function<S, T> wrap(Consumer<NamingException> handler) {
return (s) -> {
try {
return apply(s);
} catch (NamingException ex) {
handler.accept(ex);
return null;
}
};
}
}
}

View File

@@ -0,0 +1,91 @@
package org.springframework.ldap.core;
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.naming.directory.SearchControls;
class DefaultLdapClientBuilder implements LdapClient.Builder {
private ContextSource contextSource;
private Supplier<SearchControls> 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;
DefaultLdapClientBuilder() {}
DefaultLdapClientBuilder(ContextSource contextSource,
Supplier<SearchControls> searchControlsSupplier) {
this.contextSource = contextSource;
this.searchControlsSupplier = searchControlsSupplier;
}
@Override
public DefaultLdapClientBuilder contextSource(ContextSource contextSource) {
this.contextSource = contextSource;
return this;
}
@Override
public DefaultLdapClientBuilder defaultSearchControls(Supplier<SearchControls> searchControlsSupplier) {
this.searchControlsSupplier = searchControlsSupplier;
return this;
}
/**
* {@inheritDoc}
*/
@Override
public DefaultLdapClientBuilder ignorePartialResultException(boolean ignore) {
this.ignorePartialResultException = ignore;
return this;
}
/**
* {@inheritDoc}
*/
@Override
public DefaultLdapClientBuilder ignoreNameNotFoundException(boolean ignore) {
this.ignoreNameNotFoundException = ignore;
return this;
}
/**
* {@inheritDoc}
*/
@Override
public DefaultLdapClientBuilder ignoreSizeLimitExceededException(boolean ignore) {
this.ignoreSizeLimitExceededException = ignore;
return this;
}
@Override
public DefaultLdapClientBuilder apply(Consumer<LdapClient.Builder> builderConsumer) {
builderConsumer.accept(this);
return this;
}
@Override
public DefaultLdapClientBuilder clone() {
return new DefaultLdapClientBuilder(this.contextSource, this.searchControlsSupplier);
}
@Override
public LdapClient build() {
DefaultLdapClient client = new DefaultLdapClient(this.contextSource, this.searchControlsSupplier);
client.setIgnorePartialResultException(this.ignorePartialResultException);
client.setIgnoreSizeLimitExceededException(this.ignoreSizeLimitExceededException);
client.setIgnoreNameNotFoundException(this.ignoreNameNotFoundException);
return client;
}
}

View File

@@ -0,0 +1,568 @@
/*
* 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
*/
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.
*
* <p>
* Note that a {@link #modify(Name)} is different from a rebind in that
* entries are changed instead of removed and recreated.
*
* <p>
* 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.
*
* <p>
* Note that a {@link #modify(Name)} is different from a rebind in that
* entries are changed instead of removed and recreated.
*
* <p>
* 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 DefaultLdapClientBuilder().contextSource(contextSource).build();
}
/**
* Obtain a {@code LdapClient} builder.
*/
static LdapClient.Builder builder() {
return new DefaultLdapClientBuilder();
}
/**
* 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<SearchControls> 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.
* <p>This can be useful for applying pre-packaged customizations.
* @param builderConsumer the consumer to apply
*/
Builder apply(Consumer<Builder> 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
*/
<T> List<T> toList(NameClassPairMapper<T> 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
*/
<T> Stream<T> toStream(NameClassPairMapper<T> 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
*/
<T> List<T> toList(NameClassPairMapper<T> 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
*/
<T> List<T> toList(ContextMapper<T> 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
*/
<T> Stream<T> toStream(NameClassPairMapper<T> 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
*/
<T> Stream<T> toStream(ContextMapper<T> 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=*)}.
*
* <p>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<LdapQueryBuilder> consumer);
/**
* The query to execute.
*
* @param query the query to execute
* @return the {@link SearchSpec} for further configuration
*/
SearchSpec query(LdapQuery query);
default <O extends LdapDataEntry> O toEntry() {
ContextMapper<O> cast = (ctx) -> (O) ctx;
return toObject(cast);
}
/**
* Expect at most one search result, mapped by the given strategy.
*
* <p>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> O toObject(ContextMapper<O> 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> O toObject(AttributesMapper<O> mapper);
default <O extends LdapDataEntry> List<O> toEntryList() {
ContextMapper<O> 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
*/
<O> List<O> toList(ContextMapper<O> 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
*/
<O> List<O> toList(AttributesMapper<O> mapper);
default <O extends LdapDataEntry> Stream<O> toEntryStream() {
ContextMapper<O> 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
*/
<O> Stream<O> toStream(ContextMapper<O> 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
*/
<O> Stream<O> toStream(AttributesMapper<O> 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> T execute(AuthenticatedLdapEntryContextMapper<T> mapper);
}
/**
* The specifications for the {@link #bind} request.
*/
interface BindSpec {
/**
* The object to associate with this binding.
*
* <p>
* 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").
*
* <p>
* 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();
}
}

View File

@@ -20,6 +20,7 @@ import org.springframework.ldap.filter.Filter;
import org.springframework.ldap.filter.HardcodedFilter;
import org.springframework.ldap.support.LdapEncoder;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.util.Assert;
import javax.naming.Name;
import java.text.MessageFormat;
@@ -62,6 +63,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 +82,20 @@ public final class LdapQueryBuilder implements LdapQuery {
}
/**
* 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.
*
* <p>
* 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 +192,7 @@ public final class LdapQueryBuilder implements LdapQuery {
private void initRootContainer() {
assertFilterNotStarted();
rootContainer = new DefaultContainerCriteria(this);
isFilterStarted = true;
}
/**
@@ -237,9 +246,7 @@ public final class LdapQueryBuilder implements LdapQuery {
}
private void assertFilterNotStarted() {
if(rootContainer != null) {
throw new IllegalStateException("Invalid operation - filter condition specification already started");
}
Assert.state(!isFilterStarted, "Invalid operation - filter condition specification already started");
}
@Override

View File

@@ -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 <code>list</code> operations in {@link LdapTemplate}.
*
* @author Ulrik Sandberg
*/
public class DefaultLdapClientListTest {
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<Object> contextMapperMock;
private DefaultLdapClient 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 = (DefaultLdapClient) 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<String> 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<String> 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<String> 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<String> results = tested.list(NAME).toStream(NameClassPair::getName)) {
List<String> 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<String> 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<String> results = tested.listBindings(NAME).toStream(NameClassPair::getName)) {
List<String> 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<String> 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<String> results = tested.listBindings(nameMock).toStream(NameClassPair::getName)) {
List<String> 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<Object> results = tested.listBindings(NAME).toStream(contextMapperMock)) {
List<Object> 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<Object> results = tested.listBindings(nameMock).toStream(contextMapperMock)) {
List<Object> list = results.collect(Collectors.toList());
assertThat(list).isNotNull();
assertThat(list).hasSize(1);
assertThat(list.get(0)).isSameAs(expectedResult);
}
verify(dirContextMock).close();
verify(namingEnumerationMock).close();
}
}

View File

@@ -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 DefaultLdapClientLookupTest {
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<Attributes> 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<Attributes> 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<javax.naming.NamingEnumeration<SearchResult>> whenSearching(Name name) throws Exception {
return when(dirContextMock.search(eq(name), anyString(), any()));
}
private static class NamingEnumeration implements javax.naming.NamingEnumeration<SearchResult> {
private final Iterator<SearchResult> 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();
}
}
}

View File

@@ -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 DefaultLdapClientRenameTest {
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();
}
}

View File

@@ -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 DefaultLdapClientTest {
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<SearchControls> 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<Object> 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<SearchControls> {
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();
}
}
}
}