From 910624bfa09294c955280ac53b354598117fa217 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Fri, 02 Sep 2011 19:52:24 +0000
Subject: [PATCH] OPENDJ-262: Implement pass through authentication (PTA)

---
 opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java |  376 ++++++++++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 327 insertions(+), 49 deletions(-)

diff --git a/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java b/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
index a84ea63..106cf5d 100644
--- a/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
+++ b/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
@@ -29,18 +29,25 @@
 
 
 
+import static org.opends.messages.ExtensionMessages.*;
+
 import java.io.Closeable;
+import java.util.LinkedList;
 import java.util.List;
+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;
+    LDAPPassThroughAuthenticationPolicyCfg;
 import org.opends.server.api.AuthenticationPolicy;
 import org.opends.server.api.AuthenticationPolicyFactory;
 import org.opends.server.api.AuthenticationPolicyState;
 import org.opends.server.config.ConfigException;
 import org.opends.server.types.*;
+import org.opends.server.util.StaticUtils;
 
 
 
@@ -69,6 +76,8 @@
     /**
      * 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.
      *
      * @param baseDN
      *          The search base DN.
@@ -77,10 +86,10 @@
      * @param filter
      *          The search filter.
      * @return The name of the user whose entry matches the provided search
-     *         criteria, or {@code null} if no matching user entry was found.
+     *         criteria.
      * @throws DirectoryException
-     *           If the search returned more than one entry, or if the search
-     *           failed unexpectedly.
+     *           If the search returned no entries, more than one entry, or if
+     *           the search failed unexpectedly.
      */
     ByteString search(DN baseDN, SearchScope scope, SearchFilter filter)
         throws DirectoryException;
@@ -153,19 +162,284 @@
   /**
    * LDAP PTA policy implementation.
    */
