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}