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