001/*
002 * Copyright 2007-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2007-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) 2007-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.controls;
037
038
039
040import java.util.List;
041
042import com.unboundid.asn1.ASN1OctetString;
043import com.unboundid.ldap.sdk.Control;
044import com.unboundid.ldap.sdk.DecodeableControl;
045import com.unboundid.ldap.sdk.JSONControlDecodeHelper;
046import com.unboundid.ldap.sdk.LDAPException;
047import com.unboundid.ldap.sdk.LDAPResult;
048import com.unboundid.ldap.sdk.ResultCode;
049import com.unboundid.util.Debug;
050import com.unboundid.util.NotMutable;
051import com.unboundid.util.NotNull;
052import com.unboundid.util.Nullable;
053import com.unboundid.util.ThreadSafety;
054import com.unboundid.util.ThreadSafetyLevel;
055import com.unboundid.util.json.JSONField;
056import com.unboundid.util.json.JSONObject;
057
058import static com.unboundid.ldap.sdk.controls.ControlMessages.*;
059
060
061
062/**
063 * This class provides an implementation of the expiring expiring control as
064 * described in draft-vchu-ldap-pwd-policy.  It may be used to indicate that the
065 * authenticated user's password will expire in the near future.  The value of
066 * this control includes the length of time in seconds until the user's
067 * password actually expires.
068 * <BR><BR>
069 * No request control is required to trigger the server to send the password
070 * expiring response control.  If the server supports the use of this control
071 * and the user's password will expire within a time frame that the server
072 * considers to be the near future, then it will be included in the bind
073 * response returned to the client.
074 * <BR><BR>
075 * See the documentation for the {@link PasswordExpiredControl} to see an
076 * example that demonstrates the use of both the password expiring and password
077 * expired controls.
078 */
079@NotMutable()
080@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
081public final class PasswordExpiringControl
082       extends Control
083       implements DecodeableControl
084{
085  /**
086   * The OID (2.16.840.1.113730.3.4.5) for the password expiring response
087   * control.
088   */
089  @NotNull public static final String PASSWORD_EXPIRING_OID =
090       "2.16.840.1.113730.3.4.5";
091
092
093
094  /**
095   * The name of the field used to hold the seconds until expiration in the JSON
096   * representation of this control.
097   */
098  @NotNull private static final String JSON_FIELD_SECONDS_UNTIL_EXPIRATION =
099       "seconds-until-expiration";
100
101
102
103  /**
104   * The serial version UID for this serializable class.
105   */
106  private static final long serialVersionUID = 1250220480854441338L;
107
108
109
110  // The length of time in seconds until the password expires.
111  private final int secondsUntilExpiration;
112
113
114
115  /**
116   * Creates a new empty control instance that is intended to be used only for
117   * decoding controls via the {@code DecodeableControl} interface.
118   */
119  PasswordExpiringControl()
120  {
121    secondsUntilExpiration = -1;
122  }
123
124
125
126  /**
127   * Creates a new password expiring control with the provided information.
128   *
129   * @param  secondsUntilExpiration  The length of time in seconds until the
130   *                                 password expires.
131   */
132  public PasswordExpiringControl(final int secondsUntilExpiration)
133  {
134    super(PASSWORD_EXPIRING_OID, false,
135          new ASN1OctetString(String.valueOf(secondsUntilExpiration)));
136
137    this.secondsUntilExpiration = secondsUntilExpiration;
138  }
139
140
141
142  /**
143   * Creates a new password expiring control with the provided information.
144   *
145   * @param  oid         The OID for the control.
146   * @param  isCritical  Indicates whether the control should be marked
147   *                     critical.
148   * @param  value       The encoded value for the control.  This may be
149   *                     {@code null} if no value was provided.
150   *
151   * @throws  LDAPException  If the provided control cannot be decoded as a
152   *                         password expiring response control.
153   */
154  public PasswordExpiringControl(@NotNull final String oid,
155                                 final boolean isCritical,
156                                 @Nullable final ASN1OctetString value)
157         throws LDAPException
158  {
159    super(oid, isCritical, value);
160
161    if (value == null)
162    {
163      throw new LDAPException(ResultCode.DECODING_ERROR,
164                              ERR_PW_EXPIRING_NO_VALUE.get());
165    }
166
167    try
168    {
169      secondsUntilExpiration = Integer.parseInt(value.stringValue());
170    }
171    catch (final NumberFormatException nfe)
172    {
173      Debug.debugException(nfe);
174      throw new LDAPException(ResultCode.DECODING_ERROR,
175                              ERR_PW_EXPIRING_VALUE_NOT_INTEGER.get(), nfe);
176    }
177  }
178
179
180
181  /**
182   * {@inheritDoc}
183   */
184  @Override()
185  @NotNull()
186  public PasswordExpiringControl decodeControl(
187              @NotNull final String oid, final boolean isCritical,
188              @Nullable final ASN1OctetString value)
189         throws LDAPException
190  {
191    return new PasswordExpiringControl(oid, isCritical, value);
192  }
193
194
195
196  /**
197   * Extracts a password expiring control from the provided result.
198   *
199   * @param  result  The result from which to retrieve the password expiring
200   *                 control.
201   *
202   * @return  The password expiring control contained in the provided result, or
203   *          {@code null} if the result did not contain a password expiring
204   *          control.
205   *
206   * @throws  LDAPException  If a problem is encountered while attempting to
207   *                         decode the password expiring control contained in
208   *                         the provided result.
209   */
210  @Nullable()
211  public static PasswordExpiringControl get(@NotNull final LDAPResult result)
212         throws LDAPException
213  {
214    final Control c = result.getResponseControl(PASSWORD_EXPIRING_OID);
215    if (c == null)
216    {
217      return null;
218    }
219
220    if (c instanceof PasswordExpiringControl)
221    {
222      return (PasswordExpiringControl) c;
223    }
224    else
225    {
226      return new PasswordExpiringControl(c.getOID(), c.isCritical(),
227           c.getValue());
228    }
229  }
230
231
232
233  /**
234   * Retrieves the length of time in seconds until the password expires.
235   *
236   * @return  The length of time in seconds until the password expires.
237   */
238  public int getSecondsUntilExpiration()
239  {
240    return secondsUntilExpiration;
241  }
242
243
244
245  /**
246   * {@inheritDoc}
247   */
248  @Override()
249  @NotNull()
250  public String getControlName()
251  {
252    return INFO_CONTROL_NAME_PW_EXPIRING.get();
253  }
254
255
256
257  /**
258   * Retrieves a representation of this password expiring control as a JSON
259   * object.  The JSON object uses the following fields:
260   * <UL>
261   *   <LI>
262   *     {@code oid} -- A mandatory string field whose value is the object
263   *     identifier for this control.  For the password expiring control, the
264   *     OID is "2.16.840.1.113730.3.4.5".
265   *   </LI>
266   *   <LI>
267   *     {@code control-name} -- An optional string field whose value is a
268   *     human-readable name for this control.  This field is only intended for
269   *     descriptive purposes, and when decoding a control, the {@code oid}
270   *     field should be used to identify the type of control.
271   *   </LI>
272   *   <LI>
273   *     {@code criticality} -- A mandatory Boolean field used to indicate
274   *     whether this control is considered critical.
275   *   </LI>
276   *   <LI>
277   *     {@code value-base64} -- An optional string field whose value is a
278   *     base64-encoded representation of the raw value for this password
279   *     expiring control.  Exactly one of the {@code value-base64} and
280   *     {@code value-json} fields must be present.
281   *   </LI>
282   *   <LI>
283   *     {@code value-json} -- An optional JSON object field whose value is a
284   *     user-friendly representation of the value for this password expiring
285   *     control.  Exactly one of the {@code value-base64} and
286   *     {@code value-json} fields must be present, and if the
287   *     {@code value-json} field is used, then it will use the following
288   *     fields:
289   *     <UL>
290   *       <LI>
291   *         {@code seconds-until-expiration} -- An integer field whose value is
292   *         the number of seconds until the user's password expires.
293   *       </LI>
294   *     </UL>
295   *   </LI>
296   * </UL>
297   *
298   * @return  A JSON object that contains a representation of this control.
299   */
300  @Override()
301  @NotNull()
302  public JSONObject toJSONControl()
303  {
304    return new JSONObject(
305         new JSONField(JSONControlDecodeHelper.JSON_FIELD_OID,
306              PASSWORD_EXPIRING_OID),
307         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CONTROL_NAME,
308              INFO_CONTROL_NAME_PW_EXPIRING.get()),
309         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CRITICALITY,
310              isCritical()),
311         new JSONField(JSONControlDecodeHelper.JSON_FIELD_VALUE_JSON,
312              new JSONObject(
313                   new JSONField(JSON_FIELD_SECONDS_UNTIL_EXPIRATION,
314                        secondsUntilExpiration))));
315  }
316
317
318
319  /**
320   * Attempts to decode the provided object as a JSON representation of a
321   * password expiring control.
322   *
323   * @param  controlObject  The JSON object to be decoded.  It must not be
324   *                        {@code null}.
325   * @param  strict         Indicates whether to use strict mode when decoding
326   *                        the provided JSON object.  If this is {@code true},
327   *                        then this method will throw an exception if the
328   *                        provided JSON object contains any unrecognized
329   *                        fields.  If this is {@code false}, then unrecognized
330   *                        fields will be ignored.
331   *
332   * @return  The password expiring control that was decoded from the provided
333   *          JSON object.
334   *
335   * @throws  LDAPException  If the provided JSON object cannot be parsed as a
336   *                         valid password expiring control.
337   */
338  @NotNull()
339  public static PasswordExpiringControl decodeJSONControl(
340              @NotNull final JSONObject controlObject,
341              final boolean strict)
342         throws LDAPException
343  {
344    final JSONControlDecodeHelper jsonControl = new JSONControlDecodeHelper(
345         controlObject, strict, true, true);
346
347    final ASN1OctetString rawValue = jsonControl.getRawValue();
348    if (rawValue != null)
349    {
350      return new PasswordExpiringControl(jsonControl.getOID(),
351           jsonControl.getCriticality(), rawValue);
352    }
353
354
355    final JSONObject valueObject = jsonControl.getValueObject();
356
357    final Integer secondsUntilExpiration =
358         valueObject.getFieldAsInteger(JSON_FIELD_SECONDS_UNTIL_EXPIRATION);
359    if (secondsUntilExpiration == null)
360    {
361      throw new LDAPException(ResultCode.DECODING_ERROR,
362           ERR_PW_EXPIRING_JSON_MISSING_SECONDS_UNTIL_EXPIRATION.get(
363                controlObject.toSingleLineString(),
364                JSON_FIELD_SECONDS_UNTIL_EXPIRATION));
365    }
366
367
368    if (strict)
369    {
370      final List<String> unrecognizedFields =
371           JSONControlDecodeHelper.getControlObjectUnexpectedFields(
372                valueObject, JSON_FIELD_SECONDS_UNTIL_EXPIRATION);
373      if (! unrecognizedFields.isEmpty())
374      {
375        throw new LDAPException(ResultCode.DECODING_ERROR,
376             ERR_PW_EXPIRING_JSON_UNRECOGNIZED_FIELD.get(
377                  controlObject.toSingleLineString(),
378                  unrecognizedFields.get(0)));
379      }
380    }
381
382
383    return new PasswordExpiringControl(secondsUntilExpiration);
384  }
385
386
387
388  /**
389   * {@inheritDoc}
390   */
391  @Override()
392  public void toString(@NotNull final StringBuilder buffer)
393  {
394    buffer.append("PasswordExpiringControl(secondsUntilExpiration=");
395    buffer.append(secondsUntilExpiration);
396    buffer.append(", isCritical=");
397    buffer.append(isCritical());
398    buffer.append(')');
399  }
400}