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