001/*
002 * Copyright 2015-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2015-2018 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 Alcatel-Lucent 8661
064 *   server products.  These classes provide support for proprietary
065 *   functionality or for external specifications that are not considered stable
066 *   or mature enough to be guaranteed to work in an interoperable way with
067 *   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<PasswordQualityRequirementValidationResult>(
224                validationResults));
225    }
226  }
227
228
229
230  /**
231   * Creates a new password validation details response control by decoding the
232   * provided generic control information.
233   *
234   * @param  oid         The OID for the control.
235   * @param  isCritical  Indicates whether the control should be considered
236   *                     critical.
237   * @param  value       The value for the control.
238   *
239   * @throws  LDAPException  If the provided information cannot be decoded to
240   *                         create a password validation details response
241   *                         control.
242   */
243  public PasswordValidationDetailsResponseControl(final String oid,
244                                                  final boolean isCritical,
245                                                  final ASN1OctetString value)
246         throws LDAPException
247  {
248    super(oid, isCritical, value);
249
250    if (value == null)
251    {
252      throw new LDAPException(ResultCode.DECODING_ERROR,
253           ERR_PW_VALIDATION_RESPONSE_NO_VALUE.get());
254    }
255
256    try
257    {
258      final ASN1Element[] elements =
259           ASN1Sequence.decodeAsSequence(value.getValue()).elements();
260
261      responseType = PasswordValidationDetailsResponseType.forBERType(
262           elements[0].getType());
263      if (responseType == null)
264      {
265        throw new LDAPException(ResultCode.DECODING_ERROR,
266             ERR_PW_VALIDATION_RESPONSE_INVALID_RESPONSE_TYPE.get(
267                  StaticUtils.toHex(elements[0].getType())));
268      }
269
270      if (responseType ==
271          PasswordValidationDetailsResponseType.VALIDATION_DETAILS)
272      {
273        final ASN1Element[] resultElements =
274             ASN1Sequence.decodeAsSequence(elements[0]).elements();
275
276        final ArrayList<PasswordQualityRequirementValidationResult> resultList =
277             new ArrayList<PasswordQualityRequirementValidationResult>(
278                  resultElements.length);
279        for (final ASN1Element e : resultElements)
280        {
281          resultList.add(PasswordQualityRequirementValidationResult.decode(e));
282        }
283        validationResults = Collections.unmodifiableList(resultList);
284      }
285      else
286      {
287        validationResults = Collections.emptyList();
288      }
289
290      boolean missingCurrent = false;
291      boolean mustChange = false;
292      Integer secondsRemaining = null;
293      for (int i=1; i < elements.length; i++)
294      {
295        switch (elements[i].getType())
296        {
297          case TYPE_MISSING_CURRENT_PASSWORD:
298            missingCurrent =
299                 ASN1Boolean.decodeAsBoolean(elements[i]).booleanValue();
300            break;
301
302          case TYPE_MUST_CHANGE_PW:
303            mustChange =
304                 ASN1Boolean.decodeAsBoolean(elements[i]).booleanValue();
305            break;
306
307          case TYPE_SECONDS_UNTIL_EXPIRATION:
308            secondsRemaining =
309                 ASN1Integer.decodeAsInteger(elements[i]).intValue();
310            break;
311
312          default:
313            // We may update this control in the future to provide support for
314            // returning additional password-related information.  If we
315            // encounter an unrecognized element, just ignore it rather than
316            // throwing an exception.
317            break;
318        }
319      }
320
321      missingCurrentPassword = missingCurrent;
322      mustChangePassword     = mustChange;
323      secondsUntilExpiration = secondsRemaining;
324    }
325    catch (final LDAPException le)
326    {
327      Debug.debugException(le);
328      throw le;
329    }
330    catch (final Exception e)
331    {
332      Debug.debugException(e);
333      throw new LDAPException(ResultCode.DECODING_ERROR,
334           ERR_PW_VALIDATION_RESPONSE_ERROR_PARSING_VALUE.get(
335                StaticUtils.getExceptionMessage(e)),
336           e);
337    }
338  }
339
340
341
342  /**
343   * Encodes the provided information to an ASN.1 element suitable for use as
344   * the control value.
345   *
346   * @param  responseType            The response type for this password
347   *                                 validation details response control.  This
348   *                                 must not be {@code null}.
349   * @param  validationResults       A list of the results obtained when
350   *                                 validating the password against the
351   *                                 password quality requirements.  This must
352   *                                 be {@code null} or empty if the
353   *                                 {@code responseType} element has a value
354   *                                 other than {@code VALIDATION_DETAILS}.
355   * @param  missingCurrentPassword  Indicates whether the associated operation
356   *                                 is a self change that failed (or would have
357   *                                 failed if not for additional validation
358   *                                 failures) because the user did not provide
359   *                                 his/her current password as required.
360   * @param  mustChangePassword      Indicates whether the associated operation
361   *                                 is an add or administrative reset that will
362   *                                 require the user to change his/her password
363   *                                 immediately after authenticating before
364   *                                 allowing them to perform any other
365   *                                 operation in the server.
366   * @param  secondsUntilExpiration  The maximum length of time, in seconds,
367   *                                 that the newly-set password will be
368   *                                 considered valid.  This may be {@code null}
369   *                                 if the new password will be considered
370   *                                 valid indefinitely.
371   *
372   * @return  The encoded control value.
373   */
374  private static ASN1OctetString encodeValue(
375               final PasswordValidationDetailsResponseType responseType,
376               final Collection<PasswordQualityRequirementValidationResult>
377                    validationResults,
378               final boolean missingCurrentPassword,
379               final boolean mustChangePassword,
380               final Integer secondsUntilExpiration)
381  {
382    final ArrayList<ASN1Element> elements = new ArrayList<ASN1Element>(4);
383
384    switch (responseType)
385    {
386      case VALIDATION_DETAILS:
387        if (validationResults == null)
388        {
389          elements.add(new ASN1Sequence(responseType.getBERType()));
390        }
391        else
392        {
393          final ArrayList<ASN1Element> resultElements =
394               new ArrayList<ASN1Element>(validationResults.size());
395          for (final PasswordQualityRequirementValidationResult r :
396               validationResults)
397          {
398            resultElements.add(r.encode());
399          }
400          elements.add(new ASN1Sequence(responseType.getBERType(),
401               resultElements));
402        }
403        break;
404
405      case NO_PASSWORD_PROVIDED:
406      case MULTIPLE_PASSWORDS_PROVIDED:
407      case NO_VALIDATION_ATTEMPTED:
408        elements.add(new ASN1Null(responseType.getBERType()));
409        break;
410    }
411
412    if (missingCurrentPassword)
413    {
414      elements.add(new ASN1Boolean(TYPE_MISSING_CURRENT_PASSWORD,
415           missingCurrentPassword));
416    }
417
418    if (mustChangePassword)
419    {
420      elements.add(new ASN1Boolean(TYPE_MUST_CHANGE_PW, mustChangePassword));
421    }
422
423    if (secondsUntilExpiration != null)
424    {
425      elements.add(new ASN1Integer(TYPE_SECONDS_UNTIL_EXPIRATION,
426           secondsUntilExpiration));
427    }
428
429    return new ASN1OctetString(new ASN1Sequence(elements).encode());
430  }
431
432
433
434  /**
435   * Retrieves the response type for this password validation details response
436   * control.
437   *
438   * @return  The response type for this password validation details response
439   *          control.
440   */
441  public PasswordValidationDetailsResponseType getResponseType()
442  {
443    return responseType;
444  }
445
446
447
448  /**
449   * Retrieves a list of the results obtained when attempting to validate the
450   * proposed password against the password quality requirements in effect for
451   * the operation.
452   *
453   * @return  A list of the results obtained when attempting to validate the
454   *          proposed password against the password quality requirements in
455   *          effect for the operation, or an empty list if no validation
456   *          results are available.
457   */
458  public List<PasswordQualityRequirementValidationResult> getValidationResults()
459  {
460    return validationResults;
461  }
462
463
464
465  /**
466   * Indicates whether the associated operation is a self password change that
467   * requires the user to provide his/her current password when setting a new
468   * password, but no current password was provided.
469   *
470   * @return  {@code true} if the associated operation is a self password change
471   *          that requires the user to provide his/her current password when
472   *          setting a new password but none was required, or {@code false} if
473   *          the associated operation was not a self change, or if the user's
474   *          current password was provided.
475   */
476  public boolean missingCurrentPassword()
477  {
478    return missingCurrentPassword;
479  }
480
481
482
483  /**
484   * Indicates whether the user will be required to immediately change his/her
485   * password after the associated add or administrative reset is complete.
486   *
487   * @return  {@code true} if the associated operation is an add or
488   *          administrative reset and the user will be required to change
489   *          his/her password before being allowed to perform any other
490   *          operation, or {@code false} if the associated operation was not am
491   *          add or an administrative reset, or if the user will not be
492   *          required to immediately change his/her password.
493   */
494  public boolean mustChangePassword()
495  {
496    return mustChangePassword;
497  }
498
499
500
501  /**
502   * Retrieves the maximum length of time, in seconds, that the newly-set
503   * password will be considered valid.  If {@link #mustChangePassword()}
504   * returns {@code true}, then this value will be the length of time that the
505   * user has to perform a self password change before the account becomes
506   * locked.  If {@code mustChangePassword()} returns {@code false}, then this
507   * value will be the length of time until the password expires.
508   *
509   * @return  The maximum length of time, in seconds, that the newly-set
510   *          password will be considered valid, or {@code null} if the new
511   *          password will be valid indefinitely.
512   */
513  public Integer getSecondsUntilExpiration()
514  {
515    return secondsUntilExpiration;
516  }
517
518
519
520  /**
521   * {@inheritDoc}
522   */
523  @Override()
524  public PasswordValidationDetailsResponseControl decodeControl(
525              final String oid, final boolean isCritical,
526              final ASN1OctetString value)
527         throws LDAPException
528  {
529    return new PasswordValidationDetailsResponseControl(oid, isCritical, value);
530  }
531
532
533
534  /**
535   * Extracts a password validation details response control from the provided
536   * result.
537   *
538   * @param  result  The result from which to retrieve the password validation
539   *                 details response control.
540   *
541   * @return  The password validation details response control contained in the
542   *          provided result, or {@code null} if the result did not contain a
543   *          password validation details response control.
544   *
545   * @throws  LDAPException  If a problem is encountered while attempting to
546   *                         decode the password validation details response
547   *                         control contained in the provided result.
548   */
549  public static PasswordValidationDetailsResponseControl
550                     get(final LDAPResult result)
551         throws LDAPException
552  {
553    final Control c =
554         result.getResponseControl(PASSWORD_VALIDATION_DETAILS_RESPONSE_OID);
555    if (c == null)
556    {
557      return null;
558    }
559
560    if (c instanceof PasswordValidationDetailsResponseControl)
561    {
562      return (PasswordValidationDetailsResponseControl) c;
563    }
564    else
565    {
566      return new PasswordValidationDetailsResponseControl(c.getOID(),
567           c.isCritical(), c.getValue());
568    }
569  }
570
571
572
573  /**
574   * {@inheritDoc}
575   */
576  @Override()
577  public String getControlName()
578  {
579    return INFO_CONTROL_NAME_PW_VALIDATION_RESPONSE.get();
580  }
581
582
583
584  /**
585   * {@inheritDoc}
586   */
587  @Override()
588  public void toString(final StringBuilder buffer)
589  {
590    buffer.append("PasswordValidationDetailsResponseControl(responseType='");
591    buffer.append(responseType.name());
592    buffer.append('\'');
593
594    if (responseType ==
595        PasswordValidationDetailsResponseType.VALIDATION_DETAILS)
596    {
597      buffer.append(", validationDetails={");
598
599      final Iterator<PasswordQualityRequirementValidationResult> iterator =
600           validationResults.iterator();
601      while (iterator.hasNext())
602      {
603        iterator.next().toString(buffer);
604        if (iterator.hasNext())
605        {
606          buffer.append(',');
607        }
608      }
609
610      buffer.append('}');
611    }
612
613    buffer.append(", missingCurrentPassword=");
614    buffer.append(missingCurrentPassword);
615    buffer.append(", mustChangePassword=");
616    buffer.append(mustChangePassword);
617
618    if (secondsUntilExpiration != null)
619    {
620      buffer.append(", secondsUntilExpiration=");
621      buffer.append(secondsUntilExpiration);
622    }
623
624    buffer.append("})");
625  }
626}