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.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 Alcatel-Lucent 8661 057 * server products. These classes provide support for proprietary 058 * functionality or for external specifications that are not considered stable 059 * or mature enough to be guaranteed to work in an interoperable way with 060 * 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<String,String>(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 = 315 new ArrayList<ASN1Element>(2); 316 requirementElements.add(new ASN1OctetString(description)); 317 318 if (clientSideValidationType != null) 319 { 320 final ArrayList<ASN1Element> clientSideElements = 321 new ArrayList<ASN1Element>(2); 322 clientSideElements.add(new ASN1OctetString(clientSideValidationType)); 323 324 if (! clientSideValidationProperties.isEmpty()) 325 { 326 final ArrayList<ASN1Element> propertyElements = 327 new ArrayList<ASN1Element>(clientSideValidationProperties.size()); 328 for (final Map.Entry<String,String> e : 329 clientSideValidationProperties.entrySet()) 330 { 331 propertyElements.add(new ASN1Sequence( 332 new ASN1OctetString(e.getKey()), 333 new ASN1OctetString(e.getValue()))); 334 } 335 clientSideElements.add(new ASN1Set( 336 TYPE_CLIENT_SIDE_VALIDATION_PROPERTIES, propertyElements)); 337 } 338 339 requirementElements.add(new ASN1Sequence(TYPE_CLIENT_SIDE_VALIDATION_INFO, 340 clientSideElements)); 341 } 342 343 return new ASN1Sequence(requirementElements); 344 } 345 346 347 348 /** 349 * Decodes the provided ASN.1 element as a password quality requirement. 350 * 351 * @param element The ASN.1 element to decode as a password quality 352 * requirement. It must not be {@code null}. 353 * 354 * @return The decoded password quality requirement. 355 * 356 * @throws LDAPException If a problem was encountered while attempting to 357 * decode the provided ASN.1 element as a password 358 * quality requirement. 359 */ 360 public static PasswordQualityRequirement decode(final ASN1Element element) 361 throws LDAPException 362 { 363 try 364 { 365 final ASN1Element[] requirementElements = 366 ASN1Sequence.decodeAsSequence(element).elements(); 367 368 final String description = ASN1OctetString.decodeAsOctetString( 369 requirementElements[0]).stringValue(); 370 371 String clientSideValidationType = null; 372 Map<String,String> clientSideValidationProperties = null; 373 for (int i=1; i < requirementElements.length; i++) 374 { 375 final ASN1Element requirementElement = requirementElements[i]; 376 switch (requirementElement.getType()) 377 { 378 case TYPE_CLIENT_SIDE_VALIDATION_INFO: 379 final ASN1Element[] csvInfoElements = 380 ASN1Sequence.decodeAsSequence(requirementElement).elements(); 381 clientSideValidationType = ASN1OctetString.decodeAsOctetString( 382 csvInfoElements[0]).stringValue(); 383 384 for (int j=1; j < csvInfoElements.length; j++) 385 { 386 final ASN1Element csvInfoElement = csvInfoElements[j]; 387 switch (csvInfoElement.getType()) 388 { 389 case TYPE_CLIENT_SIDE_VALIDATION_PROPERTIES: 390 final ASN1Element[] csvPropElements = 391 ASN1Sequence.decodeAsSequence(csvInfoElement).elements(); 392 clientSideValidationProperties = 393 new LinkedHashMap<String,String>(csvPropElements.length); 394 for (final ASN1Element csvPropElement : csvPropElements) 395 { 396 final ASN1Element[] propElements = 397 ASN1Sequence.decodeAsSequence( 398 csvPropElement).elements(); 399 final String name = ASN1OctetString.decodeAsOctetString( 400 propElements[0]).stringValue(); 401 final String value = ASN1OctetString.decodeAsOctetString( 402 propElements[1]).stringValue(); 403 clientSideValidationProperties.put(name, value); 404 } 405 break; 406 407 default: 408 throw new LDAPException(ResultCode.DECODING_ERROR, 409 ERR_PW_QUALITY_REQ_INVALID_CSV_ELEMENT_TYPE.get( 410 StaticUtils.toHex(csvInfoElement.getType()))); 411 } 412 } 413 414 break; 415 416 default: 417 throw new LDAPException(ResultCode.DECODING_ERROR, 418 ERR_PW_QUALITY_REQ_INVALID_REQ_ELEMENT_TYPE.get( 419 StaticUtils.toHex(requirementElement.getType()))); 420 } 421 } 422 423 return new PasswordQualityRequirement(description, 424 clientSideValidationType, clientSideValidationProperties); 425 } 426 catch (final LDAPException le) 427 { 428 Debug.debugException(le); 429 throw le; 430 } 431 catch (final Exception e) 432 { 433 Debug.debugException(e); 434 throw new LDAPException(ResultCode.DECODING_ERROR, 435 ERR_PW_QUALITY_REQ_DECODE_ERROR.get( 436 StaticUtils.getExceptionMessage(e)), 437 e); 438 } 439 } 440 441 442 443 /** 444 * Retrieves a string representation of this password quality requirement. 445 * 446 * @return A string representation of this password quality requirement. 447 */ 448 @Override() 449 public String toString() 450 { 451 final StringBuilder buffer = new StringBuilder(); 452 toString(buffer); 453 return buffer.toString(); 454 } 455 456 457 458 /** 459 * Appends a string representation of this password quality requirement to the 460 * provided buffer. 461 * 462 * @param buffer The buffer to which the information should be appended. 463 */ 464 public void toString(final StringBuilder buffer) 465 { 466 buffer.append("PasswordQualityRequirement(description='"); 467 buffer.append(description); 468 buffer.append('\''); 469 470 if (clientSideValidationType != null) 471 { 472 buffer.append(", clientSideValidationType='"); 473 buffer.append(clientSideValidationType); 474 buffer.append('\''); 475 476 if (! clientSideValidationProperties.isEmpty()) 477 { 478 buffer.append(", clientSideValidationProperties={"); 479 480 final Iterator<Map.Entry<String,String>> iterator = 481 clientSideValidationProperties.entrySet().iterator(); 482 while (iterator.hasNext()) 483 { 484 final Map.Entry<String,String> e = iterator.next(); 485 486 buffer.append('\''); 487 buffer.append(e.getKey()); 488 buffer.append("'='"); 489 buffer.append(e.getValue()); 490 buffer.append('\''); 491 492 if (iterator.hasNext()) 493 { 494 buffer.append(','); 495 } 496 } 497 498 buffer.append('}'); 499 } 500 } 501 502 buffer.append(')'); 503 } 504}