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 Alcatel-Lucent 8661 064 * server products. These classes provide support for proprietary 065 * functionality or for external specifications that are not considered stable 066 * or mature enough to be guaranteed to work in an interoperable way with 067 * 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<PasswordQualityRequirementValidationResult>( 224 validationResults)); 225 } 226 } 227 228 229 230 /** 231 * Creates a new password validation details response control by decoding the 232 * provided generic control information. 233 * 234 * @param oid The OID for the control. 235 * @param isCritical Indicates whether the control should be considered 236 * critical. 237 * @param value The value for the control. 238 * 239 * @throws LDAPException If the provided information cannot be decoded to 240 * create a password validation details response 241 * control. 242 */ 243 public PasswordValidationDetailsResponseControl(final String oid, 244 final boolean isCritical, 245 final ASN1OctetString value) 246 throws LDAPException 247 { 248 super(oid, isCritical, value); 249 250 if (value == null) 251 { 252 throw new LDAPException(ResultCode.DECODING_ERROR, 253 ERR_PW_VALIDATION_RESPONSE_NO_VALUE.get()); 254 } 255 256 try 257 { 258 final ASN1Element[] elements = 259 ASN1Sequence.decodeAsSequence(value.getValue()).elements(); 260 261 responseType = PasswordValidationDetailsResponseType.forBERType( 262 elements[0].getType()); 263 if (responseType == null) 264 { 265 throw new LDAPException(ResultCode.DECODING_ERROR, 266 ERR_PW_VALIDATION_RESPONSE_INVALID_RESPONSE_TYPE.get( 267 StaticUtils.toHex(elements[0].getType()))); 268 } 269 270 if (responseType == 271 PasswordValidationDetailsResponseType.VALIDATION_DETAILS) 272 { 273 final ASN1Element[] resultElements = 274 ASN1Sequence.decodeAsSequence(elements[0]).elements(); 275 276 final ArrayList<PasswordQualityRequirementValidationResult> resultList = 277 new ArrayList<PasswordQualityRequirementValidationResult>( 278 resultElements.length); 279 for (final ASN1Element e : resultElements) 280 { 281 resultList.add(PasswordQualityRequirementValidationResult.decode(e)); 282 } 283 validationResults = Collections.unmodifiableList(resultList); 284 } 285 else 286 { 287 validationResults = Collections.emptyList(); 288 } 289 290 boolean missingCurrent = false; 291 boolean mustChange = false; 292 Integer secondsRemaining = null; 293 for (int i=1; i < elements.length; i++) 294 { 295 switch (elements[i].getType()) 296 { 297 case TYPE_MISSING_CURRENT_PASSWORD: 298 missingCurrent = 299 ASN1Boolean.decodeAsBoolean(elements[i]).booleanValue(); 300 break; 301 302 case TYPE_MUST_CHANGE_PW: 303 mustChange = 304 ASN1Boolean.decodeAsBoolean(elements[i]).booleanValue(); 305 break; 306 307 case TYPE_SECONDS_UNTIL_EXPIRATION: 308 secondsRemaining = 309 ASN1Integer.decodeAsInteger(elements[i]).intValue(); 310 break; 311 312 default: 313 // We may update this control in the future to provide support for 314 // returning additional password-related information. If we 315 // encounter an unrecognized element, just ignore it rather than 316 // throwing an exception. 317 break; 318 } 319 } 320 321 missingCurrentPassword = missingCurrent; 322 mustChangePassword = mustChange; 323 secondsUntilExpiration = secondsRemaining; 324 } 325 catch (final LDAPException le) 326 { 327 Debug.debugException(le); 328 throw le; 329 } 330 catch (final Exception e) 331 { 332 Debug.debugException(e); 333 throw new LDAPException(ResultCode.DECODING_ERROR, 334 ERR_PW_VALIDATION_RESPONSE_ERROR_PARSING_VALUE.get( 335 StaticUtils.getExceptionMessage(e)), 336 e); 337 } 338 } 339 340 341 342 /** 343 * Encodes the provided information to an ASN.1 element suitable for use as 344 * the control value. 345 * 346 * @param responseType The response type for this password 347 * validation details response control. This 348 * must not be {@code null}. 349 * @param validationResults A list of the results obtained when 350 * validating the password against the 351 * password quality requirements. This must 352 * be {@code null} or empty if the 353 * {@code responseType} element has a value 354 * other than {@code VALIDATION_DETAILS}. 355 * @param missingCurrentPassword Indicates whether the associated operation 356 * is a self change that failed (or would have 357 * failed if not for additional validation 358 * failures) because the user did not provide 359 * his/her current password as required. 360 * @param mustChangePassword Indicates whether the associated operation 361 * is an add or administrative reset that will 362 * require the user to change his/her password 363 * immediately after authenticating before 364 * allowing them to perform any other 365 * operation in the server. 366 * @param secondsUntilExpiration The maximum length of time, in seconds, 367 * that the newly-set password will be 368 * considered valid. This may be {@code null} 369 * if the new password will be considered 370 * valid indefinitely. 371 * 372 * @return The encoded control value. 373 */ 374 private static ASN1OctetString encodeValue( 375 final PasswordValidationDetailsResponseType responseType, 376 final Collection<PasswordQualityRequirementValidationResult> 377 validationResults, 378 final boolean missingCurrentPassword, 379 final boolean mustChangePassword, 380 final Integer secondsUntilExpiration) 381 { 382 final ArrayList<ASN1Element> elements = new ArrayList<ASN1Element>(4); 383 384 switch (responseType) 385 { 386 case VALIDATION_DETAILS: 387 if (validationResults == null) 388 { 389 elements.add(new ASN1Sequence(responseType.getBERType())); 390 } 391 else 392 { 393 final ArrayList<ASN1Element> resultElements = 394 new ArrayList<ASN1Element>(validationResults.size()); 395 for (final PasswordQualityRequirementValidationResult r : 396 validationResults) 397 { 398 resultElements.add(r.encode()); 399 } 400 elements.add(new ASN1Sequence(responseType.getBERType(), 401 resultElements)); 402 } 403 break; 404 405 case NO_PASSWORD_PROVIDED: 406 case MULTIPLE_PASSWORDS_PROVIDED: 407 case NO_VALIDATION_ATTEMPTED: 408 elements.add(new ASN1Null(responseType.getBERType())); 409 break; 410 } 411 412 if (missingCurrentPassword) 413 { 414 elements.add(new ASN1Boolean(TYPE_MISSING_CURRENT_PASSWORD, 415 missingCurrentPassword)); 416 } 417 418 if (mustChangePassword) 419 { 420 elements.add(new ASN1Boolean(TYPE_MUST_CHANGE_PW, mustChangePassword)); 421 } 422 423 if (secondsUntilExpiration != null) 424 { 425 elements.add(new ASN1Integer(TYPE_SECONDS_UNTIL_EXPIRATION, 426 secondsUntilExpiration)); 427 } 428 429 return new ASN1OctetString(new ASN1Sequence(elements).encode()); 430 } 431 432 433 434 /** 435 * Retrieves the response type for this password validation details response 436 * control. 437 * 438 * @return The response type for this password validation details response 439 * control. 440 */ 441 public PasswordValidationDetailsResponseType getResponseType() 442 { 443 return responseType; 444 } 445 446 447 448 /** 449 * Retrieves a list of the results obtained when attempting to validate the 450 * proposed password against the password quality requirements in effect for 451 * the operation. 452 * 453 * @return A list of the results obtained when attempting to validate the 454 * proposed password against the password quality requirements in 455 * effect for the operation, or an empty list if no validation 456 * results are available. 457 */ 458 public List<PasswordQualityRequirementValidationResult> getValidationResults() 459 { 460 return validationResults; 461 } 462 463 464 465 /** 466 * Indicates whether the associated operation is a self password change that 467 * requires the user to provide his/her current password when setting a new 468 * password, but no current password was provided. 469 * 470 * @return {@code true} if the associated operation is a self password change 471 * that requires the user to provide his/her current password when 472 * setting a new password but none was required, or {@code false} if 473 * the associated operation was not a self change, or if the user's 474 * current password was provided. 475 */ 476 public boolean missingCurrentPassword() 477 { 478 return missingCurrentPassword; 479 } 480 481 482 483 /** 484 * Indicates whether the user will be required to immediately change his/her 485 * password after the associated add or administrative reset is complete. 486 * 487 * @return {@code true} if the associated operation is an add or 488 * administrative reset and the user will be required to change 489 * his/her password before being allowed to perform any other 490 * operation, or {@code false} if the associated operation was not am 491 * add or an administrative reset, or if the user will not be 492 * required to immediately change his/her password. 493 */ 494 public boolean mustChangePassword() 495 { 496 return mustChangePassword; 497 } 498 499 500 501 /** 502 * Retrieves the maximum length of time, in seconds, that the newly-set 503 * password will be considered valid. If {@link #mustChangePassword()} 504 * returns {@code true}, then this value will be the length of time that the 505 * user has to perform a self password change before the account becomes 506 * locked. If {@code mustChangePassword()} returns {@code false}, then this 507 * value will be the length of time until the password expires. 508 * 509 * @return The maximum length of time, in seconds, that the newly-set 510 * password will be considered valid, or {@code null} if the new 511 * password will be valid indefinitely. 512 */ 513 public Integer getSecondsUntilExpiration() 514 { 515 return secondsUntilExpiration; 516 } 517 518 519 520 /** 521 * {@inheritDoc} 522 */ 523 @Override() 524 public PasswordValidationDetailsResponseControl decodeControl( 525 final String oid, final boolean isCritical, 526 final ASN1OctetString value) 527 throws LDAPException 528 { 529 return new PasswordValidationDetailsResponseControl(oid, isCritical, value); 530 } 531 532 533 534 /** 535 * Extracts a password validation details response control from the provided 536 * result. 537 * 538 * @param result The result from which to retrieve the password validation 539 * details response control. 540 * 541 * @return The password validation details response control contained in the 542 * provided result, or {@code null} if the result did not contain a 543 * password validation details response control. 544 * 545 * @throws LDAPException If a problem is encountered while attempting to 546 * decode the password validation details response 547 * control contained in the provided result. 548 */ 549 public static PasswordValidationDetailsResponseControl 550 get(final LDAPResult result) 551 throws LDAPException 552 { 553 final Control c = 554 result.getResponseControl(PASSWORD_VALIDATION_DETAILS_RESPONSE_OID); 555 if (c == null) 556 { 557 return null; 558 } 559 560 if (c instanceof PasswordValidationDetailsResponseControl) 561 { 562 return (PasswordValidationDetailsResponseControl) c; 563 } 564 else 565 { 566 return new PasswordValidationDetailsResponseControl(c.getOID(), 567 c.isCritical(), c.getValue()); 568 } 569 } 570 571 572 573 /** 574 * {@inheritDoc} 575 */ 576 @Override() 577 public String getControlName() 578 { 579 return INFO_CONTROL_NAME_PW_VALIDATION_RESPONSE.get(); 580 } 581 582 583 584 /** 585 * {@inheritDoc} 586 */ 587 @Override() 588 public void toString(final StringBuilder buffer) 589 { 590 buffer.append("PasswordValidationDetailsResponseControl(responseType='"); 591 buffer.append(responseType.name()); 592 buffer.append('\''); 593 594 if (responseType == 595 PasswordValidationDetailsResponseType.VALIDATION_DETAILS) 596 { 597 buffer.append(", validationDetails={"); 598 599 final Iterator<PasswordQualityRequirementValidationResult> iterator = 600 validationResults.iterator(); 601 while (iterator.hasNext()) 602 { 603 iterator.next().toString(buffer); 604 if (iterator.hasNext()) 605 { 606 buffer.append(','); 607 } 608 } 609 610 buffer.append('}'); 611 } 612 613 buffer.append(", missingCurrentPassword="); 614 buffer.append(missingCurrentPassword); 615 buffer.append(", mustChangePassword="); 616 buffer.append(mustChangePassword); 617 618 if (secondsUntilExpiration != null) 619 { 620 buffer.append(", secondsUntilExpiration="); 621 buffer.append(secondsUntilExpiration); 622 } 623 624 buffer.append("})"); 625 } 626}