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