001/*
002 * Copyright 2019-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2019-2024 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) 2019-2024 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.LinkedHashMap;
042import java.util.List;
043import java.util.Map;
044
045import com.unboundid.asn1.ASN1Boolean;
046import com.unboundid.asn1.ASN1Element;
047import com.unboundid.asn1.ASN1Long;
048import com.unboundid.asn1.ASN1OctetString;
049import com.unboundid.asn1.ASN1Sequence;
050import com.unboundid.ldap.sdk.Control;
051import com.unboundid.ldap.sdk.DecodeableControl;
052import com.unboundid.ldap.sdk.JSONControlDecodeHelper;
053import com.unboundid.ldap.sdk.LDAPException;
054import com.unboundid.ldap.sdk.LDAPResult;
055import com.unboundid.ldap.sdk.ResultCode;
056import com.unboundid.util.Debug;
057import com.unboundid.util.NotMutable;
058import com.unboundid.util.NotNull;
059import com.unboundid.util.Nullable;
060import com.unboundid.util.StaticUtils;
061import com.unboundid.util.ThreadSafety;
062import com.unboundid.util.ThreadSafetyLevel;
063import com.unboundid.util.json.JSONBoolean;
064import com.unboundid.util.json.JSONField;
065import com.unboundid.util.json.JSONNumber;
066import com.unboundid.util.json.JSONObject;
067import com.unboundid.util.json.JSONString;
068import com.unboundid.util.json.JSONValue;
069
070import static com.unboundid.ldap.sdk.unboundidds.controls.ControlMessages.*;
071
072
073
074/**
075 * This class provides a response control that may be used to convey the
076 * password (and other associated information) generated in response to a
077 * {@link GeneratePasswordRequestControl}.
078 * <BR>
079 * <BLOCKQUOTE>
080 *   <B>NOTE:</B>  This class, and other classes within the
081 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
082 *   supported for use against Ping Identity, UnboundID, and
083 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
084 *   for proprietary functionality or for external specifications that are not
085 *   considered stable or mature enough to be guaranteed to work in an
086 *   interoperable way with other types of LDAP servers.
087 * </BLOCKQUOTE>
088 * <BR>
089 * This control has an OID of "1.3.6.1.4.1.30221.2.5.59", a criticality of
090 * false, and a value with the following encoding:
091 * <PRE>
092 *   GeneratePasswordResponse ::= SEQUENCE {
093 *        generatedPassword          OCTET STRING,
094 *        mustChangePassword         BOOLEAN,
095 *        secondsUntilExpiration     [0] INTEGER OPTIONAL,
096 *        ... }
097 * </PRE>
098 *
099 * @see  GeneratePasswordRequestControl
100 */
101@NotMutable()
102@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
103public final class GeneratePasswordResponseControl
104       extends Control
105       implements DecodeableControl
106{
107  /**
108   * The OID (1.3.6.1.4.1.30221.2.5.59) for the generate password response
109   * control.
110   */
111  @NotNull public static final String GENERATE_PASSWORD_RESPONSE_OID =
112       "1.3.6.1.4.1.30221.2.5.59";
113
114
115
116  /**
117   * The BER type for the {@code secondsUntilExpiration} element.
118   */
119  private static final byte TYPE_SECONDS_UNTIL_EXPIRATION = (byte) 0x80;
120
121
122
123  /**
124   * The name of the field used to hold the generated password in the JSON
125   * representation of this control.
126   */
127  @NotNull private static final String JSON_FIELD_GENERATED_PASSWORD =
128       "generated-password";
129
130
131
132  /**
133   * The name of the field used to indicate whether the user must choose a new
134   * password in the JSON representation of this control.
135   */
136  @NotNull private static final String JSON_FIELD_MUST_CHANGE_PASSWORD =
137       "must-change-password";
138
139
140
141  /**
142   * The name of the field used to specify the number of seconds until the
143   * password expires in the JSON representation of this control.
144   */
145  @NotNull private static final String JSON_FIELD_SECONDS_UNTIL_EXPIRATION =
146       "seconds-until-expiration";
147
148
149
150  /**
151   * The serial version UID for this serializable class.
152   */
153  private static final long serialVersionUID = 7542512192838228238L;
154
155
156
157  // The generated password included in the control.
158  @NotNull private final ASN1OctetString generatedPassword;
159
160  // Indicates whether the user will be required to choose a new password the
161  // first time they authenticate.
162  private final boolean mustChangePassword;
163
164  // The number of seconds until the new password will expire.
165  @Nullable private final Long secondsUntilExpiration;
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  GeneratePasswordResponseControl()
174  {
175    generatedPassword = null;
176    mustChangePassword = false;
177    secondsUntilExpiration = null;
178  }
179
180
181
182  /**
183   * Creates a new generate password response control with the provided
184   * information.
185   *
186   * @param  generatedPassword       The password generated by the server.  It
187   *                                 must not be {@code null}.
188   * @param  mustChangePassword      Indicates whether the user will be required
189   *                                 to choose a new password the first time
190   *                                 they authenticate.
191   * @param  secondsUntilExpiration  The number of seconds until the new
192   *                                 password will expire.  It may be
193   *                                 {@code null} if the new password will not
194   *                                 expire.
195   */
196  public GeneratePasswordResponseControl(
197              @NotNull final String generatedPassword,
198              final boolean mustChangePassword,
199              @Nullable final Long secondsUntilExpiration)
200  {
201    this(new ASN1OctetString(generatedPassword), mustChangePassword,
202         secondsUntilExpiration);
203  }
204
205
206
207  /**
208   * Creates a new generate password response control with the provided
209   * information.
210   *
211   * @param  generatedPassword       The password generated by the server.  It
212   *                                 must not be {@code null}.
213   * @param  mustChangePassword      Indicates whether the user will be required
214   *                                 to choose a new password the first time
215   *                                 they authenticate.
216   * @param  secondsUntilExpiration  The number of seconds until the new
217   *                                 password will expire.  It may be
218   *                                 {@code null} if the new password will not
219   *                                 expire.
220   */
221  public GeneratePasswordResponseControl(
222              @NotNull final byte[] generatedPassword,
223              final boolean mustChangePassword,
224              @Nullable final Long secondsUntilExpiration)
225  {
226    this(new ASN1OctetString(generatedPassword), mustChangePassword,
227         secondsUntilExpiration);
228  }
229
230
231
232  /**
233   * Creates a new generate password response control with the provided
234   * information.
235   *
236   * @param  generatedPassword       The password generated by the server.  It
237   *                                 must not be {@code null}.
238   * @param  mustChangePassword      Indicates whether the user will be required
239   *                                 to choose a new password the first time
240   *                                 they authenticate.
241   * @param  secondsUntilExpiration  The number of seconds until the new
242   *                                 password will expire.  It may be
243   *                                 {@code null} if the new password will not
244   *                                 expire.
245   */
246  private GeneratePasswordResponseControl(
247               @NotNull final ASN1OctetString generatedPassword,
248               final boolean mustChangePassword,
249               @Nullable final Long secondsUntilExpiration)
250  {
251    super(GENERATE_PASSWORD_RESPONSE_OID, false,
252         encodeValue(generatedPassword, mustChangePassword,
253              secondsUntilExpiration));
254
255    this.generatedPassword = generatedPassword;
256    this.mustChangePassword = mustChangePassword;
257    this.secondsUntilExpiration = secondsUntilExpiration;
258  }
259
260
261
262  /**
263   * Creates a new generate password response control with the provided
264   * information.
265   *
266   * @param  oid         The OID for the control.
267   * @param  isCritical  Indicates whether the control should be marked
268   *                     critical.
269   * @param  value       The encoded value for the control.  This may be
270   *                     {@code null} if no value was provided.
271   *
272   * @throws  LDAPException  If the provided control cannot be decoded as a
273   *                         generate password response control.
274   */
275  public GeneratePasswordResponseControl(@NotNull final String oid,
276                                         final boolean isCritical,
277                                         @Nullable final ASN1OctetString value)
278         throws LDAPException
279  {
280    super(oid, isCritical,  value);
281
282    if (value == null)
283    {
284      throw new LDAPException(ResultCode.DECODING_ERROR,
285           ERR_GENERATE_PASSWORD_RESPONSE_NO_VALUE.get());
286    }
287
288    try
289    {
290      final ASN1Element valElement = ASN1Element.decode(value.getValue());
291      final ASN1Element[] elements =
292           ASN1Sequence.decodeAsSequence(valElement).elements();
293      generatedPassword = ASN1OctetString.decodeAsOctetString(elements[0]);
294      mustChangePassword =
295           ASN1Boolean.decodeAsBoolean(elements[1]).booleanValue();
296
297      Long secsUntilExp = null;
298      for (int i=2; i < elements.length; i++)
299      {
300        final ASN1Element e = elements[i];
301        switch (e.getType())
302        {
303          case TYPE_SECONDS_UNTIL_EXPIRATION:
304            secsUntilExp = ASN1Long.decodeAsLong(e).longValue();
305            break;
306          default:
307            // This is a field we don't currently recognize but might be defined
308            // in the future.
309            break;
310        }
311      }
312
313      secondsUntilExpiration = secsUntilExp;
314    }
315    catch (final Exception e)
316    {
317      Debug.debugException(e);
318      throw new LDAPException(ResultCode.DECODING_ERROR,
319           ERR_GENERATE_PASSWORD_RESPONSE_CANNOT_DECODE_VALUE.get(
320                StaticUtils.getExceptionMessage(e)),
321           e);
322    }
323  }
324
325
326
327  /**
328   * {@inheritDoc}
329   */
330  @Override()
331  @NotNull()
332  public GeneratePasswordResponseControl decodeControl(
333              @NotNull final String oid,
334              final boolean isCritical,
335              @Nullable final ASN1OctetString value)
336         throws LDAPException
337  {
338    return new GeneratePasswordResponseControl(oid, isCritical, value);
339  }
340
341
342
343  /**
344   * Extracts a generate password  response control from the provided result.
345   *
346   * @param  result  The result from which to retrieve the generate password
347   *                 response control.
348   *
349   * @return  The generate password response control contained in the provided
350   *          result, or {@code null} if the result did not contain a generate
351   *          password response control.
352   *
353   * @throws  LDAPException  If a problem is encountered while attempting to
354   *                         decode the generate password response control
355   *                         contained in the provided result.
356   */
357  @Nullable()
358  public static GeneratePasswordResponseControl get(
359                     @NotNull final LDAPResult result)
360         throws LDAPException
361  {
362    final Control c = result.getResponseControl(GENERATE_PASSWORD_RESPONSE_OID);
363    if (c == null)
364    {
365      return null;
366    }
367
368    if (c instanceof GeneratePasswordResponseControl)
369    {
370      return (GeneratePasswordResponseControl) c;
371    }
372    else
373    {
374      return new GeneratePasswordResponseControl(c.getOID(), c.isCritical(),
375           c.getValue());
376    }
377  }
378
379
380
381  /**
382   * Encodes the provided information appropriately for use as the value of this
383   * control.
384   *
385   * @param  generatedPassword        The password generated by the server.  It
386   *                                 must not be {@code null}.
387   * @param  mustChangePassword      Indicates whether the user will be required
388   *                                 to choose a new password the first time
389   *                                 they authenticate.
390   * @param  secondsUntilExpiration  The number of seconds until the new
391   *                                 password will expire.  It may be
392   *                                 {@code null} if the new password will not
393   *                                 expire.
394   *
395   * @return  The ASN.1 octet string suitable for use as the control value.
396   */
397  @NotNull()
398  private static ASN1OctetString encodeValue(
399                      @NotNull final ASN1OctetString generatedPassword,
400                      final boolean mustChangePassword,
401                      @Nullable final Long secondsUntilExpiration)
402  {
403    final ArrayList<ASN1Element> elements = new ArrayList<>(3);
404    elements.add(generatedPassword);
405    elements.add(mustChangePassword
406         ? ASN1Boolean.UNIVERSAL_BOOLEAN_TRUE_ELEMENT
407         : ASN1Boolean.UNIVERSAL_BOOLEAN_FALSE_ELEMENT);
408
409    if (secondsUntilExpiration != null)
410    {
411      elements.add(new ASN1Long(TYPE_SECONDS_UNTIL_EXPIRATION,
412           secondsUntilExpiration));
413    }
414
415    return new ASN1OctetString(new ASN1Sequence(elements).encode());
416  }
417
418
419
420  /**
421   * Retrieves the password that was generated by the server.
422   *
423   * @return  The password that was generated by the server.
424   */
425  @NotNull()
426  public ASN1OctetString getGeneratedPassword()
427  {
428    return generatedPassword;
429  }
430
431
432
433  /**
434   * Retrieves a string representation of the password that was generated by the
435   * server.
436   *
437   * @return  A string representation of the password that was generated by the
438   *          server.
439   */
440  @NotNull()
441  public String getGeneratedPasswordString()
442  {
443    return generatedPassword.stringValue();
444  }
445
446
447
448  /**
449   * Retrieves the bytes that comprise the password that was generated by the
450   * server.
451   *
452   * @return  The bytes that comprise the password that was generated by the
453   *          server.
454   */
455  @NotNull()
456  public byte[] getGeneratedPasswordBytes()
457  {
458    return generatedPassword.getValue();
459  }
460
461
462
463  /**
464   * Indicates whether the user will be required to change their password the
465   * first time they authenticate.
466   *
467   * @return  {@code true} if the user will be required to change their password
468   *          the first time they authenticate, or {@code false} if not.
469   */
470  public boolean mustChangePassword()
471  {
472    return mustChangePassword;
473  }
474
475
476
477  /**
478   * Retrieves the length of time, in seconds, until the generated password will
479   * expire.
480   *
481   * @return  The length of time, in seconds, until the generated password will
482   *          expire, or {@code null} if this is not available (e.g., because
483   *          the generated password will not expire).
484   */
485  @Nullable()
486  public Long getSecondsUntilExpiration()
487  {
488    return secondsUntilExpiration;
489  }
490
491
492
493  /**
494   * {@inheritDoc}
495   */
496  @Override()
497  @NotNull()
498  public String getControlName()
499  {
500    return INFO_CONTROL_NAME_GENERATE_PASSWORD_RESPONSE.get();
501  }
502
503
504
505  /**
506   * Retrieves a representation of this generate password response control as a
507   * JSON object.  The JSON object uses the following fields:
508   * <UL>
509   *   <LI>
510   *     {@code oid} -- A mandatory string field whose value is the object
511   *     identifier for this control.  For the generate password response
512   *     control, the OID is "1.3.6.1.4.1.30221.2.5.59".
513   *   </LI>
514   *   <LI>
515   *     {@code control-name} -- An optional string field whose value is a
516   *     human-readable name for this control.  This field is only intended for
517   *     descriptive purposes, and when decoding a control, the {@code oid}
518   *     field should be used to identify the type of control.
519   *   </LI>
520   *   <LI>
521   *     {@code criticality} -- A mandatory Boolean field used to indicate
522   *     whether this control is considered critical.
523   *   </LI>
524   *   <LI>
525   *     {@code value-base64} -- An optional string field whose value is a
526   *     base64-encoded representation of the raw value for this generate
527   *     password response control.  Exactly one of the {@code value-base64} and
528   *     {@code value-json} fields must be present.
529   *   </LI>
530   *   <LI>
531   *     {@code value-json} -- An optional JSON object field whose value is a
532   *     user-friendly representation of the value for this generate password
533   *     response control.  Exactly one of the {@code value-base64} and
534   *     {@code value-json} fields must be present, and if the
535   *     {@code value-json} field is used, then it will use the following
536   *     fields:
537   *     <UL>
538   *       <LI>
539   *         {@code generated-password} -- A string field whose value is the
540   *         password that was generated for the entry.
541   *       </LI>
542   *       <LI>
543   *         {@code must-change-password} -- A Boolean field that indicates
544   *         whether the user must choose a new password before they will be
545   *         allowed to request any other operations.
546   *       </LI>
547   *       <LI>
548   *         {@code seconds-until-expiration} -- An optional integer field whose
549   *         value is the number of seconds until the generated password
550   *         expires.
551   *       </LI>
552   *     </UL>
553   *   </LI>
554   * </UL>
555   *
556   * @return  A JSON object that contains a representation of this control.
557   */
558  @Override()
559  @NotNull()
560  public JSONObject toJSONControl()
561  {
562    final Map<String,JSONValue> valueFields = new LinkedHashMap<>();
563    valueFields.put(JSON_FIELD_GENERATED_PASSWORD,
564         new JSONString(generatedPassword.stringValue()));
565    valueFields.put(JSON_FIELD_MUST_CHANGE_PASSWORD,
566         new JSONBoolean(mustChangePassword));
567
568    if (secondsUntilExpiration != null)
569    {
570      valueFields.put(JSON_FIELD_SECONDS_UNTIL_EXPIRATION,
571           new JSONNumber(secondsUntilExpiration));
572    }
573
574
575    return new JSONObject(
576         new JSONField(JSONControlDecodeHelper.JSON_FIELD_OID,
577              GENERATE_PASSWORD_RESPONSE_OID),
578         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CONTROL_NAME,
579              INFO_CONTROL_NAME_GENERATE_PASSWORD_RESPONSE.get()),
580         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CRITICALITY,
581              isCritical()),
582         new JSONField(JSONControlDecodeHelper.JSON_FIELD_VALUE_JSON,
583              new JSONObject(valueFields)));
584  }
585
586
587
588  /**
589   * Attempts to decode the provided object as a JSON representation of a
590   * generate password response control.
591   *
592   * @param  controlObject  The JSON object to be decoded.  It must not be
593   *                        {@code null}.
594   * @param  strict         Indicates whether to use strict mode when decoding
595   *                        the provided JSON object.  If this is {@code true},
596   *                        then this method will throw an exception if the
597   *                        provided JSON object contains any unrecognized
598   *                        fields.  If this is {@code false}, then unrecognized
599   *                        fields will be ignored.
600   *
601   * @return  The generate password response control that was decoded from the
602   *          provided JSON object.
603   *
604   * @throws  LDAPException  If the provided JSON object cannot be parsed as a
605   *                         valid generate password response control.
606   */
607  @NotNull()
608  public static GeneratePasswordResponseControl decodeJSONControl(
609              @NotNull final JSONObject controlObject,
610              final boolean strict)
611         throws LDAPException
612  {
613    final JSONControlDecodeHelper jsonControl = new JSONControlDecodeHelper(
614         controlObject, strict, true, true);
615
616    final ASN1OctetString rawValue = jsonControl.getRawValue();
617    if (rawValue != null)
618    {
619      return new GeneratePasswordResponseControl(jsonControl.getOID(),
620           jsonControl.getCriticality(), rawValue);
621    }
622
623
624    final JSONObject valueObject = jsonControl.getValueObject();
625
626    final String generatedPassword =
627         valueObject.getFieldAsString(JSON_FIELD_GENERATED_PASSWORD);
628    if (generatedPassword == null)
629    {
630      throw new LDAPException(ResultCode.DECODING_ERROR,
631           ERR_GENERATE_PASSWORD_RESPONSE_JSON_MISSING_FIELD.get(
632                valueObject.toSingleLineString(),
633                JSON_FIELD_GENERATED_PASSWORD));
634    }
635
636    final Boolean mustChangePassword =
637         valueObject.getFieldAsBoolean(JSON_FIELD_MUST_CHANGE_PASSWORD);
638    if (mustChangePassword == null)
639    {
640      throw new LDAPException(ResultCode.DECODING_ERROR,
641           ERR_GENERATE_PASSWORD_RESPONSE_JSON_MISSING_FIELD.get(
642                valueObject.toSingleLineString(),
643                JSON_FIELD_MUST_CHANGE_PASSWORD));
644    }
645
646    final Long secondsUntilExpiration =
647         valueObject.getFieldAsLong(JSON_FIELD_SECONDS_UNTIL_EXPIRATION);
648
649
650    if (strict)
651    {
652      final List<String> unrecognizedFields =
653           JSONControlDecodeHelper.getControlObjectUnexpectedFields(
654                valueObject, JSON_FIELD_GENERATED_PASSWORD,
655                JSON_FIELD_MUST_CHANGE_PASSWORD,
656                JSON_FIELD_SECONDS_UNTIL_EXPIRATION);
657      if (! unrecognizedFields.isEmpty())
658      {
659        throw new LDAPException(ResultCode.DECODING_ERROR,
660             ERR_GENERATE_PASSWORD_RESPONSE_JSON_CONTROL_UNRECOGNIZED_FIELD.get(
661                  controlObject.toSingleLineString(),
662                  unrecognizedFields.get(0)));
663      }
664    }
665
666
667    return new GeneratePasswordResponseControl(generatedPassword,
668         mustChangePassword, secondsUntilExpiration);
669  }
670
671
672
673  /**
674   * {@inheritDoc}
675   */
676  @Override()
677  public void toString(@NotNull final StringBuilder buffer)
678  {
679    buffer.append("GeneratePasswordResponseControl(mustChangePassword=");
680    buffer.append(mustChangePassword);
681
682    if (secondsUntilExpiration != null)
683    {
684      buffer.append(", secondsUntilExpiration=");
685      buffer.append(secondsUntilExpiration);
686    }
687
688    buffer.append(')');
689  }
690}