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