/ldap4j

Ldap4j is an LDAP client library for Java.

Primary LanguageJavaApache License 2.0Apache-2.0

Ldap4j

Ldap4j is a java LDAP client, which can be used to query directory services. Its main goals are to be fully non-blocking, correct, and host environment and transport agnostic.

Currently, the client only supports query operations.

Also check the external issues file to help you choose an LDAP client implementation.

Table of contents

Features

Ldap4j is an LDAP v3 client. It's fully non-blocking, and supports timeouts on all operations.

Ldap4j currently supports the following operations:

Ldap4j supports TLS, with optional host name verification, and it supports the non-standard LDAPS protocol.

Ldap4j doesn't support parallel operations yet. A connection pool is provided to alleviate the need for parallel operations.

All operations are non-blocking, the client should never wait for parallel results by blocking the current thread.

All operations are subject to a timeout. All operations return a neutral result on a timeout, or raise an exception. The acquisitions and releases of system resources are not subject to timeouts.

Ldap4j is host environment agnostic, it can be used in a wide variety of environments with some glue logic. Glue has been written for:

Ldap4j is also transport agnostic. Currently, it supports the following libraries:

Maven

Various dependencies are neatly packaged into submodules. Choose according to your need.

    <dependency>
        <groupId>com.adaptiverecognition</groupId>
        <artifactId>ldap4j-java</artifactId>
        <version>1.0.0</version>
    </dependency>
    <dependency>
        <groupId>com.adaptiverecognition</groupId>
        <artifactId>ldap4j-mina</artifactId>
        <version>1.0.0</version>
    </dependency>
    <dependency>
        <groupId>com.adaptiverecognition</groupId>
        <artifactId>ldap4j-netty</artifactId>
        <version>1.0.0</version>
    </dependency>
    <dependency>
        <groupId>com.adaptiverecognition</groupId>
        <artifactId>ldap4j-reactor-netty</artifactId>
        <version>1.0.0</version>
    </dependency>

How to use

The samples subproject contains several short examples how ldap4j can be used. All samples do the simple task of:

  • connecting to the public ldap server ldap.forumsys.com:389,
  • authenticating itself to the server,
  • and querying the users of the mathematicians group.

The result of executing one of the samples should look something like this:

ldap4j trampoline sample
connected
bound
mathematicians:
uid=euclid,dc=example,dc=com
uid=riemann,dc=example,dc=com
uid=euler,dc=example,dc=com
uid=gauss,dc=example,dc=com
uid=test,dc=example,dc=com

Future

CompletableFutures can be used with the ldap4j client. This requires a thread pool.

    // new thread pool
    ScheduledExecutorService executor=Executors.newScheduledThreadPool(8);

    // connect
    CompletableFuture<Void> future=FutureLdapConnection.factoryJavaAsync(
                    null, // use the global asynchronous channel group
                    executor,
                    Log.systemErr(),
                    new InetSocketAddress("ldap.forumsys.com", 389),
                    10_000_000_000L, // timeout
                    TlsSettings.noTls()) // plain-text connection
            .get()
            .thenCompose((connection)->{
                System.out.println("connected");

                // authenticate
                CompletableFuture<Void> rest=connection.bindSimple(
                                "cn=read-only-admin,dc=example,dc=com", "password".toCharArray())
                        .thenCompose((ignore)->{
                            System.out.println("bound");
                            try {

                                // look up mathematicians
                                return connection.search(
                                        false,
                                        new SearchRequest(
                                                List.of("uniqueMember"), // attributes
                                                "ou=mathematicians,dc=example,dc=com", // base object
                                                DerefAliases.DEREF_ALWAYS,
                                                Filter.parse("(objectClass=*)"),
                                                Scope.WHOLE_SUBTREE,
                                                100, // size limit
                                                10, // time limit
                                                false)); // types only
                            }
                            catch (Throwable throwable) {
                                return CompletableFuture.failedFuture(throwable);
                            }
                        })
                        .thenCompose((searchResults)->{
                            System.out.println("mathematicians:");
                            searchResults.get(0)
                                    .asEntry()
                                    .attributes()
                                    .get("uniqueMember")
                                    .values()
                                    .forEach(System.out::println);
                            return CompletableFuture.completedFuture(null);
                        });

                // release resources, timeout only affects the LDAP and TLS shutdown sequences
                return rest
                        .thenCompose((ignore)->connection.close())
                        .exceptionallyCompose((ignore)->connection.close());
            });

    //wait for the result in this thread
    future.get(10_000_000_000L, TimeUnit.NANOSECONDS);

