001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2006-2010 Sun Microsystems, Inc. 025 * Portions Copyright 2011-2015 ForgeRock AS 026 */ 027package org.opends.server.extensions; 028 029import static org.opends.messages.CoreMessages.*; 030import static org.opends.messages.ExtensionMessages.*; 031import static org.opends.server.extensions.ExtensionsConstants.*; 032import static org.opends.server.protocols.internal.InternalClientConnection.*; 033import static org.opends.server.types.AccountStatusNotificationType.*; 034import static org.opends.server.util.CollectionUtils.*; 035import static org.opends.server.util.ServerConstants.*; 036import static org.opends.server.util.StaticUtils.*; 037 038import java.io.IOException; 039import java.util.*; 040 041import org.forgerock.i18n.LocalizableMessage; 042import org.forgerock.i18n.LocalizableMessageBuilder; 043import org.forgerock.i18n.slf4j.LocalizedLogger; 044import org.forgerock.opendj.config.server.ConfigChangeResult; 045import org.forgerock.opendj.config.server.ConfigException; 046import org.forgerock.opendj.io.ASN1; 047import org.forgerock.opendj.io.ASN1Reader; 048import org.forgerock.opendj.io.ASN1Writer; 049import org.forgerock.opendj.ldap.ByteString; 050import org.forgerock.opendj.ldap.ByteStringBuilder; 051import org.forgerock.opendj.ldap.ModificationType; 052import org.forgerock.opendj.ldap.ResultCode; 053import org.opends.server.admin.server.ConfigurationChangeListener; 054import org.opends.server.admin.std.server.ExtendedOperationHandlerCfg; 055import org.opends.server.admin.std.server.PasswordModifyExtendedOperationHandlerCfg; 056import org.opends.server.api.*; 057import org.opends.server.controls.PasswordPolicyErrorType; 058import org.opends.server.controls.PasswordPolicyResponseControl; 059import org.opends.server.controls.PasswordPolicyWarningType; 060import org.opends.server.core.DirectoryServer; 061import org.opends.server.core.ExtendedOperation; 062import org.opends.server.core.ModifyOperation; 063import org.opends.server.core.PasswordPolicyState; 064import org.opends.server.protocols.internal.InternalClientConnection; 065import org.opends.server.schema.AuthPasswordSyntax; 066import org.opends.server.schema.UserPasswordSyntax; 067import org.opends.server.types.*; 068import org.opends.server.types.LockManager.DNLock; 069 070/** 071 * This class implements the password modify extended operation defined in RFC 072 * 3062. It includes support for requiring the user's current password as well 073 * as for generating a new password if none was provided. 074 */ 075public class PasswordModifyExtendedOperation 076 extends ExtendedOperationHandler<PasswordModifyExtendedOperationHandlerCfg> 077 implements ConfigurationChangeListener<PasswordModifyExtendedOperationHandlerCfg> 078{ 079 // The following attachments may be used by post-op plugins (e.g. Samba) in 080 // order to avoid re-decoding the request parameters and also to enforce 081 // atomicity. 082 083 /** The name of the attachment which will be used to store the fully resolved target entry. */ 084 public static final String AUTHZ_DN_ATTACHMENT; 085 086 /** The name of the attachment which will be used to store the password attribute. */ 087 public static final String PWD_ATTRIBUTE_ATTACHMENT; 088 089 /** The clear text password, which may not be present if the provided password was pre-encoded. */ 090 public static final String CLEAR_PWD_ATTACHMENT; 091 092 /** A list containing the encoded passwords: plugins can perform changes atomically via CAS. */ 093 public static final String ENCODED_PWD_ATTACHMENT; 094 095 static 096 { 097 final String PREFIX = PasswordModifyExtendedOperation.class.getName(); 098 AUTHZ_DN_ATTACHMENT = PREFIX + ".AUTHZ_DN"; 099 PWD_ATTRIBUTE_ATTACHMENT = PREFIX + ".PWD_ATTRIBUTE"; 100 CLEAR_PWD_ATTACHMENT = PREFIX + ".CLEAR_PWD"; 101 ENCODED_PWD_ATTACHMENT = PREFIX + ".ENCODED_PWD"; 102 } 103 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 104 105 /** The current configuration state. */ 106 private PasswordModifyExtendedOperationHandlerCfg currentConfig; 107 108 /** The DN of the identity mapper. */ 109 private DN identityMapperDN; 110 111 /** The reference to the identity mapper. */ 112 private IdentityMapper<?> identityMapper; 113 114 115 /** 116 * Create an instance of this password modify extended operation. All initialization should be performed in the 117 * <CODE>initializeExtendedOperationHandler</CODE> method. 118 */ 119 public PasswordModifyExtendedOperation() 120 { 121 super(newHashSet(OID_LDAP_NOOP_OPENLDAP_ASSIGNED, OID_PASSWORD_POLICY_CONTROL)); 122 } 123 124 125 /** 126 * Initializes this extended operation handler based on the information in the provided configuration. 127 * It should also register itself with the Directory Server for the particular kinds of extended operations 128 * that it will process. 129 * 130 * @param config The configuration that contains the information 131 * to use to initialize this extended operation handler. 132 * 133 * @throws ConfigException If an unrecoverable problem arises in the 134 * process of performing the initialization. 135 * 136 * @throws InitializationException If a problem occurs during initialization 137 * that is not related to the server configuration. 138 */ 139 @Override 140 public void initializeExtendedOperationHandler(PasswordModifyExtendedOperationHandlerCfg config) 141 throws ConfigException, InitializationException 142 { 143 try 144 { 145 identityMapperDN = config.getIdentityMapperDN(); 146 identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN); 147 if (identityMapper == null) 148 { 149 LocalizableMessage message = ERR_EXTOP_PASSMOD_NO_SUCH_ID_MAPPER.get(identityMapperDN, config.dn()); 150 throw new ConfigException(message); 151 } 152 } 153 catch (Exception e) 154 { 155 logger.traceException(e); 156 LocalizableMessage message = ERR_EXTOP_PASSMOD_CANNOT_DETERMINE_ID_MAPPER 157 .get(config.dn(), getExceptionMessage(e)); 158 throw new InitializationException(message, e); 159 } 160 161 // Save this configuration for future reference. 162 currentConfig = config; 163 164 // Register this as a change listener. 165 config.addPasswordModifyChangeListener(this); 166 167 super.initializeExtendedOperationHandler(config); 168 } 169 170 171 /** 172 * Performs any finalization that may be necessary for this extended operation handler. 173 * By default, no finalization is performed. 174 */ 175 @Override 176 public void finalizeExtendedOperationHandler() 177 { 178 currentConfig.removePasswordModifyChangeListener(this); 179 180 super.finalizeExtendedOperationHandler(); 181 } 182 183 184 /** 185 * Processes the provided extended operation. 186 * 187 * @param operation The extended operation to be processed. 188 */ 189 @Override 190 public void processExtendedOperation(ExtendedOperation operation) 191 { 192 // Initialize the variables associated with components that may be included in the request. 193 ByteString userIdentity = null; 194 ByteString oldPassword = null; 195 ByteString newPassword = null; 196 197 // Look at the set of controls included in the request, if there are any. 198 boolean noOpRequested = false; 199 boolean pwPolicyRequested = false; 200 int pwPolicyWarningValue = 0; 201 PasswordPolicyErrorType pwPolicyErrorType = null; 202 PasswordPolicyWarningType pwPolicyWarningType = null; 203 List<Control> controls = operation.getRequestControls(); 204 if (controls != null) 205 { 206 for (Control c : controls) 207 { 208 String oid = c.getOID(); 209 if (OID_LDAP_NOOP_OPENLDAP_ASSIGNED.equals(oid)) 210 { 211 noOpRequested = true; 212 } 213 else if (OID_PASSWORD_POLICY_CONTROL.equals(oid)) 214 { 215 pwPolicyRequested = true; 216 } 217 } 218 } 219 220 // Parse the encoded request, if there is one. 221 ByteString requestValue = operation.getRequestValue(); 222 if (requestValue != null) 223 { 224 try 225 { 226 ASN1Reader reader = ASN1.getReader(requestValue); 227 reader.readStartSequence(); 228 if(reader.hasNextElement() && reader.peekType() == TYPE_PASSWORD_MODIFY_USER_ID) 229 { 230 userIdentity = reader.readOctetString(); 231 } 232 if(reader.hasNextElement() && reader.peekType() == TYPE_PASSWORD_MODIFY_OLD_PASSWORD) 233 { 234 oldPassword = reader.readOctetString(); 235 } 236 if(reader.hasNextElement() && reader.peekType() == TYPE_PASSWORD_MODIFY_NEW_PASSWORD) 237 { 238 newPassword = reader.readOctetString(); 239 } 240 reader.readEndSequence(); 241 } 242 catch (Exception ae) 243 { 244 logger.traceException(ae); 245 246 operation.setResultCode(ResultCode.PROTOCOL_ERROR); 247 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_CANNOT_DECODE_REQUEST.get(getExceptionMessage(ae))); 248 return; 249 } 250 } 251 252 // Get the entry for the user that issued the request. 253 Entry requestorEntry = operation.getAuthorizationEntry(); 254 255 // See if a user identity was provided. If so, then try to resolve it to 256 // an actual user. 257 DN userDN = null; 258 Entry userEntry = null; 259 DNLock userLock = null; 260 try 261 { 262 if (userIdentity == null) 263 { 264 // This request must be targeted at changing the password for the currently-authenticated user. 265 // Make sure that the user actually is authenticated. 266 ClientConnection clientConnection = operation.getClientConnection(); 267 AuthenticationInfo authInfo = clientConnection.getAuthenticationInfo(); 268 if (!authInfo.isAuthenticated() || requestorEntry == null) 269 { 270 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 271 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_NO_AUTH_OR_USERID.get()); 272 return; 273 } 274 275 userDN = requestorEntry.getName(); 276 userEntry = requestorEntry; 277 } 278 else 279 { 280 // There was a userIdentity field in the request. 281 String authzIDStr = userIdentity.toString(); 282 String lowerAuthzIDStr = toLowerCase(authzIDStr); 283 if (lowerAuthzIDStr.startsWith("dn:")) 284 { 285 try 286 { 287 userDN = DN.valueOf(authzIDStr.substring(3)); 288 } 289 catch (DirectoryException de) 290 { 291 logger.traceException(de); 292 293 operation.setResultCode(ResultCode.INVALID_DN_SYNTAX); 294 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_CANNOT_DECODE_AUTHZ_DN.get(authzIDStr)); 295 return; 296 } 297 298 // If the provided DN is an alternate DN for a root user, then replace it with the actual root DN. 299 DN actualRootDN = DirectoryServer.getActualRootBindDN(userDN); 300 if (actualRootDN != null) 301 { 302 userDN = actualRootDN; 303 } 304 305 userEntry = getEntryByDN(operation, userDN); 306 if (userEntry == null) 307 { 308 return; 309 } 310 } 311 else if (lowerAuthzIDStr.startsWith("u:")) 312 { 313 try 314 { 315 userEntry = identityMapper.getEntryForID(authzIDStr.substring(2)); 316 if (userEntry == null) 317 { 318 operation.setResultCode(ResultCode.NO_SUCH_OBJECT); 319 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_CANNOT_MAP_USER.get(authzIDStr)); 320 return; 321 } 322 323 userDN = userEntry.getName(); 324 } 325 catch (DirectoryException de) 326 { 327 logger.traceException(de); 328 329 //Encountered an exception while resolving identity. 330 operation.setResultCode(de.getResultCode()); 331 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_ERROR_MAPPING_USER.get(authzIDStr, de.getMessageObject())); 332 return; 333 } 334 } 335 else 336 { 337 /* 338 * the userIdentity provided does not follow Authorization Identity form. RFC3062 339 * declaration "may or may not be an LDAPDN" allows for pretty much anything in that 340 * field. we gonna try to parse it as DN first then if that fails as user ID. 341 */ 342 try 343 { 344 userDN = DN.valueOf(authzIDStr); 345 } 346 catch (DirectoryException ignored) 347 { 348 logger.traceException(ignored); 349 } 350 351 if (userDN != null && !userDN.isRootDN()) { 352 // If the provided DN is an alternate DN for a root user, then replace it with the actual root DN. 353 DN actualRootDN = DirectoryServer.getActualRootBindDN(userDN); 354 if (actualRootDN != null) { 355 userDN = actualRootDN; 356 } 357 userEntry = getEntryByDN(operation, userDN); 358 } else { 359 try 360 { 361 userEntry = identityMapper.getEntryForID(authzIDStr); 362 } 363 catch (DirectoryException ignored) 364 { 365 logger.traceException(ignored); 366 } 367 } 368 369 if (userEntry == null) { 370 // The userIdentity was invalid. 371 operation.setResultCode(ResultCode.PROTOCOL_ERROR); 372 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_INVALID_AUTHZID_STRING.get(authzIDStr)); 373 return; 374 } 375 376 userDN = userEntry.getName(); 377 } 378 } 379 380 userLock = DirectoryServer.getLockManager().tryWriteLockEntry(userDN); 381 if (userLock == null) 382 { 383 operation.setResultCode(ResultCode.BUSY); 384 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_CANNOT_LOCK_USER_ENTRY.get(userDN)); 385 return; 386 } 387 388 // At this point, we should have the user entry. Get the associated password policy. 389 PasswordPolicyState pwPolicyState; 390 try 391 { 392 AuthenticationPolicy policy = AuthenticationPolicy.forUser(userEntry, false); 393 if (!policy.isPasswordPolicy()) 394 { 395 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 396 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_ACCOUNT_NOT_LOCAL.get(userDN)); 397 return; 398 } 399 pwPolicyState = (PasswordPolicyState) policy.createAuthenticationPolicyState(userEntry); 400 } 401 catch (DirectoryException de) 402 { 403 logger.traceException(de); 404 405 operation.setResultCode(DirectoryServer.getServerErrorResultCode()); 406 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_CANNOT_GET_PW_POLICY.get(userDN, de.getMessageObject())); 407 return; 408 } 409 410 // Determine whether the user is changing his own password or if it's an administrative reset. 411 // If it's an administrative reset, then the requester must have the PASSWORD_RESET privilege. 412 boolean selfChange = isSelfChange(userIdentity, requestorEntry, userDN, oldPassword); 413 414 if (! selfChange) 415 { 416 ClientConnection clientConnection = operation.getClientConnection(); 417 if (! clientConnection.hasPrivilege(Privilege.PASSWORD_RESET, operation)) 418 { 419 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_INSUFFICIENT_PRIVILEGES.get()); 420 operation.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS); 421 return; 422 } 423 } 424 425 // See if the account is locked. If so, then reject the request. 426 if (pwPolicyState.isDisabled()) 427 { 428 if (pwPolicyRequested) 429 { 430 pwPolicyErrorType = PasswordPolicyErrorType.ACCOUNT_LOCKED; 431 operation.addResponseControl( 432 new PasswordPolicyResponseControl(pwPolicyWarningType, pwPolicyWarningValue, pwPolicyErrorType)); 433 } 434 435 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 436 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_ACCOUNT_DISABLED.get()); 437 return; 438 } 439 else if (selfChange && pwPolicyState.isLocked()) 440 { 441 if (pwPolicyRequested) 442 { 443 pwPolicyErrorType = PasswordPolicyErrorType.ACCOUNT_LOCKED; 444 operation.addResponseControl( 445 new PasswordPolicyResponseControl(pwPolicyWarningType, pwPolicyWarningValue, pwPolicyErrorType)); 446 } 447 448 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 449 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_ACCOUNT_LOCKED.get()); 450 return; 451 } 452 453 // If the current password was provided, then we'll need to verify whether it was correct. 454 // If it wasn't provided but this is a self change, then make sure that's OK. 455 if (oldPassword == null) 456 { 457 if (selfChange 458 && pwPolicyState.getAuthenticationPolicy().isPasswordChangeRequiresCurrentPassword()) 459 { 460 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 461 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_REQUIRE_CURRENT_PW.get()); 462 463 if (pwPolicyRequested) 464 { 465 pwPolicyErrorType = PasswordPolicyErrorType.MUST_SUPPLY_OLD_PASSWORD; 466 operation.addResponseControl( 467 new PasswordPolicyResponseControl(pwPolicyWarningType, pwPolicyWarningValue, pwPolicyErrorType)); 468 } 469 470 return; 471 } 472 } 473 else 474 { 475 if (pwPolicyState.getAuthenticationPolicy().isRequireSecureAuthentication() 476 && !operation.getClientConnection().isSecure()) 477 { 478 operation.setResultCode(ResultCode.CONFIDENTIALITY_REQUIRED); 479 operation.addAdditionalLogItem(AdditionalLogItem.quotedKeyValue(getClass(), "additionalInfo", 480 ERR_EXTOP_PASSMOD_SECURE_AUTH_REQUIRED.get())); 481 return; 482 } 483 484 if (pwPolicyState.passwordMatches(oldPassword)) 485 { 486 pwPolicyState.setLastLoginTime(); 487 } 488 else 489 { 490 operation.setResultCode(ResultCode.INVALID_CREDENTIALS); 491 operation.addAdditionalLogItem(AdditionalLogItem.quotedKeyValue(getClass(), "additionalInfo", 492 ERR_EXTOP_PASSMOD_INVALID_OLD_PASSWORD.get())); 493 494 pwPolicyState.updateAuthFailureTimes(); 495 List<Modification> mods = pwPolicyState.getModifications(); 496 if (! mods.isEmpty()) 497 { 498 getRootConnection().processModify(userDN, mods); 499 } 500 501 return; 502 } 503 } 504 505 // If it is a self password change and we don't allow that, then reject the request. 506 if (selfChange 507 && !pwPolicyState.getAuthenticationPolicy().isAllowUserPasswordChanges()) 508 { 509 if (pwPolicyRequested) 510 { 511 pwPolicyErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED; 512 operation.addResponseControl( 513 new PasswordPolicyResponseControl(pwPolicyWarningType, pwPolicyWarningValue, pwPolicyErrorType)); 514 } 515 516 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 517 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_USER_PW_CHANGES_NOT_ALLOWED.get()); 518 return; 519 } 520 521 // If we require secure password changes and the connection isn't secure, then reject the request. 522 if (pwPolicyState.getAuthenticationPolicy().isRequireSecurePasswordChanges() 523 && !operation.getClientConnection().isSecure()) 524 { 525 operation.setResultCode(ResultCode.CONFIDENTIALITY_REQUIRED); 526 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_SECURE_CHANGES_REQUIRED.get()); 527 return; 528 } 529 530 // If it's a self-change request and the user is within the minimum age, then reject it. 531 if (selfChange && pwPolicyState.isWithinMinimumAge()) 532 { 533 if (pwPolicyRequested) 534 { 535 pwPolicyErrorType = PasswordPolicyErrorType.PASSWORD_TOO_YOUNG; 536 operation.addResponseControl( 537 new PasswordPolicyResponseControl(pwPolicyWarningType, pwPolicyWarningValue, pwPolicyErrorType)); 538 } 539 540 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 541 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_IN_MIN_AGE.get()); 542 return; 543 } 544 545 // If the user's password is expired and it's a self-change request, then see if that's OK. 546 if (selfChange 547 && pwPolicyState.isPasswordExpired() 548 && !pwPolicyState.getAuthenticationPolicy().isAllowExpiredPasswordChanges()) 549 { 550 if (pwPolicyRequested) 551 { 552 pwPolicyErrorType = PasswordPolicyErrorType.PASSWORD_EXPIRED; 553 operation.addResponseControl( 554 new PasswordPolicyResponseControl(pwPolicyWarningType, pwPolicyWarningValue, pwPolicyErrorType)); 555 } 556 557 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 558 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_PASSWORD_IS_EXPIRED.get()); 559 return; 560 } 561 562 // If the a new password was provided, then perform any appropriate validation on it. 563 // If not, then see if we can generate one. 564 boolean generatedPassword = false; 565 boolean isPreEncoded = false; 566 if (newPassword == null) 567 { 568 try 569 { 570 newPassword = pwPolicyState.generatePassword(); 571 if (newPassword == null) 572 { 573 operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); 574 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_NO_PW_GENERATOR.get()); 575 return; 576 } 577 578 generatedPassword = true; 579 } 580 catch (DirectoryException de) 581 { 582 logger.traceException(de); 583 operation.setResultCode(de.getResultCode()); 584 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_CANNOT_GENERATE_PW.get(de.getMessageObject())); 585 return; 586 } 587 // Prepare to update the password history, if necessary. 588 if (pwPolicyState.maintainHistory()) 589 { 590 if (pwPolicyState.isPasswordInHistory(newPassword)) 591 { 592 operation.setResultCode(ResultCode.CONSTRAINT_VIOLATION); 593 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_PW_IN_HISTORY.get()); 594 return; 595 } 596 else 597 { 598 pwPolicyState.updatePasswordHistory(); 599 } 600 } 601 } 602 else if (pwPolicyState.passwordIsPreEncoded(newPassword)) 603 { 604 // The password modify extended operation isn't intended to be invoked 605 // by an internal operation or during synchronization, so we don't 606 // need to check for those cases. 607 isPreEncoded = true; 608 if (!pwPolicyState.getAuthenticationPolicy().isAllowPreEncodedPasswords()) 609 { 610 operation.setResultCode(ResultCode.CONSTRAINT_VIOLATION); 611 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_PRE_ENCODED_NOT_ALLOWED.get()); 612 return; 613 } 614 } 615 else 616 { 617 // Run the new password through the set of password validators. 618 if (selfChange || !pwPolicyState.getAuthenticationPolicy().isSkipValidationForAdministrators()) 619 { 620 Set<ByteString> clearPasswords = new HashSet<>(pwPolicyState.getClearPasswords()); 621 if (oldPassword != null) 622 { 623 clearPasswords.add(oldPassword); 624 } 625 626 LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder(); 627 if (!pwPolicyState.passwordIsAcceptable(operation, userEntry, newPassword, clearPasswords, invalidReason)) 628 { 629 if (pwPolicyRequested) 630 { 631 pwPolicyErrorType = PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY; 632 operation.addResponseControl( 633 new PasswordPolicyResponseControl(pwPolicyWarningType, pwPolicyWarningValue, pwPolicyErrorType)); 634 } 635 636 operation.setResultCode(ResultCode.CONSTRAINT_VIOLATION); 637 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_UNACCEPTABLE_PW.get(invalidReason)); 638 return; 639 } 640 } 641 642 // Prepare to update the password history, if necessary. 643 if (pwPolicyState.maintainHistory()) 644 { 645 if (pwPolicyState.isPasswordInHistory(newPassword)) 646 { 647 if (selfChange || !pwPolicyState.getAuthenticationPolicy().isSkipValidationForAdministrators()) 648 { 649 operation.setResultCode(ResultCode.CONSTRAINT_VIOLATION); 650 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_PW_IN_HISTORY.get()); 651 return; 652 } 653 } 654 else 655 { 656 pwPolicyState.updatePasswordHistory(); 657 } 658 } 659 } 660 661 // Get the encoded forms of the new password. 662 List<ByteString> encodedPasswords; 663 if (isPreEncoded) 664 { 665 encodedPasswords = newArrayList(newPassword); 666 } 667 else 668 { 669 try 670 { 671 encodedPasswords = pwPolicyState.encodePassword(newPassword); 672 } 673 catch (DirectoryException de) 674 { 675 logger.traceException(de); 676 677 operation.setResultCode(de.getResultCode()); 678 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_CANNOT_ENCODE_PASSWORD.get(de.getMessageObject())); 679 return; 680 } 681 } 682 683 // If the current password was provided, then remove all matching values from the user's entry 684 // and replace them with the new password. Otherwise replace all password values. 685 AttributeType attrType = pwPolicyState.getAuthenticationPolicy().getPasswordAttribute(); 686 List<Modification> modList = new ArrayList<>(); 687 if (oldPassword != null) 688 { 689 // Remove all existing encoded values that match the old password. 690 Set<ByteString> existingValues = pwPolicyState.getPasswordValues(); 691 Set<ByteString> deleteValues = new LinkedHashSet<>(existingValues.size()); 692 if (pwPolicyState.getAuthenticationPolicy().isAuthPasswordSyntax()) 693 { 694 for (ByteString v : existingValues) 695 { 696 try 697 { 698 StringBuilder[] components = AuthPasswordSyntax.decodeAuthPassword(v.toString()); 699 PasswordStorageScheme<?> scheme = 700 DirectoryServer.getAuthPasswordStorageScheme(components[0].toString()); 701 if (scheme == null) 702 { 703 // The password is encoded using an unknown scheme. Remove it from the user's entry. 704 deleteValues.add(v); 705 } 706 else if (scheme.authPasswordMatches(oldPassword, components[1].toString(), components[2].toString())) 707 { 708 deleteValues.add(v); 709 } 710 } 711 catch (DirectoryException de) 712 { 713 logger.traceException(de); 714 715 // We couldn't decode the provided password value, so remove it from the user's entry. 716 deleteValues.add(v); 717 } 718 } 719 } 720 else 721 { 722 for (ByteString v : existingValues) 723 { 724 try 725 { 726 String[] components = UserPasswordSyntax.decodeUserPassword(v.toString()); 727 PasswordStorageScheme<?> scheme = 728 DirectoryServer.getPasswordStorageScheme(toLowerCase(components[0])); 729 if (scheme == null) 730 { 731 // The password is encoded using an unknown scheme. Remove it from the user's entry. 732 deleteValues.add(v); 733 } 734 else if (scheme.passwordMatches(oldPassword, ByteString.valueOf(components[1]))) 735 { 736 deleteValues.add(v); 737 } 738 } 739 catch (DirectoryException de) 740 { 741 logger.traceException(de); 742 743 // We couldn't decode the provided password value, so remove it from the user's entry. 744 deleteValues.add(v); 745 } 746 } 747 } 748 749 modList.add(newModification(ModificationType.DELETE, attrType, deleteValues)); 750 modList.add(newModification(ModificationType.ADD, attrType, encodedPasswords)); 751 } 752 else 753 { 754 modList.add(newModification(ModificationType.REPLACE, attrType, encodedPasswords)); 755 } 756 757 // Update the password changed time for the user entry. 758 pwPolicyState.setPasswordChangedTime(); 759 760 // If the password was changed by an end user, then clear any reset flag that might exist. 761 // If the password was changed by an administrator, then see if we need to set the reset flag. 762 pwPolicyState.setMustChangePassword( 763 !selfChange && pwPolicyState.getAuthenticationPolicy().isForceChangeOnReset()); 764 765 // Clear any record of grace logins, auth failures, and expiration warnings. 766 pwPolicyState.clearFailureLockout(); 767 pwPolicyState.clearGraceLoginTimes(); 768 pwPolicyState.clearWarnedTime(); 769 770 // If the LDAP no-op control was included in the request, then set the 771 // appropriate response. Otherwise, process the operation. 772 if (noOpRequested) 773 { 774 operation.appendErrorMessage(WARN_EXTOP_PASSMOD_NOOP.get()); 775 operation.setResultCode(ResultCode.NO_OPERATION); 776 return; 777 } 778 779 if (selfChange && requestorEntry == null) 780 { 781 requestorEntry = userEntry; 782 } 783 784 // Get an internal connection and use it to perform the modification. 785 boolean isRoot = DirectoryServer.isRootDN(requestorEntry.getName()); 786 AuthenticationInfo authInfo = new AuthenticationInfo(requestorEntry, isRoot); 787 InternalClientConnection internalConnection = new InternalClientConnection(authInfo); 788 789 ModifyOperation modifyOperation = internalConnection.processModify(userDN, modList); 790 ResultCode resultCode = modifyOperation.getResultCode(); 791 if (resultCode != ResultCode.SUCCESS) 792 { 793 operation.setResultCode(resultCode); 794 operation.setErrorMessage(modifyOperation.getErrorMessage()); 795 // FIXME should it also call setMatchedDN() 796 operation.setReferralURLs(modifyOperation.getReferralURLs()); 797 return; 798 } 799 800 // If there were any password policy state changes, we need to apply 801 // them using a root connection because the end user may not have 802 // sufficient access to apply them. This is less efficient than 803 // doing them all in the same modification, but it's safer. 804 List<Modification> pwPolicyMods = pwPolicyState.getModifications(); 805 if (! pwPolicyMods.isEmpty()) 806 { 807 ModifyOperation modOp = getRootConnection().processModify(userDN, pwPolicyMods); 808 if (modOp.getResultCode() != ResultCode.SUCCESS) 809 { 810 // At this point, the user's password is already changed so there's 811 // not much point in returning a non-success result. However, we 812 // should at least log that something went wrong. 813 logger.warn(WARN_EXTOP_PASSMOD_CANNOT_UPDATE_PWP_STATE, userDN, modOp.getResultCode(), 814 modOp.getErrorMessage()); 815 } 816 } 817 818 // If we've gotten here, then everything is OK, so indicate that the operation was successful. 819 operation.setResultCode(ResultCode.SUCCESS); 820 821 // Save attachments for post-op plugins (e.g. Samba password plugin). 822 operation.setAttachment(AUTHZ_DN_ATTACHMENT, userDN); 823 operation.setAttachment(PWD_ATTRIBUTE_ATTACHMENT, pwPolicyState.getAuthenticationPolicy().getPasswordAttribute()); 824 if (!isPreEncoded) 825 { 826 operation.setAttachment(CLEAR_PWD_ATTACHMENT, newPassword); 827 } 828 operation.setAttachment(ENCODED_PWD_ATTACHMENT, encodedPasswords); 829 830 // If a password was generated, then include it in the response. 831 if (generatedPassword) 832 { 833 ByteStringBuilder builder = new ByteStringBuilder(); 834 ASN1Writer writer = ASN1.getWriter(builder); 835 836 try 837 { 838 writer.writeStartSequence(); 839 writer.writeOctetString(TYPE_PASSWORD_MODIFY_GENERATED_PASSWORD, newPassword); 840 writer.writeEndSequence(); 841 } 842 catch (IOException e) 843 { 844 logger.traceException(e); 845 } 846 847 operation.setResponseValue(builder.toByteString()); 848 } 849 850 851 // If this was a self password change, and the client is authenticated as the user whose password was changed, 852 // then clear the "must change password" flag in the client connection. Note that we're using the 853 // authentication DN rather than the authorization DN in this case to avoid mistakenly clearing the flag 854 // for the wrong user. 855 if (selfChange 856 && authInfo.getAuthenticationDN() != null 857 && authInfo.getAuthenticationDN().equals(userDN)) 858 { 859 operation.getClientConnection().setMustChangePassword(false); 860 } 861 862 // If the password policy control was requested, then add the appropriate response control. 863 if (pwPolicyRequested) 864 { 865 operation.addResponseControl( 866 new PasswordPolicyResponseControl(pwPolicyWarningType, pwPolicyWarningValue, pwPolicyErrorType)); 867 } 868 869 // Handle Account Status Notifications that may be needed. 870 // They are not handled by the backend for internal operations. 871 List<ByteString> currentPasswords = null; 872 if (oldPassword != null) 873 { 874 currentPasswords = newArrayList(oldPassword); 875 } 876 List<ByteString> newPasswords = newArrayList(newPassword); 877 878 Map<AccountStatusNotificationProperty, List<String>> notifProperties = 879 AccountStatusNotification.createProperties(pwPolicyState, false, -1, currentPasswords, newPasswords); 880 if (selfChange) 881 { 882 pwPolicyState.generateAccountStatusNotification( 883 PASSWORD_CHANGED, userEntry, INFO_MODIFY_PASSWORD_CHANGED.get(), notifProperties); 884 } 885 else 886 { 887 pwPolicyState.generateAccountStatusNotification( 888 PASSWORD_RESET, userEntry, INFO_MODIFY_PASSWORD_RESET.get(), notifProperties); 889 } 890 } 891 finally 892 { 893 if (userLock != null) 894 { 895 userLock.unlock(); 896 } 897 } 898 } 899 900 private boolean isSelfChange(ByteString userIdentity, Entry requestorEntry, DN userDN, ByteString oldPassword) 901 { 902 if (userIdentity == null) 903 { 904 return true; 905 } 906 else if (requestorEntry != null) 907 { 908 return userDN.equals(requestorEntry.getName()); 909 } 910 else 911 { 912 return oldPassword != null; 913 } 914 } 915 916 private Modification newModification(ModificationType modType, AttributeType attrType, Collection<ByteString> value) 917 { 918 AttributeBuilder builder = new AttributeBuilder(attrType); 919 builder.addAll(value); 920 return new Modification(modType, builder.toAttribute()); 921 } 922 923 924 /** 925 * Retrieves the entry for the specified user based on the provided DN. If any problem is encountered or 926 * the requested entry does not exist, then the provided operation will be updated with appropriate result 927 * information and this method will return <CODE>null</CODE>. 928 * The caller must hold a write lock on the specified entry. 929 * 930 * @param operation The extended operation being processed. 931 * @param entryDN The DN of the user entry to retrieve. 932 * 933 * @return The requested entry, or <CODE>null</CODE> if there was no such entry or it could not be retrieved. 934 */ 935 private Entry getEntryByDN(ExtendedOperation operation, DN entryDN) 936 { 937 // Retrieve the user's entry from the directory. If it does not exist, then fail. 938 try 939 { 940 Entry userEntry = DirectoryServer.getEntry(entryDN); 941 942 if (userEntry == null) 943 { 944 operation.setResultCode(ResultCode.NO_SUCH_OBJECT); 945 operation.appendErrorMessage(ERR_EXTOP_PASSMOD_NO_USER_ENTRY_BY_AUTHZID.get(entryDN)); 946 947 // See if one of the entry's ancestors exists. 948 operation.setMatchedDN(findMatchedDN(entryDN)); 949 return null; 950 } 951 952 return userEntry; 953 } 954 catch (DirectoryException de) 955 { 956 logger.traceException(de); 957 958 operation.setResultCode(de.getResultCode()); 959 operation.appendErrorMessage(de.getMessageObject()); 960 operation.setMatchedDN(de.getMatchedDN()); 961 operation.setReferralURLs(de.getReferralURLs()); 962 return null; 963 } 964 } 965 966 private DN findMatchedDN(DN entryDN) 967 { 968 try 969 { 970 DN matchedDN = entryDN.getParentDNInSuffix(); 971 while (matchedDN != null) 972 { 973 if (DirectoryServer.entryExists(matchedDN)) 974 { 975 return matchedDN; 976 } 977 978 matchedDN = matchedDN.getParentDNInSuffix(); 979 } 980 } 981 catch (Exception e) 982 { 983 logger.traceException(e); 984 } 985 return null; 986 } 987 988 /** {@inheritDoc} */ 989 @Override 990 public boolean isConfigurationAcceptable(ExtendedOperationHandlerCfg configuration, 991 List<LocalizableMessage> unacceptableReasons) 992 { 993 PasswordModifyExtendedOperationHandlerCfg config = (PasswordModifyExtendedOperationHandlerCfg) configuration; 994 return isConfigurationChangeAcceptable(config, unacceptableReasons); 995 } 996 997 998 999 /** 1000 * Indicates whether the provided configuration entry has an acceptable configuration for this component. 1001 * If it does not, then detailed information about the problem(s) should be added to the provided list. 1002 * 1003 * @param config The configuration entry for which to make the determination. 1004 * @param unacceptableReasons A list that can be used to hold messages about why the provided entry does not 1005 * have an acceptable configuration. 1006 * 1007 * @return <CODE>true</CODE> if the provided entry has an acceptable configuration for this component, 1008 * or <CODE>false</CODE> if not. 1009 */ 1010 @Override 1011 public boolean isConfigurationChangeAcceptable(PasswordModifyExtendedOperationHandlerCfg config, 1012 List<LocalizableMessage> unacceptableReasons) 1013 { 1014 try 1015 { 1016 // Make sure that the specified identity mapper is OK. 1017 DN mapperDN = config.getIdentityMapperDN(); 1018 IdentityMapper<?> mapper = DirectoryServer.getIdentityMapper(mapperDN); 1019 if (mapper == null) 1020 { 1021 unacceptableReasons.add(ERR_EXTOP_PASSMOD_NO_SUCH_ID_MAPPER.get(mapperDN, config.dn())); 1022 return false; 1023 } 1024 return true; 1025 } 1026 catch (Exception e) 1027 { 1028 logger.traceException(e); 1029 1030 unacceptableReasons.add(ERR_EXTOP_PASSMOD_CANNOT_DETERMINE_ID_MAPPER.get(config.dn(), getExceptionMessage(e))); 1031 return false; 1032 } 1033 } 1034 1035 1036 1037 /** 1038 * Makes a best-effort attempt to apply the configuration contained in the provided entry. 1039 * Information about the result of this processing should be added to the provided message list. 1040 * Information should always be added to this list if a configuration change could not be applied. 1041 * If detailed results are requested, then information about the changes applied successfully (and optionally 1042 * about parameters that were not changed) should also be included. 1043 * 1044 * @param config The entry containing the new configuration to apply for this component. 1045 * 1046 * @return Information about the result of the configuration update. 1047 */ 1048 @Override 1049 public ConfigChangeResult applyConfigurationChange(PasswordModifyExtendedOperationHandlerCfg config) 1050 { 1051 final ConfigChangeResult ccr = new ConfigChangeResult(); 1052 1053 // Make sure that the specified identity mapper is OK. 1054 DN mapperDN = null; 1055 IdentityMapper<?> mapper = null; 1056 try 1057 { 1058 mapperDN = config.getIdentityMapperDN(); 1059 mapper = DirectoryServer.getIdentityMapper(mapperDN); 1060 if (mapper == null) 1061 { 1062 ccr.setResultCode(ResultCode.CONSTRAINT_VIOLATION); 1063 ccr.addMessage(ERR_EXTOP_PASSMOD_NO_SUCH_ID_MAPPER.get(mapperDN, config.dn())); 1064 } 1065 } 1066 catch (Exception e) 1067 { 1068 logger.traceException(e); 1069 1070 ccr.setResultCode(DirectoryServer.getServerErrorResultCode()); 1071 ccr.addMessage(ERR_EXTOP_PASSMOD_CANNOT_DETERMINE_ID_MAPPER.get(config.dn(), getExceptionMessage(e))); 1072 } 1073 1074 // If all of the changes were acceptable, then apply them. 1075 if (ccr.getResultCode() == ResultCode.SUCCESS 1076 && ! identityMapperDN.equals(mapperDN)) 1077 { 1078 identityMapper = mapper; 1079 identityMapperDN = mapperDN; 1080 } 1081 1082 // Save this configuration for future reference. 1083 currentConfig = config; 1084 1085 return ccr; 1086 } 1087 1088 /** {@inheritDoc} */ 1089 @Override 1090 public String getExtendedOperationOID() 1091 { 1092 return OID_PASSWORD_MODIFY_REQUEST; 1093 } 1094 1095 /** {@inheritDoc} */ 1096 @Override 1097 public String getExtendedOperationName() 1098 { 1099 return "Password Modify"; 1100 } 1101}