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