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}