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