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.core; 018 019import static org.opends.messages.CoreMessages.*; 020import static org.opends.server.config.ConfigConstants.*; 021import static org.opends.server.protocols.internal.InternalClientConnection.*; 022import static org.opends.server.schema.SchemaConstants.*; 023import static org.opends.server.util.StaticUtils.*; 024 025import java.text.ParseException; 026import java.text.SimpleDateFormat; 027import java.util.ArrayList; 028import java.util.Collection; 029import java.util.Collections; 030import java.util.Date; 031import java.util.HashSet; 032import java.util.Iterator; 033import java.util.LinkedHashSet; 034import java.util.LinkedList; 035import java.util.List; 036import java.util.Map; 037import java.util.Set; 038import java.util.TimeZone; 039import java.util.TreeMap; 040 041import org.forgerock.i18n.LocalizableMessage; 042import org.forgerock.i18n.LocalizableMessageBuilder; 043import org.forgerock.i18n.slf4j.LocalizedLogger; 044import org.forgerock.opendj.ldap.ByteString; 045import org.forgerock.opendj.ldap.ConditionResult; 046import org.forgerock.opendj.ldap.GeneralizedTime; 047import org.forgerock.opendj.ldap.ModificationType; 048import org.forgerock.opendj.ldap.ResultCode; 049import org.forgerock.opendj.ldap.schema.AttributeType; 050import org.forgerock.opendj.server.config.meta.PasswordPolicyCfgDefn; 051import org.opends.server.api.AccountStatusNotificationHandler; 052import org.opends.server.api.AuthenticationPolicyState; 053import org.opends.server.api.PasswordGenerator; 054import org.opends.server.api.PasswordStorageScheme; 055import org.opends.server.api.PasswordValidator; 056import org.opends.server.protocols.internal.InternalClientConnection; 057import org.opends.server.protocols.ldap.LDAPAttribute; 058import org.opends.server.schema.AuthPasswordSyntax; 059import org.opends.server.schema.GeneralizedTimeSyntax; 060import org.opends.server.schema.UserPasswordSyntax; 061import org.opends.server.types.AccountStatusNotification; 062import org.opends.server.types.AccountStatusNotificationProperty; 063import org.opends.server.types.AccountStatusNotificationType; 064import org.opends.server.types.Attribute; 065import org.opends.server.types.AttributeBuilder; 066import org.opends.server.types.Attributes; 067import org.opends.server.types.DirectoryException; 068import org.opends.server.types.Entry; 069import org.opends.server.types.Modification; 070import org.opends.server.types.Operation; 071import org.opends.server.types.RawModification; 072 073/** 074 * This class provides a data structure for holding password policy state 075 * information for a user account. 076 */ 077public final class PasswordPolicyState extends AuthenticationPolicyState 078{ 079 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 080 081 /** The string representation of the user's DN. */ 082 private final String userDNString; 083 084 /** The password policy with which the account is associated. */ 085 private final PasswordPolicy passwordPolicy; 086 087 /** The current time for use in all password policy calculations. */ 088 private final long currentTime; 089 090 /** The time that the user's password was last changed. */ 091 private long passwordChangedTime = Long.MIN_VALUE; 092 093 /** Indicates whether the user's account is expired. */ 094 private ConditionResult isAccountExpired = ConditionResult.UNDEFINED; 095 /** Indicates whether the user's password is expired. */ 096 private ConditionResult isPasswordExpired = ConditionResult.UNDEFINED; 097 /** Indicates whether the warning to send to the client would be the first warning for the user. */ 098 private ConditionResult isFirstWarning = ConditionResult.UNDEFINED; 099 /** Indicates whether the user's account is locked by the idle lockout. */ 100 private ConditionResult isIdleLocked = ConditionResult.UNDEFINED; 101 /** 102 * Indicates whether the user may use a grace login if the password is expired and there are one 103 * or more grace logins remaining. 104 */ 105 private ConditionResult mayUseGraceLogin = ConditionResult.UNDEFINED; 106 /** Indicates whether the user's password must be changed. */ 107 private ConditionResult mustChangePassword = ConditionResult.UNDEFINED; 108 /** Indicates whether the user should be warned of an upcoming expiration. */ 109 private ConditionResult shouldWarn = ConditionResult.UNDEFINED; 110 111 /** The number of seconds until the user's account is automatically unlocked. */ 112 private int secondsUntilUnlock = Integer.MIN_VALUE; 113 114 /** The set of authentication failure times for this user. */ 115 private List<Long> authFailureTimes; 116 /** The set of grace login times for this user. */ 117 private List<Long> graceLoginTimes; 118 119 /** The time that the user's account should expire (or did expire). */ 120 private long accountExpirationTime = Long.MIN_VALUE; 121 /** The time that the user's entry was locked due to too many authentication failures. */ 122 private long failureLockedTime = Long.MIN_VALUE; 123 /** The time that the user last authenticated to the Directory Server. */ 124 private long lastLoginTime = Long.MIN_VALUE; 125 /** The time that the user's password should expire (or did expire). */ 126 private long passwordExpirationTime = Long.MIN_VALUE; 127 /** The last required change time with which the user complied. */ 128 private long requiredChangeTime = Long.MIN_VALUE; 129 /** The time that the user was first warned about an upcoming expiration. */ 130 private long warnedTime = Long.MIN_VALUE; 131 132 /** The set of modifications that should be applied to the user's entry. */ 133 private final LinkedList<Modification> modifications = new LinkedList<>(); 134 135 /** 136 * Creates a new password policy state object with the provided information. 137 * <p> 138 * Note that this version of the constructor should only be used for testing purposes when the tests should be 139 * evaluated with a fixed time rather than the actual current time. For all other purposes, the other constructor 140 * should be used. 141 * </p> 142 * 143 * @param policy The password policy associated with the state. 144 * @param userEntry The entry with the user account. 145 * @param currentTime The time to use as the current time for all time-related determinations. 146 */ 147 PasswordPolicyState(PasswordPolicy policy, Entry userEntry, long currentTime) 148 { 149 super(userEntry); 150 this.currentTime = currentTime; 151 this.userDNString = userEntry.getName().toString(); 152 this.passwordPolicy = policy; 153 } 154 155 /** 156 * Retrieves the value of the specified attribute as a string. 157 * 158 * @param attributeType The attribute type whose value should be retrieved. 159 * 160 * @return The value of the specified attribute as a string, or <CODE>null</CODE> if there is no such value. 161 */ 162 private String getValue(AttributeType attributeType) 163 { 164 Attribute attr = getFirstAttributeNotEmpty(attributeType); 165 String stringValue = attr != null ? attr.iterator().next().toString() : null; 166 if (logger.isTraceEnabled()) 167 { 168 if (stringValue != null) 169 { 170 logger.trace("Returning value %s for user %s", stringValue, userDNString); 171 } 172 else 173 { 174 logger.trace("Returning null because attribute %s does not exist in user entry %s", 175 attributeType.getNameOrOID(), userDNString); 176 } 177 } 178 179 return stringValue; 180 } 181 182 private Attribute getFirstAttributeNotEmpty(AttributeType attributeType) 183 { 184 for (Attribute a : userEntry.getAttribute(attributeType)) 185 { 186 if (!a.isEmpty()) 187 { 188 return a; 189 } 190 } 191 return null; 192 } 193 194 /** 195 * Retrieves the set of values of the specified attribute from the user's entry in generalized time format. 196 * 197 * @param attributeType The attribute type whose values should be parsed as generalized time values. 198 * 199 * @return The set of generalized time values, or an empty list if there are none. 200 * 201 * @throws DirectoryException If a problem occurs while attempting to decode a value as a generalized time. 202 */ 203 private List<Long> getGeneralizedTimes(AttributeType attributeType) 204 throws DirectoryException 205 { 206 ArrayList<Long> timeValues = new ArrayList<>(); 207 208 for (Attribute a : userEntry.getAttribute(attributeType)) 209 { 210 for (ByteString v : a) 211 { 212 try 213 { 214 timeValues.add(GeneralizedTime.valueOf(v.toString()).getTimeInMillis()); 215 } 216 catch (Exception e) 217 { 218 logger.traceException(e, "Unable to decode value %s for attribute %s in user entry %s", 219 v, attributeType.getNameOrOID(), userDNString); 220 221 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 222 ERR_PWPSTATE_CANNOT_DECODE_GENERALIZED_TIME.get(v, attributeType.getNameOrOID(), userDNString, e), 223 e); 224 } 225 } 226 } 227 228 if (timeValues.isEmpty()) 229 { 230 logger.trace("Returning an empty list because attribute %s does not exist in user entry %s", 231 attributeType.getNameOrOID(), userDNString); 232 } 233 return timeValues; 234 } 235 236 /** 237 * Get the password storage scheme used by a given password value. 238 * 239 * @param v The encoded password value to check. 240 * 241 * @return The scheme used by the password. 242 * 243 * @throws DirectoryException If the password could not be decoded. 244 */ 245 private PasswordStorageScheme<?> getPasswordStorageScheme(ByteString v) throws DirectoryException 246 { 247 if (passwordPolicy.isAuthPasswordSyntax()) 248 { 249 String[] pwComps = AuthPasswordSyntax.decodeAuthPassword(v.toString()); 250 return DirectoryServer.getAuthPasswordStorageScheme(pwComps[0]); 251 } 252 else 253 { 254 String[] pwComps = UserPasswordSyntax.decodeUserPassword(v.toString()); 255 return DirectoryServer.getPasswordStorageScheme(pwComps[0]); 256 } 257 } 258 259 @Override 260 public PasswordPolicy getAuthenticationPolicy() 261 { 262 return passwordPolicy; 263 } 264 265 /** 266 * Retrieves the time that the password was last changed. 267 * 268 * @return The time that the password was last changed. 269 */ 270 public long getPasswordChangedTime() 271 { 272 if (passwordChangedTime < 0) 273 { 274 // Get the password changed time for the user. 275 try 276 { 277 passwordChangedTime = getGeneralizedTime0(userEntry, OP_ATTR_PWPOLICY_CHANGED_TIME_LC); 278 } 279 catch (DirectoryException e) 280 { 281 /* 282 * The password change time could not be parsed (but has been logged in the debug log). 283 * The best effort we can do from here is to a) use the current time, b) use the start 284 * of the epoch (1/1/1970), or c) use the create time stamp. Lets treat this problem as if the change time 285 * attribute did not exist and resort to the create time stamp. 286 */ 287 } 288 289 if (passwordChangedTime < 0) 290 { 291 // Get the time that the user's account was created. 292 try 293 { 294 passwordChangedTime = getGeneralizedTime0(userEntry, OP_ATTR_CREATE_TIMESTAMP_LC); 295 } 296 catch (DirectoryException e) 297 { 298 /* 299 * The create time stamp could not be parsed (but has been logged in the debug log). 300 * The best effort we can do from here is to a) use the current time, or b) use the start of 301 * the epoch (1/1/1970). Lets treat this problem as if the change time attribute did not exist 302 * and use the start of the epoch. Doing so stands a greater chance of forcing a password change. 303 */ 304 } 305 306 if (passwordChangedTime < 0) 307 { 308 passwordChangedTime = 0; 309 310 if (logger.isTraceEnabled()) 311 { 312 logger.trace( 313 "Could not determine password changed time for " + "user %s.", userDNString); 314 } 315 } 316 } 317 } 318 319 return passwordChangedTime; 320 } 321 322 private long getGeneralizedTime0(Entry userEntry, String attrName) throws DirectoryException 323 { 324 return getGeneralizedTime(userEntry, DirectoryServer.getSchema().getAttributeType(attrName)); 325 } 326 327 /** 328 * Retrieves the time that this password policy state object was created. 329 * 330 * @return The time that this password policy state object was created. 331 */ 332 public long getCurrentTime() 333 { 334 return currentTime; 335 } 336 337 /** 338 * Retrieves the unmodifiable set of values for the password attribute from the user entry. 339 * 340 * @return The unmodifiable set of values for the password attribute from the user entry. 341 */ 342 public Set<ByteString> getPasswordValues() 343 { 344 final Attribute attr = getFirstAttributeNotEmpty(passwordPolicy.getPasswordAttribute()); 345 if (attr != null) 346 { 347 Set<ByteString> values = new LinkedHashSet<>(attr.size()); 348 for (ByteString value : attr) 349 { 350 values.add(value); 351 } 352 return Collections.unmodifiableSet(values); 353 } 354 return Collections.emptySet(); 355 } 356 357 /** Sets a new value for the password changed time equal to the current time. */ 358 public void setPasswordChangedTime() 359 { 360 setPasswordChangedTime(currentTime); 361 } 362 363 /** 364 * Sets a new value for the password changed time equal to the specified time. 365 * This method should generally only be used for testing purposes, since the variant that uses 366 * the current time is preferred almost everywhere else. 367 * 368 * @param passwordChangedTime The time to use 369 */ 370 public void setPasswordChangedTime(long passwordChangedTime) 371 { 372 if (logger.isTraceEnabled()) 373 { 374 logger.trace("Setting password changed time for user %s to current time of %d", userDNString, currentTime); 375 } 376 377 // passwordChangedTime is computed in the constructor from values in the entry. 378 if (getPasswordChangedTime() != passwordChangedTime) 379 { 380 this.passwordChangedTime = passwordChangedTime; 381 replaceAttribute(OP_ATTR_PWPOLICY_CHANGED_TIME, GeneralizedTimeSyntax.format(passwordChangedTime)); 382 } 383 } 384 385 /** 386 * Removes the password changed time value from the user's entry. This should only be used for testing 387 * purposes, as it can really mess things up if you don't know what you're doing. 388 */ 389 public void clearPasswordChangedTime() 390 { 391 logger.trace("Clearing password changed time for user %s", userDNString); 392 393 clearAttribute(OP_ATTR_PWPOLICY_CHANGED_TIME_LC); 394 395 // Fall back to using the entry creation time as the password changed time, if it's defined. 396 // Otherwise, use a value of zero. 397 try 398 { 399 passwordChangedTime = getGeneralizedTime0(userEntry, OP_ATTR_CREATE_TIMESTAMP_LC); 400 if (passwordChangedTime < 0) 401 { 402 passwordChangedTime = 0; 403 } 404 } 405 catch (Exception e) 406 { 407 passwordChangedTime = 0; 408 } 409 } 410 411 /** 412 * Updates the user entry to indicate whether user account has been administratively disabled. 413 * 414 * @param isDisabled 415 * Indicates whether the user account has been administratively disabled. 416 */ 417 public void setDisabled(boolean isDisabled) 418 { 419 if (logger.isTraceEnabled()) 420 { 421 logger.trace("Updating user %s to set the disabled flag to %b", userDNString, isDisabled); 422 } 423 424 if (isDisabled == isDisabled()) 425 { 426 return; // requested state matches current state 427 } 428 429 this.isDisabled = ConditionResult.not(this.isDisabled); 430 replaceAttribute(OP_ATTR_ACCOUNT_DISABLED, isDisabled); 431 } 432 433 /** 434 * Indicates whether the user's account is currently expired. 435 * 436 * @return <CODE>true</CODE> if the user's account is expired, or <CODE>false</CODE> if not. 437 */ 438 public boolean isAccountExpired() 439 { 440 if (isAccountExpired != ConditionResult.UNDEFINED) 441 { 442 if (logger.isTraceEnabled()) 443 { 444 logger.trace("Returning stored result of %b for user %s", 445 isAccountExpired == ConditionResult.TRUE, userDNString); 446 } 447 448 return isAccountExpired == ConditionResult.TRUE; 449 } 450 451 try { 452 accountExpirationTime = getGeneralizedTime0(userEntry, OP_ATTR_ACCOUNT_EXPIRATION_TIME); 453 } 454 catch (Exception e) 455 { 456 logger.traceException(e, "User %s is considered to have an expired account because an error occurred " + 457 "while attempting to make the determination.", userDNString); 458 459 isAccountExpired = ConditionResult.TRUE; 460 return true; 461 } 462 463 if (accountExpirationTime > currentTime) 464 { 465 // The user does have an expiration time, but it hasn't arrived yet. 466 isAccountExpired = ConditionResult.FALSE; 467 logger.trace("The account for user %s is not expired because the expiration time has not yet arrived.", 468 userDNString); 469 } 470 else if (accountExpirationTime >= 0) 471 { 472 // The user does have an expiration time, and it is in the past. 473 isAccountExpired = ConditionResult.TRUE; 474 logger.trace("The account for user %s is expired because the expiration time in that account has passed.", 475 userDNString); 476 } 477 else 478 { 479 // The user doesn't have an expiration time in their entry, so it can't be expired. 480 isAccountExpired = ConditionResult.FALSE; 481 logger.trace("The account for user %s is not expired because there is no expiration time in the user's entry.", 482 userDNString); 483 } 484 485 return isAccountExpired == ConditionResult.TRUE; 486 } 487 488 /** 489 * Retrieves the time at which the user's account will expire. 490 * 491 * @return The time at which the user's account will expire, or -1 if it is not configured with an expiration time. 492 */ 493 public long getAccountExpirationTime() 494 { 495 if (accountExpirationTime == Long.MIN_VALUE) 496 { 497 isAccountExpired(); 498 } 499 500 return accountExpirationTime; 501 } 502 503 /** 504 * Sets the user's account expiration time to the specified value. 505 * 506 * @param accountExpirationTime The time that the user's account should expire. 507 */ 508 public void setAccountExpirationTime(long accountExpirationTime) 509 { 510 if (accountExpirationTime < 0) 511 { 512 clearAccountExpirationTime(); 513 } 514 else 515 { 516 String timeStr = GeneralizedTimeSyntax.format(accountExpirationTime); 517 logger.trace("Setting account expiration time for user %s to %s", userDNString, timeStr); 518 519 this.accountExpirationTime = accountExpirationTime; 520 replaceAttribute(OP_ATTR_ACCOUNT_EXPIRATION_TIME, timeStr); 521 } 522 } 523 524 /** Clears the user's account expiration time. */ 525 public void clearAccountExpirationTime() 526 { 527 logger.trace("Clearing account expiration time for user %s", userDNString); 528 529 accountExpirationTime = -1; 530 clearAttribute(OP_ATTR_ACCOUNT_EXPIRATION_TIME); 531 } 532 533 /** 534 * Retrieves the set of times of failed authentication attempts for the user. If authentication failure 535 * time expiration is enabled, and there are expired times in the entry, these times are removed 536 * from the instance field and an update is provided to delete those values from the entry. 537 * 538 * @return The set of times of failed authentication attempts for the user, which will be an empty list 539 * in the case of no valid (unexpired) times in the entry. 540 */ 541 public List<Long> getAuthFailureTimes() 542 { 543 if (authFailureTimes != null) 544 { 545 if (logger.isTraceEnabled()) 546 { 547 logger.trace("Returning stored auth failure time list of %d elements for user %s", 548 authFailureTimes.size(), userDNString); 549 } 550 551 return authFailureTimes; 552 } 553 554 AttributeType type = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_FAILURE_TIME); 555 try 556 { 557 authFailureTimes = getGeneralizedTimes(type); 558 } 559 catch (Exception e) 560 { 561 logger.traceException(e, "Error while processing auth failure times for user %s", userDNString); 562 563 authFailureTimes = new ArrayList<>(); 564 clearAttribute(type); 565 return authFailureTimes; 566 } 567 568 if (authFailureTimes.isEmpty()) 569 { 570 if (logger.isTraceEnabled()) 571 { 572 logger.trace("Returning an empty auth failure time list for user %s because the attribute" + 573 " is absent from the entry.", userDNString); 574 } 575 576 return authFailureTimes; 577 } 578 579 // Remove any expired failures from the list. 580 if (passwordPolicy.getLockoutFailureExpirationInterval() > 0) 581 { 582 LinkedHashSet<ByteString> valuesToRemove = null; 583 584 long expirationTime = currentTime - passwordPolicy.getLockoutFailureExpirationInterval() * 1000L; 585 Iterator<Long> iterator = authFailureTimes.iterator(); 586 while (iterator.hasNext()) 587 { 588 long l = iterator.next(); 589 if (l < expirationTime) 590 { 591 if (logger.isTraceEnabled()) 592 { 593 logger.trace("Removing expired auth failure time %d for user %s", l, userDNString); 594 } 595 596 iterator.remove(); 597 598 if (valuesToRemove == null) 599 { 600 valuesToRemove = new LinkedHashSet<>(); 601 } 602 603 valuesToRemove.add(ByteString.valueOfUtf8(GeneralizedTimeSyntax.format(l))); 604 } 605 } 606 607 if (valuesToRemove != null) 608 { 609 Attribute a = newAttribute(type, valuesToRemove); 610 modifications.add(new Modification(ModificationType.DELETE, a, true)); 611 } 612 } 613 614 if (logger.isTraceEnabled()) 615 { 616 logger.trace("Returning auth failure time list of %d elements for user %s", 617 authFailureTimes.size(), userDNString); 618 } 619 620 return authFailureTimes; 621 } 622 623 /** 624 * Updates the set of authentication failure times to include the current time. 625 * If the number of failures reaches the policy configuration limit, lock the account. 626 */ 627 public void updateAuthFailureTimes() 628 { 629 if (passwordPolicy.getLockoutFailureCount() <= 0) 630 { 631 return; 632 } 633 634 if (logger.isTraceEnabled()) 635 { 636 logger.trace("Updating authentication failure times for user %s", userDNString); 637 } 638 639 List<Long> failureTimes = getAuthFailureTimes(); 640 long highestFailureTime = computeHighestTime(failureTimes); 641 // Update the current policy state 642 failureTimes.add(highestFailureTime); 643 644 // And the attribute in the user entry 645 AttributeType type = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_FAILURE_TIME); 646 Attribute addAttr = Attributes.create(type, GeneralizedTimeSyntax.format(highestFailureTime)); 647 modifications.add(new Modification(ModificationType.ADD, addAttr, true)); 648 649 // Now check to see if there have been sufficient failures to lock the account. 650 int lockoutCount = passwordPolicy.getLockoutFailureCount(); 651 if (lockoutCount > 0 && lockoutCount <= authFailureTimes.size()) 652 { 653 setFailureLockedTime(highestFailureTime); 654 if (logger.isTraceEnabled()) 655 { 656 logger.trace("Locking user account %s due to too many failures.", userDNString); 657 } 658 } 659 } 660 661 /** 662 * Explicitly specifies the auth failure times for the associated user. This should generally only be used 663 * for testing purposes. Note that it will also set or clear the locked time as appropriate. 664 * 665 * @param authFailureTimes The set of auth failure times to use for the account. An empty list or 666 * {@code null} will clear the account of any existing failures. 667 */ 668 public void setAuthFailureTimes(List<Long> authFailureTimes) 669 { 670 if (authFailureTimes == null || authFailureTimes.isEmpty()) 671 { 672 clearAuthFailureTimes(); 673 clearFailureLockedTime(); 674 return; 675 } 676 677 this.authFailureTimes = authFailureTimes; 678 679 AttributeBuilder builder = new AttributeBuilder(OP_ATTR_PWPOLICY_FAILURE_TIME_LC); 680 long highestFailureTime = -1; 681 682 for (long l : authFailureTimes) 683 { 684 highestFailureTime = Math.max(l, highestFailureTime); 685 builder.add(GeneralizedTimeSyntax.format(l)); 686 } 687 replaceAttribute(builder.toAttribute()); 688 689 // Now check to see if there have been sufficient failures to lock the account. 690 int lockoutCount = passwordPolicy.getLockoutFailureCount(); 691 if (lockoutCount > 0 && lockoutCount <= authFailureTimes.size()) 692 { 693 setFailureLockedTime(highestFailureTime); 694 if (logger.isTraceEnabled()) 695 { 696 logger.trace("Locking user account %s due to too many failures.", userDNString); 697 } 698 } 699 } 700 701 /** Updates the user entry to remove any record of previous authentication failure times. */ 702 private void clearAuthFailureTimes() 703 { 704 logger.trace("Clearing authentication failure times for user %s", userDNString); 705 706 List<Long> failureTimes = getAuthFailureTimes(); 707 if (!failureTimes.isEmpty()) 708 { 709 failureTimes.clear(); // Note: failureTimes != this.authFailureTimes 710 clearAttribute(OP_ATTR_PWPOLICY_FAILURE_TIME); 711 } 712 } 713 714 /** 715 * Retrieves the time of an authentication failure lockout for the user. 716 * 717 * @return The time of an authentication failure lockout for the user, or -1 if no such time is present in the entry. 718 */ 719 private long getFailureLockedTime() 720 { 721 if (failureLockedTime != Long.MIN_VALUE) 722 { 723 return failureLockedTime; 724 } 725 726 AttributeType type = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_LOCKED_TIME); 727 try 728 { 729 failureLockedTime = getGeneralizedTime(userEntry, type); 730 } 731 catch (Exception e) 732 { 733 logger.traceException(e, "Returning current time for user %s because an error occurred", userDNString); 734 735 failureLockedTime = currentTime; 736 return failureLockedTime; 737 } 738 739 // An expired locked time is handled in lockedDueToFailures. 740 return failureLockedTime; 741 } 742 743 /** 744 Sets the failure lockout attribute in the entry to the requested time. 745 746 @param time The time to which to set the entry's failure lockout attribute. 747 */ 748 private void setFailureLockedTime(final long time) 749 { 750 if (time != getFailureLockedTime()) 751 { 752 failureLockedTime = time; 753 replaceAttribute(OP_ATTR_PWPOLICY_LOCKED_TIME, GeneralizedTimeSyntax.format(failureLockedTime)); 754 } 755 } 756 757 /** Updates the user entry to remove any record of previous authentication failure lockout. */ 758 private void clearFailureLockedTime() 759 { 760 logger.trace("Clearing failure lockout time for user %s.", userDNString); 761 762 if (-1L != getFailureLockedTime()) 763 { 764 failureLockedTime = -1L; 765 clearAttribute(OP_ATTR_PWPOLICY_LOCKED_TIME); 766 } 767 } 768 769 /** 770 * Indicates whether the associated user should be considered locked out as a result of too many 771 * authentication failures. In the case of an expired lock-out, this routine produces the update 772 * to clear the lock-out attribute and the authentication failure timestamps. 773 * In case the failure lockout time is absent from the entry, but sufficient authentication failure 774 * timestamps are present in the entry, this routine produces the update to set the lock-out attribute. 775 * 776 * @return <CODE>true</CODE> if the user is currently locked out due to too many authentication failures, 777 * or <CODE>false</CODE> if not. 778 */ 779 public boolean lockedDueToFailures() 780 { 781 // FIXME: Introduce a state field to cache the computed value of this method. 782 // Note that only a cached "locked" status can be returned due to the possibility of intervening updates to 783 // this.failureLockedTime by updateAuthFailureTimes. 784 785 // Check if the feature is enabled in the policy. 786 final int maxFailures = passwordPolicy.getLockoutFailureCount(); 787 if (maxFailures <= 0) 788 { 789 if (logger.isTraceEnabled()) 790 { 791 logger.trace("Returning false for user %s because lockout due to failures is not enabled.", userDNString); 792 } 793 794 return false; 795 } 796 797 // Get the locked time from the user's entry. If it is present and not expired, the account is locked. 798 // If it is absent, the failure timestamps must be checked, since failure timestamps sufficient to lock the 799 // account could be produced across the synchronization topology within the synchronization latency. 800 // Also, note that IETF draft-behera-ldap-password-policy-09 specifies "19700101000000Z" as the value to be set 801 // under a "locked until reset" regime; however, this implementation accepts the value as a locked entry, 802 // but observes the lockout expiration policy for all values including this one. 803 // FIXME: This "getter" is unusual in that it might produce an update to the entry in two cases. 804 // Does it make sense to factor the methods so that, e.g., an expired lockout is reported, and clearing 805 // the lockout is left to the caller? 806 if (getFailureLockedTime() < 0L) 807 { 808 // There was no locked time present in the entry; however, sufficient failure times might have accumulated 809 // to trigger a lockout. 810 if (getAuthFailureTimes().size() < maxFailures) 811 { 812 if (logger.isTraceEnabled()) 813 { 814 logger.trace("Returning false for user %s because there is no locked time.", userDNString); 815 } 816 817 return false; 818 } 819 820 // The account isn't locked but should be, so do so now. 821 setFailureLockedTime(currentTime);// FIXME: set to max(failureTimes)? 822 823 if (logger.isTraceEnabled()) 824 { 825 logger.trace("Locking user %s because there were enough existing failures even though there was" + 826 " no account locked time.", userDNString); 827 } 828 // Fall through... 829 } 830 831 // There is a failure locked time, but it may be expired. 832 if (passwordPolicy.getLockoutDuration() > 0) 833 { 834 final long unlockTime = getFailureLockedTime() + 1000L * passwordPolicy.getLockoutDuration(); 835 if (unlockTime > currentTime) 836 { 837 secondsUntilUnlock = (int) ((unlockTime - currentTime) / 1000); 838 839 if (logger.isTraceEnabled()) 840 { 841 logger.trace("Returning true for user %s because there is a locked time and the lockout duration has" + 842 " not been reached.", userDNString); 843 } 844 845 return true; 846 } 847 848 // The lockout in the entry has expired... 849 clearFailureLockout(); 850 851 if (logger.isTraceEnabled()) 852 { 853 logger.trace("Returning false for user %s because the existing lockout has expired.", userDNString); 854 } 855 856 assert -1L == getFailureLockedTime(); 857 return false; 858 } 859 860 if (logger.isTraceEnabled()) 861 { 862 logger.trace("Returning true for user %s because there is a locked time and no lockout duration.", userDNString); 863 } 864 865 assert -1L <= getFailureLockedTime(); 866 return true; 867 } 868 869 /** 870 * Retrieves the length of time in seconds until the user's account is automatically unlocked. 871 * This should only be called after calling <CODE>lockedDueToFailures</CODE>. 872 * 873 * @return The length of time in seconds until the user's account is automatically unlocked, or -1 if the account 874 * is not locked or the lockout requires administrative action to clear. 875 */ 876 public int getSecondsUntilUnlock() 877 { 878 // secondsUntilUnlock is only set when failureLockedTime is present and PasswordPolicy.getLockoutDuration 879 // is enabled; hence it is not unreasonable to find secondsUntilUnlock uninitialized. 880 assert failureLockedTime != Long.MIN_VALUE; 881 882 return secondsUntilUnlock < 0 ? -1 : secondsUntilUnlock; 883 } 884 885 /** 886 * Updates the user account to remove any record of a previous lockout due to failed authentications. 887 */ 888 public void clearFailureLockout() 889 { 890 clearAuthFailureTimes(); 891 clearFailureLockedTime(); 892 } 893 894 /** 895 * Retrieves the time that the user last authenticated to the Directory Server. 896 * 897 * @return The time that the user last authenticated to the Directory Server, or -1 if it cannot be determined. 898 */ 899 public long getLastLoginTime() 900 { 901 if (lastLoginTime != Long.MIN_VALUE) 902 { 903 if (logger.isTraceEnabled()) 904 { 905 logger.trace("Returning stored last login time of %d for user %s.", lastLoginTime, userDNString); 906 } 907 908 return lastLoginTime; 909 } 910 911 // The policy configuration must be checked since the entry cannot be evaluated without both an attribute 912 // name and timestamp format. 913 AttributeType type = passwordPolicy.getLastLoginTimeAttribute(); 914 String format = passwordPolicy.getLastLoginTimeFormat(); 915 916 if (type == null || format == null) 917 { 918 lastLoginTime = -1; 919 if (logger.isTraceEnabled()) 920 { 921 logger.trace("Returning -1 for user %s because no last login time will be maintained.", userDNString); 922 } 923 924 return lastLoginTime; 925 } 926 927 boolean isGeneralizedTime = SYNTAX_GENERALIZED_TIME_NAME.equals(type.getSyntax().getName()); 928 lastLoginTime = -1; 929 for (Attribute a : userEntry.getAttribute(type)) 930 { 931 if (a.isEmpty()) 932 { 933 continue; 934 } 935 936 String valueString = a.iterator().next().toString(); 937 try 938 { 939 lastLoginTime = parseTime(format, valueString, isGeneralizedTime); 940 941 if (logger.isTraceEnabled()) 942 { 943 logger.trace("Returning last login time of %d for user %s, decoded using current last login time format.", 944 lastLoginTime, userDNString); 945 } 946 947 return lastLoginTime; 948 } 949 catch (Exception e) 950 { 951 logger.traceException(e); 952 953 // This could mean that the last login time was encoded using a previous format. 954 for (String f : passwordPolicy.getPreviousLastLoginTimeFormats()) 955 { 956 try 957 { 958 lastLoginTime = parseTime(f, valueString, isGeneralizedTime); 959 960 if (logger.isTraceEnabled()) 961 { 962 logger.trace("Returning last login time of %d for user %s decoded using previous last login time " 963 + "format of %s.", lastLoginTime, userDNString, f); 964 } 965 966 return lastLoginTime; 967 } 968 catch (Exception e2) 969 { 970 logger.traceException(e); 971 } 972 } 973 974 assert lastLoginTime == -1; 975 if (logger.isTraceEnabled()) 976 { 977 logger.trace("Returning -1 for user %s because the last login time value %s could not be parsed " 978 + "using any known format.", userDNString, valueString); 979 } 980 981 return lastLoginTime; 982 } 983 } 984 985 assert lastLoginTime == -1; 986 if (logger.isTraceEnabled()) 987 { 988 logger.trace("Returning %d for user %s because no last login time value exists.", lastLoginTime, userDNString); 989 } 990 991 return lastLoginTime; 992 } 993 994 private long parseTime(String format, String time, boolean isGeneralizedTime) throws ParseException 995 { 996 SimpleDateFormat dateFormat = new SimpleDateFormat(format); 997 if (isGeneralizedTime) 998 { 999 dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); 1000 } 1001 return dateFormat.parse(time).getTime(); 1002 } 1003 1004 /** Updates the user entry to set the current time as the last login time. */ 1005 public void setLastLoginTime() 1006 { 1007 setLastLoginTime(currentTime); 1008 } 1009 1010 /** 1011 * Updates the user entry to use the specified last login time. This should be used primarily for testing purposes, 1012 * as the variant that uses the current time should be used most of the time. 1013 * 1014 * @param lastLoginTime The last login time to set in the user entry. 1015 */ 1016 public void setLastLoginTime(long lastLoginTime) 1017 { 1018 AttributeType type = passwordPolicy.getLastLoginTimeAttribute(); 1019 String format = passwordPolicy.getLastLoginTimeFormat(); 1020 1021 if (type == null || format == null) 1022 { 1023 return; 1024 } 1025 1026 String timestamp; 1027 try 1028 { 1029 SimpleDateFormat dateFormat = new SimpleDateFormat(format); 1030 // If the attribute has a Generalized Time syntax, make it UTC time. 1031 if (SYNTAX_GENERALIZED_TIME_NAME.equals(type.getSyntax().getName())) 1032 { 1033 dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); 1034 } 1035 timestamp = dateFormat.format(new Date(lastLoginTime)); 1036 this.lastLoginTime = dateFormat.parse(timestamp).getTime(); 1037 } 1038 catch (Exception e) 1039 { 1040 logger.traceException(e, "Unable to set last login time for user %s because an error occurred", userDNString); 1041 return; 1042 } 1043 1044 String existingTimestamp = getValue(type); 1045 if (existingTimestamp != null && timestamp.equals(existingTimestamp)) 1046 { 1047 logger.trace("Not updating last login time for user %s because the new value matches the existing value.", 1048 userDNString); 1049 return; 1050 } 1051 1052 replaceAttribute(Attributes.create(type, timestamp)); 1053 1054 logger.trace("Updated the last login time for user %s to %s", userDNString, timestamp); 1055 } 1056 1057 /** 1058 * Clears the last login time from the user's entry. This should generally be used only for testing purposes. 1059 */ 1060 public void clearLastLoginTime() 1061 { 1062 logger.trace("Clearing last login time for user %s", userDNString); 1063 1064 lastLoginTime = -1; 1065 clearAttribute(OP_ATTR_LAST_LOGIN_TIME); 1066 } 1067 1068 /** 1069 * Indicates whether the user's account is currently locked because it has been idle for too long. 1070 * 1071 * @return <CODE>true</CODE> if the user's account is locked because it has been idle for too long, 1072 * or <CODE>false</CODE> if not. 1073 */ 1074 public boolean lockedDueToIdleInterval() 1075 { 1076 if (isIdleLocked != ConditionResult.UNDEFINED) 1077 { 1078 if (logger.isTraceEnabled()) 1079 { 1080 logger.trace("Returning stored result of %b for user %s", isIdleLocked == ConditionResult.TRUE, userDNString); 1081 } 1082 1083 return isIdleLocked == ConditionResult.TRUE; 1084 } 1085 1086 // Return immediately if this feature is disabled, since the feature is not responsible for any state attribute 1087 // in the entry. 1088 if (passwordPolicy.getIdleLockoutInterval() <= 0) 1089 { 1090 isIdleLocked = ConditionResult.FALSE; 1091 1092 if (logger.isTraceEnabled()) 1093 { 1094 logger.trace("Returning false for user %s because no idle lockout interval is defined.", userDNString); 1095 } 1096 return false; 1097 } 1098 1099 long lockTime = currentTime - 1000L * passwordPolicy.getIdleLockoutInterval(); 1100 if (lockTime < 0) 1101 { 1102 lockTime = 0; 1103 } 1104 1105 long theLastLoginTime = getLastLoginTime(); 1106 if (theLastLoginTime > lockTime || getPasswordChangedTime() > lockTime) 1107 { 1108 isIdleLocked = ConditionResult.FALSE; 1109 if (logger.isTraceEnabled()) 1110 { 1111 StringBuilder reason = new StringBuilder(); 1112 if(theLastLoginTime > lockTime) 1113 { 1114 reason.append("the last login time is in an acceptable window"); 1115 } 1116 else 1117 { 1118 if(theLastLoginTime < 0) 1119 { 1120 reason.append("there is no last login time, but "); 1121 } 1122 reason.append("the password changed time is in an acceptable window"); 1123 } 1124 logger.trace("Returning false for user %s because %s.", userDNString, reason); 1125 } 1126 } 1127 else 1128 { 1129 isIdleLocked = ConditionResult.TRUE; 1130 if (logger.isTraceEnabled()) 1131 { 1132 String reason = theLastLoginTime < 0 1133 ? "there is no last login time and the password changed time is not in an acceptable window" 1134 : "neither last login time nor password changed time are in an acceptable window"; 1135 logger.trace("Returning true for user %s because %s.", userDNString, reason); 1136 } 1137 } 1138 1139 return isIdleLocked == ConditionResult.TRUE; 1140 } 1141 1142/** 1143* Indicates whether the user's password must be changed before any other operation can be performed. 1144* 1145* @return <CODE>true</CODE> if the user's password must be changed before any other operation can be performed. 1146*/ 1147 public boolean mustChangePassword() 1148 { 1149 if(mustChangePassword != ConditionResult.UNDEFINED) 1150 { 1151 if (logger.isTraceEnabled()) 1152 { 1153 logger.trace("Returning stored result of %b for user %s.", 1154 mustChangePassword == ConditionResult.TRUE, userDNString); 1155 } 1156 1157 return mustChangePassword == ConditionResult.TRUE; 1158 } 1159 1160 // If the password policy doesn't use force change on add or force change on reset, or if it forbids the user 1161 // from changing his password, then return false. 1162 // FIXME: the only getter responsible for a state attribute (pwdReset) that considers the policy before 1163 // checking the entry for the presence of the attribute. 1164 if (!passwordPolicy.isAllowUserPasswordChanges() 1165 || (!passwordPolicy.isForceChangeOnAdd() && !passwordPolicy.isForceChangeOnReset())) 1166 { 1167 mustChangePassword = ConditionResult.FALSE; 1168 if (logger.isTraceEnabled()) 1169 { 1170 logger.trace("Returning false for user %s because neither force change on add nor force change on reset" + 1171 " is enabled, or users are not allowed to self-modify passwords.", userDNString); 1172 } 1173 1174 return false; 1175 } 1176 1177 AttributeType type = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_RESET_REQUIRED); 1178 try 1179 { 1180 mustChangePassword = getBoolean(userEntry, type); 1181 } 1182 catch (Exception e) 1183 { 1184 logger.traceException(e, "Returning true for user %s because an error occurred", userDNString); 1185 mustChangePassword = ConditionResult.TRUE; 1186 return true; 1187 } 1188 1189 if(mustChangePassword == ConditionResult.UNDEFINED) 1190 { 1191 mustChangePassword = ConditionResult.FALSE; 1192 logger.trace("Returning %b for user since the attribute \"%s\" is not present in the entry.", 1193 false, userDNString, OP_ATTR_PWPOLICY_RESET_REQUIRED); 1194 return false; 1195 } 1196 1197 final boolean result = mustChangePassword == ConditionResult.TRUE; 1198 logger.trace("Returning %b for user %s.", result, userDNString); 1199 return result; 1200 } 1201 1202/** 1203* Updates the user entry to indicate whether the user's password must be changed. 1204* 1205* @param mustChangePassword Indicates whether the user's password must be changed. 1206*/ 1207 public void setMustChangePassword(boolean mustChangePassword) 1208 { 1209 if (logger.isTraceEnabled()) 1210 { 1211 logger.trace("Updating user %s to set the reset flag to %b", userDNString, mustChangePassword); 1212 } 1213 1214 if (mustChangePassword == mustChangePassword()) 1215 { 1216 return; // requested state matches current state 1217 } 1218 1219 this.mustChangePassword = ConditionResult.not(this.mustChangePassword); 1220 replaceAttribute(OP_ATTR_PWPOLICY_RESET_REQUIRED, mustChangePassword); 1221 } 1222 1223 private void replaceAttribute(String attrName, boolean newValue) 1224 { 1225 if (newValue) 1226 { 1227 replaceAttribute(attrName, String.valueOf(true)); 1228 } 1229 else 1230 { 1231 clearAttribute(attrName); 1232 } 1233 } 1234 1235 private void clearAttribute(String attrName) 1236 { 1237 clearAttribute(DirectoryServer.getSchema().getAttributeType(attrName)); 1238 } 1239 1240 private void clearAttribute(AttributeType type) 1241 { 1242 replaceAttribute(Attributes.empty(type)); 1243 } 1244 1245 private void replaceAttribute(String attrName, String attrValue) 1246 { 1247 replaceAttribute(Attributes.create(attrName, attrValue)); 1248 } 1249 1250 private void replaceAttribute(Attribute a) 1251 { 1252 modifications.add(new Modification(ModificationType.REPLACE, a, true)); 1253 } 1254 1255 /** 1256 * Indicates whether the user's account is locked because the password has been reset by an administrator 1257 * but the user did not change the password in a timely manner. 1258 * 1259 * @return <CODE>true</CODE> if the user's account is locked because of the maximum reset age, 1260 * or <CODE>false</CODE> if not. 1261 */ 1262 public boolean lockedDueToMaximumResetAge() 1263 { 1264 // This feature is responsible for neither a state field nor an entry state attribute. 1265 if (passwordPolicy.getMaxPasswordResetAge() <= 0L) 1266 { 1267 if (logger.isTraceEnabled()) 1268 { 1269 logger.trace("Returning false for user %s because there is no maximum reset age.", userDNString); 1270 } 1271 1272 return false; 1273 } 1274 1275 if (! mustChangePassword()) 1276 { 1277 if (logger.isTraceEnabled()) 1278 { 1279 logger.trace("Returning false for user %s because the user's password has not been reset.", userDNString); 1280 } 1281 1282 return false; 1283 } 1284 1285 long maxResetTime = getPasswordChangedTime() + 1000L * passwordPolicy.getMaxPasswordResetAge(); 1286 boolean locked = maxResetTime < currentTime; 1287 1288 if (logger.isTraceEnabled()) 1289 { 1290 logger.trace("Returning %b for user %s after comparing the current and max reset times.", locked, userDNString); 1291 } 1292 1293 return locked; 1294 } 1295 1296 /** 1297 * Returns whether the account was locked for any reason. 1298 * 1299 * @return true if the account is locked, false otherwise 1300 */ 1301 public boolean isLocked() 1302 { 1303 return lockedDueToIdleInterval() || lockedDueToMaximumResetAge() || lockedDueToFailures(); 1304 } 1305 1306 /** 1307 * Retrieves the time that the user's password should expire (if the expiration is in the future) or 1308 * did expire (if the expiration was in the past). Note that this method should be called after the 1309 * <CODE>lockedDueToMaximumResetAge</CODE> method because grace logins will not be allowed in the case 1310 * that the maximum reset age has passed whereas they may be used for expiration due to maximum password 1311 * age or forced change time. 1312 * 1313 * @return The time that the user's password should/did expire, or -1 if it should not expire. 1314 */ 1315 public long getPasswordExpirationTime() 1316 { 1317 if (passwordExpirationTime == Long.MIN_VALUE) 1318 { 1319 passwordExpirationTime = Long.MAX_VALUE; 1320 1321 boolean checkWarning = false; 1322 1323 long maxAge = passwordPolicy.getMaxPasswordAge(); 1324 if (maxAge > 0L) 1325 { 1326 long expTime = getPasswordChangedTime() + 1000L * maxAge; 1327 if (expTime < passwordExpirationTime) 1328 { 1329 passwordExpirationTime = expTime; 1330 checkWarning = true; 1331 } 1332 } 1333 1334 long maxResetAge = passwordPolicy.getMaxPasswordResetAge(); 1335 if (mustChangePassword() && maxResetAge > 0L) 1336 { 1337 long expTime = getPasswordChangedTime() + 1000L * maxResetAge; 1338 if (expTime < passwordExpirationTime) 1339 { 1340 passwordExpirationTime = expTime; 1341 checkWarning = false; 1342 } 1343 } 1344 1345 long mustChangeTime = passwordPolicy.getRequireChangeByTime(); 1346 if (mustChangeTime > 0) 1347 { 1348 long reqChangeTime = getRequiredChangeTime(); 1349 if (reqChangeTime != mustChangeTime && mustChangeTime < passwordExpirationTime) 1350 { 1351 passwordExpirationTime = mustChangeTime; 1352 checkWarning = true; 1353 } 1354 } 1355 1356 if (passwordExpirationTime == Long.MAX_VALUE) 1357 { 1358 passwordExpirationTime = -1; 1359 shouldWarn = ConditionResult.FALSE; 1360 isFirstWarning = ConditionResult.FALSE; 1361 isPasswordExpired = ConditionResult.FALSE; 1362 mayUseGraceLogin = ConditionResult.TRUE; 1363 } 1364 else if (checkWarning) 1365 { 1366 mayUseGraceLogin = ConditionResult.TRUE; 1367 1368 long warningInterval = passwordPolicy.getPasswordExpirationWarningInterval(); 1369 if (warningInterval > 0L) 1370 { 1371 long shouldWarnTime = passwordExpirationTime - warningInterval * 1000L; 1372 if (shouldWarnTime > currentTime) 1373 { 1374 // The warning time is in the future, so we know the password isn't expired. 1375 shouldWarn = ConditionResult.FALSE; 1376 isFirstWarning = ConditionResult.FALSE; 1377 isPasswordExpired = ConditionResult.FALSE; 1378 } 1379 else 1380 { 1381 // We're at least in the warning period, but the password may be expired. 1382 long theWarnedTime = getWarnedTime(); 1383 1384 if (passwordExpirationTime > currentTime) 1385 { 1386 // The password is not expired but we should warn the user. 1387 shouldWarn = ConditionResult.TRUE; 1388 isPasswordExpired = ConditionResult.FALSE; 1389 1390 if (theWarnedTime < 0) 1391 { 1392 isFirstWarning = ConditionResult.TRUE; 1393 setWarnedTime(); 1394 1395 if (! passwordPolicy.isExpirePasswordsWithoutWarning()) 1396 { 1397 passwordExpirationTime = currentTime + warningInterval * 1000L; 1398 } 1399 } 1400 else 1401 { 1402 isFirstWarning = ConditionResult.FALSE; 1403 1404 if (! passwordPolicy.isExpirePasswordsWithoutWarning()) 1405 { 1406 passwordExpirationTime = theWarnedTime + warningInterval * 1000L; 1407 } 1408 } 1409 } 1410 else 1411 { 1412 // The expiration time has passed, but we may not actually be expired if the user has not 1413 // yet seen a warning. 1414 if (passwordPolicy.isExpirePasswordsWithoutWarning()) 1415 { 1416 shouldWarn = ConditionResult.FALSE; 1417 isFirstWarning = ConditionResult.FALSE; 1418 isPasswordExpired = ConditionResult.TRUE; 1419 } 1420 else if (theWarnedTime > 0) 1421 { 1422 passwordExpirationTime = theWarnedTime + warningInterval*1000L; 1423 if (passwordExpirationTime > currentTime) 1424 { 1425 shouldWarn = ConditionResult.TRUE; 1426 isFirstWarning = ConditionResult.FALSE; 1427 isPasswordExpired = ConditionResult.FALSE; 1428 } 1429 else 1430 { 1431 shouldWarn = ConditionResult.FALSE; 1432 isFirstWarning = ConditionResult.FALSE; 1433 isPasswordExpired = ConditionResult.TRUE; 1434 } 1435 } 1436 else 1437 { 1438 shouldWarn = ConditionResult.TRUE; 1439 isFirstWarning = ConditionResult.TRUE; 1440 isPasswordExpired = ConditionResult.FALSE; 1441 passwordExpirationTime = currentTime + warningInterval*1000L; 1442 } 1443 } 1444 } 1445 } 1446 else 1447 { 1448 // There will never be a warning, and the user's password may be expired. 1449 shouldWarn = ConditionResult.FALSE; 1450 isFirstWarning = ConditionResult.FALSE; 1451 isPasswordExpired = ConditionResult.valueOf(currentTime > passwordExpirationTime); 1452 } 1453 } 1454 else 1455 { 1456 mayUseGraceLogin = ConditionResult.FALSE; 1457 shouldWarn = ConditionResult.FALSE; 1458 isFirstWarning = ConditionResult.FALSE; 1459 isPasswordExpired = ConditionResult.valueOf(passwordExpirationTime < currentTime); 1460 } 1461 } 1462 1463 if (logger.isTraceEnabled()) 1464 { 1465 logger.trace("Returning password expiration time of %d for user %s.", passwordExpirationTime, userDNString); 1466 } 1467 1468 return passwordExpirationTime; 1469 } 1470 1471 /** 1472 * Indicates whether the user's password is currently expired. 1473 * 1474 * @return <CODE>true</CODE> if the user's password is currently expired, or <CODE>false</CODE> if not. 1475 */ 1476 public boolean isPasswordExpired() 1477 { 1478 refreshIfUndefined(isPasswordExpired); 1479 return isPasswordExpired == ConditionResult.TRUE; 1480 } 1481 1482 /** 1483 * Indicates whether the user's last password change was within the minimum password age. 1484 * 1485 * @return <CODE>true</CODE> if the password minimum age is nonzero, the account is not in force-change mode, 1486 * and the last password change was within the minimum age, or <CODE>false</CODE> otherwise. 1487 */ 1488 public boolean isWithinMinimumAge() 1489 { 1490 // This feature is responsible for neither a state field nor entry state attribute. 1491 long minAge = passwordPolicy.getMinPasswordAge(); 1492 if (minAge <= 0L) 1493 { 1494 // There is no minimum age, so the user isn't in it. 1495 if (logger.isTraceEnabled()) 1496 { 1497 logger.trace("Returning false because there is no minimum age."); 1498 } 1499 1500 return false; 1501 } 1502 else if (getPasswordChangedTime() + minAge * 1000L < currentTime) 1503 { 1504 // It's been long enough since the user changed their password. 1505 if (logger.isTraceEnabled()) 1506 { 1507 logger.trace("Returning false because the minimum age has expired."); 1508 } 1509 1510 return false; 1511 } 1512 else if (mustChangePassword()) 1513 { 1514 // The user is in a must-change mode, so the minimum age doesn't apply. 1515 if (logger.isTraceEnabled()) 1516 { 1517 logger.trace("Returning false because the account is in a must-change state."); 1518 } 1519 1520 return false; 1521 } 1522 else 1523 { 1524 // The user is within the minimum age. 1525 if (logger.isTraceEnabled()) 1526 { 1527 logger.trace("Returning true."); 1528 } 1529 1530 return true; 1531 } 1532 } 1533 1534 /** 1535 * Indicates whether the user may use a grace login if the password is expired and there is at least one 1536 * grace login remaining. Note that this does not check to see if the user's password is expired, does not 1537 * verify that there are any remaining grace logins, and does not update the set of grace login times. 1538 * 1539 * @return <CODE>true</CODE> if the user may use a grace login if the password is expired and there is 1540 * at least one grace login remaining, or <CODE>false</CODE> if the user may not use a grace 1541 * login for some reason. 1542 */ 1543 public boolean mayUseGraceLogin() 1544 { 1545 refreshIfUndefined(mayUseGraceLogin); 1546 return mayUseGraceLogin == ConditionResult.TRUE; 1547 } 1548 1549 /** 1550 * Indicates whether the user should receive a warning notification that the password is about to expire. 1551 * 1552 * @return <CODE>true</CODE> if the user should receive a warning notification that the password is about to expire, 1553 * or <CODE>false</CODE> if not. 1554 */ 1555 public boolean shouldWarn() 1556 { 1557 refreshIfUndefined(shouldWarn); 1558 return shouldWarn == ConditionResult.TRUE; 1559 } 1560 1561 /** 1562 * Indicates whether the warning that the user should receive would be the first warning for the user. 1563 * 1564 * @return <CODE>true</CODE> if the warning that should be sent to the user would be the first warning, 1565 * or <CODE>false</CODE> if not. 1566 */ 1567 public boolean isFirstWarning() 1568 { 1569 refreshIfUndefined(isFirstWarning); 1570 return isFirstWarning == ConditionResult.TRUE; 1571 } 1572 1573 private void refreshIfUndefined(ConditionResult cond) 1574 { 1575 if (cond == null || cond == ConditionResult.UNDEFINED) 1576 { 1577 getPasswordExpirationTime(); 1578 } 1579 } 1580 1581 /** 1582 * Retrieves the length of time in seconds until the user's password expires. 1583 * 1584 * @return The length of time in seconds until the user's password expires, 1585 * 0 if the password is currently expired, or -1 if the password should not expire. 1586 */ 1587 public int getSecondsUntilExpiration() 1588 { 1589 long expirationTime = getPasswordExpirationTime(); 1590 if (expirationTime < 0) 1591 { 1592 return -1; 1593 } 1594 else if (expirationTime < currentTime) 1595 { 1596 return 0; 1597 } 1598 else 1599 { 1600 return (int) ((expirationTime - currentTime) / 1000); 1601 } 1602 } 1603 1604 /** 1605 * Retrieves the timestamp for the last required change time that the user complied with. 1606 * 1607 * @return The timestamp for the last required change time that the user complied with, 1608 * or -1 if the user's password has not been changed in compliance with this configuration. 1609 */ 1610 public long getRequiredChangeTime() 1611 { 1612 if (requiredChangeTime != Long.MIN_VALUE) 1613 { 1614 if (logger.isTraceEnabled()) 1615 { 1616 logger.trace("Returning stored required change time of %d for user %s", requiredChangeTime, userDNString); 1617 } 1618 1619 return requiredChangeTime; 1620 } 1621 1622 try 1623 { 1624 requiredChangeTime = getGeneralizedTime0(userEntry, OP_ATTR_PWPOLICY_CHANGED_BY_REQUIRED_TIME); 1625 } 1626 catch (Exception e) 1627 { 1628 logger.traceException(e, "Returning %d for user %s because an error occurred", requiredChangeTime, userDNString); 1629 1630 requiredChangeTime = -1; 1631 return requiredChangeTime; 1632 } 1633 1634 logger.trace("Returning required change time of %d for user %s", requiredChangeTime, userDNString); 1635 1636 return requiredChangeTime; 1637 } 1638 1639 /** 1640 * Updates the user entry with a timestamp indicating that the password has been changed in accordance 1641 * with the require change time. 1642 */ 1643 public void setRequiredChangeTime() 1644 { 1645 long requiredChangeByTimePolicy = passwordPolicy.getRequireChangeByTime(); 1646 if (requiredChangeByTimePolicy > 0) 1647 { 1648 setRequiredChangeTime(requiredChangeByTimePolicy); 1649 } 1650 } 1651 1652 /** 1653 * Updates the user entry with a timestamp indicating that the password has been changed in accordance 1654 * with the require change time. 1655 * 1656 * @param requiredChangeTime The timestamp to use for the required change time value. 1657 */ 1658 public void setRequiredChangeTime(long requiredChangeTime) 1659 { 1660 if (logger.isTraceEnabled()) 1661 { 1662 logger.trace("Updating required change time for user %s", userDNString); 1663 } 1664 1665 if (getRequiredChangeTime() != requiredChangeTime) 1666 { 1667 this.requiredChangeTime = requiredChangeTime; 1668 replaceAttribute(OP_ATTR_PWPOLICY_CHANGED_BY_REQUIRED_TIME, GeneralizedTimeSyntax.format(requiredChangeTime)); 1669 } 1670 } 1671 1672 /** 1673 * Updates the user entry to remove any timestamp indicating that the password has been changed in accordance 1674 * with the required change time. 1675 */ 1676 public void clearRequiredChangeTime() 1677 { 1678 logger.trace("Clearing required change time for user %s", userDNString); 1679 1680 this.requiredChangeTime = Long.MIN_VALUE; 1681 clearAttribute(OP_ATTR_PWPOLICY_CHANGED_BY_REQUIRED_TIME); 1682 } 1683 1684 /** 1685 * Retrieves the time that the user was first warned about an upcoming expiration. 1686 * 1687 * @return The time that the user was first warned about an upcoming expiration, or -1 if the user has 1688 * not been warned. 1689 */ 1690 public long getWarnedTime() 1691 { 1692 if (warnedTime == Long.MIN_VALUE) 1693 { 1694 try 1695 { 1696 warnedTime = getGeneralizedTime0(userEntry, OP_ATTR_PWPOLICY_WARNED_TIME); 1697 } 1698 catch (Exception e) 1699 { 1700 logger.traceException(e, "Unable to decode the warned time for user %s", userDNString); 1701 warnedTime = -1; 1702 } 1703 } 1704 1705 logger.trace("Returning a warned time of %d for user %s", warnedTime, userDNString); 1706 return warnedTime; 1707 } 1708 1709 /** Updates the user entry to set the warned time to the current time. */ 1710 public void setWarnedTime() 1711 { 1712 setWarnedTime(currentTime); 1713 } 1714 1715 /** 1716 * Updates the user entry to set the warned time to the specified time. This method should generally 1717 * only be used for testing purposes, since the variant that uses the current time is preferred almost 1718 * everywhere else. 1719 * 1720 * @param warnedTime The value to use for the warned time. 1721 */ 1722 public void setWarnedTime(long warnedTime) 1723 { 1724 long warnTime = getWarnedTime(); 1725 if (warnTime == warnedTime) 1726 { 1727 if (logger.isTraceEnabled()) 1728 { 1729 logger.trace("Not updating warned time for user %s because the warned time is the same as the specified time.", 1730 userDNString); 1731 } 1732 1733 return; 1734 } 1735 1736 this.warnedTime = warnedTime; 1737 AttributeType type = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_WARNED_TIME); 1738 replaceAttribute(Attributes.create(type, GeneralizedTimeSyntax.createGeneralizedTimeValue(currentTime))); 1739 1740 if (logger.isTraceEnabled()) 1741 { 1742 logger.trace("Updated the warned time for user %s", userDNString); 1743 } 1744 } 1745 1746 /** Updates the user entry to clear the warned time. */ 1747 public void clearWarnedTime() 1748 { 1749 logger.trace("Clearing warned time for user %s", userDNString); 1750 1751 if (getWarnedTime() >= 0) 1752 { 1753 warnedTime = -1; 1754 clearAttribute(OP_ATTR_PWPOLICY_WARNED_TIME); 1755 1756 logger.trace("Cleared the warned time for user %s", userDNString); 1757 } 1758 } 1759 1760 /** 1761 * Retrieves the times that the user has authenticated to the server using a grace login. 1762 * 1763 * @return The times that the user has authenticated to the server using a grace login. 1764 */ 1765 public List<Long> getGraceLoginTimes() 1766 { 1767 if (graceLoginTimes == null) 1768 { 1769 AttributeType type = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME); 1770 try 1771 { 1772 graceLoginTimes = getGeneralizedTimes(type); 1773 } 1774 catch (Exception e) 1775 { 1776 logger.traceException(e, "Error while processing grace login times for user %s", userDNString); 1777 1778 graceLoginTimes = new ArrayList<>(); 1779 clearAttribute(type); 1780 } 1781 } 1782 1783 logger.trace("Returning grace login times for user %s", userDNString); 1784 return graceLoginTimes; 1785 } 1786 1787 /** 1788 * Retrieves the number of grace logins that the user has left. 1789 * 1790 * @return The number of grace logins that the user has left, or -1 if grace logins are not allowed. 1791 */ 1792 public int getGraceLoginsRemaining() 1793 { 1794 int maxGraceLogins = passwordPolicy.getGraceLoginCount(); 1795 if (maxGraceLogins <= 0) 1796 { 1797 return -1; 1798 } 1799 1800 List<Long> theGraceLoginTimes = getGraceLoginTimes(); 1801 return maxGraceLogins - theGraceLoginTimes.size(); 1802 } 1803 1804 /** Updates the set of grace login times for the user to include the current time. */ 1805 public void updateGraceLoginTimes() 1806 { 1807 if (logger.isTraceEnabled()) 1808 { 1809 logger.trace("Updating grace login times for user %s", userDNString); 1810 } 1811 1812 List<Long> graceTimes = getGraceLoginTimes(); 1813 long highestGraceTime = computeHighestTime(graceTimes); 1814 graceTimes.add(highestGraceTime); // graceTimes == this.graceLoginTimes 1815 1816 AttributeType type = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME); 1817 Attribute addAttr = Attributes.create(type, GeneralizedTimeSyntax.format(highestGraceTime)); 1818 modifications.add(new Modification(ModificationType.ADD, addAttr, true)); 1819 } 1820 1821 private long computeHighestTime(List<Long> graceTimes) 1822 { 1823 long highestTime = -1; 1824 for (long l : graceTimes) 1825 { 1826 highestTime = Math.max(l, highestTime); 1827 } 1828 1829 if (highestTime >= currentTime) 1830 { 1831 highestTime++; 1832 } 1833 else 1834 { 1835 highestTime = currentTime; 1836 } 1837 return highestTime; 1838 } 1839 1840 /** 1841 * Specifies the set of grace login use times for the associated user. If the provided list is empty 1842 * or {@code null}, then the set will be cleared. 1843 * 1844 * @param graceLoginTimes The grace login use times for the associated user. 1845 */ 1846 public void setGraceLoginTimes(List<Long> graceLoginTimes) 1847 { 1848 if (graceLoginTimes == null || graceLoginTimes.isEmpty()) 1849 { 1850 clearGraceLoginTimes(); 1851 return; 1852 } 1853 1854 if (logger.isTraceEnabled()) 1855 { 1856 logger.trace("Updating grace login times for user %s", userDNString); 1857 } 1858 1859 this.graceLoginTimes = graceLoginTimes; 1860 1861 AttributeBuilder builder = new AttributeBuilder(OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME_LC); 1862 for (long l : graceLoginTimes) 1863 { 1864 builder.add(GeneralizedTimeSyntax.format(l)); 1865 } 1866 replaceAttribute(builder.toAttribute()); 1867 } 1868 1869 /** Updates the user entry to remove any record of previous grace logins. */ 1870 public void clearGraceLoginTimes() 1871 { 1872 logger.trace("Clearing grace login times for user %s", userDNString); 1873 1874 List<Long> graceTimes = getGraceLoginTimes(); 1875 if (!graceTimes.isEmpty()) 1876 { 1877 graceTimes.clear(); // graceTimes == this.graceLoginTimes 1878 clearAttribute(OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME); 1879 } 1880 } 1881 1882 /** 1883 * Retrieves a list of the clear-text passwords for the user. If the user does not have any passwords 1884 * in the clear, then the list will be empty. 1885 * 1886 * @return A list of the clear-text passwords for the user. 1887 */ 1888 public List<ByteString> getClearPasswords() 1889 { 1890 final List<Attribute> attrList = userEntry.getAttribute(passwordPolicy.getPasswordAttribute()); 1891 if (attrList.isEmpty()) 1892 { 1893 return Collections.emptyList(); 1894 } 1895 1896 LinkedList<ByteString> clearPasswords = new LinkedList<>(); 1897 for (Attribute a : attrList) 1898 { 1899 for (ByteString v : a) 1900 { 1901 try 1902 { 1903 String[] pwComponents = getPwComponents(v); 1904 1905 String schemeName = pwComponents[0]; 1906 PasswordStorageScheme<?> scheme = getPasswordStorageScheme(schemeName); 1907 if (scheme == null) 1908 { 1909 if (logger.isTraceEnabled()) 1910 { 1911 logger.trace("User entry %s contains a password with scheme %s that is not defined in the server.", 1912 userDNString, schemeName); 1913 } 1914 1915 continue; 1916 } 1917 1918 if (scheme.isReversible()) 1919 { 1920 clearPasswords.add(getPlaintextValue(scheme, pwComponents)); 1921 } 1922 } 1923 catch (Exception e) 1924 { 1925 logger.traceException(e); 1926 1927 if (logger.isTraceEnabled()) 1928 { 1929 logger.trace("Cannot get clear password value for user %s: %s", userDNString, e); 1930 } 1931 } 1932 } 1933 } 1934 1935 return clearPasswords; 1936 } 1937 1938 private ByteString getPlaintextValue(PasswordStorageScheme<?> scheme, String[] pwComponents) 1939 throws DirectoryException 1940 { 1941 return passwordPolicy.isAuthPasswordSyntax() 1942 ? scheme.getAuthPasswordPlaintextValue(pwComponents[1], pwComponents[2]) 1943 : scheme.getPlaintextValue(ByteString.valueOfUtf8(pwComponents[1])); 1944 } 1945 1946 @Override 1947 public boolean passwordMatches(ByteString password) 1948 { 1949 List<Attribute> attrList = userEntry.getAttribute(passwordPolicy.getPasswordAttribute()); 1950 if (attrList.isEmpty()) 1951 { 1952 if (logger.isTraceEnabled()) 1953 { 1954 logger.trace("Returning false because user %s does not have any values for password attribute %s", 1955 userDNString, passwordPolicy.getPasswordAttribute().getNameOrOID()); 1956 } 1957 1958 return false; 1959 } 1960 1961 for (Attribute a : attrList) 1962 { 1963 for (ByteString v : a) 1964 { 1965 try 1966 { 1967 String[] pwComponents = getPwComponents(v); 1968 String schemeName = pwComponents[0]; 1969 PasswordStorageScheme<?> scheme = getPasswordStorageScheme(schemeName); 1970 if (scheme == null) 1971 { 1972 if (logger.isTraceEnabled()) 1973 { 1974 logger.trace("User entry %s contains a password with scheme %s that is not defined in the server.", 1975 userDNString, schemeName); 1976 } 1977 1978 continue; 1979 } 1980 1981 if (passwordMatches(password, pwComponents, scheme)) 1982 { 1983 if (logger.isTraceEnabled()) 1984 { 1985 logger.trace("Returning true for user %s because the provided password matches a value " + 1986 "encoded with scheme %s", userDNString, schemeName); 1987 } 1988 1989 return true; 1990 } 1991 } 1992 catch (Exception e) 1993 { 1994 logger.traceException(e, "An error occurred while attempting to process a password value for user %s", 1995 userDNString); 1996 } 1997 } 1998 } 1999 2000 // If we've gotten here, then we couldn't find a match. 2001 logger.trace("Returning false because the provided password does not match any of the stored password " + 2002 "values for user %s", userDNString); 2003 2004 return false; 2005 } 2006 2007 /** 2008 * Get the broken-down components of the given password value. 2009 * 2010 * @param usesAuthPasswordSyntax true if the value is an authPassword. 2011 * @param v The encoded password value to break down. 2012 * 2013 * @return An array of components. 2014 */ 2015 private String[] getPwComponents(ByteString v) throws DirectoryException 2016 { 2017 return passwordPolicy.isAuthPasswordSyntax() 2018 ? AuthPasswordSyntax.decodeAuthPassword(v.toString()) 2019 : UserPasswordSyntax.decodeUserPassword(v.toString()); 2020 } 2021 2022 /** 2023 * Indicates whether the provided password value is pre-encoded. 2024 * 2025 * @param passwordValue The value for which to make the determination. 2026 * 2027 * @return <CODE>true</CODE> if the provided password value is pre-encoded, or <CODE>false</CODE> if it is not. 2028 */ 2029 public boolean passwordIsPreEncoded(ByteString passwordValue) 2030 { 2031 return passwordPolicy.isAuthPasswordSyntax() 2032 ? AuthPasswordSyntax.isEncoded(passwordValue) 2033 : UserPasswordSyntax.isEncoded(passwordValue); 2034 } 2035 2036 /** 2037 * Encodes the provided password using the default storage schemes (using the appropriate syntax for the 2038 * password attribute). 2039 * 2040 * @param password The password to be encoded. 2041 * 2042 * @return The password encoded using the default schemes. 2043 * 2044 * @throws DirectoryException If a problem occurs while attempting to encode the password. 2045 */ 2046 public List<ByteString> encodePassword(ByteString password) 2047 throws DirectoryException 2048 { 2049 List<PasswordStorageScheme<?>> schemes = passwordPolicy.getDefaultPasswordStorageSchemes(); 2050 List<ByteString> encodedPasswords = new ArrayList<>(schemes.size()); 2051 2052 if (passwordPolicy.isAuthPasswordSyntax()) 2053 { 2054 for (PasswordStorageScheme<?> s : schemes) 2055 { 2056 encodedPasswords.add(s.encodeAuthPassword(password)); 2057 } 2058 } 2059 else 2060 { 2061 for (PasswordStorageScheme<?> s : schemes) 2062 { 2063 encodedPasswords.add(s.encodePasswordWithScheme(password)); 2064 } 2065 } 2066 2067 return encodedPasswords; 2068 } 2069 2070 /** 2071 * Indicates whether the provided password appears to be acceptable according to the password validators. 2072 * 2073 * @param operation The operation that provided the password. 2074 * @param userEntry The user entry in which the password is used. 2075 * @param newPassword The password to be validated. 2076 * @param currentPasswords The set of clear-text current passwords for the user (this may be a subset 2077 * if not all of them are available in the clear, or empty if none of them 2078 * are available in the clear). 2079 * @param invalidReason A buffer that may be used to hold the invalid reason if the password is rejected. 2080 * 2081 * @return <CODE>true</CODE> if the password is acceptable for use, or <CODE>false</CODE> if it is not. 2082 */ 2083 public boolean passwordIsAcceptable(Operation operation, Entry userEntry, ByteString newPassword, 2084 Set<ByteString> currentPasswords, LocalizableMessageBuilder invalidReason) 2085 { 2086 for (PasswordValidator<?> validator : passwordPolicy.getPasswordValidators()) 2087 { 2088 if (!validator.passwordIsAcceptable(newPassword, currentPasswords, operation, userEntry, invalidReason)) 2089 { 2090 if (logger.isTraceEnabled()) 2091 { 2092 logger.trace("The password provided for user %s failed validation: %s", userDNString, invalidReason); 2093 } 2094 return false; 2095 } 2096 } 2097 return true; 2098 } 2099 2100 /** 2101 * Performs any processing that may be necessary to remove deprecated storage schemes from the user's entry 2102 * that match the provided password and re-encodes them using the default schemes. 2103 * 2104 * @param password The clear-text password provided by the user. 2105 */ 2106 public void handleDeprecatedStorageSchemes(ByteString password) 2107 { 2108 if (passwordPolicy.getDeprecatedPasswordStorageSchemes().isEmpty()) 2109 { 2110 if (logger.isTraceEnabled()) 2111 { 2112 logger.trace("Doing nothing for user %s because no deprecated storage schemes have been defined.", 2113 userDNString); 2114 } 2115 2116 return; 2117 } 2118 2119 AttributeType type = passwordPolicy.getPasswordAttribute(); 2120 List<Attribute> attrList = userEntry.getAttribute(type); 2121 if (attrList.isEmpty()) 2122 { 2123 logger.trace("Doing nothing for entry %s because no password values were found.", userDNString); 2124 return; 2125 } 2126 2127 HashSet<String> existingDefaultSchemes = new HashSet<>(); 2128 LinkedHashSet<ByteString> removedValues = new LinkedHashSet<>(); 2129 LinkedHashSet<ByteString> updatedValues = new LinkedHashSet<>(); 2130 2131 for (Attribute a : attrList) 2132 { 2133 for (ByteString v : a) { 2134 try { 2135 String[] pwComponents = getPwComponents(v); 2136 2137 String schemeName = pwComponents[0]; 2138 PasswordStorageScheme<?> scheme = getPasswordStorageScheme(schemeName); 2139 if (scheme == null) { 2140 if (logger.isTraceEnabled()) { 2141 logger.trace("Skipping password value for user %s because the associated storage scheme %s " + 2142 "is not configured for use.", userDNString, schemeName); 2143 } 2144 continue; 2145 } 2146 2147 if (passwordMatches(password, pwComponents, scheme)) 2148 { 2149 if (passwordPolicy.isDefaultPasswordStorageScheme(schemeName)) { 2150 existingDefaultSchemes.add(schemeName); 2151 updatedValues.add(v); 2152 } else if (passwordPolicy.isDeprecatedPasswordStorageScheme(schemeName)) { 2153 if (logger.isTraceEnabled()) { 2154 logger.trace("Marking password with scheme %s for removal from user entry %s.", 2155 schemeName, userDNString); 2156 } 2157 removedValues.add(v); 2158 } else { 2159 updatedValues.add(v); 2160 } 2161 } 2162 } catch (Exception e) { 2163 logger.traceException(e, "Skipping password value for user %s because an error occurred while attempting " + 2164 "to decode it based on the user password syntax", userDNString); 2165 } 2166 } 2167 } 2168 2169 if (removedValues.isEmpty()) 2170 { 2171 logger.trace("User entry %s does not have any password values encoded using deprecated schemes.", userDNString); 2172 return; 2173 } 2174 2175 LinkedHashSet<ByteString> addedValues = new LinkedHashSet<>(); 2176 for (PasswordStorageScheme<?> s : passwordPolicy.getDefaultPasswordStorageSchemes()) 2177 { 2178 if (! existingDefaultSchemes.contains(toLowerCase(s.getStorageSchemeName()))) 2179 { 2180 try 2181 { 2182 ByteString encodedPassword = encodePassword(password, s); 2183 addedValues.add(encodedPassword); 2184 updatedValues.add(encodedPassword); 2185 } 2186 catch (Exception e) 2187 { 2188 logger.traceException(e); 2189 2190 if (logger.isTraceEnabled()) 2191 { 2192 logger.traceException(e, "Unable to encode password for user %s using default scheme %s", 2193 userDNString, s.getStorageSchemeName()); 2194 } 2195 } 2196 } 2197 } 2198 2199 if (updatedValues.isEmpty()) 2200 { 2201 logger.trace( 2202 "Not updating user entry %s because removing deprecated schemes would leave the user without a password.", 2203 userDNString); 2204 return; 2205 } 2206 2207 Attribute a = newAttribute(type, removedValues); 2208 modifications.add(new Modification(ModificationType.DELETE, a, true)); 2209 2210 if (! addedValues.isEmpty()) 2211 { 2212 Attribute a2 = newAttribute(type, addedValues); 2213 modifications.add(new Modification(ModificationType.ADD, a2, true)); 2214 } 2215 2216 if (logger.isTraceEnabled()) 2217 { 2218 logger.trace("Updating user entry %s to replace password values encoded with deprecated schemes " + 2219 "with values encoded with the default schemes.", userDNString); 2220 } 2221 } 2222 2223 private PasswordStorageScheme<?> getPasswordStorageScheme(String schemeName) 2224 { 2225 return passwordPolicy.isAuthPasswordSyntax() 2226 ? DirectoryServer.getAuthPasswordStorageScheme(schemeName) 2227 : DirectoryServer.getPasswordStorageScheme(schemeName); 2228 } 2229 2230 private boolean passwordMatches(ByteString password, String[] pwComponents, PasswordStorageScheme<?> scheme) 2231 { 2232 return passwordPolicy.isAuthPasswordSyntax() 2233 ? scheme.authPasswordMatches(password, pwComponents[1], pwComponents[2]) 2234 : scheme.passwordMatches(password, ByteString.valueOfUtf8(pwComponents[1])); 2235 } 2236 2237 private ByteString encodePassword(ByteString password, PasswordStorageScheme<?> s) throws DirectoryException 2238 { 2239 return passwordPolicy.isAuthPasswordSyntax() 2240 ? s.encodeAuthPassword(password) 2241 : s.encodePasswordWithScheme(password); 2242 } 2243 2244 /** 2245 * Indicates whether password history information should be maintained for this user. 2246 * 2247 * @return {@code true} if password history information should be maintained for this user, or {@code false} if not. 2248 */ 2249 public boolean maintainHistory() 2250 { 2251 return passwordPolicy.getPasswordHistoryCount() > 0 2252 || passwordPolicy.getPasswordHistoryDuration() > 0; 2253 } 2254 2255 /** 2256 * Indicates whether the provided password is equal to any of the current passwords, 2257 * or any of the passwords in the history. 2258 * 2259 * @param password The password for which to make the determination. 2260 * 2261 * @return {@code true} if the provided password is equal to any of the current passwords or any of the passwords 2262 * in the history, or {@code false} if not. 2263 */ 2264 public boolean isPasswordInHistory(ByteString password) 2265 { 2266 if (! maintainHistory()) 2267 { 2268 if (logger.isTraceEnabled()) 2269 { 2270 logger.trace("Returning false because password history checking is disabled."); 2271 } 2272 return false; 2273 } 2274 2275 // Check to see if the provided password is equal to any of the current passwords. 2276 // If so, then we'll consider it to be in the history. 2277 if (passwordMatches(password)) 2278 { 2279 if (logger.isTraceEnabled()) 2280 { 2281 logger.trace("Returning true because the provided password is currently in use."); 2282 } 2283 return true; 2284 } 2285 2286 // Get the attribute containing the history and check to see if any of the values is equal to the provided password. 2287 // However, first prune the list by size and duration if necessary. 2288 TreeMap<Long, ByteString> historyMap = getSortedHistoryValues(null); 2289 2290 int historyCount = passwordPolicy.getPasswordHistoryCount(); 2291 if (historyCount > 0 && historyMap.size() > historyCount) 2292 { 2293 int numToDelete = historyMap.size() - historyCount; 2294 Iterator<Long> iterator = historyMap.keySet().iterator(); 2295 while (iterator.hasNext() && numToDelete > 0) 2296 { 2297 iterator.next(); 2298 iterator.remove(); 2299 numToDelete--; 2300 } 2301 } 2302 2303 long historyDuration = passwordPolicy.getPasswordHistoryDuration(); 2304 if (historyDuration > 0L) 2305 { 2306 long retainDate = currentTime - 1000 * historyDuration; 2307 Iterator<Long> iterator = historyMap.keySet().iterator(); 2308 while (iterator.hasNext()) 2309 { 2310 long historyDate = iterator.next(); 2311 if (historyDate >= retainDate) 2312 { 2313 break; 2314 } 2315 iterator.remove(); 2316 } 2317 } 2318 2319 for (ByteString v : historyMap.values()) 2320 { 2321 if (historyValueMatches(password, v)) 2322 { 2323 if (logger.isTraceEnabled()) 2324 { 2325 logger.trace("Returning true because the password is in the history."); 2326 } 2327 2328 return true; 2329 } 2330 } 2331 2332 // If we've gotten here, then the password isn't in the history. 2333 if (logger.isTraceEnabled()) 2334 { 2335 logger.trace("Returning false because the password isn't in the history."); 2336 } 2337 return false; 2338 } 2339 2340 /** 2341 * Gets a sorted list of the password history values contained in the user's entry. 2342 * The values will be sorted by timestamp. 2343 * 2344 * @param removeAttrs A list into which any values will be placed that could not be properly decoded. 2345 * It may be {@code null} if this is not needed. 2346 */ 2347 private TreeMap<Long,ByteString> getSortedHistoryValues(List<Attribute> removeAttrs) 2348 { 2349 TreeMap<Long, ByteString> historyMap = new TreeMap<>(); 2350 AttributeType historyType = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC); 2351 for (Attribute a : userEntry.getAttribute(historyType)) 2352 { 2353 for (ByteString v : a) 2354 { 2355 String histStr = v.toString(); 2356 int hashPos = histStr.indexOf('#'); 2357 if (hashPos <= 0) 2358 { 2359 logger.trace("Found value %s in the history with no timestamp. Marking it for removal.", histStr); 2360 2361 if (removeAttrs != null) 2362 { 2363 removeAttrs.add(Attributes.create(a.getAttributeDescription().getAttributeType(), v)); 2364 } 2365 } 2366 else 2367 { 2368 try 2369 { 2370 ByteString timeValue = ByteString.valueOfUtf8(histStr.substring(0, hashPos)); 2371 long timestamp = GeneralizedTimeSyntax.decodeGeneralizedTimeValue(timeValue); 2372 historyMap.put(timestamp, v); 2373 } 2374 catch (Exception e) 2375 { 2376 if (logger.isTraceEnabled()) 2377 { 2378 logger.traceException(e); 2379 logger.trace("Could not decode the timestamp in history value %s -- %s. Marking it for removal.", 2380 histStr, e.getLocalizedMessage()); 2381 } 2382 2383 if (removeAttrs != null) 2384 { 2385 removeAttrs.add(Attributes.create(a.getAttributeDescription().getAttributeType(), v)); 2386 } 2387 } 2388 } 2389 } 2390 } 2391 2392 return historyMap; 2393 } 2394 2395 /** 2396 * Indicates whether the provided password matches the given history value. 2397 * 2398 * @param password The clear-text password for which to make the determination. 2399 * @param historyValue The encoded history value to compare against the clear-text password. 2400 * 2401 * @return {@code true} if the provided password matches the history value, or {@code false} if not. 2402 */ 2403 private boolean historyValueMatches(ByteString password, ByteString historyValue) { 2404 // According to draft-behera-ldap-password-policy, password history values should be in the format 2405 // time#syntaxoid#encodedvalue. In this method, we only care about the syntax OID and encoded password. 2406 try 2407 { 2408 String histStr = historyValue.toString(); 2409 int hashPos1 = histStr.indexOf('#'); 2410 if (hashPos1 <= 0) 2411 { 2412 if (logger.isTraceEnabled()) 2413 { 2414 logger.trace("Returning false because the password history value didn't include any hash characters."); 2415 } 2416 2417 return false; 2418 } 2419 2420 int hashPos2 = histStr.indexOf('#', hashPos1+1); 2421 if (hashPos2 < 0) 2422 { 2423 if (logger.isTraceEnabled()) 2424 { 2425 logger.trace("Returning false because the password history value only had one hash character."); 2426 } 2427 2428 return false; 2429 } 2430 2431 String syntaxOID = toLowerCase(histStr.substring(hashPos1+1, hashPos2)); 2432 if (SYNTAX_AUTH_PASSWORD_OID.equals(syntaxOID)) 2433 { 2434 boolean passwordMatches = encodedAuthPasswordMatches(password, histStr.substring(hashPos2+1)); 2435 logResult("auth", passwordMatches); 2436 return passwordMatches; 2437 } 2438 else if (SYNTAX_USER_PASSWORD_OID.equals(syntaxOID) || SYNTAX_OCTET_STRING_OID.equals(syntaxOID)) 2439 { 2440 boolean passwordMatches = encodedUserPasswordMatches(password, histStr.substring(hashPos2+1)); 2441 logResult("user", passwordMatches); 2442 return passwordMatches; 2443 } 2444 else 2445 { 2446 if (logger.isTraceEnabled()) 2447 { 2448 logger.trace("Returning false because the syntax OID " + syntaxOID + 2449 " didn't match for either the auth or user password syntax."); 2450 } 2451 2452 return false; 2453 } 2454 } 2455 catch (Exception e) 2456 { 2457 if (logger.isTraceEnabled()) 2458 { 2459 logger.traceException(e); 2460 logger.trace("Returning false because of an exception: " + stackTraceToSingleLineString(e)); 2461 } 2462 2463 return false; 2464 } 2465 } 2466 2467 private boolean encodedAuthPasswordMatches(ByteString password, String encodedAuthPassword) throws DirectoryException 2468 { 2469 String[] authPWComponents = AuthPasswordSyntax.decodeAuthPassword(encodedAuthPassword); 2470 PasswordStorageScheme<?> scheme = DirectoryServer.getAuthPasswordStorageScheme(authPWComponents[0]); 2471 return scheme.authPasswordMatches(password, authPWComponents[1], authPWComponents[2]); 2472 } 2473 2474 private boolean encodedUserPasswordMatches(ByteString password, String encodedUserPassword) throws DirectoryException 2475 { 2476 String[] userPWComponents = UserPasswordSyntax.decodeUserPassword(encodedUserPassword); 2477 PasswordStorageScheme<?> scheme = DirectoryServer.getPasswordStorageScheme(userPWComponents[0]); 2478 return scheme.passwordMatches(password, ByteString.valueOfUtf8(userPWComponents[1])); 2479 } 2480 2481 private void logResult(String passwordType, boolean passwordMatches) 2482 { 2483 if (passwordMatches) 2484 { 2485 logger.trace("Returning true because the %s password history value matched.", passwordType); 2486 } 2487 else 2488 { 2489 logger.trace("Returning false because the %s password history value did not match.", passwordType); 2490 } 2491 } 2492 2493 /** 2494 * Updates the password history information for this user by adding one of the passwords to it. 2495 * It will choose the first password encoded using a secure storage scheme, and will fall back to 2496 * a password encoded using an insecure storage scheme if necessary. 2497 */ 2498 public void updatePasswordHistory() 2499 { 2500 for (Attribute a : userEntry.getAttribute(passwordPolicy.getPasswordAttribute())) 2501 { 2502 ByteString insecurePassword = null; 2503 for (ByteString v : a) 2504 { 2505 try 2506 { 2507 PasswordStorageScheme<?> scheme = getPasswordStorageScheme(v); 2508 2509 if (scheme.isStorageSchemeSecure()) 2510 { 2511 addPasswordToHistory(v.toString()); 2512 insecurePassword = null; 2513 // no need to check any more values for this attribute 2514 break; 2515 } 2516 else if (insecurePassword == null) 2517 { 2518 insecurePassword = v; 2519 } 2520 } 2521 catch (DirectoryException e) 2522 { 2523 if (logger.isTraceEnabled()) 2524 { 2525 logger.trace("Encoded password " + v + " cannot be decoded and cannot be added to history."); 2526 } 2527 } 2528 } 2529 // If we get here we haven't found a password encoded securely, so we have to use one of the 2530 // other values. 2531 if (insecurePassword != null) 2532 { 2533 addPasswordToHistory(insecurePassword.toString()); 2534 } 2535 } 2536 } 2537 2538 /** 2539 * Adds the provided password to the password history. If appropriate, one or more old passwords may be 2540 * evicted from the list if the total size would exceed the configured count, or if passwords are older 2541 * than the configured duration. 2542 * 2543 * @param encodedPassword The encoded password (in either user password or auth password format) 2544 * to be added to the history. 2545 */ 2546 private void addPasswordToHistory(String encodedPassword) 2547 { 2548 if (! maintainHistory()) 2549 { 2550 if (logger.isTraceEnabled()) 2551 { 2552 logger.trace("Not doing anything because password history maintenance is disabled."); 2553 } 2554 2555 return; 2556 } 2557 2558 // Get a sorted list of the existing values to see if there are any that should be removed. 2559 LinkedList<Attribute> removeAttrs = new LinkedList<>(); 2560 TreeMap<Long, ByteString> historyMap = getSortedHistoryValues(removeAttrs); 2561 2562 // If there is a maximum number of values to retain and we would be over the limit with the new value, 2563 // then get rid of enough values (oldest first) to satisfy the count. 2564 AttributeType historyType = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC); 2565 int historyCount = passwordPolicy.getPasswordHistoryCount(); 2566 if (historyCount > 0 && historyMap.size() >= historyCount) 2567 { 2568 int numToDelete = historyMap.size() - historyCount + 1; 2569 LinkedHashSet<ByteString> removeValues = new LinkedHashSet<>(numToDelete); 2570 Iterator<ByteString> iterator = historyMap.values().iterator(); 2571 while (iterator.hasNext() && numToDelete > 0) 2572 { 2573 ByteString v = iterator.next(); 2574 removeValues.add(v); 2575 iterator.remove(); 2576 numToDelete--; 2577 2578 if (logger.isTraceEnabled()) 2579 { 2580 logger.trace("Removing history value %s to preserve the history count.", v); 2581 } 2582 } 2583 2584 if (! removeValues.isEmpty()) 2585 { 2586 removeAttrs.add(newAttribute(historyType, removeValues)); 2587 } 2588 } 2589 2590 // If there is a maximum duration, then get rid of any values that would be over the duration. 2591 long historyDuration = passwordPolicy.getPasswordHistoryDuration(); 2592 if (historyDuration > 0L) 2593 { 2594 long minAgeToKeep = currentTime - 1000L * historyDuration; 2595 Iterator<Long> iterator = historyMap.keySet().iterator(); 2596 LinkedHashSet<ByteString> removeValues = new LinkedHashSet<>(); 2597 while (iterator.hasNext()) 2598 { 2599 long timestamp = iterator.next(); 2600 if (timestamp >= minAgeToKeep) 2601 { 2602 break; 2603 } 2604 2605 ByteString v = historyMap.get(timestamp); 2606 removeValues.add(v); 2607 iterator.remove(); 2608 2609 if (logger.isTraceEnabled()) 2610 { 2611 logger.trace("Removing history value %s to preserve the history duration.", v); 2612 } 2613 } 2614 2615 if (! removeValues.isEmpty()) 2616 { 2617 removeAttrs.add(newAttribute(historyType, removeValues)); 2618 } 2619 } 2620 2621 // At this point, we can add the new value. However, we want to make sure that its timestamp 2622 // (which is the current time) doesn't conflict with any value already in the list. If there is a conflict, 2623 // then simply add one to it until we don't have any more conflicts. 2624 long newTimestamp = currentTime; 2625 while (historyMap.containsKey(newTimestamp)) 2626 { 2627 newTimestamp++; 2628 } 2629 String newHistStr = GeneralizedTimeSyntax.format(newTimestamp) + "#" + 2630 passwordPolicy.getPasswordAttribute().getSyntax().getOID() + "#" + encodedPassword; 2631 Attribute newHistAttr = Attributes.create(historyType, newHistStr); 2632 2633 if (logger.isTraceEnabled()) 2634 { 2635 logger.trace("Going to add history value " + newHistStr); 2636 } 2637 2638 // Apply the changes, either by adding modifications or by directly updating the entry. 2639 for (Attribute a : removeAttrs) 2640 { 2641 modifications.add(new Modification(ModificationType.DELETE, a, true)); 2642 } 2643 2644 modifications.add(new Modification(ModificationType.ADD, newHistAttr, true)); 2645 } 2646 2647 private Attribute newAttribute(AttributeType type, LinkedHashSet<ByteString> values) 2648 { 2649 AttributeBuilder builder = new AttributeBuilder(type); 2650 builder.addAll(values); 2651 return builder.toAttribute(); 2652 } 2653 2654 /** 2655 * Retrieves the password history state values for the user. This is only intended for testing purposes. 2656 * 2657 * @return The password history state values for the user. 2658 */ 2659 public String[] getPasswordHistoryValues() 2660 { 2661 ArrayList<String> historyValues = new ArrayList<>(); 2662 AttributeType historyType = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC); 2663 for (Attribute a : userEntry.getAttribute(historyType)) 2664 { 2665 for (ByteString v : a) 2666 { 2667 historyValues.add(v.toString()); 2668 } 2669 } 2670 return historyValues.toArray(new String[historyValues.size()]); 2671 } 2672 2673 /** 2674 * Clears the password history state information for the user. This is only intended for testing purposes. 2675 */ 2676 public void clearPasswordHistory() 2677 { 2678 logger.trace("Clearing password history for user %s", userDNString); 2679 2680 clearAttribute(OP_ATTR_PWPOLICY_HISTORY_LC); 2681 } 2682 2683 /** 2684 * Generates a new password for the user. 2685 * 2686 * @return The new password that has been generated, or <CODE>null</CODE> if no password generator has been defined. 2687 * 2688 * @throws DirectoryException If an error occurs while attempting to generate the new password. 2689 */ 2690 public ByteString generatePassword() 2691 throws DirectoryException 2692 { 2693 PasswordGenerator<?> generator = passwordPolicy.getPasswordGenerator(); 2694 if (generator == null) 2695 { 2696 if (logger.isTraceEnabled()) 2697 { 2698 logger.trace("Unable to generate a new password for user %s because no password generator has been defined" + 2699 "in the associated password policy.", userDNString); 2700 } 2701 2702 return null; 2703 } 2704 2705 return generator.generatePassword(userEntry); 2706 } 2707 2708 /** 2709 * Generates an account status notification for this user. 2710 * 2711 * @param notificationType The type for the account status notification. 2712 * @param userEntry The entry for the user to which this notification applies. 2713 * @param message The human-readable message for the notification. 2714 * @param notificationProperties The set of properties for the notification. 2715 */ 2716 public void generateAccountStatusNotification( 2717 AccountStatusNotificationType notificationType, 2718 Entry userEntry, LocalizableMessage message, 2719 Map<AccountStatusNotificationProperty,List<String>> notificationProperties) 2720 { 2721 generateAccountStatusNotification( 2722 new AccountStatusNotification(notificationType, userEntry, message, notificationProperties)); 2723 } 2724 2725 private void generateAccountStatusNotification(AccountStatusNotification notification) 2726 { 2727 Collection<AccountStatusNotificationHandler<?>> handlers = passwordPolicy.getAccountStatusNotificationHandlers(); 2728 for (AccountStatusNotificationHandler<?> handler : handlers) 2729 { 2730 handler.handleStatusNotification(notification); 2731 } 2732 } 2733 2734 /** 2735 * Retrieves the set of modifications that correspond to changes made in password policy processing 2736 * that may need to be applied to the user entry. 2737 * 2738 * @return The set of modifications that correspond to changes made in password policy processing 2739 * that may need to be applied to the user entry. 2740 */ 2741 public List<Modification> getModifications() 2742 { 2743 return modifications; 2744 } 2745 2746 @Override 2747 public void finalizeStateAfterBind() 2748 throws DirectoryException 2749 { 2750 // If there are no modifications, then there's nothing to do. 2751 if (modifications.isEmpty()) 2752 { 2753 return; 2754 } 2755 2756 // Convert the set of modifications to a set of LDAP modifications. 2757 ArrayList<RawModification> modList = new ArrayList<>(); 2758 for (Modification m : modifications) 2759 { 2760 modList.add(RawModification.create(m.getModificationType(), new LDAPAttribute(m.getAttribute()))); 2761 } 2762 2763 InternalClientConnection conn = getRootConnection(); 2764 ModifyOperation internalModify = conn.processModify(ByteString.valueOfUtf8(userDNString), modList); 2765 2766 ResultCode resultCode = internalModify.getResultCode(); 2767 if (resultCode != ResultCode.SUCCESS) 2768 { 2769 LocalizableMessage message = ERR_PWPSTATE_CANNOT_UPDATE_USER_ENTRY.get( 2770 userDNString, internalModify.getErrorMessage()); 2771 2772 // If this is a root user, or if the password policy says that we should ignore these problems, 2773 // then log a warning message. Otherwise, cause the bind to fail. 2774 if (DirectoryServer.isRootDN(userEntry.getName()) 2775 || passwordPolicy.getStateUpdateFailurePolicy() == PasswordPolicyCfgDefn.StateUpdateFailurePolicy.IGNORE) 2776 { 2777 logger.error(message); 2778 } 2779 else 2780 { 2781 throw new DirectoryException(resultCode, message); 2782 } 2783 } 2784 } 2785}