Add LdapClient
Closes gh-675
This commit is contained in:
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
568
core/src/main/java/org/springframework/ldap/core/LdapClient.java
Normal file
568
core/src/main/java/org/springframework/ldap/core/LdapClient.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user