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