From 7eec159c2d6083b89135ee061fd76126ae83831b Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Tue, 06 Sep 2011 16:21:18 +0000
Subject: [PATCH] Issue OPENDJ-262: Implement pass through authentication (PTA)

---
 opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java |  596 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 579 insertions(+), 17 deletions(-)

diff --git a/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java b/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
index 106cf5d..f5ec6b7 100644
--- a/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
+++ b/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
@@ -34,14 +34,17 @@
 import java.io.Closeable;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
 
 import org.opends.messages.Message;
 import org.opends.server.admin.server.ConfigurationChangeListener;
-import org.opends.server.admin.std.server.
-    LDAPPassThroughAuthenticationPolicyCfg;
+import org.opends.server.admin.std.server.*;
 import org.opends.server.api.AuthenticationPolicy;
 import org.opends.server.api.AuthenticationPolicyFactory;
 import org.opends.server.api.AuthenticationPolicyState;
@@ -58,6 +61,11 @@
     AuthenticationPolicyFactory<LDAPPassThroughAuthenticationPolicyCfg>
 {
 
+  // TODO: retry operations transparently until all connections exhausted.
+  // TODO: handle password policy response controls? AD?
+  // TODO: periodically ping offline servers in order to detect when they come
+  // back.
+
   /**
    * An LDAP connection which will be used in order to search for or
    * authenticate users.
@@ -75,9 +83,9 @@
 
     /**
      * Returns the name of the user whose entry matches the provided search
-     * criteria.
-     * <p>
-     * TODO: define result codes used when no entries found or too many entries.
+     * criteria. This will return CLIENT_SIDE_NO_RESULTS_RETURNED/NO_SUCH_OBJECT
+     * if no search results were returned, or CLIENT_SIDE_MORE_RESULTS_TO_RETURN
+     * if too many results were returned.
      *
      * @param baseDN
      *          The search base DN.
@@ -165,6 +173,483 @@
   private final class PolicyImpl extends AuthenticationPolicy implements
       ConfigurationChangeListener<LDAPPassThroughAuthenticationPolicyCfg>
   {
+
+    /**
+     * A factory which returns pre-authenticated connections for searches.
+     */
+    private final class AuthenticatedConnectionFactory implements
+        ConnectionFactory
+    {
+
+      private final ConnectionFactory factory;
+
+
+
+      private AuthenticatedConnectionFactory(final ConnectionFactory factory)
+      {
+        this.factory = factory;
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public Connection getConnection() throws DirectoryException
+      {
+        final DN username = configuration.getMappedSearchBindDN();
+        final String password = configuration.getMappedSearchBindPassword();
+
+        final Connection connection = factory.getConnection();
+        if (username != null && !username.isNullDN())
+        {
+          try
+          {
+            connection.simpleBind(ByteString.valueOf(username.toString()),
+                ByteString.valueOf(password));
+          }
+          catch (final DirectoryException e)
+          {
+            connection.close();
+            throw e;
+          }
+        }
+        return connection;
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public String toString()
+      {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("AuthenticationConnectionFactory(");
+        builder.append(factory);
+        builder.append(')');
+        return builder.toString();
+      }
+
+    }
+
+
+
+    /**
+     * PTA connection pool.
+     */
+    private final class ConnectionPool implements ConnectionFactory, Closeable
+    {
+
+      /**
+       * Pooled connection's intercept close and release connection back to the
+       * pool.
+       */
+      private final class PooledConnection implements Connection
+      {
+        private final Connection connection;
+        private boolean connectionIsClosed = false;
+
+
+
+        private PooledConnection(final Connection connection)
+        {
+          this.connection = connection;
+        }
+
+
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public void close()
+        {
+          if (!connectionIsClosed)
+          {
+            connectionIsClosed = true;
+
+            // Guarded by PolicyImpl
+            if (poolIsClosed)
+            {
+              connection.close();
+            }
+            else
+            {
+              pooledConnections.offer(this);
+            }
+            availableConnections.release();
+          }
+        }
+
+
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public ByteString search(final DN baseDN, final SearchScope scope,
+            final SearchFilter filter) throws DirectoryException
+        {
+          try
+          {
+            return connection.search(baseDN, scope, filter);
+          }
+          catch (final DirectoryException e)
+          {
+            // Don't put the connection back in the pool if it has failed.
+            closeConnectionOnFatalError(e);
+            throw e;
+          }
+        }
+
+
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public void simpleBind(final ByteString username,
+            final ByteString password) throws DirectoryException
+        {
+          try
+          {
+            connection.simpleBind(username, password);
+          }
+          catch (final DirectoryException e)
+          {
+            // Don't put the connection back in the pool if it has failed.
+            closeConnectionOnFatalError(e);
+            throw e;
+          }
+        }
+
+
+
+        private void closeConnectionOnFatalError(final DirectoryException e)
+        {
+          if (isFatalResultCode(e.getResultCode()))
+          {
+            connectionIsClosed = true;
+            connection.close();
+            availableConnections.release();
+          }
+        }
+
+      }
+
+
+
+      // Guarded by PolicyImpl.lock.
+      private boolean poolIsClosed = false;
+
+      private final ConnectionFactory factory;
+      private final int poolSize =
+        Runtime.getRuntime().availableProcessors() * 2;
+      private final Semaphore availableConnections = new Semaphore(poolSize);
+      private final LinkedBlockingQueue<PooledConnection> pooledConnections =
+        new LinkedBlockingQueue<PooledConnection>();
+
+
+
+      private ConnectionPool(final ConnectionFactory factory)
+      {
+        this.factory = factory;
+      }
+
+
+
+      /**
+       * Release all connections: do we want to block?
+       */
+      @Override
+      public void close()
+      {
+        // No need for synchronization as this can only be called with the
+        // policy's exclusive lock.
+        poolIsClosed = true;
+
+        PooledConnection pooledConnection;
+        while ((pooledConnection = pooledConnections.poll()) != null)
+        {
+          pooledConnection.connection.close();
+        }
+
+        // Since we have the exclusive lock, there should be no more connections
+        // in use.
+        if (availableConnections.availablePermits() != poolSize)
+        {
+          throw new IllegalStateException(
+              "Pool has remaining connections open after close");
+        }
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public Connection getConnection() throws DirectoryException
+      {
+        // This should only be called with the policy's shared lock.
+        if (poolIsClosed)
+        {
+          throw new IllegalStateException("pool is closed");
+        }
+
+        availableConnections.acquireUninterruptibly();
+
+        // There is either a pooled connection or we are allowed to create
+        // one.
+        PooledConnection pooledConnection = pooledConnections.poll();
+        if (pooledConnection == null)
+        {
+          try
+          {
+            final Connection connection = factory.getConnection();
+            pooledConnection = new PooledConnection(connection);
+          }
+          catch (final DirectoryException e)
+          {
+            availableConnections.release();
+            throw e;
+          }
+        }
+
+        return pooledConnection;
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public String toString()
+      {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("ConnectionPool(");
+        builder.append(factory);
+        builder.append(", poolSize=");
+        builder.append(poolSize);
+        builder.append(", inPool=");
+        builder.append(pooledConnections.size());
+        builder.append(", available=");
+        builder.append(availableConnections.availablePermits());
+        builder.append(')');
+        return builder.toString();
+      }
+    }
+
+
+
+    /**
+     * A simplistic two-way fail-over connection factory implementation.
+     */
+    private final class FailoverConnectionFactory implements ConnectionFactory,
+        Closeable
+    {
+      private final LoadBalancer primary;
+      private final LoadBalancer secondary;
+
+
+
+      private FailoverConnectionFactory(final LoadBalancer primary,
+          final LoadBalancer secondary)
+      {
+        this.primary = primary;
+        this.secondary = secondary;
+      }
+
+
+
+      /**
+       * Close underlying load-balancers.
+       */
+      @Override
+      public void close()
+      {
+        primary.close();
+        if (secondary != null)
+        {
+          secondary.close();
+        }
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public Connection getConnection() throws DirectoryException
+      {
+        if (secondary == null)
+        {
+          // No fail-over so just use the primary.
+          return primary.getConnection();
+        }
+        else
+        {
+          try
+          {
+            return primary.getConnection();
+          }
+          catch (final DirectoryException e)
+          {
+            return secondary.getConnection();
+          }
+        }
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public String toString()
+      {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("FailoverConnectionFactory(");
+        builder.append(primary);
+        builder.append(", ");
+        builder.append(secondary);
+        builder.append(')');
+        return builder.toString();
+      }
+
+    }
+
+
+
+    /**
+     * A simplistic load-balancer connection factory implementation using
+     * approximately round-robin balancing.
+     */
+    private final class LoadBalancer implements ConnectionFactory, Closeable
+    {
+      private final ConnectionPool[] factories;
+      private final AtomicInteger nextIndex = new AtomicInteger();
+      private final int maxIndex;
+
+
+
+      private LoadBalancer(final ConnectionPool[] factories)
+      {
+        this.factories = factories;
+        this.maxIndex = factories.length;
+      }
+
+
+
+      /**
+       * Close underlying connection pools.
+       */
+      @Override
+      public void close()
+      {
+        for (final ConnectionPool pool : factories)
+        {
+          pool.close();
+        }
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public Connection getConnection() throws DirectoryException
+      {
+        final int startIndex = getStartIndex();
+        int index = startIndex;
+        for (;;)
+        {
+          final ConnectionFactory factory = factories[index];
+
+          try
+          {
+            return factory.getConnection();
+          }
+          catch (final DirectoryException e)
+          {
+            // Try the next index.
+            if (++index == maxIndex)
+            {
+              index = 0;
+            }
+
+            // If all the factories have been tried then give up and throw the
+            // exception.
+            if (index == startIndex)
+            {
+              throw e;
+            }
+          }
+        }
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public String toString()
+      {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("LoadBalancer(");
+        builder.append(nextIndex);
+        for (final ConnectionFactory factory : factories)
+        {
+          builder.append(", ");
+          builder.append(factory);
+        }
+        builder.append(')');
+        return builder.toString();
+      }
+
+
+
+      // Determine the start index.
+      private int getStartIndex()
+      {
+        // A round robin pool of one connection factories is unlikely in
+        // practice and requires special treatment.
+        if (maxIndex == 1)
+        {
+          return 0;
+        }
+
+        // Determine the next factory to use: avoid blocking algorithm.
+        int oldNextIndex;
+        int newNextIndex;
+        do
+        {
+          oldNextIndex = nextIndex.get();
+          newNextIndex = oldNextIndex + 1;
+          if (newNextIndex == maxIndex)
+          {
+            newNextIndex = 0;
+          }
+        }
+        while (!nextIndex.compareAndSet(oldNextIndex, newNextIndex));
+
+        // There's a potential, but benign, race condition here: other threads
+        // could jump in and rotate through the list before we return the
+        // connection factory.
+        return newNextIndex;
+      }
+
+    }
+
+
+
     /**
      * LDAP PTA policy state implementation.
      */
@@ -329,8 +814,6 @@
               {
                 switch (e.getResultCode())
                 {
-                // FIXME: specify possible result codes. What about authz
-                // errors?
                 case NO_SUCH_OBJECT:
                 case CLIENT_SIDE_NO_RESULTS_RETURNED:
                   // Ignore and try next base DN.
@@ -389,7 +872,6 @@
           {
             switch (e.getResultCode())
             {
-            // FIXME: specify possible result codes.
             case NO_SUCH_OBJECT:
             case INVALID_CREDENTIALS:
               return false;
@@ -430,9 +912,8 @@
     // Current configuration.
     private LDAPPassThroughAuthenticationPolicyCfg configuration;
 
-    // FIXME: initialize connection factories.
-    private ConnectionFactory searchFactory = null;
-    private ConnectionFactory bindFactory = null;
+    private FailoverConnectionFactory searchFactory = null;
+    private FailoverConnectionFactory bindFactory = null;
 
 
 
@@ -529,7 +1010,18 @@
       exclusiveLock.lock();
       try
       {
-        // TODO: close all connections.
+        if (searchFactory != null)
+        {
+          searchFactory.close();
+          searchFactory = null;
+        }
+
+        if (bindFactory != null)
+        {
+          bindFactory.close();
+          bindFactory = null;
+        }
+
       }
       finally
       {
@@ -544,11 +1036,54 @@
     {
       this.configuration = configuration;
 
-      // TODO: implement FO/LB/CP + authenticated search factory.
-      final String hostPort = configuration.getPrimaryRemoteLDAPServer()
-          .first();
-      searchFactory = newLDAPConnectionFactory(hostPort);
-      bindFactory = newLDAPConnectionFactory(hostPort);
+      // Create load-balancers for primary servers.
+      final LoadBalancer primarySearchLoadBalancer;
+      final LoadBalancer primaryBindLoadBalancer;
+
+      Set<String> servers = configuration.getPrimaryRemoteLDAPServer();
+      ConnectionPool[] searchPool = new ConnectionPool[servers.size()];
+      ConnectionPool[] bindPool = new ConnectionPool[servers.size()];
+      int index = 0;
+      for (final String hostPort : servers)
+      {
+        final ConnectionFactory factory = newLDAPConnectionFactory(hostPort);
+        searchPool[index] = new ConnectionPool(
+            new AuthenticatedConnectionFactory(factory));
+        bindPool[index++] = new ConnectionPool(factory);
+      }
+      primarySearchLoadBalancer = new LoadBalancer(searchPool);
+      primaryBindLoadBalancer = new LoadBalancer(bindPool);
+
+      // Create load-balancers for secondary servers.
+      final LoadBalancer secondarySearchLoadBalancer;
+      final LoadBalancer secondaryBindLoadBalancer;
+
+      servers = configuration.getSecondaryRemoteLDAPServer();
+      if (servers.isEmpty())
+      {
+        secondarySearchLoadBalancer = null;
+        secondaryBindLoadBalancer = null;
+      }
+      else
+      {
+        searchPool = new ConnectionPool[servers.size()];
+        bindPool = new ConnectionPool[servers.size()];
+        index = 0;
+        for (final String hostPort : servers)
+        {
+          final ConnectionFactory factory = newLDAPConnectionFactory(hostPort);
+          searchPool[index] = new ConnectionPool(
+              new AuthenticatedConnectionFactory(factory));
+          bindPool[index++] = new ConnectionPool(factory);
+        }
+        secondarySearchLoadBalancer = new LoadBalancer(searchPool);
+        secondaryBindLoadBalancer = new LoadBalancer(bindPool);
+      }
+
+      searchFactory = new FailoverConnectionFactory(primarySearchLoadBalancer,
+          secondarySearchLoadBalancer);
+      bindFactory = new FailoverConnectionFactory(primaryBindLoadBalancer,
+          secondaryBindLoadBalancer);
     }
 
 
@@ -589,6 +1124,33 @@
 
 
   /**
+   * Determines whether or no a result code is expected to trigger the
+   * associated connection to be closed immediately.
+   *
+   * @param resultCode
+   *          The result code.
+   * @return {@code true} if the result code is expected to trigger the
+   *         associated connection to be closed immediately.
+   */
+  static boolean isFatalResultCode(final ResultCode resultCode)
+  {
+    switch (resultCode)
+    {
+    case BUSY:
+    case UNAVAILABLE:
+    case PROTOCOL_ERROR:
+    case OTHER:
+    case UNWILLING_TO_PERFORM:
+    case OPERATIONS_ERROR:
+      return true;
+    default:
+      return false;
+    }
+  }
+
+
+
+  /**
    * Public default constructor used by the admin framework. This will use the
    * default LDAP connection factory provider.
    */

--
Gitblit v1.10.0