Lava

Lava is the internal language of ldap4j. This is the most feature-rich way to use the client. Lava can be used reactive-style.

    private static @NotNull Lava<Void> main() {
        return Lava.supplier(()->{
            System.out.println("ldap4j lava sample");

            // create a connection, and guard the computation
            return Closeable.withCloseable(
                    ()->LdapConnection.factory(
                            // use the global asynchronous channel group
                            JavaAsyncChannelConnection.factory(null, Map.of()),
                            new InetSocketAddress("ldap.forumsys.com", 389),
                            TlsSettings.noTls()), // plain-text connection
                    (connection)->{
                        System.out.println("connected");

                        // authenticate
                        return connection.bindSimple(
                                        "cn=read-only-admin,dc=example,dc=com", "password".toCharArray())
                                .composeIgnoreResult(()->{
                                    System.out.println("bound");

                                    // look up mathematicians
                                    return connection.search(
                                            false, // manage DSA IT
                                            new SearchRequest(
                                                    List.of("uniqueMember"), // attributes
                                                    "ou=mathematicians,dc=example,dc=com", // base object
                                                    DerefAliases.DEREF_ALWAYS,
                                                    Filter.parse("(objectClass=*)"),
                                                    Scope.WHOLE_SUBTREE,
                                                    100, // size limit
                                                    10, // time limit
                                                    false)); // types only
                                })
                                .compose((searchResults)->{
                                    System.out.println("mathematicians:");
                                    searchResults.get(0)
                                            .asEntry()
                                            .attributes()
                                            .get("uniqueMember")
                                            .values()
                                            .forEach(System.out::println);
                                    return Lava.VOID;
                                });
                    });
        });
    }

    public static void main(String[] args) throws Throwable {
        // new thread pool
        ScheduledExecutorService executor=Executors.newScheduledThreadPool(8);
        try {
            ScheduledExecutorContext context=ScheduledExecutorContext.createDelayNanos(
                    10_000_000_000L, // timeout
                    executor,
                    Log.systemErr());

            // going to wait for the result in this thread
            JoinCallback<Void> join=Callback.join(context);

            // compute the result
            context.get(join, main());

            // wait for the result
            join.joinEndNanos(context.endNanos());
        }
        finally {
            executor.shutdown();
        }
    }

Reactor

Glue is provided to use ldap4j as a Reactor publisher. All asynchronous operations return a Mono object. The transport is hardcoded to Netty.

A pool is provided to amortize the cost of repeated TCP and TLS negotiations.

