001/* 002 * Copyright 2015-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2015-2024 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2015-2024 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldap.sdk.unboundidds.extensions; 037 038 039 040import java.io.Serializable; 041import java.util.ArrayList; 042import java.util.Collections; 043import java.util.Iterator; 044import java.util.LinkedHashMap; 045import java.util.Map; 046 047import com.unboundid.asn1.ASN1Element; 048import com.unboundid.asn1.ASN1OctetString; 049import com.unboundid.asn1.ASN1Sequence; 050import com.unboundid.asn1.ASN1Set; 051import com.unboundid.ldap.sdk.LDAPException; 052import com.unboundid.ldap.sdk.ResultCode; 053import com.unboundid.util.Debug; 054import com.unboundid.util.NotMutable; 055import com.unboundid.util.NotNull; 056import com.unboundid.util.Nullable; 057import com.unboundid.util.StaticUtils; 058import com.unboundid.util.ThreadSafety; 059import com.unboundid.util.ThreadSafetyLevel; 060import com.unboundid.util.Validator; 061 062import static com.unboundid.ldap.sdk.unboundidds.extensions.ExtOpMessages.*; 063 064 065 066/** 067 * This class provides a data structure that describes a requirement that 068 * passwords must satisfy in order to be accepted by the server. 069 * <BR> 070 * <BLOCKQUOTE> 071 * <B>NOTE:</B> This class, and other classes within the 072 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 073 * supported for use against Ping Identity, UnboundID, and 074 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 075 * for proprietary functionality or for external specifications that are not 076 * considered stable or mature enough to be guaranteed to work in an 077 * interoperable way with other types of LDAP servers. 078 * </BLOCKQUOTE> 079 * <BR> 080 * A password quality requirement will always include a description, which 081 * should be a string that provides a user-friendly description of the 082 * constraints that a proposed password must satisfy in order to meet this 083 * requirement and be accepted by the server. It may optionally include 084 * additional information that could allow an application to attempt some kind 085 * of pre-validation in order to determine whether a proposed password might 086 * fall outside the constraints associated with this requirement and would 087 * therefore be rejected by the server. This could allow a client to provide 088 * better performance (by not having to submit a password to the server and wait 089 * for the response in order to detect certain kinds of problems) and a better 090 * user experience (for example, by interactively indicating whether the value 091 * is acceptable as the user is entering it). 092 * <BR><BR> 093 * If a password quality requirement object does provide client-side validation 094 * data, then it will include at least a validation type (which indicates the 095 * nature of the validation that will be performed), and an optional set of 096 * properties that provide additional information about the specific nature of 097 * the validation. For example, if the server is configured with a length-based 098 * password validator that requires passwords to be between eight and 20 099 * characters, then the requirement may have a validation type of "length" and 100 * two validation properties: "minimum-length" with a value of "8" and 101 * "maximum-length" with a value of "20". An application that supports this 102 * type of client-side validation could prevent a user from supplying a password 103 * that is too short or too long without the need to communicate with the 104 * server. 105 * <BR><BR> 106 * Note that not all types of password requirements will support client-side 107 * validation. For example, the server may be configured to use a dictionary 108 * with some of the most commonly-used passwords in an attempt to prevent 109 * users from selecting passwords that may be easily guessed, or the server 110 * may be configured with a password history to prevent users from selecting a 111 * password that they had already used. In these kinds of cases, the 112 * application will not have access to the information necessary to make the 113 * determination using client-side logic. The server is the ultimate authority 114 * as to whether a proposed password will be accepted, and even applications 115 * should be prepared to handle the case in which a password is rejected by the 116 * server even if client-side validation does not indicate that there are any 117 * problems with the password. There may also be cases in which the reason that 118 * an attempt to set a password fails for a reason that is not related to the 119 * quality of the provided password. 120 * <BR><BR> 121 * However, even in cases where an application may not be able to perform any 122 * client-side validation, the server may still offer a client-side validation 123 * type and validation properties. This is not intended to help the client 124 * determine whether a proposed password is acceptable, but could allow the 125 * client to convey information about the requirement to the user in a more 126 * flexible manner than simply providing the requirement description (e.g., it 127 * could allow the client to provide information about the requirement to the 128 * user in a different language than the server-provided description, or it 129 * could allow information about one requirement to be split into multiple 130 * elements, or multiple requirements combined into a single element. 131 * <BR><BR> 132 * If it appears in an LDAP protocol element (e.g., a get password quality 133 * requirements extended response, or a password validation details response 134 * control), it should have the following ASN.1 encoding: 135 * <PRE> 136 * PasswordQualityRequirement ::= SEQUENCE { 137 * description OCTET STRING, 138 * clientSideValidationInfo [0] SEQUENCE { 139 * validationType OCTET STRING, 140 * properties [0] SET OF SEQUENCE { 141 * name OCTET STRING, 142 * value OCTET STRING } OPTIONAL } OPTIONAL } 143 * </PRE> 144 */ 145@NotMutable() 146@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 147public final class PasswordQualityRequirement 148 implements Serializable 149{ 150 /** 151 * The BER type that will be used for the optional client-side validation info 152 * element of an encoded password quality requirement. 153 */ 154 private static final byte TYPE_CLIENT_SIDE_VALIDATION_INFO = (byte) 0xA1; 155 156 157 158 /** 159 * The BER type that will be used for the optional validation properties 160 * element of an encoded client-side validation info element. 161 */ 162 private static final byte TYPE_CLIENT_SIDE_VALIDATION_PROPERTIES = 163 (byte) 0xA1; 164 165 166 167 /** 168 * The serial version UID for this serializable class. 169 */ 170 private static final long serialVersionUID = 2956655422853571644L; 171 172 173 174 // A set of properties that may be used to indicate constraints that the 175 // server will impose when validating the password in accordance with this 176 // requirement. 177 @NotNull private final Map<String,String> clientSideValidationProperties; 178 179 // The name of the client-side validation type for this requirement, if any. 180 @Nullable private final String clientSideValidationType; 181 182 // A user-friendly description of the constraints that proposed passwords must 183 // satisfy in order to be accepted by the server. 184 @NotNull private final String description; 185 186 187 188 /** 189 * Creates a new password quality requirement object without any support for 190 * client-side validation. 191 * 192 * @param description A user-friendly description of the constraints that a 193 * proposed password must satisfy in order to meet this 194 * requirement and be accepted by the server. This must 195 * not be {@code null}. 196 */ 197 public PasswordQualityRequirement(@NotNull final String description) 198 { 199 this(description, null, null); 200 } 201 202 203 204 /** 205 * Creates a new password quality requirement object with optional support for 206 * client-side validation. 207 * 208 * @param description A user-friendly description of the 209 * constraints that a proposed 210 * password must satisfy in order to 211 * meet this requirement and be 212 * accepted by the server. This must 213 * not be {@code null}. 214 * @param clientSideValidationType An optional string that identifies 215 * the type of validation associated 216 * with this requirement. 217 * Applications that support 218 * client-side validation and 219 * recognize this validation type can 220 * attempt to use their own logic in 221 * attempt to determine whether a 222 * proposed password may be rejected 223 * by the server because it does not 224 * satisfy this requirement. This may 225 * be {@code null} if no client-side 226 * validation is available for this 227 * requirement. 228 * @param clientSideValidationProperties An optional map of property names 229 * and values that may provide 230 * additional information that can be 231 * used for client-side validation. 232 * The properties that may be included 233 * depend on the validation type. 234 * This must be empty or {@code null} 235 * if the provided validation type is 236 * {@code null}. It may also be empty 237 * or {@code null} if no additional 238 * properties are required for the 239 * associated type of client-side 240 * validation. 241 */ 242 public PasswordQualityRequirement(@NotNull final String description, 243 @Nullable final String clientSideValidationType, 244 @Nullable final Map<String,String> clientSideValidationProperties) 245 { 246 Validator.ensureNotNull(description); 247 248 if (clientSideValidationType == null) 249 { 250 Validator.ensureTrue((clientSideValidationProperties == null) || 251 clientSideValidationProperties.isEmpty()); 252 } 253 254 this.description = description; 255 this.clientSideValidationType = clientSideValidationType; 256 257 if (clientSideValidationProperties == null) 258 { 259 this.clientSideValidationProperties = Collections.emptyMap(); 260 } 261 else 262 { 263 this.clientSideValidationProperties = Collections.unmodifiableMap( 264 new LinkedHashMap<>(clientSideValidationProperties)); 265 } 266 } 267 268 269 270 /** 271 * Retrieves a user-friendly description of the constraints that a proposed 272 * password must satisfy in order to meet this requirement and be accepted 273 * by the server. 274 * 275 * @return A user-friendly description for this password quality requirement. 276 */ 277 @NotNull() 278 public String getDescription() 279 { 280 return description; 281 } 282 283 284 285 /** 286 * Retrieves a string that identifies the type of client-side validation that 287 * may be performed by applications in order to identify potential problems 288 * with a proposed password before sending it to the server. Client-side 289 * validation may not be available for all types of password quality 290 * requirements. 291 * 292 * @return The client side validation type for this password quality 293 * requirement, or {@code null} if client-side validation is not 294 * supported for this password quality requirement. 295 */ 296 @Nullable() 297 public String getClientSideValidationType() 298 { 299 return clientSideValidationType; 300 } 301 302 303 304 /** 305 * Retrieves a set of properties that may be used in the course of performing 306 * client-side validation for a proposed password. The types of properties 307 * that may be included depend on the client-side validation type. 308 * 309 * @return A map of properties that may be used in the course of performing 310 * client-side validation, or an empty map if client-side validation 311 * is not available for this password quality requirement, or if no 312 * additional properties required for the associated type of 313 * client-side validation. 314 */ 315 @NotNull() 316 public Map<String,String> getClientSideValidationProperties() 317 { 318 return clientSideValidationProperties; 319 } 320 321 322 323 /** 324 * Encodes this password quality requirement to an ASN.1 element that may be 325 * included in LDAP protocol elements that may need to include it (e.g., a 326 * get password quality requirements extended response or a password 327 * validation details response control). 328 * 329 * @return An ASN.1-encoded representation of this password quality 330 * requirement. 331 */ 332 @NotNull() 333 public ASN1Element encode() 334 { 335 final ArrayList<ASN1Element> requirementElements = new ArrayList<>(2); 336 requirementElements.add(new ASN1OctetString(description)); 337 338 if (clientSideValidationType != null) 339 { 340 final ArrayList<ASN1Element> clientSideElements = new ArrayList<>(2); 341 clientSideElements.add(new ASN1OctetString(clientSideValidationType)); 342 343 if (! clientSideValidationProperties.isEmpty()) 344 { 345 final ArrayList<ASN1Element> propertyElements = 346 new ArrayList<>(clientSideValidationProperties.size()); 347 for (final Map.Entry<String,String> e : 348 clientSideValidationProperties.entrySet()) 349 { 350 propertyElements.add(new ASN1Sequence( 351 new ASN1OctetString(e.getKey()), 352 new ASN1OctetString(e.getValue()))); 353 } 354 clientSideElements.add(new ASN1Set( 355 TYPE_CLIENT_SIDE_VALIDATION_PROPERTIES, propertyElements)); 356 } 357 358 requirementElements.add(new ASN1Sequence(TYPE_CLIENT_SIDE_VALIDATION_INFO, 359 clientSideElements)); 360 } 361 362 return new ASN1Sequence(requirementElements); 363 } 364 365 366 367 /** 368 * Decodes the provided ASN.1 element as a password quality requirement. 369 * 370 * @param element The ASN.1 element to decode as a password quality 371 * requirement. It must not be {@code null}. 372 * 373 * @return The decoded password quality requirement. 374 * 375 * @throws LDAPException If a problem was encountered while attempting to 376 * decode the provided ASN.1 element as a password 377 * quality requirement. 378 */ 379 @NotNull() 380 public static PasswordQualityRequirement decode( 381 @NotNull final ASN1Element element) 382 throws LDAPException 383 { 384 try 385 { 386 final ASN1Element[] requirementElements = 387 ASN1Sequence.decodeAsSequence(element).elements(); 388 389 final String description = ASN1OctetString.decodeAsOctetString( 390 requirementElements[0]).stringValue(); 391 392 String clientSideValidationType = null; 393 Map<String,String> clientSideValidationProperties = null; 394 for (int i=1; i < requirementElements.length; i++) 395 { 396 final ASN1Element requirementElement = requirementElements[i]; 397 switch (requirementElement.getType()) 398 { 399 case TYPE_CLIENT_SIDE_VALIDATION_INFO: 400 final ASN1Element[] csvInfoElements = 401 ASN1Sequence.decodeAsSequence(requirementElement).elements(); 402 clientSideValidationType = ASN1OctetString.decodeAsOctetString( 403 csvInfoElements[0]).stringValue(); 404 405 for (int j=1; j < csvInfoElements.length; j++) 406 { 407 final ASN1Element csvInfoElement = csvInfoElements[j]; 408 switch (csvInfoElement.getType()) 409 { 410 case TYPE_CLIENT_SIDE_VALIDATION_PROPERTIES: 411 final ASN1Element[] csvPropElements = 412 ASN1Sequence.decodeAsSequence(csvInfoElement).elements(); 413 clientSideValidationProperties = new LinkedHashMap<>( 414 StaticUtils.computeMapCapacity(csvPropElements.length)); 415 for (final ASN1Element csvPropElement : csvPropElements) 416 { 417 final ASN1Element[] propElements = 418 ASN1Sequence.decodeAsSequence( 419 csvPropElement).elements(); 420 final String name = ASN1OctetString.decodeAsOctetString( 421 propElements[0]).stringValue(); 422 final String value = ASN1OctetString.decodeAsOctetString( 423 propElements[1]).stringValue(); 424 clientSideValidationProperties.put(name, value); 425 } 426 break; 427 428 default: 429 throw new LDAPException(ResultCode.DECODING_ERROR, 430 ERR_PW_QUALITY_REQ_INVALID_CSV_ELEMENT_TYPE.get( 431 StaticUtils.toHex(csvInfoElement.getType()))); 432 } 433 } 434 435 break; 436 437 default: 438 throw new LDAPException(ResultCode.DECODING_ERROR, 439 ERR_PW_QUALITY_REQ_INVALID_REQ_ELEMENT_TYPE.get( 440 StaticUtils.toHex(requirementElement.getType()))); 441 } 442 } 443 444 return new PasswordQualityRequirement(description, 445 clientSideValidationType, clientSideValidationProperties); 446 } 447 catch (final LDAPException le) 448 { 449 Debug.debugException(le); 450 throw le; 451 } 452 catch (final Exception e) 453 { 454 Debug.debugException(e); 455 throw new LDAPException(ResultCode.DECODING_ERROR, 456 ERR_PW_QUALITY_REQ_DECODE_ERROR.get( 457 StaticUtils.getExceptionMessage(e)), 458 e); 459 } 460 } 461 462 463 464 /** 465 * Retrieves a string representation of this password quality requirement. 466 * 467 * @return A string representation of this password quality requirement. 468 */ 469 @Override() 470 @NotNull() 471 public String toString() 472 { 473 final StringBuilder buffer = new StringBuilder(); 474 toString(buffer); 475 return buffer.toString(); 476 } 477 478 479 480 /** 481 * Appends a string representation of this password quality requirement to the 482 * provided buffer. 483 * 484 * @param buffer The buffer to which the information should be appended. 485 */ 486 public void toString(@NotNull final StringBuilder buffer) 487 { 488 buffer.append("PasswordQualityRequirement(description='"); 489 buffer.append(description); 490 buffer.append('\''); 491 492 if (clientSideValidationType != null) 493 { 494 buffer.append(", clientSideValidationType='"); 495 buffer.append(clientSideValidationType); 496 buffer.append('\''); 497 498 if (! clientSideValidationProperties.isEmpty()) 499 { 500 buffer.append(", clientSideValidationProperties={"); 501 502 final Iterator<Map.Entry<String,String>> iterator = 503 clientSideValidationProperties.entrySet().iterator(); 504 while (iterator.hasNext()) 505 { 506 final Map.Entry<String,String> e = iterator.next(); 507 508 buffer.append('\''); 509 buffer.append(e.getKey()); 510 buffer.append("'='"); 511 buffer.append(e.getValue()); 512 buffer.append('\''); 513 514 if (iterator.hasNext()) 515 { 516 buffer.append(','); 517 } 518 } 519 520 buffer.append('}'); 521 } 522 } 523 524 buffer.append(')'); 525 } 526}