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