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