mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Matthew Swift
02.52.2011 910624bfa09294c955280ac53b354598117fa217
OPENDJ-262: Implement pass through authentication (PTA)

Implement LDAP PTA core algorithm + document additional unit test TODO items.

3 files modified
396 ■■■■ changed files
opends/src/messages/messages/extension.properties 16 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java 376 ●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java 4 ●●● patch | view | raw | blame | history
opends/src/messages/messages/extension.properties
@@ -1434,3 +1434,19 @@
MILD_ERR_EXTOP_PWPSTATE_ACCOUNT_NOT_LOCAL_585=The password policy state \
 extended operation is not supported for user %s because the account is not \
 managed locally
MILD_ERR_LDAP_PTA_MAPPING_ATTRIBUTE_NOT_FOUND_586=The user "%s" could not be \
 authenticated using LDAP PTA policy "%s" because the following \
 mapping attributes were not found in the user's entry: %s
MILD_ERR_LDAP_PTA_MAPPED_SEARCH_TOO_MANY_CANDIDATES_587=The user "%s" could \
 not be authenticated using LDAP PTA policy "%s" because the search of base \
 DN "%s" returned more than one entry matching the filter "%s"
MILD_ERR_LDAP_PTA_MAPPED_SEARCH_NO_CANDIDATES_588=The user "%s" could \
 not be authenticated using LDAP PTA policy "%s" because the search did not \
 return any entries matching the filter "%s"
MILD_ERR_LDAP_PTA_MAPPED_SEARCH_FAILED_589=The user "%s" could \
 not be authenticated using LDAP PTA policy "%s" because the search failed \
 unexpectedly for the following reason: %s
MILD_ERR_LDAP_PTA_MAPPED_BIND_FAILED_590=The user "%s" could \
 not be authenticated using LDAP PTA policy "%s" because the bind failed \
 unexpectedly for the following reason: %s
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);
    }
  }
opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
@@ -64,7 +64,9 @@
{
  // TODO: multiple search base DNs
  // TODO: multiple match attributes/values
  // TODO: multiple mapping attributes/values
  // TODO: searches returning no results or too many
  // TODO: missing mapping attributes
  // TODO: connection pooling
  // TODO: load balancing
  // TODO: fail-over