001 /* 002 * Copyright 2015 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005 /* 006 * Copyright (C) 2015 UnboundID Corp. 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021 package com.unboundid.ldap.sdk.unboundidds.controls; 022 023 024 025 import java.util.ArrayList; 026 import java.util.Collection; 027 import java.util.Collections; 028 import java.util.Iterator; 029 import java.util.List; 030 031 import com.unboundid.asn1.ASN1Boolean; 032 import com.unboundid.asn1.ASN1Element; 033 import com.unboundid.asn1.ASN1Integer; 034 import com.unboundid.asn1.ASN1Null; 035 import com.unboundid.asn1.ASN1OctetString; 036 import com.unboundid.asn1.ASN1Sequence; 037 import com.unboundid.ldap.sdk.Control; 038 import com.unboundid.ldap.sdk.DecodeableControl; 039 import com.unboundid.ldap.sdk.LDAPException; 040 import com.unboundid.ldap.sdk.LDAPResult; 041 import com.unboundid.ldap.sdk.ResultCode; 042 import com.unboundid.util.Debug; 043 import com.unboundid.util.NotMutable; 044 import com.unboundid.util.StaticUtils; 045 import com.unboundid.util.ThreadSafety; 046 import com.unboundid.util.ThreadSafetyLevel; 047 048 import static com.unboundid.ldap.sdk.unboundidds.controls.ControlMessages.*; 049 050 051 052 /** 053 * <BLOCKQUOTE> 054 * <B>NOTE:</B> This class is part of the Commercial Edition of the UnboundID 055 * LDAP SDK for Java. It is not available for use in applications that 056 * include only the Standard Edition of the LDAP SDK, and is not supported for 057 * use in conjunction with non-UnboundID products. 058 * </BLOCKQUOTE> 059 * This class provides an implementation for a response control that can be 060 * returned by the server in the response for add, modify, and password modify 061 * requests that include the password validation details request control. This 062 * response control will provide details about the password quality requirements 063 * that are in effect for the operation and whether the password included in the 064 * request satisfies each of those requirements. 065 * <BR><BR> 066 * This response control has an OID of 1.3.6.1.4.1.30221.2.5.41, a criticality 067 * of {@code false}, and a value with the provided encoding: 068 * <PRE> 069 * PasswordValidationDetailsResponse ::= SEQUENCE { 070 * validationResult CHOICE { 071 * validationDetails [0] SEQUENCE OF 072 * PasswordQualityRequirementValidationResult, 073 * noPasswordProvided [1] NULL, 074 * multiplePasswordsProvided [2] NULL, 075 * noValidationAttempted [3] NULL, 076 * ... }, 077 * missingCurrentPassword [3] BOOLEAN DEFAULT FALSE, 078 * mustChangePassword [4] BOOLEAN DEFAULT FALSE, 079 * secondsUntilExpiration [5] INTEGER OPTIONAL, 080 * ... } 081 * </PRE> 082 */ 083 @NotMutable() 084 @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 085 public final class PasswordValidationDetailsResponseControl 086 extends Control 087 implements DecodeableControl 088 { 089 /** 090 * The OID (1.3.6.1.4.1.30221.2.5.41) for the password validation details 091 * response control. 092 */ 093 public static final String PASSWORD_VALIDATION_DETAILS_RESPONSE_OID = 094 "1.3.6.1.4.1.30221.2.5.41"; 095 096 097 098 /** 099 * The BER type for the missing current password element. 100 */ 101 private static final byte TYPE_MISSING_CURRENT_PASSWORD = (byte) 0x83; 102 103 104 105 /** 106 * The BER type for the must change password element. 107 */ 108 private static final byte TYPE_MUST_CHANGE_PW = (byte) 0x84; 109 110 111 112 /** 113 * The BER type for the seconds until expiration element. 114 */ 115 private static final byte TYPE_SECONDS_UNTIL_EXPIRATION = (byte) 0x85; 116 117 118 119 /** 120 * The serial version UID for this serializable class. 121 */ 122 private static final long serialVersionUID = -2205640814914704074L; 123 124 125 126 // Indicates whether the associated password self change operation failed 127 // (or would fail if attempted without validation errors) because the user is 128 // required to provide his/her current password when performing a self change 129 // but did not do so. 130 private final boolean missingCurrentPassword; 131 132 // Indicates whether the user will be required to change his/her password 133 // immediately after the associated add or administrative password reset is 134 // complete. 135 private final boolean mustChangePassword; 136 137 // The length of time in seconds that the new password will be considered 138 // valid. 139 private final Integer secondsUntilExpiration; 140 141 // The list of the validation results for the associated operation. 142 private final List<PasswordQualityRequirementValidationResult> 143 validationResults; 144 145 // The response type for this password validation details response control. 146 private final PasswordValidationDetailsResponseType responseType; 147 148 149 150 /** 151 * Creates a new empty control instance that is intended to be used only for 152 * decoding controls via the {@code DecodeableControl} interface. 153 */ 154 PasswordValidationDetailsResponseControl() 155 { 156 responseType = null; 157 validationResults = null; 158 missingCurrentPassword = true; 159 mustChangePassword = true; 160 secondsUntilExpiration = null; 161 } 162 163 164 165 /** 166 * Creates a password validation details response control with the provided 167 * information. 168 * 169 * @param responseType The response type for this password 170 * validation details response control. This 171 * must not be {@code null}. 172 * @param validationResults A list of the results obtained when 173 * validating the password against the 174 * password quality requirements. This must 175 * be {@code null} or empty if the 176 * {@code responseType} element has a value 177 * other than {@code VALIDATION_DETAILS}. 178 * @param missingCurrentPassword Indicates whether the associated operation 179 * is a self change that failed (or would have 180 * failed if not for additional validation 181 * failures) because the user did not provide 182 * his/her current password as required. 183 * @param mustChangePassword Indicates whether the associated operation 184 * is an add or administrative reset that will 185 * require the user to change his/her password 186 * immediately after authenticating before 187 * allowing them to perform any other 188 * operation in the server. 189 * @param secondsUntilExpiration The maximum length of time, in seconds, 190 * that the newly-set password will be 191 * considered valid. This may be {@code null} 192 * if the new password will be considered 193 * valid indefinitely. 194 */ 195 public PasswordValidationDetailsResponseControl( 196 final PasswordValidationDetailsResponseType responseType, 197 final Collection<PasswordQualityRequirementValidationResult> 198 validationResults, 199 final boolean missingCurrentPassword, 200 final boolean mustChangePassword, 201 final Integer secondsUntilExpiration) 202 { 203 super(PASSWORD_VALIDATION_DETAILS_RESPONSE_OID, false, 204 encodeValue(responseType, validationResults, missingCurrentPassword, 205 mustChangePassword, secondsUntilExpiration)); 206 207 this.responseType = responseType; 208 this.missingCurrentPassword = missingCurrentPassword; 209 this.mustChangePassword = mustChangePassword; 210 this.secondsUntilExpiration = secondsUntilExpiration; 211 212 if (validationResults == null) 213 { 214 this.validationResults = Collections.emptyList(); 215 } 216 else 217 { 218 this.validationResults = Collections.unmodifiableList( 219 new ArrayList<PasswordQualityRequirementValidationResult>( 220 validationResults)); 221 } 222 } 223 224 225 226 /** 227 * Creates a new password validation details response control by decoding the 228 * provided generic control information. 229 * 230 * @param oid The OID for the control. 231 * @param isCritical Indicates whether the control should be considered 232 * critical. 233 * @param value The value for the control. 234 * 235 * @throws LDAPException If the provided information cannot be decoded to 236 * create a password validation details response 237 * control. 238 */ 239 public PasswordValidationDetailsResponseControl(final String oid, 240 final boolean isCritical, 241 final ASN1OctetString value) 242 throws LDAPException 243 { 244 super(oid, isCritical, value); 245 246 if (value == null) 247 { 248 throw new LDAPException(ResultCode.DECODING_ERROR, 249 ERR_PW_VALIDATION_RESPONSE_NO_VALUE.get()); 250 } 251 252 try 253 { 254 final ASN1Element[] elements = 255 ASN1Sequence.decodeAsSequence(value.getValue()).elements(); 256 257 responseType = PasswordValidationDetailsResponseType.forBERType( 258 elements[0].getType()); 259 if (responseType == null) 260 { 261 throw new LDAPException(ResultCode.DECODING_ERROR, 262 ERR_PW_VALIDATION_RESPONSE_INVALID_RESPONSE_TYPE.get( 263 StaticUtils.toHex(elements[0].getType()))); 264 } 265 266 if (responseType == 267 PasswordValidationDetailsResponseType.VALIDATION_DETAILS) 268 { 269 final ASN1Element[] resultElements = 270 ASN1Sequence.decodeAsSequence(elements[0]).elements(); 271 272 final ArrayList<PasswordQualityRequirementValidationResult> resultList = 273 new ArrayList<PasswordQualityRequirementValidationResult>( 274 resultElements.length); 275 for (final ASN1Element e : resultElements) 276 { 277 resultList.add(PasswordQualityRequirementValidationResult.decode(e)); 278 } 279 validationResults = Collections.unmodifiableList(resultList); 280 } 281 else 282 { 283 validationResults = Collections.emptyList(); 284 } 285 286 boolean missingCurrent = false; 287 boolean mustChange = false; 288 Integer secondsRemaining = null; 289 for (int i=1; i < elements.length; i++) 290 { 291 switch (elements[i].getType()) 292 { 293 case TYPE_MISSING_CURRENT_PASSWORD: 294 missingCurrent = 295 ASN1Boolean.decodeAsBoolean(elements[i]).booleanValue(); 296 break; 297 298 case TYPE_MUST_CHANGE_PW: 299 mustChange = 300 ASN1Boolean.decodeAsBoolean(elements[i]).booleanValue(); 301 break; 302 303 case TYPE_SECONDS_UNTIL_EXPIRATION: 304 secondsRemaining = 305 ASN1Integer.decodeAsInteger(elements[i]).intValue(); 306 break; 307 308 default: 309 // We may update this control in the future to provide support for 310 // returning additional password-related information. If we 311 // encounter an unrecognized element, just ignore it rather than 312 // throwing an exception. 313 break; 314 } 315 } 316 317 missingCurrentPassword = missingCurrent; 318 mustChangePassword = mustChange; 319 secondsUntilExpiration = secondsRemaining; 320 } 321 catch (final LDAPException le) 322 { 323 Debug.debugException(le); 324 throw le; 325 } 326 catch (final Exception e) 327 { 328 Debug.debugException(e); 329 throw new LDAPException(ResultCode.DECODING_ERROR, 330 ERR_PW_VALIDATION_RESPONSE_ERROR_PARSING_VALUE.get( 331 StaticUtils.getExceptionMessage(e)), 332 e); 333 } 334 } 335 336 337 338 /** 339 * Encodes the provided information to an ASN.1 element suitable for use as 340 * the control value. 341 * 342 * @param responseType The response type for this password 343 * validation details response control. This 344 * must not be {@code null}. 345 * @param validationResults A list of the results obtained when 346 * validating the password against the 347 * password quality requirements. This must 348 * be {@code null} or empty if the 349 * {@code responseType} element has a value 350 * other than {@code VALIDATION_DETAILS}. 351 * @param missingCurrentPassword Indicates whether the associated operation 352 * is a self change that failed (or would have 353 * failed if not for additional validation 354 * failures) because the user did not provide 355 * his/her current password as required. 356 * @param mustChangePassword Indicates whether the associated operation 357 * is an add or administrative reset that will 358 * require the user to change his/her password 359 * immediately after authenticating before 360 * allowing them to perform any other 361 * operation in the server. 362 * @param secondsUntilExpiration The maximum length of time, in seconds, 363 * that the newly-set password will be 364 * considered valid. This may be {@code null} 365 * if the new password will be considered 366 * valid indefinitely. 367 * 368 * @return The encoded control value. 369 */ 370 private static ASN1OctetString encodeValue( 371 final PasswordValidationDetailsResponseType responseType, 372 final Collection<PasswordQualityRequirementValidationResult> 373 validationResults, 374 final boolean missingCurrentPassword, 375 final boolean mustChangePassword, 376 final Integer secondsUntilExpiration) 377 { 378 final ArrayList<ASN1Element> elements = new ArrayList<ASN1Element>(4); 379 380 switch (responseType) 381 { 382 case VALIDATION_DETAILS: 383 if (validationResults == null) 384 { 385 elements.add(new ASN1Sequence(responseType.getBERType())); 386 } 387 else 388 { 389 final ArrayList<ASN1Element> resultElements = 390 new ArrayList<ASN1Element>(validationResults.size()); 391 for (final PasswordQualityRequirementValidationResult r : 392 validationResults) 393 { 394 resultElements.add(r.encode()); 395 } 396 elements.add(new ASN1Sequence(responseType.getBERType(), 397 resultElements)); 398 } 399 break; 400 401 case NO_PASSWORD_PROVIDED: 402 case MULTIPLE_PASSWORDS_PROVIDED: 403 case NO_VALIDATION_ATTEMPTED: 404 elements.add(new ASN1Null(responseType.getBERType())); 405 break; 406 } 407 408 if (missingCurrentPassword) 409 { 410 elements.add(new ASN1Boolean(TYPE_MISSING_CURRENT_PASSWORD, 411 missingCurrentPassword)); 412 } 413 414 if (mustChangePassword) 415 { 416 elements.add(new ASN1Boolean(TYPE_MUST_CHANGE_PW, mustChangePassword)); 417 } 418 419 if (secondsUntilExpiration != null) 420 { 421 elements.add(new ASN1Integer(TYPE_SECONDS_UNTIL_EXPIRATION, 422 secondsUntilExpiration)); 423 } 424 425 return new ASN1OctetString(new ASN1Sequence(elements).encode()); 426 } 427 428 429 430 /** 431 * Retrieves the response type for this password validation details response 432 * control. 433 * 434 * @return The response type for this password validation details response 435 * control. 436 */ 437 public PasswordValidationDetailsResponseType getResponseType() 438 { 439 return responseType; 440 } 441 442 443 444 /** 445 * Retrieves a list of the results obtained when attempting to validate the 446 * proposed password against the password quality requirements in effect for 447 * the operation. 448 * 449 * @return A list of the results obtained when attempting to validate the 450 * proposed password against the password quality requirements in 451 * effect for the operation, or an empty list if no validation 452 * results are available. 453 */ 454 public List<PasswordQualityRequirementValidationResult> getValidationResults() 455 { 456 return validationResults; 457 } 458 459 460 461 /** 462 * Indicates whether the associated operation is a self password change that 463 * requires the user to provide his/her current password when setting a new 464 * password, but no current password was provided. 465 * 466 * @return {@code true} if the associated operation is a self password change 467 * that requires the user to provide his/her current password when 468 * setting a new password but none was required, or {@code false} if 469 * the associated operation was not a self change, or if the user's 470 * current password was provided. 471 */ 472 public boolean missingCurrentPassword() 473 { 474 return missingCurrentPassword; 475 } 476 477 478 479 /** 480 * Indicates whether the user will be required to immediately change his/her 481 * password after the associated add or administrative reset is complete. 482 * 483 * @return {@code true} if the associated operation is an add or 484 * administrative reset and the user will be required to change 485 * his/her password before being allowed to perform any other 486 * operation, or {@code false} if the associated operation was not am 487 * add or an administrative reset, or if the user will not be 488 * required to immediately change his/her password. 489 */ 490 public boolean mustChangePassword() 491 { 492 return mustChangePassword; 493 } 494 495 496 497 /** 498 * Retrieves the maximum length of time, in seconds, that the newly-set 499 * password will be considered valid. If {@link #mustChangePassword()} 500 * returns {@code true}, then this value will be the length of time that the 501 * user has to perform a self password change before the account becomes 502 * locked. If {@code mustChangePassword()} returns {@code false}, then this 503 * value will be the length of time until the password expires. 504 * 505 * @return The maximum length of time, in seconds, that the newly-set 506 * password will be considered valid, or {@code null} if the new 507 * password will be valid indefinitely. 508 */ 509 public Integer getSecondsUntilExpiration() 510 { 511 return secondsUntilExpiration; 512 } 513 514 515 516 /** 517 * {@inheritDoc} 518 */ 519 public PasswordValidationDetailsResponseControl decodeControl( 520 final String oid, final boolean isCritical, 521 final ASN1OctetString value) 522 throws LDAPException 523 { 524 return new PasswordValidationDetailsResponseControl(oid, isCritical, value); 525 } 526 527 528 529 /** 530 * Extracts a password validation details response control from the provided 531 * result. 532 * 533 * @param result The result from which to retrieve the password validation 534 * details response control. 535 * 536 * @return The password validation details response control contained in the 537 * provided result, or {@code null} if the result did not contain a 538 * password validation details response control. 539 * 540 * @throws LDAPException If a problem is encountered while attempting to 541 * decode the password validation details response 542 * control contained in the provided result. 543 */ 544 public static PasswordValidationDetailsResponseControl 545 get(final LDAPResult result) 546 throws LDAPException 547 { 548 final Control c = 549 result.getResponseControl(PASSWORD_VALIDATION_DETAILS_RESPONSE_OID); 550 if (c == null) 551 { 552 return null; 553 } 554 555 if (c instanceof PasswordValidationDetailsResponseControl) 556 { 557 return (PasswordValidationDetailsResponseControl) c; 558 } 559 else 560 { 561 return new PasswordValidationDetailsResponseControl(c.getOID(), 562 c.isCritical(), c.getValue()); 563 } 564 } 565 566 567 568 /** 569 * {@inheritDoc} 570 */ 571 @Override() 572 public String getControlName() 573 { 574 return INFO_CONTROL_NAME_PW_VALIDATION_RESPONSE.get(); 575 } 576 577 578 579 /** 580 * {@inheritDoc} 581 */ 582 @Override() 583 public void toString(final StringBuilder buffer) 584 { 585 buffer.append("PasswordValidationDetailsResponseControl(responseType='"); 586 buffer.append(responseType.name()); 587 buffer.append('\''); 588 589 if (responseType == 590 PasswordValidationDetailsResponseType.VALIDATION_DETAILS) 591 { 592 buffer.append(", validationDetails={"); 593 594 final Iterator<PasswordQualityRequirementValidationResult> iterator = 595 validationResults.iterator(); 596 while (iterator.hasNext()) 597 { 598 iterator.next().toString(buffer); 599 if (iterator.hasNext()) 600 { 601 buffer.append(','); 602 } 603 } 604 605 buffer.append('}'); 606 } 607 608 buffer.append(", missingCurrentPassword="); 609 buffer.append(missingCurrentPassword); 610 buffer.append(", mustChangePassword="); 611 buffer.append(mustChangePassword); 612 613 if (secondsUntilExpiration != null) 614 { 615 buffer.append(", secondsUntilExpiration="); 616 buffer.append(secondsUntilExpiration); 617 } 618 619 buffer.append("})"); 620 } 621 }