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.controls;
022    
023    
024    
025    import java.util.ArrayList;
026    import java.util.Collection;
027    import java.util.Collections;
028    import java.util.Iterator;
029    import java.util.List;
030    
031    import com.unboundid.asn1.ASN1Boolean;
032    import com.unboundid.asn1.ASN1Element;
033    import com.unboundid.asn1.ASN1Integer;
034    import com.unboundid.asn1.ASN1Null;
035    import com.unboundid.asn1.ASN1OctetString;
036    import com.unboundid.asn1.ASN1Sequence;
037    import com.unboundid.ldap.sdk.Control;
038    import com.unboundid.ldap.sdk.DecodeableControl;
039    import com.unboundid.ldap.sdk.LDAPException;
040    import com.unboundid.ldap.sdk.LDAPResult;
041    import com.unboundid.ldap.sdk.ResultCode;
042    import com.unboundid.util.Debug;
043    import com.unboundid.util.NotMutable;
044    import com.unboundid.util.StaticUtils;
045    import com.unboundid.util.ThreadSafety;
046    import com.unboundid.util.ThreadSafetyLevel;
047    
048    import static com.unboundid.ldap.sdk.unboundidds.controls.ControlMessages.*;
049    
050    
051    
052    /**
053     * <BLOCKQUOTE>
054     *   <B>NOTE:</B>  This class is part of the Commercial Edition of the UnboundID
055     *   LDAP SDK for Java.  It is not available for use in applications that
056     *   include only the Standard Edition of the LDAP SDK, and is not supported for
057     *   use in conjunction with non-UnboundID products.
058     * </BLOCKQUOTE>
059     * This class provides an implementation for a response control that can be
060     * returned by the server in the response for add, modify, and password modify
061     * requests that include the password validation details request control.  This
062     * response control will provide details about the password quality requirements
063     * that are in effect for the operation and whether the password included in the
064     * request satisfies each of those requirements.
065     * <BR><BR>
066     * This response control has an OID of 1.3.6.1.4.1.30221.2.5.41, a criticality
067     * of {@code false}, and a value with the provided encoding:
068     * <PRE>
069     *   PasswordValidationDetailsResponse ::= SEQUENCE {
070     *        validationResult            CHOICE {
071     *             validationDetails             [0] SEQUENCE OF
072     *                  PasswordQualityRequirementValidationResult,
073     *             noPasswordProvided            [1] NULL,
074     *             multiplePasswordsProvided     [2] NULL,
075     *             noValidationAttempted         [3] NULL,
076     *             ... },
077     *        missingCurrentPassword     [3] BOOLEAN DEFAULT FALSE,
078     *        mustChangePassword         [4] BOOLEAN DEFAULT FALSE,
079     *        secondsUntilExpiration     [5] INTEGER OPTIONAL,
080     *        ... }
081     * </PRE>
082     */
083    @NotMutable()
084    @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
085    public final class PasswordValidationDetailsResponseControl
086           extends Control
087           implements DecodeableControl
088    {
089     /**
090      * The OID (1.3.6.1.4.1.30221.2.5.41) for the password validation details
091      * response control.
092      */
093     public static final String PASSWORD_VALIDATION_DETAILS_RESPONSE_OID =
094          "1.3.6.1.4.1.30221.2.5.41";
095    
096    
097    
098      /**
099       * The BER type for the missing current password element.
100       */
101      private static final byte TYPE_MISSING_CURRENT_PASSWORD = (byte) 0x83;
102    
103    
104    
105      /**
106       * The BER type for the must change password element.
107       */
108      private static final byte TYPE_MUST_CHANGE_PW = (byte) 0x84;
109    
110    
111    
112      /**
113       * The BER type for the seconds until expiration element.
114       */
115      private static final byte TYPE_SECONDS_UNTIL_EXPIRATION = (byte) 0x85;
116    
117    
118    
119     /**
120      * The serial version UID for this serializable class.
121      */
122     private static final long serialVersionUID = -2205640814914704074L;
123    
124    
125    
126      // Indicates whether the associated password self change operation failed
127      // (or would fail if attempted without validation errors) because the user is
128      // required to provide his/her current password when performing a self change
129      // but did not do so.
130      private final boolean missingCurrentPassword;
131    
132      // Indicates whether the user will be required to change his/her password
133      // immediately after the associated add or administrative password reset is
134      // complete.
135      private final boolean mustChangePassword;
136    
137      // The length of time in seconds that the new password will be considered
138      // valid.
139      private final Integer secondsUntilExpiration;
140    
141      // The list of the validation results for the associated operation.
142      private final List<PasswordQualityRequirementValidationResult>
143          validationResults;
144    
145      // The response type for this password validation details response control.
146      private final PasswordValidationDetailsResponseType responseType;
147    
148    
149    
150      /**
151       * Creates a new empty control instance that is intended to be used only for
152       * decoding controls via the {@code DecodeableControl} interface.
153       */
154      PasswordValidationDetailsResponseControl()
155      {
156        responseType = null;
157        validationResults = null;
158        missingCurrentPassword = true;
159        mustChangePassword = true;
160        secondsUntilExpiration = null;
161      }
162    
163    
164    
165      /**
166       * Creates a password validation details response control with the provided
167       * information.
168       *
169       * @param  responseType            The response type for this password
170       *                                 validation details response control.  This
171       *                                 must not be {@code null}.
172       * @param  validationResults       A list of the results obtained when
173       *                                 validating the password against the
174       *                                 password quality requirements.  This must
175       *                                 be {@code null} or empty if the
176       *                                 {@code responseType} element has a value
177       *                                 other than {@code VALIDATION_DETAILS}.
178       * @param  missingCurrentPassword  Indicates whether the associated operation
179       *                                 is a self change that failed (or would have
180       *                                 failed if not for additional validation
181       *                                 failures) because the user did not provide
182       *                                 his/her current password as required.
183       * @param  mustChangePassword      Indicates whether the associated operation
184       *                                 is an add or administrative reset that will
185       *                                 require the user to change his/her password
186       *                                 immediately after authenticating before
187       *                                 allowing them to perform any other
188       *                                 operation in the server.
189       * @param  secondsUntilExpiration  The maximum length of time, in seconds,
190       *                                 that the newly-set password will be
191       *                                 considered valid.  This may be {@code null}
192       *                                 if the new password will be considered
193       *                                 valid indefinitely.
194       */
195      public PasswordValidationDetailsResponseControl(
196                  final PasswordValidationDetailsResponseType responseType,
197                  final Collection<PasswordQualityRequirementValidationResult>
198                       validationResults,
199                  final boolean missingCurrentPassword,
200                  final boolean mustChangePassword,
201                  final Integer secondsUntilExpiration)
202      {
203        super(PASSWORD_VALIDATION_DETAILS_RESPONSE_OID, false,
204             encodeValue(responseType, validationResults, missingCurrentPassword,
205                  mustChangePassword, secondsUntilExpiration));
206    
207        this.responseType           = responseType;
208        this.missingCurrentPassword = missingCurrentPassword;
209        this.mustChangePassword     = mustChangePassword;
210        this.secondsUntilExpiration = secondsUntilExpiration;
211    
212        if (validationResults == null)
213        {
214          this.validationResults = Collections.emptyList();
215        }
216        else
217        {
218          this.validationResults = Collections.unmodifiableList(
219               new ArrayList<PasswordQualityRequirementValidationResult>(
220                    validationResults));
221        }
222      }
223    
224    
225    
226      /**
227       * Creates a new password validation details response control by decoding the
228       * provided generic control information.
229       *
230       * @param  oid         The OID for the control.
231       * @param  isCritical  Indicates whether the control should be considered
232       *                     critical.
233       * @param  value       The value for the control.
234       *
235       * @throws  LDAPException  If the provided information cannot be decoded to
236       *                         create a password validation details response
237       *                         control.
238       */
239      public PasswordValidationDetailsResponseControl(final String oid,
240                                                      final boolean isCritical,
241                                                      final ASN1OctetString value)
242             throws LDAPException
243      {
244        super(oid, isCritical, value);
245    
246        if (value == null)
247        {
248          throw new LDAPException(ResultCode.DECODING_ERROR,
249               ERR_PW_VALIDATION_RESPONSE_NO_VALUE.get());
250        }
251    
252        try
253        {
254          final ASN1Element[] elements =
255               ASN1Sequence.decodeAsSequence(value.getValue()).elements();
256    
257          responseType = PasswordValidationDetailsResponseType.forBERType(
258               elements[0].getType());
259          if (responseType == null)
260          {
261            throw new LDAPException(ResultCode.DECODING_ERROR,
262                 ERR_PW_VALIDATION_RESPONSE_INVALID_RESPONSE_TYPE.get(
263                      StaticUtils.toHex(elements[0].getType())));
264          }
265    
266          if (responseType ==
267              PasswordValidationDetailsResponseType.VALIDATION_DETAILS)
268          {
269            final ASN1Element[] resultElements =
270                 ASN1Sequence.decodeAsSequence(elements[0]).elements();
271    
272            final ArrayList<PasswordQualityRequirementValidationResult> resultList =
273                 new ArrayList<PasswordQualityRequirementValidationResult>(
274                      resultElements.length);
275            for (final ASN1Element e : resultElements)
276            {
277              resultList.add(PasswordQualityRequirementValidationResult.decode(e));
278            }
279            validationResults = Collections.unmodifiableList(resultList);
280          }
281          else
282          {
283            validationResults = Collections.emptyList();
284          }
285    
286          boolean missingCurrent = false;
287          boolean mustChange = false;
288          Integer secondsRemaining = null;
289          for (int i=1; i < elements.length; i++)
290          {
291            switch (elements[i].getType())
292            {
293              case TYPE_MISSING_CURRENT_PASSWORD:
294                missingCurrent =
295                     ASN1Boolean.decodeAsBoolean(elements[i]).booleanValue();
296                break;
297    
298              case TYPE_MUST_CHANGE_PW:
299                mustChange =
300                     ASN1Boolean.decodeAsBoolean(elements[i]).booleanValue();
301                break;
302    
303              case TYPE_SECONDS_UNTIL_EXPIRATION:
304                secondsRemaining =
305                     ASN1Integer.decodeAsInteger(elements[i]).intValue();
306                break;
307    
308              default:
309                // We may update this control in the future to provide support for
310                // returning additional password-related information.  If we
311                // encounter an unrecognized element, just ignore it rather than
312                // throwing an exception.
313                break;
314            }
315          }
316    
317          missingCurrentPassword = missingCurrent;
318          mustChangePassword     = mustChange;
319          secondsUntilExpiration = secondsRemaining;
320        }
321        catch (final LDAPException le)
322        {
323          Debug.debugException(le);
324          throw le;
325        }
326        catch (final Exception e)
327        {
328          Debug.debugException(e);
329          throw new LDAPException(ResultCode.DECODING_ERROR,
330               ERR_PW_VALIDATION_RESPONSE_ERROR_PARSING_VALUE.get(
331                    StaticUtils.getExceptionMessage(e)),
332               e);
333        }
334      }
335    
336    
337    
338      /**
339       * Encodes the provided information to an ASN.1 element suitable for use as
340       * the control value.
341       *
342       * @param  responseType            The response type for this password
343       *                                 validation details response control.  This
344       *                                 must not be {@code null}.
345       * @param  validationResults       A list of the results obtained when
346       *                                 validating the password against the
347       *                                 password quality requirements.  This must
348       *                                 be {@code null} or empty if the
349       *                                 {@code responseType} element has a value
350       *                                 other than {@code VALIDATION_DETAILS}.
351       * @param  missingCurrentPassword  Indicates whether the associated operation
352       *                                 is a self change that failed (or would have
353       *                                 failed if not for additional validation
354       *                                 failures) because the user did not provide
355       *                                 his/her current password as required.
356       * @param  mustChangePassword      Indicates whether the associated operation
357       *                                 is an add or administrative reset that will
358       *                                 require the user to change his/her password
359       *                                 immediately after authenticating before
360       *                                 allowing them to perform any other
361       *                                 operation in the server.
362       * @param  secondsUntilExpiration  The maximum length of time, in seconds,
363       *                                 that the newly-set password will be
364       *                                 considered valid.  This may be {@code null}
365       *                                 if the new password will be considered
366       *                                 valid indefinitely.
367       *
368       * @return  The encoded control value.
369       */
370      private static ASN1OctetString encodeValue(
371                   final PasswordValidationDetailsResponseType responseType,
372                   final Collection<PasswordQualityRequirementValidationResult>
373                        validationResults,
374                   final boolean missingCurrentPassword,
375                   final boolean mustChangePassword,
376                   final Integer secondsUntilExpiration)
377      {
378        final ArrayList<ASN1Element> elements = new ArrayList<ASN1Element>(4);
379    
380        switch (responseType)
381        {
382          case VALIDATION_DETAILS:
383            if (validationResults == null)
384            {
385              elements.add(new ASN1Sequence(responseType.getBERType()));
386            }
387            else
388            {
389              final ArrayList<ASN1Element> resultElements =
390                   new ArrayList<ASN1Element>(validationResults.size());
391              for (final PasswordQualityRequirementValidationResult r :
392                   validationResults)
393              {
394                resultElements.add(r.encode());
395              }
396              elements.add(new ASN1Sequence(responseType.getBERType(),
397                   resultElements));
398            }
399            break;
400    
401          case NO_PASSWORD_PROVIDED:
402          case MULTIPLE_PASSWORDS_PROVIDED:
403          case NO_VALIDATION_ATTEMPTED:
404            elements.add(new ASN1Null(responseType.getBERType()));
405            break;
406        }
407    
408        if (missingCurrentPassword)
409        {
410          elements.add(new ASN1Boolean(TYPE_MISSING_CURRENT_PASSWORD,
411               missingCurrentPassword));
412        }
413    
414        if (mustChangePassword)
415        {
416          elements.add(new ASN1Boolean(TYPE_MUST_CHANGE_PW, mustChangePassword));
417        }
418    
419        if (secondsUntilExpiration != null)
420        {
421          elements.add(new ASN1Integer(TYPE_SECONDS_UNTIL_EXPIRATION,
422               secondsUntilExpiration));
423        }
424    
425        return new ASN1OctetString(new ASN1Sequence(elements).encode());
426      }
427    
428    
429    
430      /**
431       * Retrieves the response type for this password validation details response
432       * control.
433       *
434       * @return  The response type for this password validation details response
435       *          control.
436       */
437      public PasswordValidationDetailsResponseType getResponseType()
438      {
439        return responseType;
440      }
441    
442    
443    
444      /**
445       * Retrieves a list of the results obtained when attempting to validate the
446       * proposed password against the password quality requirements in effect for
447       * the operation.
448       *
449       * @return  A list of the results obtained when attempting to validate the
450       *          proposed password against the password quality requirements in
451       *          effect for the operation, or an empty list if no validation
452       *          results are available.
453       */
454      public List<PasswordQualityRequirementValidationResult> getValidationResults()
455      {
456        return validationResults;
457      }
458    
459    
460    
461      /**
462       * Indicates whether the associated operation is a self password change that
463       * requires the user to provide his/her current password when setting a new
464       * password, but no current password was provided.
465       *
466       * @return  {@code true} if the associated operation is a self password change
467       *          that requires the user to provide his/her current password when
468       *          setting a new password but none was required, or {@code false} if
469       *          the associated operation was not a self change, or if the user's
470       *          current password was provided.
471       */
472      public boolean missingCurrentPassword()
473      {
474        return missingCurrentPassword;
475      }
476    
477    
478    
479      /**
480       * Indicates whether the user will be required to immediately change his/her
481       * password after the associated add or administrative reset is complete.
482       *
483       * @return  {@code true} if the associated operation is an add or
484       *          administrative reset and the user will be required to change
485       *          his/her password before being allowed to perform any other
486       *          operation, or {@code false} if the associated operation was not am
487       *          add or an administrative reset, or if the user will not be
488       *          required to immediately change his/her password.
489       */
490      public boolean mustChangePassword()
491      {
492        return mustChangePassword;
493      }
494    
495    
496    
497      /**
498       * Retrieves the maximum length of time, in seconds, that the newly-set
499       * password will be considered valid.  If {@link #mustChangePassword()}
500       * returns {@code true}, then this value will be the length of time that the
501       * user has to perform a self password change before the account becomes
502       * locked.  If {@code mustChangePassword()} returns {@code false}, then this
503       * value will be the length of time until the password expires.
504       *
505       * @return  The maximum length of time, in seconds, that the newly-set
506       *          password will be considered valid, or {@code null} if the new
507       *          password will be valid indefinitely.
508       */
509      public Integer getSecondsUntilExpiration()
510      {
511        return secondsUntilExpiration;
512      }
513    
514    
515    
516      /**
517       * {@inheritDoc}
518       */
519      public PasswordValidationDetailsResponseControl decodeControl(
520                  final String oid, final boolean isCritical,
521                  final ASN1OctetString value)
522             throws LDAPException
523      {
524        return new PasswordValidationDetailsResponseControl(oid, isCritical, value);
525      }
526    
527    
528    
529      /**
530       * Extracts a password validation details response control from the provided
531       * result.
532       *
533       * @param  result  The result from which to retrieve the password validation
534       *                 details response control.
535       *
536       * @return  The password validation details response control contained in the
537       *          provided result, or {@code null} if the result did not contain a
538       *          password validation details response control.
539       *
540       * @throws  LDAPException  If a problem is encountered while attempting to
541       *                         decode the password validation details response
542       *                         control contained in the provided result.
543       */
544      public static PasswordValidationDetailsResponseControl
545                         get(final LDAPResult result)
546             throws LDAPException
547      {
548        final Control c =
549             result.getResponseControl(PASSWORD_VALIDATION_DETAILS_RESPONSE_OID);
550        if (c == null)
551        {
552          return null;
553        }
554    
555        if (c instanceof PasswordValidationDetailsResponseControl)
556        {
557          return (PasswordValidationDetailsResponseControl) c;
558        }
559        else
560        {
561          return new PasswordValidationDetailsResponseControl(c.getOID(),
562               c.isCritical(), c.getValue());
563        }
564      }
565    
566    
567    
568      /**
569       * {@inheritDoc}
570       */
571      @Override()
572      public String getControlName()
573      {
574        return INFO_CONTROL_NAME_PW_VALIDATION_RESPONSE.get();
575      }
576    
577    
578    
579      /**
580       * {@inheritDoc}
581       */
582      @Override()
583      public void toString(final StringBuilder buffer)
584      {
585        buffer.append("PasswordValidationDetailsResponseControl(responseType='");
586        buffer.append(responseType.name());
587        buffer.append('\'');
588    
589        if (responseType ==
590            PasswordValidationDetailsResponseType.VALIDATION_DETAILS)
591        {
592          buffer.append(", validationDetails={");
593    
594          final Iterator<PasswordQualityRequirementValidationResult> iterator =
595               validationResults.iterator();
596          while (iterator.hasNext())
597          {
598            iterator.next().toString(buffer);
599            if (iterator.hasNext())
600            {
601              buffer.append(',');
602            }
603          }
604    
605          buffer.append('}');
606        }
607    
608        buffer.append(", missingCurrentPassword=");
609        buffer.append(missingCurrentPassword);
610        buffer.append(", mustChangePassword=");
611        buffer.append(mustChangePassword);
612    
613        if (secondsUntilExpiration != null)
614        {
615          buffer.append(", secondsUntilExpiration=");
616          buffer.append(secondsUntilExpiration);
617        }
618    
619        buffer.append("})");
620      }
621    }