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