001/*
002 * Copyright 2020-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2020-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) 2020-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.ASN1OctetString;
046import com.unboundid.ldap.sdk.BindResult;
047import com.unboundid.ldap.sdk.Control;
048import com.unboundid.ldap.sdk.DecodeableControl;
049import com.unboundid.ldap.sdk.JSONControlDecodeHelper;
050import com.unboundid.ldap.sdk.LDAPException;
051import com.unboundid.ldap.sdk.ResultCode;
052import com.unboundid.util.Debug;
053import com.unboundid.util.NotMutable;
054import com.unboundid.util.NotNull;
055import com.unboundid.util.Nullable;
056import com.unboundid.util.ThreadSafety;
057import com.unboundid.util.ThreadSafetyLevel;
058import com.unboundid.util.json.JSONArray;
059import com.unboundid.util.json.JSONException;
060import com.unboundid.util.json.JSONField;
061import com.unboundid.util.json.JSONObject;
062import com.unboundid.util.json.JSONValue;
063
064import static com.unboundid.ldap.sdk.unboundidds.controls.ControlMessages.*;
065
066
067
068/**
069 * This class provides an implementation of a response control that can be
070 * included in the response to a successful bind operation to provide
071 * information about recent successful and failed authentication attempts.
072 * <BR>
073 * <BLOCKQUOTE>
074 *   <B>NOTE:</B>  This class, and other classes within the
075 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
076 *   supported for use against Ping Identity, UnboundID, and
077 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
078 *   for proprietary functionality or for external specifications that are not
079 *   considered stable or mature enough to be guaranteed to work in an
080 *   interoperable way with other types of LDAP servers.
081 * </BLOCKQUOTE>
082 * <BR>
083 * This control has an OID of 1.3.6.1.4.1.30221.2.5.62, a criticality of
084 * {@code false}, and a value that is a JSON object with two top-level fields:
085 * successful-attempts and failed-attempts.  The value for each of these fields
086 * will be an array of JSON objects with the following fields:
087 * <UL>
088 *   <LI>timestamp -- The timestamp of the login attempt in the ISO 8601 format
089 *       described in RFC 3339.</LI>
090 *   <LI>client-ip-address -- A string representation of the IP address of the
091 *       client that tried to authenticate.</LI>
092 *   <LI>authentication-method -- The name of the method that the client used
093 *       when trying to authenticate.</LI>
094 *   <LI>failure-reason -- A string providing a general reason that the
095 *       authentication attempt failed (only used for failed attempts).</LI>
096 *   <LI>additional-attempt-count -- An integer value that indicates how many
097 *       other attempts were made on the same date with the same settings for
098 *       all fields except the timestamp.</LI>
099 * </UL>
100 *
101 * @see  GetRecentLoginHistoryRequestControl
102 */
103@NotMutable()
104@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
105public final class GetRecentLoginHistoryResponseControl
106       extends Control
107       implements DecodeableControl
108{
109  /**
110   * The OID (1.3.6.1.4.1.30221.2.5.62) for the get recent login history
111   * response control.
112   */
113  @NotNull public static final String GET_RECENT_LOGIN_HISTORY_RESPONSE_OID =
114       "1.3.6.1.4.1.30221.2.5.62";
115
116
117
118  /**
119   * The name of the field used to hold the array of failed attempts in the JSON
120   * representation of this control.
121   */
122  @NotNull private static final String JSON_FIELD_FAILED_ATTEMPTS =
123       "failed-attempts";
124
125
126
127  /**
128   * The name of the field used to hold the array of successful attempts in the
129   * JSON representation of this control.
130   */
131  @NotNull private static final String JSON_FIELD_SUCCESSFUL_ATTEMPTS =
132       "successful-attempts";
133
134
135
136  /**
137   * The serial version UID for this serializable class.
138   */
139  private static final long serialVersionUID = -4604204310334007290L;
140
141
142
143  // The recent login history contained in the response control.
144  @NotNull private final RecentLoginHistory recentLoginHistory;
145
146
147
148  /**
149   * Creates a new empty control instance that is intended to be used only for
150   * decoding controls via the {@code DecodeableControl} interface.
151   */
152  GetRecentLoginHistoryResponseControl()
153  {
154    recentLoginHistory = null;
155  }
156
157
158
159  /**
160   * Creates a new instance of this control with the provided information.
161   *
162   * @param  recentLoginHistory  The recent login history to include in the
163   *                             response control.  It must not be {@code null}.
164   */
165  public GetRecentLoginHistoryResponseControl(
166              @NotNull final RecentLoginHistory recentLoginHistory)
167  {
168    super(GET_RECENT_LOGIN_HISTORY_RESPONSE_OID, false,
169         new ASN1OctetString(recentLoginHistory.asJSONObject().toString()));
170
171    this.recentLoginHistory = recentLoginHistory;
172  }
173
174
175
176  /**
177   * Creates a new instance of this control that is decoded from the provided
178   * generic control.
179   *
180   * @param  oid         The OID for the control.
181   * @param  isCritical  Indicates whether this control should be marked
182   *                     critical.
183   * @param  value       The encoded value for the control.
184   *
185   * @throws LDAPException  If a problem is encountered while attempting to
186   *                         decode the provided control as a get recent login
187   *                         history response control.
188   */
189  public GetRecentLoginHistoryResponseControl(@NotNull final String oid,
190              final boolean isCritical, @Nullable final ASN1OctetString value)
191         throws LDAPException
192  {
193    super(oid, isCritical, value);
194
195    if (value == null)
196    {
197      throw new LDAPException(ResultCode.DECODING_ERROR,
198           ERR_GET_RECENT_LOGIN_HISTORY_RESPONSE_NO_VALUE.get());
199    }
200
201    final JSONObject jsonObject;
202    try
203    {
204      jsonObject = new JSONObject(value.stringValue());
205    }
206    catch (final JSONException e)
207    {
208      Debug.debugException(e);
209      throw new LDAPException(ResultCode.DECODING_ERROR,
210           ERR_GET_RECENT_LOGIN_HISTORY_RESPONSE_VALUE_NOT_JSON.get(
211                e.getMessage()),
212           e);
213    }
214
215    try
216    {
217      recentLoginHistory = new RecentLoginHistory(jsonObject);
218    }
219    catch (final LDAPException e)
220    {
221      Debug.debugException(e);
222      throw new LDAPException(ResultCode.DECODING_ERROR,
223           ERR_GET_RECENT_LOGIN_HISTORY_RESPONSE_CANNOT_PARSE_VALUE.get(
224                e.getMessage()),
225           e);
226    }
227  }
228
229
230
231  /**
232   * {@inheritDoc}
233   */
234  @Override()
235  @NotNull()
236  public GetRecentLoginHistoryResponseControl decodeControl(
237              @NotNull final String oid, final boolean isCritical,
238              @Nullable final ASN1OctetString value)
239          throws LDAPException
240  {
241    return new GetRecentLoginHistoryResponseControl(oid, isCritical, value);
242  }
243
244
245
246  /**
247   * Retrieves the recent login history contained in this response control.
248   *
249   * @return  The recent login history contained in this response control.
250   */
251  @NotNull()
252  public RecentLoginHistory getRecentLoginHistory()
253  {
254    return recentLoginHistory;
255  }
256
257
258
259  /**
260   * Extracts a get recent login history response control from the provided bind
261   * result.
262   *
263   * @param  bindResult  The bind result from which to retrieve the get recent
264   *                     login history response control.
265   *
266   * @return  The get recent login history response control contained in the
267   *          provided bind result, or {@code null} if the bind result did not
268   *          contain a get recent login history response control.
269   *
270   * @throws  LDAPException  If a problem is encountered while attempting to
271   *                         decode the get recent login history response
272   *                         control contained in the provided bind result.
273   */
274  @Nullable()
275  public static GetRecentLoginHistoryResponseControl get(
276                     @NotNull final BindResult bindResult)
277         throws LDAPException
278  {
279    final Control c =
280         bindResult.getResponseControl(GET_RECENT_LOGIN_HISTORY_RESPONSE_OID);
281    if (c == null)
282    {
283      return null;
284    }
285
286    if (c instanceof GetRecentLoginHistoryResponseControl)
287    {
288      return (GetRecentLoginHistoryResponseControl) c;
289    }
290    else
291    {
292      return new GetRecentLoginHistoryResponseControl(c.getOID(),
293           c.isCritical(), c.getValue());
294    }
295  }
296
297
298
299  /**
300   * {@inheritDoc}
301   */
302  @Override()
303  @NotNull()
304  public String getControlName()
305  {
306    return INFO_CONTROL_NAME_GET_RECENT_LOGIN_HISTORY_RESPONSE.get();
307  }
308
309
310
311  /**
312   * Retrieves a representation of this get recent login history response
313   * control as a JSON object.  The JSON object uses the following fields:
314   * <UL>
315   *   <LI>
316   *     {@code oid} -- A mandatory string field whose value is the object
317   *     identifier for this control.  For the get recent login history response
318   *     control, the OID is "1.3.6.1.4.1.30221.2.5.62".
319   *   </LI>
320   *   <LI>
321   *     {@code control-name} -- An optional string field whose value is a
322   *     human-readable name for this control.  This field is only intended for
323   *     descriptive purposes, and when decoding a control, the {@code oid}
324   *     field should be used to identify the type of control.
325   *   </LI>
326   *   <LI>
327   *     {@code criticality} -- A mandatory Boolean field used to indicate
328   *     whether this control is considered critical.
329   *   </LI>
330   *   <LI>
331   *     {@code value-base64} -- An optional string field whose value is a
332   *     base64-encoded representation of the raw value for this get recent
333   *     login history response control.  Exactly one of the
334   *     {@code value-base64} and {@code value-json} fields must be present.
335   *   </LI>
336   *   <LI>
337   *     {@code value-json} -- An optional JSON object field whose value is a
338   *     user-friendly representation of the value for this get recent login
339   *     history response control.  Exactly one of the {@code value-base64} and
340   *     {@code value-json} fields must be present, and if the
341   *     {@code value-json} field is used, then it will use the following
342   *     fields:
343   *     <UL>
344   *       <LI>
345   *         {@code successful-attempts} -- An optional array field whose values
346   *         are JSON objects with information about recent successful
347   *         authentication attempts by the user.  These JSON objects will use
348   *         the following fields:
349   *         <UL>
350   *           <LI>
351   *             {@code successful} -- A Boolean field that indicates whether
352   *             the attempt was successful.  For JSON objects in the
353   *             {@code successful-attempts} field, the value of this field will
354   *             always be {@code true}.
355   *           </LI>
356   *           <LI>
357   *             {@code timestamp} -- A string field whose value is a timestamp
358   *             (in the ISO 8601 format described in RFC 3339) for the
359   *             associated authentication attempt.
360   *           </LI>
361   *           <LI>
362   *             {@code authentication-method} -- A string field whose value is
363   *             the name of the attempted authentication method.
364   *           </LI>
365   *           <LI>
366   *             {@code client-ip-address} -- A string field whose value is
367   *             the IP address of the client that tried to authenticate.
368   *           </LI>
369   *           <LI>
370   *             {@code additional-attempt-count} -- An optional integer field
371   *             whose value is the number of additional similar successful
372   *             attempts on the same date for the same user.
373   *           </LI>
374   *         </UL>
375   *       </LI>
376   *       <LI>
377   *         {@code failed-attempts} -- An optional array field whose values
378   *         are JSON objects with information about recent failed
379   *         authentication attempts by the user.  These JSON objects will use
380   *         the following fields:
381   *         <UL>
382   *           <LI>
383   *             {@code successful} -- A Boolean field that indicates whether
384   *             the attempt was successful.  For JSON objects in the
385   *             {@code failed-attempts} field, the value of this field will
386   *             always be {@code false}.
387   *           </LI>
388   *           <LI>
389   *             {@code timestamp} -- A string field whose value is a timestamp
390   *             (in the ISO 8601 format described in RFC 3339) for the
391   *             associated authentication attempt.
392   *           </LI>
393   *           <LI>
394   *             {@code authentication-method} -- A string field whose value is
395   *             the name of the attempted authentication method.
396   *           </LI>
397   *           <LI>
398   *             {@code client-ip-address} -- A string field whose value is
399   *             the IP address of the client that tried to authenticate.
400   *           </LI>
401   *           <LI>
402   *             {@code failure-reason} -- A string field whose value is
403   *             a general reason that the authentication attempt failed.
404   *           </LI>
405   *           <LI>
406   *             {@code additional-attempt-count} -- An optional integer field
407   *             whose value is the number of additional similar successful
408   *             attempts on the same date for the same user.
409   *           </LI>
410   *         </UL>
411   *       </LI>
412   *     </UL>
413   *   </LI>
414   * </UL>
415   *
416   * @return  A JSON object that contains a representation of this control.
417   */
418  @Override()
419  @NotNull()
420  public JSONObject toJSONControl()
421  {
422    final Map<String,JSONValue> valueFields = new LinkedHashMap<>();
423
424    if (! recentLoginHistory.getSuccessfulAttempts().isEmpty())
425    {
426      final List<JSONValue> successfulAttemptObjects = new ArrayList<>(
427           recentLoginHistory.getSuccessfulAttempts().size());
428      for (final RecentLoginHistoryAttempt attempt :
429           recentLoginHistory.getSuccessfulAttempts())
430      {
431        successfulAttemptObjects.add(attempt.asJSONObject());
432      }
433
434      valueFields.put(JSON_FIELD_SUCCESSFUL_ATTEMPTS,
435           new JSONArray(successfulAttemptObjects));
436    }
437
438    if (! recentLoginHistory.getFailedAttempts().isEmpty())
439    {
440      final List<JSONValue> failedAttemptObjects = new ArrayList<>(
441           recentLoginHistory.getFailedAttempts().size());
442      for (final RecentLoginHistoryAttempt attempt :
443           recentLoginHistory.getFailedAttempts())
444      {
445        failedAttemptObjects.add(attempt.asJSONObject());
446      }
447
448      valueFields.put(JSON_FIELD_FAILED_ATTEMPTS,
449           new JSONArray(failedAttemptObjects));
450    }
451
452    return new JSONObject(
453         new JSONField(JSONControlDecodeHelper.JSON_FIELD_OID,
454              GET_RECENT_LOGIN_HISTORY_RESPONSE_OID),
455         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CONTROL_NAME,
456              INFO_CONTROL_NAME_GET_RECENT_LOGIN_HISTORY_RESPONSE.get()),
457         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CRITICALITY,
458              isCritical()),
459         new JSONField(JSONControlDecodeHelper.JSON_FIELD_VALUE_JSON,
460              new JSONObject(valueFields)));
461  }
462
463
464
465  /**
466   * Attempts to decode the provided object as a JSON representation of a get
467   * recent login history response control.
468   *
469   * @param  controlObject  The JSON object to be decoded.  It must not be
470   *                        {@code null}.
471   * @param  strict         Indicates whether to use strict mode when decoding
472   *                        the provided JSON object.  If this is {@code true},
473   *                        then this method will throw an exception if the
474   *                        provided JSON object contains any unrecognized
475   *                        fields.  If this is {@code false}, then unrecognized
476   *                        fields will be ignored.
477   *
478   * @return  The get recent login history response control that was decoded
479   *          from the provided JSON object.
480   *
481   * @throws  LDAPException  If the provided JSON object cannot be parsed as a
482   *                         valid get recent login history response control.
483   */
484  @NotNull()
485  public static GetRecentLoginHistoryResponseControl decodeJSONControl(
486              @NotNull final JSONObject controlObject,
487              final boolean strict)
488         throws LDAPException
489  {
490    final JSONControlDecodeHelper jsonControl = new JSONControlDecodeHelper(
491         controlObject, strict, true, true);
492
493    final ASN1OctetString rawValue = jsonControl.getRawValue();
494    if (rawValue != null)
495    {
496      return new GetRecentLoginHistoryResponseControl(jsonControl.getOID(),
497           jsonControl.getCriticality(), rawValue);
498    }
499
500
501    final JSONObject valueObject = jsonControl.getValueObject();
502
503    final List<RecentLoginHistoryAttempt> successfulAttempts;
504    final List<JSONValue> successObjects =
505         valueObject.getFieldAsArray(JSON_FIELD_SUCCESSFUL_ATTEMPTS);
506    if (successObjects == null)
507    {
508      successfulAttempts = null;
509    }
510    else
511    {
512      successfulAttempts = new ArrayList<>(successObjects.size());
513      for (final JSONValue successValue : successObjects)
514      {
515        if (successValue instanceof JSONObject)
516        {
517          try
518          {
519            successfulAttempts.add(new RecentLoginHistoryAttempt(
520                 (JSONObject) successValue));
521          }
522          catch (final LDAPException e)
523          {
524            Debug.debugException(e);
525            throw new LDAPException(ResultCode.DECODING_ERROR,
526                 ERR_GET_RECENT_LOGIN_HISTORY_RESPONSE_JSON_MALFORMED_ATTEMPT.
527                      get(controlObject.toSingleLineString(),
528                           JSON_FIELD_SUCCESSFUL_ATTEMPTS, e.getMessage()),
529                 e);
530          }
531        }
532        else
533        {
534          throw new LDAPException(ResultCode.DECODING_ERROR,
535               ERR_GET_RECENT_LOGIN_HISTORY_RESPONSE_JSON_ATTEMPT_NOT_OBJECT.
536                    get(controlObject.toSingleLineString(),
537                         JSON_FIELD_SUCCESSFUL_ATTEMPTS));
538        }
539      }
540    }
541
542    final List<RecentLoginHistoryAttempt> failedAttempts;
543    final List<JSONValue> failureObjects =
544         valueObject.getFieldAsArray(JSON_FIELD_FAILED_ATTEMPTS);
545    if (failureObjects == null)
546    {
547      failedAttempts = null;
548    }
549    else
550    {
551      failedAttempts = new ArrayList<>(failureObjects.size());
552      for (final JSONValue failureValue : failureObjects)
553      {
554        if (failureValue instanceof JSONObject)
555        {
556          try
557          {
558            failedAttempts.add(new RecentLoginHistoryAttempt(
559                 (JSONObject) failureValue));
560          }
561          catch (final LDAPException e)
562          {
563            Debug.debugException(e);
564            throw new LDAPException(ResultCode.DECODING_ERROR,
565                 ERR_GET_RECENT_LOGIN_HISTORY_RESPONSE_JSON_MALFORMED_ATTEMPT.
566                      get(controlObject.toSingleLineString(),
567                           JSON_FIELD_FAILED_ATTEMPTS, e.getMessage()),
568                 e);
569          }
570        }
571        else
572        {
573          throw new LDAPException(ResultCode.DECODING_ERROR,
574               ERR_GET_RECENT_LOGIN_HISTORY_RESPONSE_JSON_ATTEMPT_NOT_OBJECT.
575                    get(controlObject.toSingleLineString(),
576                         JSON_FIELD_FAILED_ATTEMPTS));
577        }
578      }
579    }
580
581
582    if (strict)
583    {
584      final List<String> unrecognizedFields =
585           JSONControlDecodeHelper.getControlObjectUnexpectedFields(
586                valueObject, JSON_FIELD_SUCCESSFUL_ATTEMPTS,
587                JSON_FIELD_FAILED_ATTEMPTS);
588      if (! unrecognizedFields.isEmpty())
589      {
590        throw new LDAPException(ResultCode.DECODING_ERROR,
591             ERR_GET_RECENT_LOGIN_HISTORY_RESPONSE_JSON_UNRECOGNIZED_FIELD.get(
592                  controlObject.toSingleLineString(),
593                  unrecognizedFields.get(0)));
594      }
595    }
596
597
598    return new GetRecentLoginHistoryResponseControl(new RecentLoginHistory(
599         successfulAttempts, failedAttempts));
600  }
601
602
603
604  /**
605   * {@inheritDoc}
606   */
607  @Override()
608  public void toString(@NotNull final StringBuilder buffer)
609  {
610    buffer.append("GetRecentLoginHistoryResponseControl(recentLoginHistory=");
611    buffer.append(recentLoginHistory.toString());
612    buffer.append(')');
613  }
614}