001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Portions Copyright 2011-2016 ForgeRock AS.
015 */
016package org.opends.server.extensions;
017
018import java.io.BufferedReader;
019import java.io.Closeable;
020import java.io.File;
021import java.io.FileReader;
022import java.io.IOException;
023import java.net.ConnectException;
024import java.net.InetAddress;
025import java.net.InetSocketAddress;
026import java.net.Socket;
027import java.net.SocketTimeoutException;
028import java.net.UnknownHostException;
029import java.util.ArrayList;
030import java.util.Collection;
031import java.util.Iterator;
032import java.util.LinkedHashSet;
033import java.util.LinkedList;
034import java.util.List;
035import java.util.Queue;
036import java.util.Set;
037import java.util.IllegalFormatConversionException;
038import java.util.MissingFormatArgumentException;
039import java.util.concurrent.ConcurrentLinkedQueue;
040import java.util.concurrent.Executors;
041import java.util.concurrent.ScheduledExecutorService;
042import java.util.concurrent.ScheduledFuture;
043import java.util.concurrent.Semaphore;
044import java.util.concurrent.ThreadFactory;
045import java.util.concurrent.TimeUnit;
046import java.util.concurrent.atomic.AtomicInteger;
047import java.util.concurrent.locks.ReentrantReadWriteLock;
048import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
049import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
050
051import javax.net.ssl.SSLContext;
052import javax.net.ssl.SSLException;
053import javax.net.ssl.SSLSocket;
054import javax.net.ssl.SSLSocketFactory;
055import javax.net.ssl.TrustManager;
056
057import org.forgerock.i18n.LocalizableMessage;
058import org.forgerock.i18n.LocalizedIllegalArgumentException;
059import org.forgerock.i18n.slf4j.LocalizedLogger;
060import org.forgerock.opendj.config.server.ConfigChangeResult;
061import org.forgerock.opendj.config.server.ConfigException;
062import org.forgerock.opendj.config.server.ConfigurationChangeListener;
063import org.forgerock.opendj.ldap.ByteString;
064import org.forgerock.opendj.ldap.DN;
065import org.forgerock.opendj.ldap.DecodeException;
066import org.forgerock.opendj.ldap.DereferenceAliasesPolicy;
067import org.forgerock.opendj.ldap.GeneralizedTime;
068import org.forgerock.opendj.ldap.ModificationType;
069import org.forgerock.opendj.ldap.ResultCode;
070import org.forgerock.opendj.ldap.SearchScope;
071import org.forgerock.opendj.ldap.Filter;
072import org.forgerock.opendj.ldap.schema.AttributeType;
073import org.forgerock.opendj.server.config.meta.LDAPPassThroughAuthenticationPolicyCfgDefn.MappingPolicy;
074import org.forgerock.opendj.server.config.server.LDAPPassThroughAuthenticationPolicyCfg;
075import org.opends.server.api.AuthenticationPolicy;
076import org.opends.server.api.AuthenticationPolicyFactory;
077import org.opends.server.api.AuthenticationPolicyState;
078import org.opends.server.api.DirectoryThread;
079import org.opends.server.api.PasswordStorageScheme;
080import org.opends.server.api.TrustManagerProvider;
081import org.opends.server.core.DirectoryServer;
082import org.opends.server.core.ModifyOperation;
083import org.opends.server.core.ServerContext;
084import org.opends.server.protocols.ldap.BindRequestProtocolOp;
085import org.opends.server.protocols.ldap.BindResponseProtocolOp;
086import org.opends.server.protocols.ldap.ExtendedResponseProtocolOp;
087import org.opends.server.protocols.ldap.LDAPMessage;
088import org.opends.server.protocols.ldap.ProtocolOp;
089import org.opends.server.protocols.ldap.SearchRequestProtocolOp;
090import org.opends.server.protocols.ldap.SearchResultDoneProtocolOp;
091import org.opends.server.protocols.ldap.SearchResultEntryProtocolOp;
092import org.opends.server.protocols.ldap.UnbindRequestProtocolOp;
093import org.opends.server.schema.SchemaConstants;
094import org.opends.server.schema.UserPasswordSyntax;
095import org.opends.server.tools.LDAPReader;
096import org.opends.server.tools.LDAPWriter;
097import org.opends.server.types.Attribute;
098import org.opends.server.types.DirectoryException;
099import org.opends.server.types.Entry;
100import org.opends.server.types.HostPort;
101import org.opends.server.types.InitializationException;
102import org.opends.server.types.LDAPException;
103import org.opends.server.types.RawFilter;
104import org.opends.server.types.RawModification;
105import org.opends.server.types.SearchFilter;
106import org.opends.server.util.StaticUtils;
107import org.opends.server.util.TimeThread;
108
109import static org.opends.messages.ExtensionMessages.*;
110import static org.opends.server.config.ConfigConstants.*;
111import static org.opends.server.core.DirectoryServer.*;
112import static org.opends.server.protocols.internal.InternalClientConnection.*;
113import static org.opends.server.protocols.ldap.LDAPConstants.*;
114import static org.opends.server.util.StaticUtils.*;
115
116/** LDAP pass through authentication policy implementation. */
117public final class LDAPPassThroughAuthenticationPolicyFactory implements
118    AuthenticationPolicyFactory<LDAPPassThroughAuthenticationPolicyCfg>
119{
120  // TODO: handle password policy response controls? AD?
121  // TODO: custom aliveness pings
122  // TODO: improve debug logging and error messages.
123
124  /**
125   * A simplistic load-balancer connection factory implementation using
126   * approximately round-robin balancing.
127   */
128  static abstract class AbstractLoadBalancer implements ConnectionFactory,
129      Runnable
130  {
131    /** A connection which automatically retries operations on other servers. */
132    private final class FailoverConnection implements Connection
133    {
134      private Connection connection;
135      private MonitoredConnectionFactory factory;
136      private final int startIndex;
137      private int nextIndex;
138
139      private FailoverConnection(final int startIndex)
140          throws DirectoryException
141      {
142        this.startIndex = nextIndex = startIndex;
143
144        DirectoryException lastException;
145        do
146        {
147          factory = factories[nextIndex];
148          if (factory.isAvailable)
149          {
150            try
151            {
152              connection = factory.getConnection();
153              incrementNextIndex();
154              return;
155            }
156            catch (final DirectoryException e)
157            {
158              // Ignore this error and try the next factory.
159              logger.traceException(e);
160              lastException = e;
161            }
162          }
163          else
164          {
165            lastException = factory.lastException;
166          }
167          incrementNextIndex();
168        }
169        while (nextIndex != startIndex);
170
171        // All the factories have been tried so give up and throw the exception.
172        throw lastException;
173      }
174
175      @Override
176      public void close()
177      {
178        connection.close();
179      }
180
181      @Override
182      public ByteString search(final DN baseDN, final SearchScope scope,
183          final SearchFilter filter) throws DirectoryException
184      {
185        for (;;)
186        {
187          try
188          {
189            return connection.search(baseDN, scope, filter);
190          }
191          catch (final DirectoryException e)
192          {
193            logger.traceException(e);
194            handleDirectoryException(e);
195          }
196        }
197      }
198
199      @Override
200      public void simpleBind(final ByteString username,
201          final ByteString password) throws DirectoryException
202      {
203        for (;;)
204        {
205          try
206          {
207            connection.simpleBind(username, password);
208            return;
209          }
210          catch (final DirectoryException e)
211          {
212            logger.traceException(e);
213            handleDirectoryException(e);
214          }
215        }
216      }
217
218      private void handleDirectoryException(final DirectoryException e)
219          throws DirectoryException
220      {
221        // If the error does not indicate that the connection has failed, then
222        // pass this back to the caller.
223        if (!isServiceError(e.getResultCode()))
224        {
225          throw e;
226        }
227
228        // The associated server is unavailable, so close the connection and
229        // try the next connection factory.
230        connection.close();
231        factory.lastException = e;
232        factory.isAvailable = false; // publishes lastException
233
234        while (nextIndex != startIndex)
235        {
236          factory = factories[nextIndex];
237          if (factory.isAvailable)
238          {
239            try
240            {
241              connection = factory.getConnection();
242              incrementNextIndex();
243              return;
244            }
245            catch (final DirectoryException de)
246            {
247              // Ignore this error and try the next factory.
248              logger.traceException(de);
249            }
250          }
251          incrementNextIndex();
252        }
253
254        // All the factories have been tried so give up and throw the exception.
255        throw e;
256      }
257
258      private void incrementNextIndex()
259      {
260        // Try the next index.
261        if (++nextIndex == maxIndex)
262        {
263          nextIndex = 0;
264        }
265      }
266    }
267
268    /**
269     * A connection factory which caches its online/offline state in order to
270     * avoid unnecessary connection attempts when it is known to be offline.
271     */
272    private final class MonitoredConnectionFactory implements ConnectionFactory
273    {
274      private final ConnectionFactory factory;
275
276      /** IsAvailable acts as memory barrier for lastException. */
277      private volatile boolean isAvailable = true;
278      private DirectoryException lastException;
279
280      private MonitoredConnectionFactory(final ConnectionFactory factory)
281      {
282        this.factory = factory;
283      }
284
285      @Override
286      public void close()
287      {
288        factory.close();
289      }
290
291      @Override
292      public Connection getConnection() throws DirectoryException
293      {
294        try
295        {
296          final Connection connection = factory.getConnection();
297          isAvailable = true;
298          return connection;
299        }
300        catch (final DirectoryException e)
301        {
302          logger.traceException(e);
303          lastException = e;
304          isAvailable = false; // publishes lastException
305          throw e;
306        }
307      }
308    }
309
310    private final MonitoredConnectionFactory[] factories;
311    private final int maxIndex;
312    private final ScheduledFuture<?> monitorFuture;
313
314    /**
315     * Creates a new abstract load-balancer.
316     *
317     * @param factories
318     *          The list of underlying connection factories.
319     * @param scheduler
320     *          The monitoring scheduler.
321     */
322    AbstractLoadBalancer(final ConnectionFactory[] factories,
323        final ScheduledExecutorService scheduler)
324    {
325      this.factories = new MonitoredConnectionFactory[factories.length];
326      this.maxIndex = factories.length;
327
328      for (int i = 0; i < maxIndex; i++)
329      {
330        this.factories[i] = new MonitoredConnectionFactory(factories[i]);
331      }
332
333      this.monitorFuture = scheduler.scheduleWithFixedDelay(this, 5, 5,
334          TimeUnit.SECONDS);
335    }
336
337    /** Close underlying connection pools. */
338    @Override
339    public final void close()
340    {
341      monitorFuture.cancel(true);
342
343      for (final ConnectionFactory factory : factories)
344      {
345        factory.close();
346      }
347    }
348
349    @Override
350    public final Connection getConnection() throws DirectoryException
351    {
352      final int startIndex = getStartIndex();
353      return new FailoverConnection(startIndex);
354    }
355
356    /** Try to connect to any offline connection factories. */
357    @Override
358    public void run()
359    {
360      for (final MonitoredConnectionFactory factory : factories)
361      {
362        if (!factory.isAvailable)
363        {
364          try
365          {
366            factory.getConnection().close();
367          }
368          catch (final DirectoryException e)
369          {
370            logger.traceException(e);
371          }
372        }
373      }
374    }
375
376    /**
377     * Return the start which should be used for the next connection attempt.
378     *
379     * @return The start which should be used for the next connection attempt.
380     */
381    abstract int getStartIndex();
382  }
383
384  /**
385   * A factory which returns pre-authenticated connections for searches.
386   * <p>
387   * Package private for testing.
388   */
389  static final class AuthenticatedConnectionFactory implements
390      ConnectionFactory
391  {
392    private final ConnectionFactory factory;
393    private final DN username;
394    private final String password;
395
396    /**
397     * Creates a new authenticated connection factory which will bind on
398     * connect.
399     *
400     * @param factory
401     *          The underlying connection factory whose connections are to be
402     *          authenticated.
403     * @param username
404     *          The username taken from the configuration.
405     * @param password
406     *          The password taken from the configuration.
407     */
408    AuthenticatedConnectionFactory(final ConnectionFactory factory,
409        final DN username, final String password)
410    {
411      this.factory = factory;
412      this.username = username;
413      this.password = password;
414    }
415
416    @Override
417    public void close()
418    {
419      factory.close();
420    }
421
422    @Override
423    public Connection getConnection() throws DirectoryException
424    {
425      final Connection connection = factory.getConnection();
426      if (username != null && !username.isRootDN() && password != null
427          && password.length() > 0)
428      {
429        try
430        {
431          connection.simpleBind(ByteString.valueOfUtf8(username.toString()),
432              ByteString.valueOfUtf8(password));
433        }
434        catch (final DirectoryException e)
435        {
436          connection.close();
437          throw e;
438        }
439      }
440      return connection;
441    }
442  }
443
444  /** An LDAP connection which will be used in order to search for or authenticate users. */
445  static interface Connection extends Closeable
446  {
447    /** Closes this connection. */
448    @Override
449    void close();
450
451    /**
452     * Returns the name of the user whose entry matches the provided search
453     * criteria. This will return CLIENT_SIDE_NO_RESULTS_RETURNED/NO_SUCH_OBJECT
454     * if no search results were returned, or CLIENT_SIDE_MORE_RESULTS_TO_RETURN
455     * if too many results were returned.
456     *
457     * @param baseDN
458     *          The search base DN.
459     * @param scope
460     *          The search scope.
461     * @param filter
462     *          The search filter.
463     * @return The name of the user whose entry matches the provided search
464     *         criteria.
465     * @throws DirectoryException
466     *           If the search returned no entries, more than one entry, or if
467     *           the search failed unexpectedly.
468     */
469    ByteString search(DN baseDN, SearchScope scope, SearchFilter filter)
470        throws DirectoryException;
471
472    /**
473     * Performs a simple bind for the user.
474     *
475     * @param username
476     *          The user name (usually a bind DN).
477     * @param password
478     *          The user's password.
479     * @throws DirectoryException
480     *           If the credentials were invalid, or the authentication failed
481     *           unexpectedly.
482     */
483    void simpleBind(ByteString username, ByteString password)
484        throws DirectoryException;
485  }
486
487  /**
488   * An interface for obtaining connections: users of this interface will obtain
489   * a connection, perform a single operation (search or bind), and then close
490   * it.
491   */
492  static interface ConnectionFactory extends Closeable
493  {
494    /**
495     * {@inheritDoc}
496     * <p>
497     * Must never throw an exception.
498     */
499    @Override
500    void close();
501
502    /**
503     * Returns a connection which can be used in order to search for or
504     * authenticate users.
505     *
506     * @return The connection.
507     * @throws DirectoryException
508     *           If an unexpected error occurred while attempting to obtain a
509     *           connection.
510     */
511    Connection getConnection() throws DirectoryException;
512  }
513
514
515
516  /**
517   * PTA connection pool.
518   * <p>
519   * Package private for testing.
520   */
521  static final class ConnectionPool implements ConnectionFactory
522  {
523    /** Pooled connection's intercept close and release connection back to the pool. */
524    private final class PooledConnection implements Connection
525    {
526      private Connection connection;
527      private boolean connectionIsClosed;
528
529      private PooledConnection(final Connection connection)
530      {
531        this.connection = connection;
532      }
533
534      @Override
535      public void close()
536      {
537        if (!connectionIsClosed)
538        {
539          connectionIsClosed = true;
540
541          // Guarded by PolicyImpl
542          if (poolIsClosed)
543          {
544            connection.close();
545          }
546          else
547          {
548            connectionPool.offer(connection);
549          }
550
551          connection = null;
552          availableConnections.release();
553        }
554      }
555
556      @Override
557      public ByteString search(final DN baseDN, final SearchScope scope,
558          final SearchFilter filter) throws DirectoryException
559      {
560        try
561        {
562          return connection.search(baseDN, scope, filter);
563        }
564        catch (final DirectoryException e1)
565        {
566          // Fail immediately if the result indicates that the operation failed
567          // for a reason other than connection/server failure.
568          reconnectIfConnectionFailure(e1);
569
570          // The connection has failed, so retry the operation using the new
571          // connection.
572          try
573          {
574            return connection.search(baseDN, scope, filter);
575          }
576          catch (final DirectoryException e2)
577          {
578            // If the connection has failed again then give up: don't put the
579            // connection back in the pool.
580            closeIfConnectionFailure(e2);
581            throw e2;
582          }
583        }
584      }
585
586      @Override
587      public void simpleBind(final ByteString username,
588          final ByteString password) throws DirectoryException
589      {
590        try
591        {
592          connection.simpleBind(username, password);
593        }
594        catch (final DirectoryException e1)
595        {
596          // Fail immediately if the result indicates that the operation failed
597          // for a reason other than connection/server failure.
598          reconnectIfConnectionFailure(e1);
599
600          // The connection has failed, so retry the operation using the new
601          // connection.
602          try
603          {
604            connection.simpleBind(username, password);
605          }
606          catch (final DirectoryException e2)
607          {
608            // If the connection has failed again then give up: don't put the
609            // connection back in the pool.
610            closeIfConnectionFailure(e2);
611            throw e2;
612          }
613        }
614      }
615
616      private void closeIfConnectionFailure(final DirectoryException e)
617          throws DirectoryException
618      {
619        if (isServiceError(e.getResultCode()))
620        {
621          connectionIsClosed = true;
622          connection.close();
623          connection = null;
624          availableConnections.release();
625        }
626      }
627
628      private void reconnectIfConnectionFailure(final DirectoryException e)
629          throws DirectoryException
630      {
631        if (!isServiceError(e.getResultCode()))
632        {
633          throw e;
634        }
635
636        // The connection has failed (e.g. idle timeout), so repeat the
637        // request on a new connection.
638        connection.close();
639        try
640        {
641          connection = factory.getConnection();
642        }
643        catch (final DirectoryException e2)
644        {
645          // Give up - the server is unreachable.
646          connectionIsClosed = true;
647          connection = null;
648          availableConnections.release();
649          throw e2;
650        }
651      }
652    }
653
654    /** Guarded by PolicyImpl.lock. */
655    private boolean poolIsClosed;
656
657    private final ConnectionFactory factory;
658    private final int poolSize = Runtime.getRuntime().availableProcessors() * 2;
659    private final Semaphore availableConnections = new Semaphore(poolSize);
660    private final Queue<Connection> connectionPool = new ConcurrentLinkedQueue<>();
661
662    /**
663     * Creates a new connection pool for the provided factory.
664     *
665     * @param factory
666     *          The underlying connection factory whose connections are to be
667     *          pooled.
668     */
669    ConnectionPool(final ConnectionFactory factory)
670    {
671      this.factory = factory;
672    }
673
674    /** Release all connections: do we want to block? */
675    @Override
676    public void close()
677    {
678      // No need for synchronization as this can only be called with the
679      // policy's exclusive lock.
680      poolIsClosed = true;
681
682      Connection connection;
683      while ((connection = connectionPool.poll()) != null)
684      {
685        connection.close();
686      }
687
688      factory.close();
689
690      // Since we have the exclusive lock, there should be no more connections
691      // in use.
692      if (availableConnections.availablePermits() != poolSize)
693      {
694        throw new IllegalStateException(
695            "Pool has remaining connections open after close");
696      }
697    }
698
699    @Override
700    public Connection getConnection() throws DirectoryException
701    {
702      // This should only be called with the policy's shared lock.
703      if (poolIsClosed)
704      {
705        throw new IllegalStateException("pool is closed");
706      }
707
708      availableConnections.acquireUninterruptibly();
709
710      // There is either a pooled connection or we are allowed to create
711      // one.
712      Connection connection = connectionPool.poll();
713      if (connection == null)
714      {
715        try
716        {
717          connection = factory.getConnection();
718        }
719        catch (final DirectoryException e)
720        {
721          availableConnections.release();
722          throw e;
723        }
724      }
725
726      return new PooledConnection(connection);
727    }
728  }
729
730  /**
731   * A simplistic two-way fail-over connection factory implementation.
732   * <p>
733   * Package private for testing.
734   */
735  static final class FailoverLoadBalancer extends AbstractLoadBalancer
736  {
737    /**
738     * Creates a new fail-over connection factory which will always try the
739     * primary connection factory first, before trying the second.
740     *
741     * @param primary
742     *          The primary connection factory.
743     * @param secondary
744     *          The secondary connection factory.
745     * @param scheduler
746     *          The monitoring scheduler.
747     */
748    FailoverLoadBalancer(final ConnectionFactory primary,
749        final ConnectionFactory secondary,
750        final ScheduledExecutorService scheduler)
751    {
752      super(new ConnectionFactory[] { primary, secondary }, scheduler);
753    }
754
755    @Override
756    int getStartIndex()
757    {
758      // Always start with the primaries.
759      return 0;
760    }
761  }
762
763  /**
764   * The PTA design guarantees that connections are only used by a single thread
765   * at a time, so we do not need to perform any synchronization.
766   * <p>
767   * Package private for testing.
768   */
769  static final class LDAPConnectionFactory implements ConnectionFactory
770  {
771    /** LDAP connection implementation. */
772    private final class LDAPConnection implements Connection
773    {
774      private final Socket plainSocket;
775      private final Socket ldapSocket;
776      private final LDAPWriter writer;
777      private final LDAPReader reader;
778      private int nextMessageID = 1;
779      private boolean isClosed;
780
781      private LDAPConnection(final Socket plainSocket, final Socket ldapSocket,
782          final LDAPReader reader, final LDAPWriter writer)
783      {
784        this.plainSocket = plainSocket;
785        this.ldapSocket = ldapSocket;
786        this.reader = reader;
787        this.writer = writer;
788      }
789
790      @Override
791      public void close()
792      {
793        /*
794         * This method is intentionally a bit "belt and braces" because we have
795         * seen far too many subtle resource leaks due to bugs within JDK,
796         * especially when used in conjunction with SSL (e.g.
797         * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7025227).
798         */
799        if (isClosed)
800        {
801          return;
802        }
803        isClosed = true;
804
805        // Send an unbind request.
806        final LDAPMessage message = new LDAPMessage(nextMessageID++,
807            new UnbindRequestProtocolOp());
808        try
809        {
810          writer.writeMessage(message);
811        }
812        catch (final IOException e)
813        {
814          logger.traceException(e);
815        }
816
817        // Close all IO resources.
818        StaticUtils.close(writer, reader);
819        StaticUtils.close(ldapSocket, plainSocket);
820      }
821
822      @Override
823      public ByteString search(final DN baseDN, final SearchScope scope,
824          final SearchFilter filter) throws DirectoryException
825      {
826        // Create the search request and send it to the server.
827        final SearchRequestProtocolOp searchRequest =
828          new SearchRequestProtocolOp(
829            ByteString.valueOfUtf8(baseDN.toString()), scope,
830            DereferenceAliasesPolicy.ALWAYS, 1 /* size limit */,
831            (timeoutMS / 1000), true /* types only */,
832            RawFilter.create(filter), NO_ATTRIBUTES);
833        sendRequest(searchRequest);
834
835        // Read the responses from the server. We cannot fail-fast since this
836        // could leave unread search response messages.
837        byte opType;
838        ByteString username = null;
839        int resultCount = 0;
840
841        do
842        {
843          final LDAPMessage responseMessage = readResponse();
844          opType = responseMessage.getProtocolOpType();
845
846          switch (opType)
847          {
848          case OP_TYPE_SEARCH_RESULT_ENTRY:
849            final SearchResultEntryProtocolOp searchEntry = responseMessage
850                .getSearchResultEntryProtocolOp();
851            if (username == null)
852            {
853              username = ByteString.valueOfUtf8(searchEntry.getDN().toString());
854            }
855            resultCount++;
856            break;
857
858          case OP_TYPE_SEARCH_RESULT_REFERENCE:
859            // The reference does not necessarily mean that there would have
860            // been any matching results, so lets ignore it.
861            break;
862
863          case OP_TYPE_SEARCH_RESULT_DONE:
864            final SearchResultDoneProtocolOp searchResult = responseMessage
865                .getSearchResultDoneProtocolOp();
866
867            final ResultCode resultCode = ResultCode.valueOf(searchResult
868                .getResultCode());
869            switch (resultCode.asEnum())
870            {
871            case SUCCESS:
872              // The search succeeded. Drop out of the loop and check that we
873              // got a matching entry.
874              break;
875
876            case SIZE_LIMIT_EXCEEDED:
877              // Multiple matching candidates.
878              throw new DirectoryException(
879                  ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
880                  ERR_LDAP_PTA_CONNECTION_SEARCH_SIZE_LIMIT.get(host, port, cfg.dn(), baseDN, filter));
881
882            default:
883              // The search failed for some reason.
884              throw new DirectoryException(resultCode,
885                  ERR_LDAP_PTA_CONNECTION_SEARCH_FAILED.get(host, port,
886                      cfg.dn(), baseDN, filter, resultCode.intValue(),
887                      resultCode.getName(), searchResult.getErrorMessage()));
888            }
889
890            break;
891
892          default:
893            // Check for disconnect notifications.
894            handleUnexpectedResponse(responseMessage);
895            break;
896          }
897        }
898        while (opType != OP_TYPE_SEARCH_RESULT_DONE);
899
900        if (resultCount > 1)
901        {
902          // Multiple matching candidates.
903          throw new DirectoryException(
904              ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
905              ERR_LDAP_PTA_CONNECTION_SEARCH_SIZE_LIMIT.get(host, port,
906                  cfg.dn(), baseDN, filter));
907        }
908
909        if (username == null)
910        {
911          // No matching entries found.
912          throw new DirectoryException(
913              ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED,
914              ERR_LDAP_PTA_CONNECTION_SEARCH_NO_MATCHES.get(host, port,
915                  cfg.dn(), baseDN, filter));
916        }
917
918        return username;
919      }
920
921      @Override
922      public void simpleBind(final ByteString username,
923          final ByteString password) throws DirectoryException
924      {
925        // Create the bind request and send it to the server.
926        final BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
927            username, 3, password);
928        sendRequest(bindRequest);
929
930        // Read the response from the server.
931        final LDAPMessage responseMessage = readResponse();
932        switch (responseMessage.getProtocolOpType())
933        {
934        case OP_TYPE_BIND_RESPONSE:
935          final BindResponseProtocolOp bindResponse = responseMessage
936              .getBindResponseProtocolOp();
937
938          final ResultCode resultCode = ResultCode.valueOf(bindResponse
939              .getResultCode());
940          if (resultCode == ResultCode.SUCCESS)
941          {
942            // FIXME: need to look for things like password expiration
943            // warning, reset notice, etc.
944            return;
945          }
946          else
947          {
948            // The bind failed for some reason.
949            throw new DirectoryException(resultCode,
950                ERR_LDAP_PTA_CONNECTION_BIND_FAILED.get(host, port,
951                    cfg.dn(), username,
952                    resultCode.intValue(), resultCode.getName(),
953                    bindResponse.getErrorMessage()));
954          }
955
956        default:
957          // Check for disconnect notifications.
958          handleUnexpectedResponse(responseMessage);
959          break;
960        }
961      }
962
963      @Override
964      protected void finalize()
965      {
966        close();
967      }
968
969      private void handleUnexpectedResponse(final LDAPMessage responseMessage)
970          throws DirectoryException
971      {
972        if (responseMessage.getProtocolOpType() == OP_TYPE_EXTENDED_RESPONSE)
973        {
974          final ExtendedResponseProtocolOp extendedResponse = responseMessage
975              .getExtendedResponseProtocolOp();
976          final String responseOID = extendedResponse.getOID();
977
978          if (OID_NOTICE_OF_DISCONNECTION.equals(responseOID))
979          {
980            ResultCode resultCode = ResultCode.valueOf(extendedResponse.getResultCode());
981
982            /*
983             * Since the connection has been disconnected we want to ensure that
984             * upper layers treat all disconnect notifications as fatal and
985             * close the connection. Therefore we map the result code to a fatal
986             * error code if needed. A good example of a non-fatal error code
987             * being returned is INVALID_CREDENTIALS which is used to indicate
988             * that the currently bound user has had their entry removed. We
989             * definitely don't want to pass this straight back to the caller
990             * since it will be misinterpreted as an authentication failure if
991             * the operation being performed is a bind.
992             */
993            ResultCode mappedResultCode = isServiceError(resultCode) ?
994                resultCode : ResultCode.UNAVAILABLE;
995
996            throw new DirectoryException(mappedResultCode,
997                ERR_LDAP_PTA_CONNECTION_DISCONNECTING.get(host, port,
998                    cfg.dn(), resultCode.intValue(), resultCode.getName(),
999                    extendedResponse.getErrorMessage()));
1000          }
1001        }
1002
1003        // Unexpected response type.
1004        throw new DirectoryException(ResultCode.CLIENT_SIDE_DECODING_ERROR,
1005            ERR_LDAP_PTA_CONNECTION_WRONG_RESPONSE.get(host, port,
1006                cfg.dn(), responseMessage.getProtocolOp()));
1007      }
1008
1009      /** Reads a response message and adapts errors to directory exceptions. */
1010      private LDAPMessage readResponse() throws DirectoryException
1011      {
1012        final LDAPMessage responseMessage;
1013        try
1014        {
1015          responseMessage = reader.readMessage();
1016        }
1017        catch (final DecodeException e)
1018        {
1019          // ASN1 layer hides all underlying IO exceptions.
1020          if (e.getCause() instanceof SocketTimeoutException)
1021          {
1022            throw new DirectoryException(ResultCode.CLIENT_SIDE_TIMEOUT,
1023                ERR_LDAP_PTA_CONNECTION_TIMEOUT.get(host, port, cfg.dn()), e);
1024          }
1025          else if (e.getCause() instanceof IOException)
1026          {
1027            throw new DirectoryException(ResultCode.CLIENT_SIDE_SERVER_DOWN,
1028                ERR_LDAP_PTA_CONNECTION_OTHER_ERROR.get(host, port, cfg.dn(), e.getMessage()), e);
1029          }
1030          else
1031          {
1032            throw new DirectoryException(ResultCode.CLIENT_SIDE_DECODING_ERROR,
1033                ERR_LDAP_PTA_CONNECTION_DECODE_ERROR.get(host, port, cfg.dn(), e.getMessage()), e);
1034          }
1035        }
1036        catch (final LDAPException e)
1037        {
1038          throw new DirectoryException(ResultCode.CLIENT_SIDE_DECODING_ERROR,
1039              ERR_LDAP_PTA_CONNECTION_DECODE_ERROR.get(host, port,
1040                  cfg.dn(), e.getMessage()), e);
1041        }
1042        catch (final SocketTimeoutException e)
1043        {
1044          throw new DirectoryException(ResultCode.CLIENT_SIDE_TIMEOUT,
1045              ERR_LDAP_PTA_CONNECTION_TIMEOUT.get(host, port, cfg.dn()), e);
1046        }
1047        catch (final IOException e)
1048        {
1049          throw new DirectoryException(ResultCode.CLIENT_SIDE_SERVER_DOWN,
1050              ERR_LDAP_PTA_CONNECTION_OTHER_ERROR.get(host, port, cfg.dn(), e.getMessage()), e);
1051        }
1052
1053        if (responseMessage == null)
1054        {
1055          throw new DirectoryException(ResultCode.CLIENT_SIDE_SERVER_DOWN,
1056              ERR_LDAP_PTA_CONNECTION_CLOSED.get(host, port, cfg.dn()));
1057        }
1058        return responseMessage;
1059      }
1060
1061      /** Sends a request message and adapts errors to directory exceptions. */
1062      private void sendRequest(final ProtocolOp request)
1063          throws DirectoryException
1064      {
1065        final LDAPMessage requestMessage = new LDAPMessage(nextMessageID++,
1066            request);
1067        try
1068        {
1069          writer.writeMessage(requestMessage);
1070        }
1071        catch (final IOException e)
1072        {
1073          throw new DirectoryException(ResultCode.CLIENT_SIDE_SERVER_DOWN,
1074              ERR_LDAP_PTA_CONNECTION_OTHER_ERROR.get(host, port, cfg.dn(), e.getMessage()), e);
1075        }
1076      }
1077    }
1078
1079    private final String host;
1080    private final int port;
1081    private final LDAPPassThroughAuthenticationPolicyCfg cfg;
1082    private final int timeoutMS;
1083
1084    /**
1085     * LDAP connection factory implementation is package private so that it can
1086     * be tested.
1087     *
1088     * @param host
1089     *          The server host name.
1090     * @param port
1091     *          The server port.
1092     * @param cfg
1093     *          The configuration (for SSL).
1094     */
1095    LDAPConnectionFactory(final String host, final int port,
1096        final LDAPPassThroughAuthenticationPolicyCfg cfg)
1097    {
1098      this.host = host;
1099      this.port = port;
1100      this.cfg = cfg;
1101
1102      // Normalize the timeoutMS to an integer (admin framework ensures that the
1103      // value is non-negative).
1104      this.timeoutMS = (int) Math.min(cfg.getConnectionTimeout(),
1105          Integer.MAX_VALUE);
1106    }
1107
1108    @Override
1109    public void close()
1110    {
1111      // Nothing to do.
1112    }
1113
1114    @Override
1115    public Connection getConnection() throws DirectoryException
1116    {
1117      try
1118      {
1119        // Create the remote ldapSocket address.
1120        final InetAddress address = InetAddress.getByName(host);
1121        final InetSocketAddress socketAddress = new InetSocketAddress(address,
1122            port);
1123
1124        // Create the ldapSocket and connect to the remote server.
1125        final Socket plainSocket = new Socket();
1126        Socket ldapSocket = null;
1127        LDAPReader reader = null;
1128        LDAPWriter writer = null;
1129        LDAPConnection ldapConnection = null;
1130
1131        try
1132        {
1133          // Set ldapSocket cfg before connecting.
1134          plainSocket.setTcpNoDelay(cfg.isUseTCPNoDelay());
1135          plainSocket.setKeepAlive(cfg.isUseTCPKeepAlive());
1136          plainSocket.setSoTimeout(timeoutMS);
1137          if (cfg.getSourceAddress() != null)
1138          {
1139            InetSocketAddress local = new InetSocketAddress(cfg.getSourceAddress(), 0);
1140            plainSocket.bind(local);
1141          }
1142          // Connect the ldapSocket.
1143          plainSocket.connect(socketAddress, timeoutMS);
1144
1145          if (cfg.isUseSSL())
1146          {
1147            // Obtain the optional configured trust manager which will be used
1148            // in order to determine the trust of the remote LDAP server.
1149            TrustManager[] tm = null;
1150            final DN trustManagerDN = cfg.getTrustManagerProviderDN();
1151            if (trustManagerDN != null)
1152            {
1153              final TrustManagerProvider<?> trustManagerProvider =
1154                DirectoryServer.getTrustManagerProvider(trustManagerDN);
1155              if (trustManagerProvider != null)
1156              {
1157                tm = trustManagerProvider.getTrustManagers();
1158              }
1159            }
1160
1161            // Create the SSL context and initialize it.
1162            final SSLContext sslContext = SSLContext.getInstance("TLS");
1163            sslContext.init(null /* key managers */, tm, null /* rng */);
1164
1165            // Create the SSL socket.
1166            final SSLSocketFactory sslSocketFactory = sslContext
1167                .getSocketFactory();
1168            final SSLSocket sslSocket = (SSLSocket) sslSocketFactory
1169                .createSocket(plainSocket, host, port, true);
1170            ldapSocket = sslSocket;
1171
1172            sslSocket.setUseClientMode(true);
1173            if (!cfg.getSSLProtocol().isEmpty())
1174            {
1175              sslSocket.setEnabledProtocols(cfg.getSSLProtocol().toArray(
1176                  new String[0]));
1177            }
1178            if (!cfg.getSSLCipherSuite().isEmpty())
1179            {
1180              sslSocket.setEnabledCipherSuites(cfg.getSSLCipherSuite().toArray(
1181                  new String[0]));
1182            }
1183
1184            // Force TLS negotiation.
1185            sslSocket.startHandshake();
1186          }
1187          else
1188          {
1189            ldapSocket = plainSocket;
1190          }
1191
1192          reader = new LDAPReader(ldapSocket);
1193          writer = new LDAPWriter(ldapSocket);
1194
1195          ldapConnection = new LDAPConnection(plainSocket, ldapSocket, reader,
1196              writer);
1197
1198          return ldapConnection;
1199        }
1200        finally
1201        {
1202          if (ldapConnection == null)
1203          {
1204            // Connection creation failed for some reason, so clean up IO
1205            // resources.
1206            StaticUtils.close(reader, writer);
1207            StaticUtils.close(ldapSocket);
1208
1209            if (ldapSocket != plainSocket)
1210            {
1211              StaticUtils.close(plainSocket);
1212            }
1213          }
1214        }
1215      }
1216      catch (final UnknownHostException e)
1217      {
1218        logger.traceException(e);
1219        throw new DirectoryException(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
1220            ERR_LDAP_PTA_CONNECT_UNKNOWN_HOST.get(host, port, cfg.dn(), host), e);
1221      }
1222      catch (final ConnectException e)
1223      {
1224        logger.traceException(e);
1225        throw new DirectoryException(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
1226            ERR_LDAP_PTA_CONNECT_ERROR.get(host, port, cfg.dn(), port), e);
1227      }
1228      catch (final SocketTimeoutException e)
1229      {
1230        logger.traceException(e);
1231        throw new DirectoryException(ResultCode.CLIENT_SIDE_TIMEOUT,
1232            ERR_LDAP_PTA_CONNECT_TIMEOUT.get(host, port, cfg.dn()), e);
1233      }
1234      catch (final SSLException e)
1235      {
1236        logger.traceException(e);
1237        throw new DirectoryException(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
1238            ERR_LDAP_PTA_CONNECT_SSL_ERROR.get(host, port, cfg.dn(), e.getMessage()), e);
1239      }
1240      catch (final Exception e)
1241      {
1242        logger.traceException(e);
1243        throw new DirectoryException(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
1244            ERR_LDAP_PTA_CONNECT_OTHER_ERROR.get(host, port, cfg.dn(), e.getMessage()), e);
1245      }
1246    }
1247  }
1248
1249  /**
1250   * An interface for obtaining a connection factory for LDAP connections to a
1251   * named LDAP server and the monitoring scheduler.
1252   */
1253  static interface Provider
1254  {
1255    /**
1256     * Returns a connection factory which can be used for obtaining connections
1257     * to the specified LDAP server.
1258     *
1259     * @param host
1260     *          The LDAP server host name.
1261     * @param port
1262     *          The LDAP server port.
1263     * @param cfg
1264     *          The LDAP connection configuration.
1265     * @return A connection factory which can be used for obtaining connections
1266     *         to the specified LDAP server.
1267     */
1268    ConnectionFactory getLDAPConnectionFactory(String host, int port,
1269        LDAPPassThroughAuthenticationPolicyCfg cfg);
1270
1271    /**
1272     * Returns the scheduler which should be used to periodically ping
1273     * connection factories to determine when they are online.
1274     *
1275     * @return The scheduler which should be used to periodically ping
1276     *         connection factories to determine when they are online.
1277     */
1278    ScheduledExecutorService getScheduledExecutorService();
1279
1280    /**
1281     * Returns the current time in order to perform cached password expiration
1282     * checks. The returned string will be formatted as a a generalized time
1283     * string
1284     *
1285     * @return The current time.
1286     */
1287    String getCurrentTime();
1288
1289    /**
1290     * Returns the current time in order to perform cached password expiration
1291     * checks.
1292     *
1293     * @return The current time in MS.
1294     */
1295    long getCurrentTimeMS();
1296  }
1297
1298  /**
1299   * A simplistic load-balancer connection factory implementation using
1300   * approximately round-robin balancing.
1301   */
1302  static final class RoundRobinLoadBalancer extends AbstractLoadBalancer
1303  {
1304    private final AtomicInteger nextIndex = new AtomicInteger();
1305    private final int maxIndex;
1306
1307    /**
1308     * Creates a new load-balancer which will distribute connection requests
1309     * across a set of underlying connection factories.
1310     *
1311     * @param factories
1312     *          The list of underlying connection factories.
1313     * @param scheduler
1314     *          The monitoring scheduler.
1315     */
1316    RoundRobinLoadBalancer(final ConnectionFactory[] factories,
1317        final ScheduledExecutorService scheduler)
1318    {
1319      super(factories, scheduler);
1320      this.maxIndex = factories.length;
1321    }
1322
1323    @Override
1324    int getStartIndex()
1325    {
1326      // A round robin pool of one connection factories is unlikely in
1327      // practice and requires special treatment.
1328      if (maxIndex == 1)
1329      {
1330        return 0;
1331      }
1332
1333      // Determine the next factory to use: avoid blocking algorithm.
1334      int oldNextIndex;
1335      int newNextIndex;
1336      do
1337      {
1338        oldNextIndex = nextIndex.get();
1339        newNextIndex = oldNextIndex + 1;
1340        if (newNextIndex == maxIndex)
1341        {
1342          newNextIndex = 0;
1343        }
1344      }
1345      while (!nextIndex.compareAndSet(oldNextIndex, newNextIndex));
1346
1347      // There's a potential, but benign, race condition here: other threads
1348      // could jump in and rotate through the list before we return the
1349      // connection factory.
1350      return oldNextIndex;
1351    }
1352  }
1353
1354  /** LDAP PTA policy implementation. */
1355  private final class PolicyImpl extends AuthenticationPolicy implements
1356      ConfigurationChangeListener<LDAPPassThroughAuthenticationPolicyCfg>
1357  {
1358    /** LDAP PTA policy state implementation. */
1359    private final class StateImpl extends AuthenticationPolicyState
1360    {
1361      private final AttributeType cachedPasswordAttribute;
1362      private final AttributeType cachedPasswordTimeAttribute;
1363
1364      private ByteString newCachedPassword;
1365
1366      private StateImpl(final Entry userEntry)
1367      {
1368        super(userEntry);
1369
1370        this.cachedPasswordAttribute = getSchema().getAttributeType(OP_ATTR_PTAPOLICY_CACHED_PASSWORD);
1371        this.cachedPasswordTimeAttribute = getSchema().getAttributeType(OP_ATTR_PTAPOLICY_CACHED_PASSWORD_TIME);
1372      }
1373
1374      @Override
1375      public void finalizeStateAfterBind() throws DirectoryException
1376      {
1377        sharedLock.lock();
1378        try
1379        {
1380          if (cfg.isUsePasswordCaching() && newCachedPassword != null)
1381          {
1382            // Update the user's entry to contain the cached password and
1383            // time stamp.
1384            ByteString encodedPassword = pwdStorageScheme
1385                .encodePasswordWithScheme(newCachedPassword);
1386
1387            List<RawModification> modifications = new ArrayList<>(2);
1388            modifications.add(RawModification.create(ModificationType.REPLACE,
1389                OP_ATTR_PTAPOLICY_CACHED_PASSWORD, encodedPassword));
1390            modifications.add(RawModification.create(ModificationType.REPLACE,
1391                OP_ATTR_PTAPOLICY_CACHED_PASSWORD_TIME,
1392                provider.getCurrentTime()));
1393
1394            ModifyOperation internalModify = getRootConnection().processModify(
1395                ByteString.valueOfObject(userEntry.getName()), modifications);
1396
1397            ResultCode resultCode = internalModify.getResultCode();
1398            if (resultCode != ResultCode.SUCCESS)
1399            {
1400              // The modification failed for some reason. This should not
1401              // prevent the bind from succeeded since we are only updating
1402              // cache data. However, the performance of the server may be
1403              // impacted, so log a debug warning message.
1404              if (logger.isTraceEnabled())
1405              {
1406                logger.trace(
1407                    "An error occurred while trying to update the LDAP PTA "
1408                        + "cached password for user %s: %s",
1409                        userEntry.getName(), internalModify.getErrorMessage());
1410              }
1411            }
1412
1413            newCachedPassword = null;
1414          }
1415        }
1416        finally
1417        {
1418          sharedLock.unlock();
1419        }
1420      }
1421
1422      @Override
1423      public AuthenticationPolicy getAuthenticationPolicy()
1424      {
1425        return PolicyImpl.this;
1426      }
1427
1428      @Override
1429      public boolean passwordMatches(final ByteString password)
1430          throws DirectoryException
1431      {
1432        sharedLock.lock();
1433        try
1434        {
1435          // First check the cached password if enabled and available.
1436          if (passwordMatchesCachedPassword(password))
1437          {
1438            return true;
1439          }
1440
1441          // The cache lookup failed, so perform full PTA.
1442          ByteString username = null;
1443
1444          switch (cfg.getMappingPolicy())
1445          {
1446          case UNMAPPED:
1447            // The bind DN is the name of the user's entry.
1448            username = ByteString.valueOfUtf8(userEntry.getName().toString());
1449            break;
1450          case MAPPED_BIND:
1451            // The bind DN is contained in an attribute in the user's entry.
1452            mapBind: for (final AttributeType at : cfg.getMappedAttribute())
1453            {
1454              for (final Attribute attribute : userEntry.getAttribute(at))
1455              {
1456                if (!attribute.isEmpty())
1457                {
1458                  username = attribute.iterator().next();
1459                  break mapBind;
1460                }
1461              }
1462            }
1463
1464            if (username == null)
1465            {
1466              /*
1467               * The mapping attribute(s) is not present in the entry. This
1468               * could be a configuration error, but it could also be because
1469               * someone is attempting to authenticate using a bind DN which
1470               * references a non-user entry.
1471               */
1472              throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
1473                  ERR_LDAP_PTA_MAPPING_ATTRIBUTE_NOT_FOUND.get(
1474                      userEntry.getName(), cfg.dn(),
1475                      mappedAttributesAsString(cfg.getMappedAttribute())));
1476            }
1477
1478            break;
1479          case MAPPED_SEARCH:
1480            // A search against the remote directory is required in order to
1481            // determine the bind DN.
1482
1483            final String filterTemplate =  cfg.getMappedSearchFilterTemplate();
1484
1485            // Construct the search filter.
1486            final LinkedList<SearchFilter> filterComponents = new LinkedList<>();
1487            for (final AttributeType at : cfg.getMappedAttribute())
1488            {
1489              for (final Attribute attribute : userEntry.getAttribute(at))
1490              {
1491                for (final ByteString value : attribute)
1492                {
1493                  if (filterTemplate != null)
1494                  {
1495                    filterComponents.add(SearchFilter.createFilterFromString(
1496                        Filter.format(filterTemplate, value).toString()));
1497                  }
1498                  else
1499                  {
1500                    filterComponents.add(SearchFilter.createEqualityFilter(at, value));
1501                  }
1502                }
1503              }
1504            }
1505
1506            if (filterComponents.isEmpty())
1507            {
1508              /*
1509               * The mapping attribute(s) is not present in the entry. This
1510               * could be a configuration error, but it could also be because
1511               * someone is attempting to authenticate using a bind DN which
1512               * references a non-user entry.
1513               */
1514              throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
1515                  ERR_LDAP_PTA_MAPPING_ATTRIBUTE_NOT_FOUND.get(
1516                      userEntry.getName(), cfg.dn(),
1517                      mappedAttributesAsString(cfg.getMappedAttribute())));
1518            }
1519
1520            final SearchFilter filter;
1521            if (filterComponents.size() == 1)
1522            {
1523              filter = filterComponents.getFirst();
1524            }
1525            else
1526            {
1527              filter = SearchFilter.createORFilter(filterComponents);
1528            }
1529
1530            // Now search the configured base DNs, stopping at the first
1531            // success.
1532            for (final DN baseDN : cfg.getMappedSearchBaseDN())
1533            {
1534              Connection connection = null;
1535              try
1536              {
1537                connection = searchFactory.getConnection();
1538                username = connection.search(baseDN, SearchScope.WHOLE_SUBTREE,
1539                    filter);
1540              }
1541              catch (final DirectoryException e)
1542              {
1543                switch (e.getResultCode().asEnum())
1544                {
1545                case NO_SUCH_OBJECT:
1546                case CLIENT_SIDE_NO_RESULTS_RETURNED:
1547                  // Ignore and try next base DN.
1548                  break;
1549                case CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED:
1550                  // More than one matching entry was returned.
1551                  throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
1552                      ERR_LDAP_PTA_MAPPED_SEARCH_TOO_MANY_CANDIDATES.get(
1553                          userEntry.getName(), cfg.dn(), baseDN, filter));
1554                default:
1555                  // We don't want to propagate this internal error to the
1556                  // client. We should log it and map it to a more appropriate
1557                  // error.
1558                  throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
1559                      ERR_LDAP_PTA_MAPPED_SEARCH_FAILED.get(
1560                          userEntry.getName(), cfg.dn(), e.getMessageObject()), e);
1561                }
1562              }
1563              finally
1564              {
1565                StaticUtils.close(connection);
1566              }
1567            }
1568
1569            if (username == null)
1570            {
1571              /* No matching entries were found in the remote directory. */
1572              throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
1573                  ERR_LDAP_PTA_MAPPED_SEARCH_NO_CANDIDATES.get(
1574                      userEntry.getName(), cfg.dn(), filter));
1575            }
1576
1577            break;
1578          }
1579
1580          // Now perform the bind.
1581          try (Connection connection = bindFactory.getConnection())
1582          {
1583            connection.simpleBind(username, password);
1584
1585            // The password matched, so cache it, it will be stored in the
1586            // user's entry when the state is finalized and only if caching is
1587            // enabled.
1588            newCachedPassword = password;
1589            return true;
1590          }
1591          catch (final DirectoryException e)
1592          {
1593            switch (e.getResultCode().asEnum())
1594            {
1595            case NO_SUCH_OBJECT:
1596            case INVALID_CREDENTIALS:
1597              return false;
1598            default:
1599              // We don't want to propagate this internal error to the
1600              // client. We should log it and map it to a more appropriate
1601              // error.
1602              throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
1603                  ERR_LDAP_PTA_MAPPED_BIND_FAILED.get(
1604                      userEntry.getName(), cfg.dn(), e.getMessageObject()), e);
1605            }
1606          }
1607        }
1608        finally
1609        {
1610          sharedLock.unlock();
1611        }
1612      }
1613
1614      private boolean passwordMatchesCachedPassword(ByteString password)
1615      {
1616        if (!cfg.isUsePasswordCaching())
1617        {
1618          return false;
1619        }
1620
1621        // First determine if the cached password time is present and valid.
1622        boolean foundValidCachedPasswordTime = false;
1623
1624        foundCachedPasswordTime:
1625        for (Attribute attribute : userEntry.getAttribute(cachedPasswordTimeAttribute))
1626        {
1627          // Ignore any attributes with options.
1628          if (!attribute.getAttributeDescription().hasOptions())
1629          {
1630            for (ByteString value : attribute)
1631            {
1632              try
1633              {
1634                long cachedPasswordTime = GeneralizedTime.valueOf(value.toString()).getTimeInMillis();
1635                long currentTime = provider.getCurrentTimeMS();
1636                long expiryTime = cachedPasswordTime + (cfg.getCachedPasswordTTL() * 1000);
1637                foundValidCachedPasswordTime = expiryTime > currentTime;
1638              }
1639              catch (LocalizedIllegalArgumentException e)
1640              {
1641                // Fall-through and give up immediately.
1642                logger.traceException(e);
1643              }
1644              break foundCachedPasswordTime;
1645            }
1646          }
1647        }
1648
1649        if (!foundValidCachedPasswordTime)
1650        {
1651          // The cached password time was not found or it has expired, so give
1652          // up immediately.
1653          return false;
1654        }
1655
1656        // Next determine if there is a cached password.
1657        ByteString cachedPassword = null;
1658        foundCachedPassword:
1659        for (Attribute attribute : userEntry.getAttribute(cachedPasswordAttribute))
1660        {
1661          // Ignore any attributes with options.
1662          if (!attribute.getAttributeDescription().hasOptions())
1663          {
1664            for (ByteString value : attribute)
1665            {
1666              cachedPassword = value;
1667              break foundCachedPassword;
1668            }
1669          }
1670        }
1671
1672        if (cachedPassword == null)
1673        {
1674          // The cached password was not found, so give up immediately.
1675          return false;
1676        }
1677
1678        // Decode the password and match it according to its storage scheme.
1679        try
1680        {
1681          String[] userPwComponents = UserPasswordSyntax
1682              .decodeUserPassword(cachedPassword.toString());
1683          PasswordStorageScheme<?> scheme = DirectoryServer
1684              .getPasswordStorageScheme(userPwComponents[0]);
1685          if (scheme != null)
1686          {
1687            return scheme.passwordMatches(password,
1688                ByteString.valueOfUtf8(userPwComponents[1]));
1689          }
1690        }
1691        catch (DirectoryException e)
1692        {
1693          // Unable to decode the cached password, so give up.
1694          logger.traceException(e);
1695        }
1696
1697        return false;
1698      }
1699    }
1700
1701    /** Guards against configuration changes. */
1702    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
1703    private final ReadLock sharedLock = lock.readLock();
1704    private final WriteLock exclusiveLock = lock.writeLock();
1705
1706    /** Current configuration. */
1707    private LDAPPassThroughAuthenticationPolicyCfg cfg;
1708
1709    private ConnectionFactory searchFactory;
1710    private ConnectionFactory bindFactory;
1711
1712    private PasswordStorageScheme<?> pwdStorageScheme;
1713
1714    private PolicyImpl(
1715        final LDAPPassThroughAuthenticationPolicyCfg configuration)
1716    {
1717      initializeConfiguration(configuration);
1718    }
1719
1720    @Override
1721    public ConfigChangeResult applyConfigurationChange(
1722        final LDAPPassThroughAuthenticationPolicyCfg cfg)
1723    {
1724      exclusiveLock.lock();
1725      try
1726      {
1727        closeConnections();
1728        initializeConfiguration(cfg);
1729      }
1730      finally
1731      {
1732        exclusiveLock.unlock();
1733      }
1734      return new ConfigChangeResult();
1735    }
1736
1737    @Override
1738    public AuthenticationPolicyState createAuthenticationPolicyState(
1739        final Entry userEntry, final long time) throws DirectoryException
1740    {
1741      // The current time is not needed for LDAP PTA.
1742      return new StateImpl(userEntry);
1743    }
1744
1745    @Override
1746    public void finalizeAuthenticationPolicy()
1747    {
1748      exclusiveLock.lock();
1749      try
1750      {
1751        cfg.removeLDAPPassThroughChangeListener(this);
1752        closeConnections();
1753      }
1754      finally
1755      {
1756        exclusiveLock.unlock();
1757      }
1758    }
1759
1760    @Override
1761    public DN getDN()
1762    {
1763      return cfg.dn();
1764    }
1765
1766    @Override
1767    public boolean isConfigurationChangeAcceptable(
1768        final LDAPPassThroughAuthenticationPolicyCfg cfg,
1769        final List<LocalizableMessage> unacceptableReasons)
1770    {
1771      return LDAPPassThroughAuthenticationPolicyFactory.this
1772          .isConfigurationAcceptable(cfg, unacceptableReasons);
1773    }
1774
1775    private void closeConnections()
1776    {
1777      exclusiveLock.lock();
1778      try
1779      {
1780        if (searchFactory != null)
1781        {
1782          searchFactory.close();
1783          searchFactory = null;
1784        }
1785
1786        if (bindFactory != null)
1787        {
1788          bindFactory.close();
1789          bindFactory = null;
1790        }
1791      }
1792      finally
1793      {
1794        exclusiveLock.unlock();
1795      }
1796    }
1797
1798    private void initializeConfiguration(
1799        final LDAPPassThroughAuthenticationPolicyCfg cfg)
1800    {
1801      this.cfg = cfg;
1802
1803      // First obtain the mapped search password if needed, ignoring any errors
1804      // since these should have already been detected during configuration
1805      // validation.
1806      final String mappedSearchPassword;
1807      if (cfg.getMappingPolicy() == MappingPolicy.MAPPED_SEARCH
1808          && cfg.getMappedSearchBindDN() != null
1809          && !cfg.getMappedSearchBindDN().isRootDN())
1810      {
1811        mappedSearchPassword = getMappedSearchBindPassword(cfg,
1812            new LinkedList<LocalizableMessage>());
1813      }
1814      else
1815      {
1816        mappedSearchPassword = null;
1817      }
1818
1819      // Use two pools per server: one for authentication (bind) and one for
1820      // searches. Even if the searches are performed anonymously we cannot use
1821      // the same pool, otherwise they will be performed as the most recently
1822      // authenticated user.
1823
1824      // Create load-balancers for primary servers.
1825      final RoundRobinLoadBalancer primarySearchLoadBalancer;
1826      final RoundRobinLoadBalancer primaryBindLoadBalancer;
1827      final ScheduledExecutorService scheduler = provider
1828          .getScheduledExecutorService();
1829
1830      Set<String> servers = cfg.getPrimaryRemoteLDAPServer();
1831      ConnectionPool[] searchPool = new ConnectionPool[servers.size()];
1832      ConnectionPool[] bindPool = new ConnectionPool[servers.size()];
1833      int index = 0;
1834      for (final String hostPort : servers)
1835      {
1836        final ConnectionFactory factory = newLDAPConnectionFactory(hostPort);
1837        searchPool[index] = new ConnectionPool(
1838            new AuthenticatedConnectionFactory(factory,
1839                cfg.getMappedSearchBindDN(),
1840                mappedSearchPassword));
1841        bindPool[index++] = new ConnectionPool(factory);
1842      }
1843      primarySearchLoadBalancer = new RoundRobinLoadBalancer(searchPool,
1844          scheduler);
1845      primaryBindLoadBalancer = new RoundRobinLoadBalancer(bindPool, scheduler);
1846
1847      // Create load-balancers for secondary servers.
1848      servers = cfg.getSecondaryRemoteLDAPServer();
1849      if (servers.isEmpty())
1850      {
1851        searchFactory = primarySearchLoadBalancer;
1852        bindFactory = primaryBindLoadBalancer;
1853      }
1854      else
1855      {
1856        searchPool = new ConnectionPool[servers.size()];
1857        bindPool = new ConnectionPool[servers.size()];
1858        index = 0;
1859        for (final String hostPort : servers)
1860        {
1861          final ConnectionFactory factory = newLDAPConnectionFactory(hostPort);
1862          searchPool[index] = new ConnectionPool(
1863              new AuthenticatedConnectionFactory(factory,
1864                  cfg.getMappedSearchBindDN(),
1865                  mappedSearchPassword));
1866          bindPool[index++] = new ConnectionPool(factory);
1867        }
1868        final RoundRobinLoadBalancer secondarySearchLoadBalancer =
1869          new RoundRobinLoadBalancer(searchPool, scheduler);
1870        final RoundRobinLoadBalancer secondaryBindLoadBalancer =
1871          new RoundRobinLoadBalancer(bindPool, scheduler);
1872        searchFactory = new FailoverLoadBalancer(primarySearchLoadBalancer,
1873            secondarySearchLoadBalancer, scheduler);
1874        bindFactory = new FailoverLoadBalancer(primaryBindLoadBalancer,
1875            secondaryBindLoadBalancer, scheduler);
1876      }
1877
1878      if (cfg.isUsePasswordCaching())
1879      {
1880        pwdStorageScheme = DirectoryServer.getPasswordStorageScheme(cfg
1881            .getCachedPasswordStorageSchemeDN());
1882      }
1883    }
1884
1885    private ConnectionFactory newLDAPConnectionFactory(final String hostPort)
1886    {
1887      // Validation already performed by admin framework.
1888      final HostPort hp = HostPort.valueOf(hostPort);
1889      return provider.getLDAPConnectionFactory(hp.getHost(), hp.getPort(), cfg);
1890    }
1891  }
1892
1893  /** Debug tracer for this class. */
1894  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
1895
1896  /** Attribute list for searches requesting no attributes. */
1897  static final LinkedHashSet<String> NO_ATTRIBUTES = new LinkedHashSet<>(1);
1898  static
1899  {
1900    NO_ATTRIBUTES.add(SchemaConstants.NO_ATTRIBUTES);
1901  }
1902
1903  /** The provider which should be used by policies to create LDAP connections. */
1904  private final Provider provider;
1905
1906  private ServerContext serverContext;
1907
1908  /** The default LDAP connection factory provider. */
1909  private static final Provider DEFAULT_PROVIDER = new Provider()
1910  {
1911
1912    /**
1913     * Global scheduler used for periodically monitoring connection factories in
1914     * order to detect when they are online.
1915     */
1916    private final ScheduledExecutorService scheduler = Executors
1917        .newScheduledThreadPool(2, new ThreadFactory()
1918        {
1919
1920          @Override
1921          public Thread newThread(final Runnable r)
1922          {
1923            final Thread t = new DirectoryThread(r,
1924                "LDAP PTA connection monitor thread");
1925            t.setDaemon(true);
1926            return t;
1927          }
1928        });
1929
1930    @Override
1931    public ConnectionFactory getLDAPConnectionFactory(final String host,
1932        final int port, final LDAPPassThroughAuthenticationPolicyCfg cfg)
1933    {
1934      return new LDAPConnectionFactory(host, port, cfg);
1935    }
1936
1937    @Override
1938    public ScheduledExecutorService getScheduledExecutorService()
1939    {
1940      return scheduler;
1941    }
1942
1943    @Override
1944    public String getCurrentTime()
1945    {
1946      return TimeThread.getGMTTime();
1947    }
1948
1949    @Override
1950    public long getCurrentTimeMS()
1951    {
1952      return TimeThread.getTime();
1953    }
1954
1955  };
1956
1957  /**
1958   * Determines whether or no a result code is expected to trigger the
1959   * associated connection to be closed immediately.
1960   *
1961   * @param resultCode
1962   *          The result code.
1963   * @return {@code true} if the result code is expected to trigger the
1964   *         associated connection to be closed immediately.
1965   */
1966  static boolean isServiceError(final ResultCode resultCode)
1967  {
1968    switch (resultCode.asEnum())
1969    {
1970    case OPERATIONS_ERROR:
1971    case PROTOCOL_ERROR:
1972    case TIME_LIMIT_EXCEEDED:
1973    case ADMIN_LIMIT_EXCEEDED:
1974    case UNAVAILABLE_CRITICAL_EXTENSION:
1975    case BUSY:
1976    case UNAVAILABLE:
1977    case UNWILLING_TO_PERFORM:
1978    case LOOP_DETECT:
1979    case OTHER:
1980    case CLIENT_SIDE_CONNECT_ERROR:
1981    case CLIENT_SIDE_DECODING_ERROR:
1982    case CLIENT_SIDE_ENCODING_ERROR:
1983    case CLIENT_SIDE_LOCAL_ERROR:
1984    case CLIENT_SIDE_SERVER_DOWN:
1985    case CLIENT_SIDE_TIMEOUT:
1986      return true;
1987    default:
1988      return false;
1989    }
1990  }
1991
1992  /**
1993   * Get the search bind password performing mapped searches.
1994   * We will offer several places to look for the password, and we will
1995   * do so in the following order:
1996   * - In a specified Java property
1997   * - In a specified environment variable
1998   * - In a specified file on the server filesystem.
1999   * - As the value of a configuration attribute.
2000   * In any case, the password must be in the clear.
2001   */
2002  private static String getMappedSearchBindPassword(
2003      final LDAPPassThroughAuthenticationPolicyCfg cfg,
2004      final List<LocalizableMessage> unacceptableReasons)
2005  {
2006    String password = null;
2007
2008    if (cfg.getMappedSearchBindPasswordProperty() != null)
2009    {
2010      String propertyName = cfg.getMappedSearchBindPasswordProperty();
2011      password = System.getProperty(propertyName);
2012      if (password == null)
2013      {
2014        unacceptableReasons.add(ERR_LDAP_PTA_PWD_PROPERTY_NOT_SET.get(cfg.dn(), propertyName));
2015      }
2016    }
2017    else if (cfg.getMappedSearchBindPasswordEnvironmentVariable() != null)
2018    {
2019      String envVarName = cfg.getMappedSearchBindPasswordEnvironmentVariable();
2020      password = System.getenv(envVarName);
2021      if (password == null)
2022      {
2023        unacceptableReasons.add(ERR_LDAP_PTA_PWD_ENVAR_NOT_SET.get(cfg.dn(), envVarName));
2024      }
2025    }
2026    else if (cfg.getMappedSearchBindPasswordFile() != null)
2027    {
2028      String fileName = cfg.getMappedSearchBindPasswordFile();
2029      File passwordFile = getFileForPath(fileName);
2030      if (!passwordFile.exists())
2031      {
2032        unacceptableReasons.add(ERR_LDAP_PTA_PWD_NO_SUCH_FILE.get(cfg.dn(), fileName));
2033      }
2034      else
2035      {
2036        BufferedReader br = null;
2037        try
2038        {
2039          br = new BufferedReader(new FileReader(passwordFile));
2040          password = br.readLine();
2041          if (password == null)
2042          {
2043            unacceptableReasons.add(ERR_LDAP_PTA_PWD_FILE_EMPTY.get(cfg.dn(), fileName));
2044          }
2045        }
2046        catch (IOException e)
2047        {
2048          unacceptableReasons.add(ERR_LDAP_PTA_PWD_FILE_CANNOT_READ.get(
2049              cfg.dn(), fileName, getExceptionMessage(e)));
2050        }
2051        finally
2052        {
2053          StaticUtils.close(br);
2054        }
2055      }
2056    }
2057    else if (cfg.getMappedSearchBindPassword() != null)
2058    {
2059      password = cfg.getMappedSearchBindPassword();
2060    }
2061    else
2062    {
2063      // Password wasn't defined anywhere.
2064      unacceptableReasons.add(ERR_LDAP_PTA_NO_PWD.get(cfg.dn()));
2065    }
2066
2067    return password;
2068  }
2069
2070  private static boolean isMappedFilterTemplateValid(
2071      final String filterTemplate,
2072      final List<LocalizableMessage> unacceptableReasons)
2073  {
2074    if (filterTemplate != null)
2075    {
2076      try
2077      {
2078        Filter.format(filterTemplate, "testValue");
2079      }
2080      catch(IllegalFormatConversionException | MissingFormatArgumentException | LocalizedIllegalArgumentException e)
2081      {
2082        unacceptableReasons.add(ERR_LDAP_PTA_INVALID_FILTER_TEMPLATE.get(filterTemplate));
2083        return false;
2084      }
2085    }
2086
2087    return true;
2088  }
2089
2090  private static boolean isServerAddressValid(
2091      final LDAPPassThroughAuthenticationPolicyCfg configuration,
2092      final List<LocalizableMessage> unacceptableReasons, final String hostPort)
2093  {
2094    try
2095    {
2096      // validate provided string
2097      HostPort.valueOf(hostPort);
2098      return true;
2099    }
2100    catch (RuntimeException e)
2101    {
2102      if (unacceptableReasons != null)
2103      {
2104        unacceptableReasons.add(ERR_LDAP_PTA_INVALID_PORT_NUMBER.get(configuration.dn(), hostPort));
2105      }
2106      return false;
2107    }
2108  }
2109
2110  private static String mappedAttributesAsString(
2111      final Collection<AttributeType> attributes)
2112  {
2113    switch (attributes.size())
2114    {
2115    case 0:
2116      return "";
2117    case 1:
2118      return attributes.iterator().next().getNameOrOID();
2119    default:
2120      final StringBuilder builder = new StringBuilder();
2121      final Iterator<AttributeType> i = attributes.iterator();
2122      builder.append(i.next().getNameOrOID());
2123      while (i.hasNext())
2124      {
2125        builder.append(", ");
2126        builder.append(i.next().getNameOrOID());
2127      }
2128      return builder.toString();
2129    }
2130  }
2131
2132  /**
2133   * Public default constructor used by the admin framework. This will use the
2134   * default LDAP connection factory provider.
2135   */
2136  public LDAPPassThroughAuthenticationPolicyFactory()
2137  {
2138    this(DEFAULT_PROVIDER);
2139  }
2140
2141  /**
2142   * Sets the server context.
2143   *
2144   * @param serverContext
2145   *            The server context.
2146   */
2147  @Override
2148  public void setServerContext(ServerContext serverContext) {
2149    this.serverContext = serverContext;
2150  }
2151
2152  /**
2153   * Package private constructor allowing unit tests to provide mock connection
2154   * implementations.
2155   *
2156   * @param provider
2157   *          The LDAP connection factory provider implementation which LDAP PTA
2158   *          authentication policies will use.
2159   */
2160  LDAPPassThroughAuthenticationPolicyFactory(final Provider provider)
2161  {
2162    this.provider = provider;
2163  }
2164
2165  @Override
2166  public AuthenticationPolicy createAuthenticationPolicy(
2167      final LDAPPassThroughAuthenticationPolicyCfg configuration)
2168      throws ConfigException, InitializationException
2169  {
2170    final PolicyImpl policy = new PolicyImpl(configuration);
2171    configuration.addLDAPPassThroughChangeListener(policy);
2172    return policy;
2173  }
2174
2175  @Override
2176  public boolean isConfigurationAcceptable(
2177      final LDAPPassThroughAuthenticationPolicyCfg cfg,
2178      final List<LocalizableMessage> unacceptableReasons)
2179  {
2180    // Check that the port numbers are valid. We won't actually try and connect
2181    // to the server since they may not be available (hence we have fail-over
2182    // capabilities).
2183    boolean configurationIsAcceptable = true;
2184
2185    for (final String hostPort : cfg.getPrimaryRemoteLDAPServer())
2186    {
2187      configurationIsAcceptable &= isServerAddressValid(cfg,
2188          unacceptableReasons, hostPort);
2189    }
2190
2191    for (final String hostPort : cfg.getSecondaryRemoteLDAPServer())
2192    {
2193      configurationIsAcceptable &= isServerAddressValid(cfg,
2194          unacceptableReasons, hostPort);
2195    }
2196
2197    // Ensure that the search bind password is defined somewhere.
2198    if (cfg.getMappingPolicy() == MappingPolicy.MAPPED_SEARCH
2199        && cfg.getMappedSearchBindDN() != null
2200        && !cfg.getMappedSearchBindDN().isRootDN()
2201        && getMappedSearchBindPassword(cfg, unacceptableReasons) == null)
2202    {
2203      configurationIsAcceptable = false;
2204    }
2205
2206    if (!isMappedFilterTemplateValid(cfg.getMappedSearchFilterTemplate(),
2207        unacceptableReasons))
2208    {
2209      configurationIsAcceptable = false;
2210    }
2211
2212    return configurationIsAcceptable;
2213  }
2214}