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