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