-  private static final class PolicyImpl extends AuthenticationPolicy implements
+  private final class PolicyImpl extends AuthenticationPolicy implements
       ConfigurationChangeListener<LDAPPassThroughAuthenticationPolicyCfg>
   {
+    /**
+     * LDAP PTA policy state implementation.
+     */
+    private final class StateImpl extends AuthenticationPolicyState
+    {
+
+      private final Entry userEntry;
+      private ByteString cachedPassword = null;
+
+
+
+      private StateImpl(final Entry userEntry)
+      {
+        this.userEntry = userEntry;
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public void finalizeStateAfterBind() throws DirectoryException
+      {
+        if (cachedPassword != null)
+        {
+          // TODO: persist cached password if needed.
+          cachedPassword = null;
+        }
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public AuthenticationPolicy getAuthenticationPolicy()
+      {
+        return PolicyImpl.this;
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public boolean passwordMatches(final ByteString password)
+          throws DirectoryException
+      {
+        sharedLock.lock();
+        try
+        {
+          // First of determine the user name to use when binding to the remote
+          // directory.
+          ByteString username = null;
+
+          switch (configuration.getMappingPolicy())
+          {
+          case UNMAPPED:
+            // The bind DN is the name of the user's entry.
+            username = ByteString.valueOf(userEntry.getDN().toString());
+            break;
+          case MAPPED_BIND:
+            // The bind DN is contained in an attribute in the user's entry.
+            mapBind: for (final AttributeType at : configuration
+                .getMappedAttribute())
+            {
+              final List<Attribute> attributes = userEntry.getAttribute(at);
+              if (attributes != null && !attributes.isEmpty())
+              {
+                for (final Attribute attribute : attributes)
+                {
+                  if (!attribute.isEmpty())
+                  {
+                    username = attribute.iterator().next().getValue();
+                    break mapBind;
+                  }
+                }
+              }
+            }
+
+            if (username == null)
+            {
+              /*
+               * The mapping attribute(s) is not present in the entry. This
+               * could be a configuration error, but it could also be because
+               * someone is attempting to authenticate using a bind DN which
+               * references a non-user entry.
+               */
+              throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
+                  ERR_LDAP_PTA_MAPPING_ATTRIBUTE_NOT_FOUND.get(
+                      String.valueOf(userEntry.getDN()),
+                      String.valueOf(configuration.dn()),
+                      StaticUtils.collectionToString(
+                          configuration.getMappedAttribute(), ", ")));
+            }
+
+            break;
+          case MAPPED_SEARCH:
+            // A search against the remote directory is required in order to
+            // determine the bind DN.
+
+            // Construct the search filter.
+            final LinkedList<SearchFilter> filterComponents =
+              new LinkedList<SearchFilter>();
+            for (final AttributeType at : configuration.getMappedAttribute())
+            {
+              final List<Attribute> attributes = userEntry.getAttribute(at);
+              if (attributes != null && !attributes.isEmpty())
+              {
+                for (final Attribute attribute : attributes)
+                {
+                  for (final AttributeValue value : attribute)
+                  {
+                    filterComponents.add(SearchFilter.createEqualityFilter(at,
+                        value));
+                  }
+                }
+              }
+            }
+
+            if (filterComponents.isEmpty())
+            {
+              /*
+               * The mapping attribute(s) is not present in the entry. This
+               * could be a configuration error, but it could also be because
+               * someone is attempting to authenticate using a bind DN which
+               * references a non-user entry.
+               */
+              throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
+                  ERR_LDAP_PTA_MAPPING_ATTRIBUTE_NOT_FOUND.get(
+                      String.valueOf(userEntry.getDN()),
+                      String.valueOf(configuration.dn()),
+                      StaticUtils.collectionToString(
+                          configuration.getMappedAttribute(), ", ")));
+            }
+
+            final SearchFilter filter;
+            if (filterComponents.size() == 1)
+            {
+              filter = filterComponents.getFirst();
+            }
+            else
+            {
+              filter = SearchFilter.createORFilter(filterComponents);
+            }
+
+            // Now search the configured base DNs, stopping at the first
+            // success.
+            for (final DN baseDN : configuration.getMappedSearchBaseDN())
+            {
+              Connection connection = null;
+              try
+              {
+                connection = searchFactory.getConnection();
+                username = connection.search(baseDN, SearchScope.WHOLE_SUBTREE,
+                    filter);
+              }
+              catch (final DirectoryException e)
+              {
+                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.
+                  break;
+                case CLIENT_SIDE_MORE_RESULTS_TO_RETURN:
+                  // More than one matching entry was returned.
+                  throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
+                      ERR_LDAP_PTA_MAPPED_SEARCH_TOO_MANY_CANDIDATES.get(
+                          String.valueOf(userEntry.getDN()),
+                          String.valueOf(configuration.dn()),
+                          String.valueOf(baseDN), String.valueOf(filter)));
+                default:
+                  // We don't want to propagate this internal error to the
+                  // client. We should log it and map it to a more appropriate
+                  // error.
+                  throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
+                      ERR_LDAP_PTA_MAPPED_SEARCH_FAILED.get(
+                          String.valueOf(userEntry.getDN()),
+                          String.valueOf(configuration.dn()),
+                          e.getMessageObject()), e);
+                }
+              }
+              finally
+              {
+                if (connection != null)
+                {
+                  connection.close();
+                }
+              }
+            }
+
+            if (username == null)
+            {
+              /*
+               * No matching entries were found in the remote directory.
+               */
+              throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
+                  ERR_LDAP_PTA_MAPPED_SEARCH_NO_CANDIDATES.get(
+                      String.valueOf(userEntry.getDN()),
+                      String.valueOf(configuration.dn()),
+                      String.valueOf(filter)));
+            }
+
+            break;
+          }
+
+          // Now perform the bind.
+          Connection connection = null;
+          try
+          {
+            connection = bindFactory.getConnection();
+            connection.simpleBind(username, password);
+            return true;
+          }
+          catch (final DirectoryException e)
+          {
+            switch (e.getResultCode())
+            {
+            // FIXME: specify possible result codes.
+            case NO_SUCH_OBJECT:
+            case INVALID_CREDENTIALS:
+              return false;
+            default:
+              // We don't want to propagate this internal error to the
+              // client. We should log it and map it to a more appropriate
+              // error.
+              throw new DirectoryException(
+                  ResultCode.INVALID_CREDENTIALS,
+                  ERR_LDAP_PTA_MAPPED_BIND_FAILED.get(
+                      String.valueOf(userEntry.getDN()),
+                      String.valueOf(configuration.dn()), e.getMessageObject()),
+                  e);
+            }
+          }
+          finally
+          {
+            if (connection != null)
+            {
+              connection.close();
+            }
+          }
+        }
+        finally
+        {
+          sharedLock.unlock();
+        }
+      }
+    }
+
+
+
+    // Guards against configuration changes.
+    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+    private final ReadLock sharedLock = lock.readLock();
+    private final WriteLock exclusiveLock = lock.writeLock();
 
     // Current configuration.
     private LDAPPassThroughAuthenticationPolicyCfg configuration;
 
