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 010 * trunk/opends/resource/legal-notices/OpenDS.LICENSE 011 * or https://OpenDS.dev.java.net/OpenDS.LICENSE. 012 * See the License for the specific language governing permissions 013 * and limitations under the License. 014 * 015 * When distributing Covered Code, include this CDDL HEADER in each 016 * file and include the License file at 017 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, 018 * add the following below this CDDL HEADER, with the fields enclosed 019 * by brackets "[]" replaced with your own identifying information: 020 * Portions Copyright [yyyy] [name of copyright owner] 021 * 022 * CDDL HEADER END 023 * 024 * 025 * Copyright 2006-2008 Sun Microsystems, Inc. 026 */ 027 package org.opends.server.extensions; 028 import org.opends.messages.Message; 029 030 031 032 import java.util.ArrayList; 033 import java.util.Arrays; 034 import java.util.LinkedHashSet; 035 import java.util.List; 036 import java.util.HashSet; 037 import java.util.Set; 038 import java.util.concurrent.locks.Lock; 039 040 import org.opends.server.admin.server.ConfigurationChangeListener; 041 import org.opends.server.admin.std.server.ExtendedOperationHandlerCfg; 042 import org.opends.server.admin.std.server. 043 PasswordModifyExtendedOperationHandlerCfg; 044 import org.opends.server.api.ClientConnection; 045 import org.opends.server.api.ExtendedOperationHandler; 046 import org.opends.server.api.IdentityMapper; 047 import org.opends.server.api.PasswordStorageScheme; 048 import org.opends.server.config.ConfigException; 049 import org.opends.server.controls.PasswordPolicyResponseControl; 050 import org.opends.server.controls.PasswordPolicyWarningType; 051 import org.opends.server.controls.PasswordPolicyErrorType; 052 import org.opends.server.core.DirectoryServer; 053 import org.opends.server.core.ExtendedOperation; 054 import org.opends.server.core.ModifyOperation; 055 import org.opends.server.core.PasswordPolicyState; 056 import org.opends.server.loggers.debug.DebugTracer; 057 import org.opends.server.protocols.asn1.ASN1Element; 058 import org.opends.server.protocols.asn1.ASN1Exception; 059 import org.opends.server.protocols.asn1.ASN1OctetString; 060 import org.opends.server.protocols.asn1.ASN1Sequence; 061 import org.opends.server.protocols.internal.InternalClientConnection; 062 import org.opends.server.schema.AuthPasswordSyntax; 063 import org.opends.server.schema.UserPasswordSyntax; 064 import org.opends.server.types.Attribute; 065 import org.opends.server.types.AttributeType; 066 import org.opends.server.types.AttributeValue; 067 import org.opends.server.types.AuthenticationInfo; 068 import org.opends.server.types.ByteString; 069 import org.opends.server.types.ConfigChangeResult; 070 import org.opends.server.types.Control; 071 import org.opends.server.types.DebugLogLevel; 072 import org.opends.server.types.DirectoryException; 073 import org.opends.server.types.DN; 074 import org.opends.server.types.Entry; 075 import org.opends.server.types.InitializationException; 076 import org.opends.server.types.LockManager; 077 import org.opends.server.types.Modification; 078 import org.opends.server.types.ModificationType; 079 import org.opends.server.types.Privilege; 080 import org.opends.server.types.ResultCode; 081 082 import static org.opends.server.extensions.ExtensionsConstants.*; 083 import static org.opends.server.loggers.debug.DebugLogger.*; 084 import org.opends.server.loggers.ErrorLogger; 085 import static org.opends.messages.ExtensionMessages.*; 086 087 import org.opends.messages.MessageBuilder; 088 import static org.opends.server.util.ServerConstants.*; 089 import static org.opends.server.util.StaticUtils.*; 090 091 092 /** 093 * This class implements the password modify extended operation defined in RFC 094 * 3062. It includes support for requiring the user's current password as well 095 * as for generating a new password if none was provided. 096 */ 097 public class PasswordModifyExtendedOperation 098 extends ExtendedOperationHandler< 099 PasswordModifyExtendedOperationHandlerCfg> 100 implements ConfigurationChangeListener< 101 PasswordModifyExtendedOperationHandlerCfg> 102 { 103 /** 104 * The tracer object for the debug logger. 105 */ 106 private static final DebugTracer TRACER = getTracer(); 107 108 109 110 // The current configuration state. 111 private PasswordModifyExtendedOperationHandlerCfg currentConfig; 112 113 // The DN of the identity mapper. 114 private DN identityMapperDN; 115 116 // The reference to the identity mapper. 117 private IdentityMapper identityMapper; 118 119 // The default set of supported control OIDs for this extended 120 private Set<String> supportedControlOIDs = new HashSet<String>(0); 121 122 123 124 /** 125 * Create an instance of this password modify extended operation. All 126 * initialization should be performed in the 127 * <CODE>initializeExtendedOperationHandler</CODE> method. 128 */ 129 public PasswordModifyExtendedOperation() 130 { 131 super(); 132 133 } 134 135 136 137 138 /** 139 * Initializes this extended operation handler based on the information in the 140 * provided configuration. It should also register itself with the 141 * Directory Server for the particular kinds of extended operations that it 142 * will process. 143 * 144 * @param config The configuration that contains the information 145 * to use to initialize this extended operation handler. 146 * 147 * @throws ConfigException If an unrecoverable problem arises in the 148 * process of performing the initialization. 149 * 150 * @throws InitializationException If a problem occurs during initialization 151 * that is not related to the server 152 * configuration. 153 */ 154 public void initializeExtendedOperationHandler( 155 PasswordModifyExtendedOperationHandlerCfg config) 156 throws ConfigException, InitializationException 157 { 158 try 159 { 160 identityMapperDN = config.getIdentityMapperDN(); 161 identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN); 162 if (identityMapper == null) 163 { 164 Message message = ERR_EXTOP_PASSMOD_NO_SUCH_ID_MAPPER.get( 165 String.valueOf(identityMapperDN), String.valueOf(config.dn())); 166 throw new ConfigException(message); 167 } 168 } 169 catch (Exception e) 170 { 171 if (debugEnabled()) 172 { 173 TRACER.debugCaught(DebugLogLevel.ERROR, e); 174 } 175 Message message = ERR_EXTOP_PASSMOD_CANNOT_DETERMINE_ID_MAPPER.get( 176 String.valueOf(config.dn()), getExceptionMessage(e)); 177 throw new InitializationException(message, e); 178 } 179 180 181 supportedControlOIDs = new HashSet<String>(); 182 supportedControlOIDs.add(OID_LDAP_NOOP_OPENLDAP_ASSIGNED); 183 supportedControlOIDs.add(OID_PASSWORD_POLICY_CONTROL); 184 185 186 // Save this configuration for future reference. 187 currentConfig = config; 188 189 // Register this as a change listener. 190 config.addPasswordModifyChangeListener(this); 191 192 DirectoryServer.registerSupportedExtension(OID_PASSWORD_MODIFY_REQUEST, 193 this); 194 195 registerControlsAndFeatures(); 196 } 197 198 199 200 /** 201 * Performs any finalization that may be necessary for this extended 202 * operation handler. By default, no finalization is performed. 203 */ 204 public void finalizeExtendedOperationHandler() 205 { 206 currentConfig.removePasswordModifyChangeListener(this); 207 208 DirectoryServer.deregisterSupportedExtension(OID_PASSWORD_MODIFY_REQUEST); 209 210 deregisterControlsAndFeatures(); 211 } 212 213 214 215 /** 216 * {@inheritDoc} 217 */ 218 @Override() 219 public Set<String> getSupportedControls() 220 { 221 return supportedControlOIDs; 222 } 223 224 225 226 /** 227 * Processes the provided extended operation. 228 * 229 * @param operation The extended operation to be processed. 230 */ 231 public void processExtendedOperation(ExtendedOperation operation) 232 { 233 // Initialize the variables associated with components that may be included 234 // in the request. 235 ByteString userIdentity = null; 236 ByteString oldPassword = null; 237 ByteString newPassword = null; 238 239 240 // Look at the set of controls included in the request, if there are any. 241 boolean noOpRequested = false; 242 boolean pwPolicyRequested = false; 243 int pwPolicyWarningValue = 0; 244 PasswordPolicyErrorType pwPolicyErrorType = null; 245 PasswordPolicyWarningType pwPolicyWarningType = null; 246 List<Control> controls = operation.getRequestControls(); 247 if (controls != null) 248 { 249 for (Control c : controls) 250 { 251 String oid = c.getOID(); 252 if (oid.equals(OID_LDAP_NOOP_OPENLDAP_ASSIGNED)) 253 { 254 noOpRequested = true; 255 } 256 else if (oid.equals(OID_PASSWORD_POLICY_CONTROL)) 257 { 258 pwPolicyRequested = true; 259 } 260 } 261 } 262 263 264 // Parse the encoded request, if there is one. 265 ByteString requestValue = operation.getRequestValue(); 266 if (requestValue != null) 267 { 268 try 269 { 270 ASN1Sequence requestSequence = 271 ASN1Sequence.decodeAsSequence(requestValue.value()); 272 273 for (ASN1Element e : requestSequence.elements()) 274 { 275 switch (e.getType()) 276 { 277 case TYPE_PASSWORD_MODIFY_USER_ID: 278 userIdentity = e.decodeAsOctetString(); 279 break; 280 case TYPE_PASSWORD_MODIFY_OLD_PASSWORD: 281 oldPassword = e.decodeAsOctetString(); 282 break; 283 case TYPE_PASSWORD_MODIFY_NEW_PASSWORD: 284 newPassword = e.decodeAsOctetString(); 285 break; 286 default: 287 operation.setResultCode(ResultCode.PROTOCOL_ERROR); 288 289 290 operation.appendErrorMessage( 291 ERR_EXTOP_PASSMOD_ILLEGAL_REQUEST_ELEMENT_TYPE.get( 292 byteToHex(e.getType()))); 293 return; 294 } 295 } 296 } 297 catch (ASN1Exception ae) 298 { 299 if (debugEnabled()) 300 { 301 TRACER.debugCaught(DebugLogLevel.ERROR, ae); 302 } 303 304 operation.setResultCode(ResultCode.PROTOCOL_ERROR); 305 306 Message message = ERR_EXTOP_PASSMOD_CANNOT_DECODE_REQUEST.get( 307 getExceptionMessage(ae)); 308 operation.appendErrorMessage(message); 309 310 return; 311 } 312 } 313 314 315 // Get the entry for the user that issued the request. 316 Entry requestorEntry = operation.getAuthorizationEntry(); 317 318 319 // See if a user identity was provided. If so, then try to resolve it to 320 // an actual user. 321 DN userDN = null; 322 Entry userEntry; 323 Lock userLock = null; 324 325 try 326 { 327 if (userIdentity == null) 328 { 329 // This request must be targeted at changing the password for the 330 // currently-authenticated user. Make sure that the user actually is 331 // authenticated. 332 ClientConnection clientConnection = operation.getClientConnection(); 333 AuthenticationInfo authInfo = clientConnection.getAuthenticationInfo(); 334 if ((! authInfo.isAuthenticated()) || (requestorEntry == null)) 335 { 336 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 337 338 operation.appendErrorMessage( 339 ERR_EXTOP_PASSMOD_NO_AUTH_OR_USERID.get()); 340 341 return; 342 } 343 344 345 // Retrieve a write lock on that user's entry. 346 userDN = requestorEntry.getDN(); 347 348 for (int i=0; i < 3; i++) 349 { 350 userLock = LockManager.lockWrite(userDN); 351 352 if (userLock != null) 353 { 354 break; 355 } 356 } 357 358 if (userLock == null) 359 { 360 operation.setResultCode(DirectoryServer.getServerErrorResultCode()); 361 362 Message message = 363 ERR_EXTOP_PASSMOD_CANNOT_LOCK_USER_ENTRY.get( 364 String.valueOf(userDN)); 365 operation.appendErrorMessage(message); 366 367 return; 368 } 369 370 371 userEntry = requestorEntry; 372 } 373 else 374 { 375 // There was a userIdentity section in the request. It should have 376 // started with either "dn:" to indicate that it contained a DN, or 377 // "u:" to indicate that it contained a user ID. 378 String authzIDStr = userIdentity.stringValue(); 379 String lowerAuthzIDStr = toLowerCase(authzIDStr); 380 if (lowerAuthzIDStr.startsWith("dn:")) 381 { 382 try 383 { 384 userDN = DN.decode(authzIDStr.substring(3)); 385 } 386 catch (DirectoryException de) 387 { 388 if (debugEnabled()) 389 { 390 TRACER.debugCaught(DebugLogLevel.ERROR, de); 391 } 392 393 operation.setResultCode(ResultCode.INVALID_DN_SYNTAX); 394 395 operation.appendErrorMessage( 396 ERR_EXTOP_PASSMOD_CANNOT_DECODE_AUTHZ_DN.get(authzIDStr)); 397 398 return; 399 } 400 401 // If the provided DN is an alternate DN for a root user, then replace 402 // it with the actual root DN. 403 DN actualRootDN = DirectoryServer.getActualRootBindDN(userDN); 404 if (actualRootDN != null) 405 { 406 userDN = actualRootDN; 407 } 408 409 userEntry = getEntryByDN(operation, userDN); 410 if (userEntry == null) 411 { 412 return; 413 } 414 } 415 else if (lowerAuthzIDStr.startsWith("u:")) 416 { 417 try 418 { 419 userEntry = identityMapper.getEntryForID(authzIDStr.substring(2)); 420 if (userEntry == null) 421 { 422 if (oldPassword == null) 423 { 424 operation.setResultCode(ResultCode.NO_SUCH_OBJECT); 425 426 operation.appendErrorMessage( 427 ERR_EXTOP_PASSMOD_CANNOT_MAP_USER.get(authzIDStr)); 428 } 429 else 430 { 431 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 432 433 operation.appendAdditionalLogMessage( 434 ERR_EXTOP_PASSMOD_CANNOT_MAP_USER.get(authzIDStr)); 435 } 436 437 return; 438 } 439 else 440 { 441 userDN = userEntry.getDN(); 442 } 443 } 444 catch (DirectoryException de) 445 { 446 if (debugEnabled()) 447 { 448 TRACER.debugCaught(DebugLogLevel.ERROR, de); 449 } 450 451 if (oldPassword == null) 452 { 453 operation.setResultCode(de.getResultCode()); 454 455 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_ERROR_MAPPING_USER 456 .get(authzIDStr,de.getMessageObject())); 457 } 458 else 459 { 460 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 461 462 operation.appendAdditionalLogMessage( 463 ERR_EXTOP_PASSMOD_ERROR_MAPPING_USER.get( 464 authzIDStr, 465 de.getMessageObject())); 466 } 467 468 return; 469 } 470 } 471 else 472 { 473 // The authorization ID was in an illegal format. 474 operation.setResultCode(ResultCode.PROTOCOL_ERROR); 475 476 operation.appendErrorMessage( 477 ERR_EXTOP_PASSMOD_INVALID_AUTHZID_STRING.get(authzIDStr)); 478 479 return; 480 } 481 } 482 483 484 // At this point, we should have the user entry. Get the associated 485 // password policy. 486 PasswordPolicyState pwPolicyState; 487 try 488 { 489 pwPolicyState = new PasswordPolicyState(userEntry, false); 490 } 491 catch (DirectoryException de) 492 { 493 if (debugEnabled()) 494 { 495 TRACER.debugCaught(DebugLogLevel.ERROR, de); 496 } 497 498 operation.setResultCode(DirectoryServer.getServerErrorResultCode()); 499 500 operation.appendErrorMessage( 501 ERR_EXTOP_PASSMOD_CANNOT_GET_PW_POLICY.get( 502 String.valueOf(userDN), 503 de.getMessageObject())); 504 return; 505 } 506 507 508 // Determine whether the user is changing his own password or if it's an 509 // administrative reset. If it's an administrative reset, then the 510 // requester must have the PASSWORD_RESET privilege. 511 boolean selfChange; 512 if (userIdentity == null) 513 { 514 selfChange = true; 515 } 516 else if (requestorEntry == null) 517 { 518 selfChange = (oldPassword != null); 519 } 520 else 521 { 522 selfChange = userDN.equals(requestorEntry.getDN()); 523 } 524 525 if (! selfChange) 526 { 527 ClientConnection clientConnection = operation.getClientConnection(); 528 if (! clientConnection.hasPrivilege(Privilege.PASSWORD_RESET, 529 operation)) 530 { 531 operation.appendErrorMessage( 532 ERR_EXTOP_PASSMOD_INSUFFICIENT_PRIVILEGES.get()); 533 operation.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS); 534 return; 535 } 536 } 537 538 539 // See if the account is locked. If so, then reject the request. 540 if (pwPolicyState.isDisabled()) 541 { 542 if (pwPolicyRequested) 543 { 544 pwPolicyErrorType = 545 PasswordPolicyErrorType.ACCOUNT_LOCKED; 546 operation.addResponseControl( 547 new PasswordPolicyResponseControl(pwPolicyWarningType, 548 pwPolicyWarningValue, 549 pwPolicyErrorType)); 550 } 551 552 Message message = ERR_EXTOP_PASSMOD_ACCOUNT_DISABLED.get(); 553 554 if (oldPassword == null) 555 { 556 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 557 operation.appendErrorMessage(message); 558 } 559 else 560 { 561 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 562 operation.appendAdditionalLogMessage(message); 563 } 564 565 return; 566 } 567 else if (selfChange && 568 (pwPolicyState.lockedDueToFailures() || 569 pwPolicyState.lockedDueToIdleInterval() || 570 pwPolicyState.lockedDueToMaximumResetAge())) 571 { 572 if (pwPolicyRequested) 573 { 574 pwPolicyErrorType = 575 PasswordPolicyErrorType.ACCOUNT_LOCKED; 576 operation.addResponseControl( 577 new PasswordPolicyResponseControl(pwPolicyWarningType, 578 pwPolicyWarningValue, 579 pwPolicyErrorType)); 580 } 581 582 Message message = ERR_EXTOP_PASSMOD_ACCOUNT_LOCKED.get(); 583 584 if (oldPassword == null) 585 { 586 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 587 operation.appendErrorMessage(message); 588 } 589 else 590 { 591 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 592 operation.appendAdditionalLogMessage(message); 593 } 594 595 return; 596 } 597 598 599 // If the current password was provided, then we'll need to verify whether 600 // it was correct. If it wasn't provided but this is a self change, then 601 // make sure that's OK. 602 if (oldPassword == null) 603 { 604 if (selfChange && pwPolicyState.getPolicy().requireCurrentPassword()) 605 { 606 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 607 608 operation.appendErrorMessage( 609 ERR_EXTOP_PASSMOD_REQUIRE_CURRENT_PW.get()); 610 611 if (pwPolicyRequested) 612 { 613 pwPolicyErrorType = 614 PasswordPolicyErrorType.MUST_SUPPLY_OLD_PASSWORD; 615 operation.addResponseControl( 616 new PasswordPolicyResponseControl(pwPolicyWarningType, 617 pwPolicyWarningValue, 618 pwPolicyErrorType)); 619 } 620 621 return; 622 } 623 } 624 else 625 { 626 if (pwPolicyState.getPolicy().requireSecureAuthentication() && 627 (! operation.getClientConnection().isSecure())) 628 { 629 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 630 631 operation.appendAdditionalLogMessage( 632 ERR_EXTOP_PASSMOD_SECURE_AUTH_REQUIRED.get()); 633 return; 634 } 635 636 if (pwPolicyState.passwordMatches(oldPassword)) 637 { 638 pwPolicyState.setLastLoginTime(); 639 } 640 else 641 { 642 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 643 644 operation.appendAdditionalLogMessage( 645 ERR_EXTOP_PASSMOD_INVALID_OLD_PASSWORD.get()); 646 647 pwPolicyState.updateAuthFailureTimes(); 648 List<Modification> mods = pwPolicyState.getModifications(); 649 if (! mods.isEmpty()) 650 { 651 InternalClientConnection conn = 652 InternalClientConnection.getRootConnection(); 653 conn.processModify(userDN, mods); 654 } 655 656 return; 657 } 658 } 659 660 661 // If it is a self password change and we don't allow that, then reject 662 // the request. 663 if (selfChange && 664 (! pwPolicyState.getPolicy().allowUserPasswordChanges())) 665 { 666 if (pwPolicyRequested) 667 { 668 pwPolicyErrorType = 669 PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED; 670 operation.addResponseControl( 671 new PasswordPolicyResponseControl(pwPolicyWarningType, 672 pwPolicyWarningValue, 673 pwPolicyErrorType)); 674 } 675 676 if (oldPassword == null) 677 { 678 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 679 680 operation.appendErrorMessage( 681 ERR_EXTOP_PASSMOD_USER_PW_CHANGES_NOT_ALLOWED.get()); 682 } 683 else 684 { 685 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 686 687 operation.appendAdditionalLogMessage( 688 ERR_EXTOP_PASSMOD_USER_PW_CHANGES_NOT_ALLOWED.get()); 689 } 690 691 return; 692 } 693 694 695 // If we require secure password changes and the connection isn't secure, 696 // then reject the request. 697 if (pwPolicyState.getPolicy().requireSecurePasswordChanges() && 698 (! operation.getClientConnection().isSecure())) 699 { 700 if (oldPassword == null) 701 { 702 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 703 704 operation.appendErrorMessage( 705 ERR_EXTOP_PASSMOD_SECURE_CHANGES_REQUIRED.get()); 706 } 707 else 708 { 709 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 710 711 operation.appendAdditionalLogMessage( 712 ERR_EXTOP_PASSMOD_SECURE_CHANGES_REQUIRED.get()); 713 } 714 715 return; 716 } 717 718 719 // If it's a self-change request and the user is within the minimum age, 720 // then reject it. 721 if (selfChange && pwPolicyState.isWithinMinimumAge()) 722 { 723 if (pwPolicyRequested) 724 { 725 pwPolicyErrorType = 726 PasswordPolicyErrorType.PASSWORD_TOO_YOUNG; 727 operation.addResponseControl( 728 new PasswordPolicyResponseControl(pwPolicyWarningType, 729 pwPolicyWarningValue, 730 pwPolicyErrorType)); 731 } 732 733 if (oldPassword == null) 734 { 735 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 736 737 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_IN_MIN_AGE.get()); 738 } 739 else 740 { 741 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 742 743 operation.appendAdditionalLogMessage( 744 ERR_EXTOP_PASSMOD_IN_MIN_AGE.get()); 745 } 746 747 return; 748 } 749 750 751 // If the user's password is expired and it's a self-change request, then 752 // see if that's OK. 753 if ((selfChange && pwPolicyState.isPasswordExpired() && 754 (! pwPolicyState.getPolicy().allowExpiredPasswordChanges()))) 755 { 756 if (pwPolicyRequested) 757 { 758 pwPolicyErrorType = 759 PasswordPolicyErrorType.PASSWORD_EXPIRED; 760 operation.addResponseControl( 761 new PasswordPolicyResponseControl(pwPolicyWarningType, 762 pwPolicyWarningValue, 763 pwPolicyErrorType)); 764 } 765 766 if (oldPassword == null) 767 { 768 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 769 770 operation.appendErrorMessage( 771 ERR_EXTOP_PASSMOD_PASSWORD_IS_EXPIRED.get()); 772 } 773 else 774 { 775 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 776 777 operation.appendAdditionalLogMessage( 778 ERR_EXTOP_PASSMOD_PASSWORD_IS_EXPIRED.get()); 779 } 780 781 return; 782 } 783 784 785 786 // If the a new password was provided, then peform any appropriate 787 // validation on it. If not, then see if we can generate one. 788 boolean generatedPassword = false; 789 boolean isPreEncoded = false; 790 if (newPassword == null) 791 { 792 try 793 { 794 newPassword = pwPolicyState.generatePassword(); 795 if (newPassword == null) 796 { 797 if (oldPassword == null) 798 { 799 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 800 801 operation.appendErrorMessage( 802 ERR_EXTOP_PASSMOD_NO_PW_GENERATOR.get()); 803 } 804 else 805 { 806 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 807 808 operation.appendAdditionalLogMessage( 809 ERR_EXTOP_PASSMOD_NO_PW_GENERATOR.get()); 810 } 811 812 return; 813 } 814 else 815 { 816 generatedPassword = true; 817 } 818 } 819 catch (DirectoryException de) 820 { 821 if (debugEnabled()) 822 { 823 TRACER.debugCaught(DebugLogLevel.ERROR, de); 824 } 825 826 if (oldPassword == null) 827 { 828 operation.setResultCode(de.getResultCode()); 829 830 operation.appendErrorMessage( 831 ERR_EXTOP_PASSMOD_CANNOT_GENERATE_PW.get( 832 de.getMessageObject())); 833 } 834 else 835 { 836 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 837 838 operation.appendAdditionalLogMessage( 839 ERR_EXTOP_PASSMOD_CANNOT_GENERATE_PW.get( 840 de.getMessageObject())); 841 } 842 843 return; 844 } 845 } 846 else 847 { 848 if (pwPolicyState.passwordIsPreEncoded(newPassword)) 849 { 850 // The password modify extended operation isn't intended to be invoked 851 // by an internal operation or during synchronization, so we don't 852 // need to check for those cases. 853 isPreEncoded = true; 854 if (! pwPolicyState.getPolicy().allowPreEncodedPasswords()) 855 { 856 if (oldPassword == null) 857 { 858 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 859 860 operation.appendErrorMessage( 861 ERR_EXTOP_PASSMOD_PRE_ENCODED_NOT_ALLOWED.get()); 862 } 863 else 864 { 865 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 866 867 operation.appendAdditionalLogMessage( 868 ERR_EXTOP_PASSMOD_PRE_ENCODED_NOT_ALLOWED.get()); 869 } 870 871 return; 872 } 873 } 874 else 875 { 876 // Run the new password through the set of password validators. 877 if (selfChange || 878 (! pwPolicyState.getPolicy().skipValidationForAdministrators())) 879 { 880 HashSet<ByteString> clearPasswords; 881 if (oldPassword == null) 882 { 883 clearPasswords = 884 new HashSet<ByteString>(pwPolicyState.getClearPasswords()); 885 } 886 else 887 { 888 clearPasswords = new HashSet<ByteString>(); 889 clearPasswords.add(oldPassword); 890 for (ByteString pw : pwPolicyState.getClearPasswords()) 891 { 892 if (! Arrays.equals(pw.value(), oldPassword.value())) 893 { 894 clearPasswords.add(pw); 895 } 896 } 897 } 898 899 MessageBuilder invalidReason = new MessageBuilder(); 900 if (! pwPolicyState.passwordIsAcceptable(operation, userEntry, 901 newPassword, 902 clearPasswords, 903 invalidReason)) 904 { 905 if (pwPolicyRequested) 906 { 907 pwPolicyErrorType = 908 PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY; 909 operation.addResponseControl( 910 new PasswordPolicyResponseControl(pwPolicyWarningType, 911 pwPolicyWarningValue, 912 pwPolicyErrorType)); 913 } 914 915 if (oldPassword == null) 916 { 917 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 918 919 operation.appendErrorMessage( 920 ERR_EXTOP_PASSMOD_UNACCEPTABLE_PW.get( 921 String.valueOf(invalidReason))); 922 } 923 else 924 { 925 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 926 927 operation.appendAdditionalLogMessage( 928 ERR_EXTOP_PASSMOD_UNACCEPTABLE_PW.get( 929 String.valueOf(invalidReason))); 930 } 931 932 return; 933 } 934 } 935 936 937 // Prepare to update the password history, if necessary. 938 if (pwPolicyState.maintainHistory()) 939 { 940 if (pwPolicyState.isPasswordInHistory(newPassword)) 941 { 942 if (oldPassword == null) 943 { 944 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 945 946 operation.appendErrorMessage( 947 ERR_EXTOP_PASSMOD_PW_IN_HISTORY.get()); 948 } 949 else 950 { 951 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 952 953 operation.appendAdditionalLogMessage( 954 ERR_EXTOP_PASSMOD_PW_IN_HISTORY.get()); 955 } 956 } 957 else 958 { 959 pwPolicyState.updatePasswordHistory(); 960 } 961 } 962 } 963 } 964 965 966 // Get the encoded forms of the new password. 967 List<ByteString> encodedPasswords; 968 if (isPreEncoded) 969 { 970 encodedPasswords = new ArrayList<ByteString>(1); 971 encodedPasswords.add(newPassword); 972 } 973 else 974 { 975 try 976 { 977 encodedPasswords = pwPolicyState.encodePassword(newPassword); 978 } 979 catch (DirectoryException de) 980 { 981 if (debugEnabled()) 982 { 983 TRACER.debugCaught(DebugLogLevel.ERROR, de); 984 } 985 986 if (oldPassword == null) 987 { 988 operation.setResultCode(de.getResultCode()); 989 990 operation.appendErrorMessage( 991 ERR_EXTOP_PASSMOD_CANNOT_ENCODE_PASSWORD.get( 992 de.getMessageObject())); 993 } 994 else 995 { 996 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 997 998 operation.appendAdditionalLogMessage( 999 ERR_EXTOP_PASSMOD_CANNOT_ENCODE_PASSWORD.get( 1000 de.getMessageObject())); 1001 } 1002 1003 return; 1004 } 1005 } 1006 1007 1008 // If the current password was provided, then remove all matching values 1009 // from the user's entry and replace them with the new password. 1010 // Otherwise replace all password values. 1011 AttributeType attrType = pwPolicyState.getPolicy().getPasswordAttribute(); 1012 List<Modification> modList = new ArrayList<Modification>(); 1013 if (oldPassword != null) 1014 { 1015 // Remove all existing encoded values that match the old password. 1016 LinkedHashSet<AttributeValue> existingValues = 1017 pwPolicyState.getPasswordValues(); 1018 LinkedHashSet<AttributeValue> deleteValues = 1019 new LinkedHashSet<AttributeValue>(existingValues.size()); 1020 if (pwPolicyState.getPolicy().usesAuthPasswordSyntax()) 1021 { 1022 for (AttributeValue v : existingValues) 1023 { 1024 try 1025 { 1026 StringBuilder[] components = 1027 AuthPasswordSyntax.decodeAuthPassword(v.getStringValue()); 1028 PasswordStorageScheme scheme = 1029 DirectoryServer.getAuthPasswordStorageScheme( 1030 components[0].toString()); 1031 if (scheme == null) 1032 { 1033 // The password is encoded using an unknown scheme. Remove it 1034 // from the user's entry. 1035 deleteValues.add(v); 1036 } 1037 else 1038 { 1039 if (scheme.authPasswordMatches(oldPassword, 1040 components[1].toString(), 1041 components[2].toString())) 1042 { 1043 deleteValues.add(v); 1044 } 1045 } 1046 } 1047 catch (DirectoryException de) 1048 { 1049 if (debugEnabled()) 1050 { 1051 TRACER.debugCaught(DebugLogLevel.ERROR, de); 1052 } 1053 1054 // We couldn't decode the provided password value, so remove it 1055 // from the user's entry. 1056 deleteValues.add(v); 1057 } 1058 } 1059 } 1060 else 1061 { 1062 for (AttributeValue v : existingValues) 1063 { 1064 try 1065 { 1066 String[] components = 1067 UserPasswordSyntax.decodeUserPassword(v.getStringValue()); 1068 PasswordStorageScheme scheme = 1069 DirectoryServer.getPasswordStorageScheme( 1070 toLowerCase(components[0])); 1071 if (scheme == null) 1072 { 1073 // The password is encoded using an unknown scheme. Remove it 1074 // from the user's entry. 1075 deleteValues.add(v); 1076 } 1077 else 1078 { 1079 if (scheme.passwordMatches(oldPassword, 1080 new ASN1OctetString(components[1]))) 1081 { 1082 deleteValues.add(v); 1083 } 1084 } 1085 } 1086 catch (DirectoryException de) 1087 { 1088 if (debugEnabled()) 1089 { 1090 TRACER.debugCaught(DebugLogLevel.ERROR, de); 1091 } 1092 1093 // We couldn't decode the provided password value, so remove it 1094 // from the user's entry. 1095 deleteValues.add(v); 1096 } 1097 } 1098 } 1099 1100 Attribute deleteAttr = new Attribute(attrType, attrType.getNameOrOID(), 1101 deleteValues); 1102 modList.add(new Modification(ModificationType.DELETE, deleteAttr)); 1103 1104 1105 // Add the new encoded values. 1106 LinkedHashSet<AttributeValue> addValues = 1107 new LinkedHashSet<AttributeValue>(encodedPasswords.size()); 1108 for (ByteString s : encodedPasswords) 1109 { 1110 addValues.add(new AttributeValue(attrType, s)); 1111 } 1112 1113 Attribute addAttr = new Attribute(attrType, attrType.getNameOrOID(), 1114 addValues); 1115 modList.add(new Modification(ModificationType.ADD, addAttr)); 1116 } 1117 else 1118 { 1119 LinkedHashSet<AttributeValue> replaceValues = 1120 new LinkedHashSet<AttributeValue>(encodedPasswords.size()); 1121 for (ByteString s : encodedPasswords) 1122 { 1123 replaceValues.add(new AttributeValue(attrType, s)); 1124 } 1125 1126 Attribute addAttr = new Attribute(attrType, attrType.getNameOrOID(), 1127 replaceValues); 1128 modList.add(new Modification(ModificationType.REPLACE, addAttr)); 1129 } 1130 1131 1132 // Update the password changed time for the user entry. 1133 pwPolicyState.setPasswordChangedTime(); 1134 1135 1136 // If the password was changed by an end user, then clear any reset flag 1137 // that might exist. If the password was changed by an administrator, 1138 // then see if we need to set the reset flag. 1139 if (selfChange) 1140 { 1141 pwPolicyState.setMustChangePassword(false); 1142 } 1143 else 1144 { 1145 pwPolicyState.setMustChangePassword( 1146 pwPolicyState.getPolicy().forceChangeOnReset()); 1147 } 1148 1149 1150 // Clear any record of grace logins, auth failures, and expiration 1151 // warnings. 1152 pwPolicyState.clearFailureLockout(); 1153 pwPolicyState.clearGraceLoginTimes(); 1154 pwPolicyState.clearWarnedTime(); 1155 1156 1157 // If the LDAP no-op control was included in the request, then set the 1158 // appropriate response. Otherwise, process the operation. 1159 if (noOpRequested) 1160 { 1161 operation.appendErrorMessage(WARN_EXTOP_PASSMOD_NOOP.get()); 1162 1163 operation.setResultCode(ResultCode.NO_OPERATION); 1164 } 1165 else 1166 { 1167 if (selfChange && (requestorEntry == null)) 1168 { 1169 requestorEntry = userEntry; 1170 } 1171 1172 // Get an internal connection and use it to perform the modification. 1173 boolean isRoot = DirectoryServer.isRootDN(requestorEntry.getDN()); 1174 AuthenticationInfo authInfo = new AuthenticationInfo(requestorEntry, 1175 isRoot); 1176 InternalClientConnection internalConnection = new 1177 InternalClientConnection(authInfo); 1178 1179 ModifyOperation modifyOperation = 1180 internalConnection.processModify(userDN, modList); 1181 ResultCode resultCode = modifyOperation.getResultCode(); 1182 if (resultCode != ResultCode.SUCCESS) 1183 { 1184 operation.setResultCode(resultCode); 1185 operation.setErrorMessage(modifyOperation.getErrorMessage()); 1186 operation.setReferralURLs(modifyOperation.getReferralURLs()); 1187 return; 1188 } 1189 1190 1191 // If there were any password policy state changes, we need to apply 1192 // them using a root connection because the end user may not have 1193 // sufficient access to apply them. This is less efficient than 1194 // doing them all in the same modification, but it's safer. 1195 List<Modification> pwPolicyMods = pwPolicyState.getModifications(); 1196 if (! pwPolicyMods.isEmpty()) 1197 { 1198 InternalClientConnection rootConnection = 1199 InternalClientConnection.getRootConnection(); 1200 ModifyOperation modOp = 1201 rootConnection.processModify(userDN, pwPolicyMods); 1202 if (modOp.getResultCode() != ResultCode.SUCCESS) 1203 { 1204 // At this point, the user's password is already changed so there's 1205 // not much point in returning a non-success result. However, we 1206 // should at least log that something went wrong. 1207 Message message = WARN_EXTOP_PASSMOD_CANNOT_UPDATE_PWP_STATE.get( 1208 String.valueOf(userDN), 1209 String.valueOf(modOp.getResultCode()), 1210 modOp.getErrorMessage()); 1211 ErrorLogger.logError(message); 1212 } 1213 } 1214 1215 1216 // If we've gotten here, then everything is OK, so indicate that the 1217 // operation was successful. If a password was generated, then include 1218 // it in the response. 1219 operation.setResultCode(ResultCode.SUCCESS); 1220 1221 if (generatedPassword) 1222 { 1223 ArrayList<ASN1Element> valueElements = new ArrayList<ASN1Element>(1); 1224 1225 ASN1OctetString newPWString = 1226 new ASN1OctetString(TYPE_PASSWORD_MODIFY_GENERATED_PASSWORD, 1227 newPassword.value()); 1228 valueElements.add(newPWString); 1229 1230 ASN1Sequence valueSequence = new ASN1Sequence(valueElements); 1231 operation.setResponseValue(new ASN1OctetString( 1232 valueSequence.encode())); 1233 } 1234 1235 1236 // If this was a self password change, and the client is authenticated 1237 // as the user whose password was changed, then clear the "must change 1238 // password" flag in the client connection. Note that we're using the 1239 // authentication DN rather than the authorization DN in this case to 1240 // avoid mistakenly clearing the flag for the wrong user. 1241 if (selfChange && (authInfo.getAuthenticationDN() != null) && 1242 (authInfo.getAuthenticationDN().equals(userDN))) 1243 { 1244 operation.getClientConnection().setMustChangePassword(false); 1245 } 1246 1247 1248 // If the password policy control was requested, then add the 1249 // appropriate response control. 1250 if (pwPolicyRequested) 1251 { 1252 operation.addResponseControl( 1253 new PasswordPolicyResponseControl(pwPolicyWarningType, 1254 pwPolicyWarningValue, 1255 pwPolicyErrorType)); 1256 } 1257 } 1258 } 1259 finally 1260 { 1261 if (userLock != null) 1262 { 1263 LockManager.unlock(userDN, userLock); 1264 } 1265 } 1266 } 1267 1268 1269 1270 /** 1271 * Retrieves the entry for the specified user based on the provided DN. If 1272 * any problem is encountered or the requested entry does not exist, then the 1273 * provided operation will be updated with appropriate result information and 1274 * this method will return <CODE>null</CODE>. The caller must hold a write 1275 * lock on the specified entry. 1276 * 1277 * @param operation The extended operation being processed. 1278 * @param entryDN The DN of the user entry to retrieve. 1279 * 1280 * @return The requested entry, or <CODE>null</CODE> if there was no such 1281 * entry or it could not be retrieved. 1282 */ 1283 private Entry getEntryByDN(ExtendedOperation operation, DN entryDN) 1284 { 1285 // Retrieve the user's entry from the directory. If it does not exist, then 1286 // fail. 1287 try 1288 { 1289 Entry userEntry = DirectoryServer.getEntry(entryDN); 1290 1291 if (userEntry == null) 1292 { 1293 operation.setResultCode(ResultCode.NO_SUCH_OBJECT); 1294 1295 operation.appendErrorMessage( 1296 ERR_EXTOP_PASSMOD_NO_USER_ENTRY_BY_AUTHZID.get( 1297 String.valueOf(entryDN))); 1298 1299 // See if one of the entry's ancestors exists. 1300 DN parentDN = entryDN.getParentDNInSuffix(); 1301 while (parentDN != null) 1302 { 1303 try 1304 { 1305 if (DirectoryServer.entryExists(parentDN)) 1306 { 1307 operation.setMatchedDN(parentDN); 1308 break; 1309 } 1310 } 1311 catch (Exception e) 1312 { 1313 if (debugEnabled()) 1314 { 1315 TRACER.debugCaught(DebugLogLevel.ERROR, e); 1316 } 1317 break; 1318 } 1319 1320 parentDN = parentDN.getParentDNInSuffix(); 1321 } 1322 1323 return null; 1324 } 1325 1326 return userEntry; 1327 } 1328 catch (DirectoryException de) 1329 { 1330 if (debugEnabled()) 1331 { 1332 TRACER.debugCaught(DebugLogLevel.ERROR, de); 1333 } 1334 1335 operation.setResultCode(de.getResultCode()); 1336 operation.appendErrorMessage(de.getMessageObject()); 1337 operation.setMatchedDN(de.getMatchedDN()); 1338 operation.setReferralURLs(de.getReferralURLs()); 1339 1340 return null; 1341 } 1342 } 1343 1344 1345 1346 /** 1347 * {@inheritDoc} 1348 */ 1349 @Override() 1350 public boolean isConfigurationAcceptable(ExtendedOperationHandlerCfg 1351 configuration, 1352 List<Message> unacceptableReasons) 1353 { 1354 PasswordModifyExtendedOperationHandlerCfg config = 1355 (PasswordModifyExtendedOperationHandlerCfg) configuration; 1356 return isConfigurationChangeAcceptable(config, unacceptableReasons); 1357 } 1358 1359 1360 1361 /** 1362 * Indicates whether the provided configuration entry has an acceptable 1363 * configuration for this component. If it does not, then detailed 1364 * information about the problem(s) should be added to the provided list. 1365 * 1366 * @param config The configuration entry for which to make the 1367 * determination. 1368 * @param unacceptableReasons A list that can be used to hold messages about 1369 * why the provided entry does not have an 1370 * acceptable configuration. 1371 * 1372 * @return <CODE>true</CODE> if the provided entry has an acceptable 1373 * configuration for this component, or <CODE>false</CODE> if not. 1374 */ 1375 public boolean isConfigurationChangeAcceptable( 1376 PasswordModifyExtendedOperationHandlerCfg config, 1377 List<Message> unacceptableReasons) 1378 { 1379 // Make sure that the specified identity mapper is OK. 1380 try 1381 { 1382 DN mapperDN = config.getIdentityMapperDN(); 1383 IdentityMapper mapper = DirectoryServer.getIdentityMapper(mapperDN); 1384 if (mapper == null) 1385 { 1386 Message message = ERR_EXTOP_PASSMOD_NO_SUCH_ID_MAPPER.get( 1387 String.valueOf(mapperDN), 1388 String.valueOf(config.dn())); 1389 unacceptableReasons.add(message); 1390 return false; 1391 } 1392 } 1393 catch (Exception e) 1394 { 1395 if (debugEnabled()) 1396 { 1397 TRACER.debugCaught(DebugLogLevel.ERROR, e); 1398 } 1399 1400 Message message = ERR_EXTOP_PASSMOD_CANNOT_DETERMINE_ID_MAPPER.get( 1401 String.valueOf(config.dn()), 1402 getExceptionMessage(e)); 1403 unacceptableReasons.add(message); 1404 return false; 1405 } 1406 1407 1408 // If we've gotten here, then everything is OK. 1409 return true; 1410 } 1411 1412 1413 1414 /** 1415 * Makes a best-effort attempt to apply the configuration contained in the 1416 * provided entry. Information about the result of this processing should be 1417 * added to the provided message list. Information should always be added to 1418 * this list if a configuration change could not be applied. If detailed 1419 * results are requested, then information about the changes applied 1420 * successfully (and optionally about parameters that were not changed) should 1421 * also be included. 1422 * 1423 * @param config The entry containing the new configuration to 1424 * apply for this component. 1425 * 1426 * @return Information about the result of the configuration update. 1427 */ 1428 public ConfigChangeResult applyConfigurationChange( 1429 PasswordModifyExtendedOperationHandlerCfg config) 1430 { 1431 ResultCode resultCode = ResultCode.SUCCESS; 1432 boolean adminActionRequired = false; 1433 ArrayList<Message> messages = new ArrayList<Message>(); 1434 1435 1436 // Make sure that the specified identity mapper is OK. 1437 DN mapperDN = null; 1438 IdentityMapper mapper = null; 1439 try 1440 { 1441 mapperDN = config.getIdentityMapperDN(); 1442 mapper = DirectoryServer.getIdentityMapper(mapperDN); 1443 if (mapper == null) 1444 { 1445 resultCode = ResultCode.CONSTRAINT_VIOLATION; 1446 1447 messages.add(ERR_EXTOP_PASSMOD_NO_SUCH_ID_MAPPER.get( 1448 String.valueOf(mapperDN), 1449 String.valueOf(config.dn()))); 1450 } 1451 } 1452 catch (Exception e) 1453 { 1454 if (debugEnabled()) 1455 { 1456 TRACER.debugCaught(DebugLogLevel.ERROR, e); 1457 } 1458 1459 resultCode = DirectoryServer.getServerErrorResultCode(); 1460 1461 messages.add(ERR_EXTOP_PASSMOD_CANNOT_DETERMINE_ID_MAPPER.get( 1462 String.valueOf(config.dn()), 1463 getExceptionMessage(e))); 1464 } 1465 1466 1467 // If all of the changes were acceptable, then apply them. 1468 if (resultCode == ResultCode.SUCCESS) 1469 { 1470 if (! identityMapperDN.equals(mapperDN)) 1471 { 1472 identityMapper = mapper; 1473 identityMapperDN = mapperDN; 1474 } 1475 } 1476 1477 1478 // Save this configuration for future reference. 1479 currentConfig = config; 1480 1481 return new ConfigChangeResult(resultCode, adminActionRequired, messages); 1482 } 1483 } 1484