After starting the sample, the application can be reached here.

    @Autowired
    public EventLoopGroup eventLoopGroup;
    @Autowired
    public ReactorLdapPool pool;

    public Mono<String> noPool() {
        StringBuilder output=new StringBuilder();
        output.append("<html><body>");
        output.append("ldap4j reactor no-pool sample<br>");

        // create a connection, and guard the computation
        return ReactorLdapConnection.withConnection(
                        (evenLoopGroup)->Mono.empty(), // event loop group close
                        ()->Mono.just(eventLoopGroup), // event loop group factory
                        (connection)->run(connection, output),
                        new InetSocketAddress("ldap.forumsys.com", 389),
                        10_000_000_000L, // timeout
                        TlsSettings.noTls()) // plaint-text connection
                .flatMap((ignore)->{
                    output.append("</body></html>");
                    return Mono.just(output.toString());
                });
    }

    @GetMapping(value = "/pool", produces = "text/html")
    public Mono<String> pool() {
        StringBuilder output=new StringBuilder();
        output.append("<html><body>");
        output.append("ldap4j reactor pool sample<br>");

        // lease a connection, and guard the computation
        return pool.lease((connection)->run(connection, output))
                .flatMap((ignore)->{
                    output.append("</body></html>");
                    return Mono.just(output.toString());
                });
    }

    private Mono<Object> run(ReactorLdapConnection connection, StringBuilder output) {
        output.append("connected<br>");

        // authenticate
        return connection.bindSimple(
                        "cn=read-only-admin,dc=example,dc=com",
                        "password".toCharArray())
                .flatMap((ignore)->{
                    output.append("bound<br>");
                    try {

                        // look up mathematicians
                        return connection.search(
                                false, // manage DSA IT
                                new SearchRequest(
                                        List.of("uniqueMember"), // attributes
                                        "ou=mathematicians,dc=example,dc=com", // base object
                                        DerefAliases.DEREF_ALWAYS,
                                        Filter.parse("(objectClass=*)"),
                                        Scope.WHOLE_SUBTREE,
                                        100, // size limit
                                        10, // time limit
                                        false)); // types only
                    }
                    catch (Throwable throwable) {
                        return Mono.error(throwable);
                    }
                })
                .flatMap((searchResults)->{
                    output.append("mathematicians:<br>");
                    searchResults.get(0)
                            .asEntry()
                            .attributes()
                            .get("uniqueMember")
                            .values()
                            .forEach((value)->{
                                output.append(value);
                                output.append("<br>");
                            });
                    return Mono.just(new Object());
                });
    }

    @Bean
    public EventLoopGroup evenLoopGroup() {
        return new NioEventLoopGroup(4);
    }
    
    @Bean
    public ReactorLdapPool pool(@Autowired EventLoopGroup eventLoopGroup) {
        return ReactorLdapPool.create(
            eventLoopGroup,
            (eventLoopGroup2)->Mono.empty(), // event loop group close
            Log.slf4j(), // log to SLF4J
            4, // pool size
            new InetSocketAddress("ldap.forumsys.com", 389),
            10_000_000_000L, // timeout
            TlsSettings.noTls()); // plaint-text connection
    }

Trampoline

A trampoline is used to convert the asynchronous operations to synchronous ones. This can be used in simple command line or desktop applications. The transport is hardcoded to Java NIO polling.

    // single timeout for all operations
    long endNanos=System.nanoTime()+10_000_000_000L;

    TrampolineLdapConnection connection=TrampolineLdapConnection.createJavaPoll(
            endNanos,
            Log.systemErr(), // log everything to the standard error
            new InetSocketAddress("ldap.forumsys.com", 389),
            TlsSettings.noTls()); // plain-text connection

    // authenticate
    connection.bindSimple("cn=read-only-admin,dc=example,dc=com", endNanos, "password".toCharArray());

    // look up mathematicians
    List<SearchResult> searchResults=connection.search(
            endNanos,
            false, // manage DSA IT
            new SearchRequest(
                    List.of("uniqueMember"), // attributes
                    "ou=mathematicians,dc=example,dc=com", // base object
                    DerefAliases.DEREF_ALWAYS,
                    Filter.parse("(objectClass=*)"),
                    Scope.WHOLE_SUBTREE,
                    100, // size limit
                    10, // time limit
                    false)); // types only
    System.out.println("mathematicians:");
    searchResults.get(0)
            .asEntry()
            .attributes()
            .get("uniqueMember")
            .values()
            .forEach(System.out::println);

    // release resources, timeout only affects the LDAP and TLS shutdown sequences
    connection.close(endNanos);

ldap4j.sh

Ldap4j contains a command line client. Its main purpose is to facilitate field debugging.

It prints all options when it runs without any arguments.

./ldap4j.sh -plaintext ldap.forumsys.com \
    bind-simple cn=read-only-admin,dc=example,dc=com console \
    search -attribute uniqueMember ou=mathematicians,dc=example,dc=com '(objectClass=*)'

connecting to ldap.forumsys.com/54.80.223.88:389
connected
Password for cn=read-only-admin,dc=example,dc=com: 
bind simple, cn=read-only-admin,dc=example,dc=com
bind successful
search
    attributes: [uniqueMember]
    base object: ou=mathematicians,dc=example,dc=com
    deref. aliases: DEREF_ALWAYS
    filter: (objectClass=*)
    manage dsa it: false
    scope: WHOLE_SUBTREE
    size limit: 0 entries
    time limit: 0 sec
    types only: false
search entry
    dn: ou=mathematicians,dc=example,dc=com
    uniqueMember: PartialAttribute[type=uniqueMember, values=[uid=euclid,dc=example,dc=com, uid=riemann,dc=example,dc=com, uid=euler,dc=example,dc=com, uid=gauss,dc=example,dc=com, uid=test,dc=example,dc=com]]