+    // FIXME: initialize connection factories.
+    private ConnectionFactory searchFactory = null;
+    private ConnectionFactory bindFactory = null;
+
 
 
     private PolicyImpl(
         final LDAPPassThroughAuthenticationPolicyCfg configuration)
     {
-      this.configuration = configuration;
+      initializeConfiguration(configuration);
     }
 
 
@@ -177,8 +451,16 @@
     public ConfigChangeResult applyConfigurationChange(
         final LDAPPassThroughAuthenticationPolicyCfg configuration)
     {
-      // TODO: close and re-open connections if servers have changed.
-      this.configuration = configuration;
+      exclusiveLock.lock();
+      try
+      {
+        closeConnections();
+        initializeConfiguration(configuration);
+      }
+      finally
+      {
+        exclusiveLock.unlock();
+      }
       return new ConfigChangeResult(ResultCode.SUCCESS, false);
     }
 
@@ -191,7 +473,8 @@
     public AuthenticationPolicyState createAuthenticationPolicyState(
         final Entry userEntry, final long time) throws DirectoryException
     {
-      return new StateImpl(this);
+      // The current time is not needed for LDAP PTA.
+      return new StateImpl(userEntry);
     }
 
 
@@ -202,7 +485,16 @@
     @Override
     public void finalizeAuthenticationPolicy()
     {
-      // TODO: release pooled connections, etc.
+      exclusiveLock.lock();
+      try
+      {
+        configuration.removeLDAPPassThroughChangeListener(this);
+        closeConnections();
+      }
+      finally
+      {
+        exclusiveLock.unlock();
+      }
     }
 
 
@@ -230,58 +522,44 @@
       return true;
     }
 
-  }
 
 
-
-  /**
-   * LDAP PTA policy state implementation.
-   */
-  private static final class StateImpl extends AuthenticationPolicyState
-  {
-
-    private final PolicyImpl policy;
-
-
-
-    private StateImpl(final PolicyImpl policy)
+    private void closeConnections()
     {
-      this.policy = policy;
+      exclusiveLock.lock();
+      try
+      {
+        // TODO: close all connections.
+      }
+      finally
+      {
+        exclusiveLock.unlock();
+      }
     }
 
 
 
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void finalizeStateAfterBind() throws DirectoryException
+    private void initializeConfiguration(
+        final LDAPPassThroughAuthenticationPolicyCfg configuration)
     {
-      // TODO: cache password if needed.
+      this.configuration = configuration;
+
+      // TODO: implement FO/LB/CP + authenticated search factory.
+      final String hostPort = configuration.getPrimaryRemoteLDAPServer()
+          .first();
+      searchFactory = newLDAPConnectionFactory(hostPort);
+      bindFactory = newLDAPConnectionFactory(hostPort);
     }
 
 
 
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public AuthenticationPolicy getAuthenticationPolicy()
+    private ConnectionFactory newLDAPConnectionFactory(final String hostPort)
     {
-      return policy;
-    }
-
-
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public boolean passwordMatches(final ByteString password)
-        throws DirectoryException
-    {
-      // TODO: perform PTA here.
-      return false;
+      // Validation already performed by admin framework.
+      final int colonIndex = hostPort.lastIndexOf(":");
+      final String hostname = hostPort.substring(0, colonIndex);
+      final int port = Integer.parseInt(hostPort.substring(colonIndex + 1));
+      return provider.getLDAPConnectionFactory(hostname, port, configuration);
     }
 
   }

--
Gitblit v1.10.0