From 57961fede2f8c4992593563f6ef643f60c3b3c46 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Wed, 07 Sep 2011 18:13:05 +0000
Subject: [PATCH] Issue OPENDJ-262: Implement pass through authentication (PTA)

---
 opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java |  588 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 585 insertions(+), 3 deletions(-)

diff --git a/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java b/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
index f5ec6b7..41077a6 100644
--- a/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
+++ b/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
@@ -30,8 +30,13 @@
 
 
 import static org.opends.messages.ExtensionMessages.*;
+import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
+import static org.opends.server.protocols.ldap.LDAPConstants.*;
 
 import java.io.Closeable;
+import java.io.IOException;
+import java.net.*;
+import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
@@ -42,6 +47,8 @@
 import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
 
+import javax.net.ssl.SSLException;
+
 import org.opends.messages.Message;
 import org.opends.server.admin.server.ConfigurationChangeListener;
 import org.opends.server.admin.std.server.*;
@@ -49,6 +56,12 @@
 import org.opends.server.api.AuthenticationPolicyFactory;
 import org.opends.server.api.AuthenticationPolicyState;
 import org.opends.server.config.ConfigException;
+import org.opends.server.loggers.debug.DebugLogger;
+import org.opends.server.loggers.debug.DebugTracer;
+import org.opends.server.protocols.asn1.ASN1Exception;
+import org.opends.server.protocols.ldap.*;
+import org.opends.server.tools.LDAPReader;
+import org.opends.server.tools.LDAPWriter;
 import org.opends.server.types.*;
 import org.opends.server.util.StaticUtils;
 
@@ -65,6 +78,7 @@
   // TODO: handle password policy response controls? AD?
   // TODO: periodically ping offline servers in order to detect when they come
   // back.
