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