search done

Docs

DNS lookups

Ldap4j expects a resolved InetSocketAddress to connect to a server. This conveniently sidesteps the question of how to obtain a resolved address.

Standard java libraries can only resolve addresses through a blocking API.

I/O exceptions

Java lacks the ability to get back exact error codes on I/O errors. To classify an exception ldap4j first checks the type of the exception, and when this fails, checks the exception message. There's some properties files in ldap4j-java resources to list known types and message patterns:

  • Exceptions.connection.closed.properties,
  • Exceptions.timeout.properties,
  • Exceptions.unknown.host.properties.

Lava

Lava is the monadic library used to implement ldap4j. It abstracts java computations, and it carries around multiple objects:

  • a clock to measure time,
  • a suggested deadline for computations,
  • an executor,
  • a log,
  • a timer, to wait for time to elapse.

There are three objects central to lava.

The Callback is the visitor of java computations. A visitor can be used to pattern match. This is not unlike a CompletionHandler.

    public interface Callback<T> {
        void completed(T value);

        void failed(@NotNull Throwable throwable);
    }

The Context groups together useful objects. It also provides a few methods to uncouple the calling of Callback.completed(), Callback.failed() and Lava.get() from the current thread.

    public interface Context extends Executor {
        @NotNull Runnable awaitEndNanos(@NotNull Callback<Void> callback);

        @NotNull Clock clock();

        default <T> void complete(@NotNull Callback<T> callback, T value);

        long endNanos();
    
        default <T> void fail(@NotNull Callback<T> callback, @NotNull Throwable throwable);
    
        default <T> void get(@NotNull Callback<T> callback, @NotNull Lava<T> supplier);

        @NotNull Log log();
    }

Lava is the monad. The creation of a lava object should do nothing most of the time, and a new computation should be started for every call of Lava.get(). This class also provides some monadic constructors and compositions.

    public interface Lava<T> {
        static <E extends Throwable, T> @NotNull Lava<T> catchErrors(
                @NotNull Function<@NotNull E, @NotNull Lava<T>> function,
                @NotNull Supplier<@NotNull Lava<T>> supplier,
                @NotNull Class<E> type);
    
        static <T> @NotNull Lava<T> complete(T value);

        default <U> @NotNull Lava<U> compose(@NotNull Function<T, @NotNull Lava<U>> function);
        
        static <T> @NotNull Lava<T> fail(@NotNull Throwable throwable);

        static <T> @NotNull Lava<T> finallyGet(
                @NotNull Supplier<@NotNull Lava<Void>> finallyBlock,
                @NotNull Supplier<@NotNull Lava<T>> tryBlock);

        static <T, U> @NotNull Lava<@NotNull Pair<T, U>> forkJoin(
                @NotNull Supplier<@NotNull Lava<T>> left,
                @NotNull Supplier<@NotNull Lava<U>> right);

        void get(@NotNull Callback<T> callback, @NotNull Context context) throws Throwable;
    }

Executor

Computing a lava monad requires a Context, which is mostly an Executor. A Context implementation for ScheduledExecutorServices is provided, it's called ScheduledExecutorContext.

When the environment doesn't provide a scheduled executor, the MinHeap class can be used to implement Context.awaitEndNanos().

Trampoline

A trampoline evaluates lava objects in a single thread, completely synchronously. It uses a queue to delay tasks, and consumes tasks in a loop until the result is produced. It supports waits without busy-waiting, using Object.wait().

Transports

Ldap4j is transport agnostic, at the lava level the network library can be chosen freely. Glue logic for multiple libraries are provided.

Apache MINA

A MinaConnection requires an IoProcessor.

Java NIO channel, asynchronous

A JavaAsyncChannelConnection uses a AsynchronousSocketChannels. If the given channel group is null, it will use the global JVM channel group.

Java NIO channel, polling

A JavaChannelPollConnection uses a SocketChannel. This requires no external threads to work. It polls repeatedly the underlying channel for the result, but uses exponential backoff to limit the time increase to a small linear factor, and the number of unsuccessful polls to logarithmic in time.

This is intended for the same use cases as the trampoline, single threaded single user applications.

Netty

A NettyConnection requires an EventLoopGroup, and a matching DuplexChannel class.

License

Ldap4j is licensed under the Apache License, Version 2.0.