+  // FIXME: validate host/port (check port in range).
 
   /**
    * An LDAP connection which will be used in order to search for or
@@ -168,6 +182,556 @@
 
 
   /**
+   * The PTA design guarantees that connections are only used by a single thread
+   * at a time, so we do not need to perform any synchronization.
+   */
+  private static final class LDAPConnectionFactory implements ConnectionFactory
+  {
+    /**
+     * LDAP connection implementation.
+     */
+    private final class LDAPConnection implements Connection
+    {
+      private final Socket plainSocket;
+      private final Socket ldapSocket;
+      private final LDAPWriter writer;
+      private final LDAPReader reader;
+      private int nextMessageID = 0;
+      private boolean isClosed = false;
+
+
+
+      private LDAPConnection(final Socket plainSocket, final Socket ldapSocket,
+          final LDAPReader reader, final LDAPWriter writer)
+      {
+        this.plainSocket = plainSocket;
+        this.ldapSocket = ldapSocket;
+        this.reader = reader;
+        this.writer = writer;
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public void close()
+      {
+        /*
+         * This method is intentionally a bit "belt and braces" because we have
+         * seen far too many subtle resource leaks due to bugs within JDK,
+         * especially when used in conjunction with SSL (e.g.
+         * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7025227).
+         */
+        if (isClosed)
+        {
+          return;
+        }
+        isClosed = true;
+
+        // Send an unbind request.
+        final LDAPMessage message = new LDAPMessage(nextMessageID++,
+            new UnbindRequestProtocolOp());
+        try
+        {
+          writer.writeMessage(message);
+        }
+        catch (final IOException e)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, e);
+          }
+        }
+
+        // Close all IO resources.
+        writer.close();
+        reader.close();
+
+        try
+        {
+          ldapSocket.close();
+        }
+        catch (final IOException e)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, e);
+          }
+        }
+
+        try
+        {
+          plainSocket.close();
+        }
+        catch (final IOException e)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, e);
+          }
+        }
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public ByteString search(final DN baseDN, final SearchScope scope,
+          final SearchFilter filter) throws DirectoryException
+      {
+        // Create the search request and send it to the server.
+        final SearchRequestProtocolOp searchRequest =
+          new SearchRequestProtocolOp(
+            ByteString.valueOf(baseDN.toString()), scope,
+            DereferencePolicy.DEREF_ALWAYS, 1 /* size limit */,
+            (timeoutMS / 1000), false /* types only */,
+            RawFilter.create(filter), NO_ATTRIBUTES);
+        sendRequest(searchRequest);
+
+        // Read the responses from the server. We cannot fail-fast since this
+        // could leave unread search response messages.
+        byte opType;
+        ByteString username = null;
+        int resultCount = 0;
+
+        do
+        {
+          final LDAPMessage responseMessage = readResponse();
+          opType = responseMessage.getProtocolOpType();
+
+          switch (opType)
+          {
+          case OP_TYPE_SEARCH_RESULT_ENTRY:
+            final SearchResultEntryProtocolOp searchEntry = responseMessage
+                .getSearchResultEntryProtocolOp();
+            if (username != null)
+            {
+              username = ByteString.valueOf(searchEntry.getDN().toString());
+            }
+            resultCount++;
+            break;
+
+          case OP_TYPE_SEARCH_RESULT_REFERENCE:
+            // Count this as a result.
+            resultCount++;
+            break;
+
+          case OP_TYPE_SEARCH_RESULT_DONE:
+            final SearchResultDoneProtocolOp searchResult = responseMessage
+                .getSearchResultDoneProtocolOp();
+
+            final ResultCode resultCode = ResultCode.valueOf(searchResult
+                .getResultCode());
+            switch (resultCode)
+            {
+            case SUCCESS:
+              // The search succeeded. Drop out of the loop and check that we
+              // got a matching entry.
+              break;
+
+            case SIZE_LIMIT_EXCEEDED:
+              // TODO: Too many entries would have been returned.
+              throw new DirectoryException(
+                  ResultCode.CLIENT_SIDE_MORE_RESULTS_TO_RETURN,
+                  (Message) null);
+
+            case TIME_LIMIT_EXCEEDED:
+              // FIXME: search timed out.
+              throw new DirectoryException(ResultCode.CLIENT_SIDE_TIMEOUT,
+                  (Message) null);
+
+            default:
+              // FIXME: The search failed for some reason.
+              throw new DirectoryException(resultCode, (Message) null);
+            }
+
+            break;
+
+          default:
+            // Check for disconnect notifications.
+            handleUnexpectedResponse(responseMessage);
+            break;
+          }
+        }
+        while (opType != OP_TYPE_SEARCH_RESULT_DONE);
+
+        if (resultCount > 1)
+        {
+          // FIXME: too many matching entries found.
+          throw new DirectoryException(
+              ResultCode.CLIENT_SIDE_MORE_RESULTS_TO_RETURN, (Message) null);
+        }
+
+        if (username == null)
+        {
+          // FIXME: no matching entries found.
+          throw new DirectoryException(
+              ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED, (Message) null);
+        }
+
+        return username;
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public void simpleBind(final ByteString username,
+          final ByteString password) throws DirectoryException
+      {
+        // Create the bind request and send it to the server.
+        final BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
+            username, 3, password);
+        sendRequest(bindRequest);
+
+        // Read the response from the server.
+        final LDAPMessage responseMessage = readResponse();
+        switch (responseMessage.getProtocolOpType())
+        {
+        case OP_TYPE_BIND_RESPONSE:
+          final BindResponseProtocolOp bindResponse = responseMessage
+              .getBindResponseProtocolOp();
+
+          final ResultCode resultCode = ResultCode.valueOf(bindResponse
+              .getResultCode());
+          if (resultCode == ResultCode.SUCCESS)
+          {
+            // FIXME: need to look for things like password expiration
+            // warning, reset notice, etc.
+            return;
+          }
+          else
+          {
+            // The bind failed for some reason.
+            throw new DirectoryException(resultCode,
+                ERR_LDAP_PTA_CONNECTION_BIND_FAILED.get(host, port,
+                    String.valueOf(options.dn()), String.valueOf(username),
+                    resultCode.getIntValue(), resultCode.getResultCodeName(),
+                    bindResponse.getErrorMessage()));
+          }
+
+        default:
+          // Check for disconnect notifications.
+          handleUnexpectedResponse(responseMessage);
+          break;
+        }
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public String toString()
+      {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("LDAPConnection(");
+        builder.append(String.valueOf(ldapSocket.getLocalSocketAddress()));
+        builder.append(", ");
+        builder.append(String.valueOf(ldapSocket.getRemoteSocketAddress()));
+        builder.append(')');
+        return builder.toString();
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      protected void finalize()
+      {
+        close();
+      }
+
+
+
+      private void handleUnexpectedResponse(final LDAPMessage responseMessage)
+          throws DirectoryException
+      {
+        if (responseMessage.getProtocolOpType() == OP_TYPE_EXTENDED_RESPONSE)
+        {
+          final ExtendedResponseProtocolOp extendedResponse = responseMessage
+              .getExtendedResponseProtocolOp();
+          final String responseOID = extendedResponse.getOID();
+
+          if ((responseOID != null)
+              && responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
+          {
+            throw new DirectoryException(ResultCode.valueOf(extendedResponse
+                .getResultCode()), ERR_LDAP_PTA_CONNECTION_DISCONNECTING.get(
+                host, port, String.valueOf(options.dn()),
+                extendedResponse.getErrorMessage()));
+          }
+        }
+
+        // Unexpected response type.
+        throw new DirectoryException(ResultCode.CLIENT_SIDE_DECODING_ERROR,
+            ERR_LDAP_PTA_CONNECTION_WRONG_RESPONSE.get(host, port,
+                String.valueOf(options.dn()),
+                String.valueOf(responseMessage.getProtocolOp())));
+      }
+
+
+
+      // Reads a response message and adapts errors to directory exceptions.
+      private LDAPMessage readResponse() throws DirectoryException
+      {
+        final LDAPMessage responseMessage;
+        try
+        {
+          responseMessage = reader.readMessage();
+        }
+        catch (final ASN1Exception e)
+        {
+          throw new DirectoryException(ResultCode.CLIENT_SIDE_DECODING_ERROR,
+              ERR_LDAP_PTA_CONNECTION_DECODE_ERROR.get(host, port,
+                  String.valueOf(options.dn()), e.getMessage()), e);
+        }
+        catch (final LDAPException e)
+        {
+          throw new DirectoryException(ResultCode.CLIENT_SIDE_DECODING_ERROR,
+              ERR_LDAP_PTA_CONNECTION_DECODE_ERROR.get(host, port,
+                  String.valueOf(options.dn()), e.getMessage()), e);
+        }
+        catch (final SocketTimeoutException e)
+        {
+          throw new DirectoryException(ResultCode.CLIENT_SIDE_TIMEOUT,
+              ERR_LDAP_PTA_CONNECTION_TIMEOUT.get(host, port,
+                  String.valueOf(options.dn())), e);
+        }
+        catch (final IOException e)
+        {
+          throw new DirectoryException(ResultCode.CLIENT_SIDE_SERVER_DOWN,
+              ERR_LDAP_PTA_CONNECTION_OTHER_ERROR.get(host, port,
+                  String.valueOf(options.dn()), e.getMessage()), e);
+        }
+
+        if (responseMessage == null)
+        {
+          throw new DirectoryException(ResultCode.CLIENT_SIDE_SERVER_DOWN,
+              ERR_LDAP_PTA_CONNECTION_CLOSED.get(host, port,
+                  String.valueOf(options.dn())));
+        }
+        return responseMessage;
+      }
+
+
+
+      // Sends a request message and adapts errors to directory exceptions.
+      private void sendRequest(final ProtocolOp request)
+          throws DirectoryException
+      {
+        final LDAPMessage requestMessage = new LDAPMessage(nextMessageID++,
+            request);
+        try
+        {
+          writer.writeMessage(requestMessage);
+        }
+        catch (final IOException e)
+        {
+          throw new DirectoryException(ResultCode.CLIENT_SIDE_SERVER_DOWN,
+              ERR_LDAP_PTA_CONNECTION_OTHER_ERROR.get(host, port,
+                  String.valueOf(options.dn()), e.getMessage()), e);
+        }
+      }
+    }
+
+
+
+    private final String host;
+    private final int port;
+    private final LDAPPassThroughAuthenticationPolicyCfg options;
+
+    private final int timeoutMS;
+
+
+
+    private LDAPConnectionFactory(final String host, final int port,
+        final LDAPPassThroughAuthenticationPolicyCfg options)
+    {
+      this.host = host;
+      this.port = port;
+      this.options = options;
+
+      // Normalize the timeoutMS to an integer (admin framework ensures that the
+      // value is non-negative).
+      this.timeoutMS = (int) Math.min(options.getConnectionTimeout(),
+          Integer.MAX_VALUE);
+    }
+
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Connection getConnection() throws DirectoryException
+    {
+      try
+      {
+        // Create the remote ldapSocket address.
+        final InetAddress address = InetAddress.getByName(host);
+        final InetSocketAddress socketAddress = new InetSocketAddress(address,
+            port);
+
+        // Create the ldapSocket and connect to the remote server.
+        final Socket plainSocket = new Socket();
+        Socket ldapSocket = null;
+        LDAPReader reader = null;
+        LDAPWriter writer = null;
+        LDAPConnection ldapConnection = null;
+
+        try
+        {
+          // Set ldapSocket options before connecting.
+          plainSocket.setTcpNoDelay(options.isUseTCPNoDelay());
+          plainSocket.setKeepAlive(options.isUseTCPKeepAlive());
+          plainSocket.setSoTimeout(timeoutMS);
+
+          // Connect the ldapSocket.
+          plainSocket.connect(socketAddress, timeoutMS);
+
+          if (options.isUseSSL())
+          {
+            // TODO: SSL configuration.
+            ldapSocket = plainSocket;
+          }
+          else
+          {
+            ldapSocket = plainSocket;
+          }
+
+          reader = new LDAPReader(ldapSocket);
+          writer = new LDAPWriter(ldapSocket);
+
+          ldapConnection = new LDAPConnection(plainSocket, ldapSocket, reader,
+              writer);
+
+          return ldapConnection;
+        }
+        finally
+        {
+          if (ldapConnection == null)
+          {
+            // Connection creation failed for some reason, so clean up IO
+            // resources.
+            if (reader != null)
+            {
+              reader.close();
+            }
+            if (writer != null)
+            {
+              writer.close();
+            }
+
+            if (ldapSocket != null)
+            {
+              try
+              {
+                ldapSocket.close();
+              }
+              catch (final IOException ignored)
+              {
+                // Ignore.
+              }
+            }
+
+            if (ldapSocket != plainSocket)
+            {
+              try
+              {
+                plainSocket.close();
+              }
+              catch (final IOException ignored)
+              {
+                // Ignore.
+              }
+            }
+          }
+        }
+      }
+      catch (final UnknownHostException e)
+      {
+        if (debugEnabled())
+        {
+          TRACER.debugCaught(DebugLogLevel.ERROR, e);
+        }
+        throw new DirectoryException(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
+            ERR_LDAP_PTA_CONNECT_UNKNOWN_HOST.get(host, port,
+                String.valueOf(options.dn()), host), e);
+      }
+      catch (final ConnectException e)
+      {
+        if (debugEnabled())
+        {
+          TRACER.debugCaught(DebugLogLevel.ERROR, e);
+        }
+        throw new DirectoryException(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
+            ERR_LDAP_PTA_CONNECT_ERROR.get(host, port,
+                String.valueOf(options.dn()), port), e);
+      }
+      catch (final SocketTimeoutException e)
+      {
+        if (debugEnabled())
+        {
+          TRACER.debugCaught(DebugLogLevel.ERROR, e);
+        }
+        throw new DirectoryException(ResultCode.CLIENT_SIDE_TIMEOUT,
+            ERR_LDAP_PTA_CONNECT_TIMEOUT.get(host, port,
+                String.valueOf(options.dn())), e);
+      }
+      catch (final SSLException e)
+      {
+        if (debugEnabled())
+        {
+          TRACER.debugCaught(DebugLogLevel.ERROR, e);
+        }
+        throw new DirectoryException(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
+            ERR_LDAP_PTA_CONNECT_SSL_ERROR.get(host, port,
+                String.valueOf(options.dn()), e.getMessage()), e);
+      }
+      catch (final IOException e)
+      {
+        if (debugEnabled())
+        {
+          TRACER.debugCaught(DebugLogLevel.ERROR, e);
+        }
+        throw new DirectoryException(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
+            ERR_LDAP_PTA_CONNECT_OTHER_ERROR.get(host, port,
+                String.valueOf(options.dn()), e.getMessage()), e);
+      }
+
+    }
+
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString()
+    {
+      final StringBuilder builder = new StringBuilder();
+      builder.append("LDAPConnectionFactory(");
+      builder.append(host);
+      builder.append(':');
+      builder.append(port);
+      builder.append(')');
+      return builder.toString();
+    }
+  }
+
+
+
+  /**
    * LDAP PTA policy implementation.
    */
   private final class PolicyImpl extends AuthenticationPolicy implements
@@ -202,7 +766,8 @@
         final String password = configuration.getMappedSearchBindPassword();
 
         final Connection connection = factory.getConnection();
-        if (username != null && !username.isNullDN())
+        if (username != null && !username.isNullDN() && password != null
+            && password.length() > 0)
         {
           try
           {
@@ -1101,6 +1666,18 @@
 
 
 
+  // Debug tracer for this class.
+  private static final DebugTracer TRACER = DebugLogger.getTracer();
+
+  // Attribute list for searches requesting no attributes.
+  private static final LinkedHashSet<String> NO_ATTRIBUTES;
+
+  static
+  {
+    NO_ATTRIBUTES = new LinkedHashSet<String>(1);
+    NO_ATTRIBUTES.add("1.1");
+  }
+
   // The provider which should be used by policies to create LDAP connections.
   private final LDAPConnectionFactoryProvider provider;
 
@@ -1115,8 +1692,7 @@
     public ConnectionFactory getLDAPConnectionFactory(final String host,
         final int port, final LDAPPassThroughAuthenticationPolicyCfg options)
     {
-      // TODO: not yet implemented.
-      return null;
+      return new LDAPConnectionFactory(host, port, options);
     }
 
   };
@@ -1142,6 +1718,12 @@
     case OTHER:
     case UNWILLING_TO_PERFORM:
     case OPERATIONS_ERROR:
+    case CLIENT_SIDE_CONNECT_ERROR:
+    case CLIENT_SIDE_DECODING_ERROR:
+    case CLIENT_SIDE_ENCODING_ERROR:
+    case CLIENT_SIDE_LOCAL_ERROR:
+    case CLIENT_SIDE_SERVER_DOWN:
+    case CLIENT_SIDE_TIMEOUT:
       return true;
     default:
       return false;

--
Gitblit v1.10.0