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.io.Serializable;
041import java.text.ParseException;
042import java.util.Date;
043import java.util.LinkedHashMap;
044import java.util.Map;
045import java.util.Objects;
046
047import com.unboundid.ldap.sdk.LDAPException;
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.StaticUtils;
054import com.unboundid.util.ThreadSafety;
055import com.unboundid.util.ThreadSafetyLevel;
056import com.unboundid.util.Validator;
057import com.unboundid.util.json.JSONBoolean;
058import com.unboundid.util.json.JSONObject;
059import com.unboundid.util.json.JSONNumber;
060import com.unboundid.util.json.JSONString;
061import com.unboundid.util.json.JSONValue;
062
063import static com.unboundid.ldap.sdk.unboundidds.controls.ControlMessages.*;
064
065
066
067/**
068 * This class provides a data structure with information about a recent login
069 * attempt for a user.
070 * <BR>
071 * <BLOCKQUOTE>
072 *   <B>NOTE:</B>  This class, and other classes within the
073 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
074 *   supported for use against Ping Identity, UnboundID, and
075 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
076 *   for proprietary functionality or for external specifications that are not
077 *   considered stable or mature enough to be guaranteed to work in an
078 *   interoperable way with other types of LDAP servers.
079 * </BLOCKQUOTE>
080 *
081 * @see  GetRecentLoginHistoryRequestControl
082 * @see  GetRecentLoginHistoryResponseControl
083 */
084@NotMutable()
085@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
086public final class RecentLoginHistoryAttempt
087       implements Serializable, Comparable<RecentLoginHistoryAttempt>
088{
089  /**
090   * The name of the JSON field used to hold the additional attempt count.
091   */
092  @NotNull private static final String JSON_FIELD_ADDITIONAL_ATTEMPT_COUNT =
093       "additional-attempt-count";
094
095
096
097  /**
098   * The name of the JSON field used to hold the authentication method.
099   */
100  @NotNull private static final String JSON_FIELD_AUTHENTICATION_METHOD =
101       "authentication-method";
102
103
104
105  /**
106   * The name of the JSON field used to hold the client IP address.
107   */
108  @NotNull private static final String JSON_FIELD_CLIENT_IP_ADDRESS =
109       "client-ip-address";
110
111
112
113  /**
114   * The name of the JSON field used to provide a general reason that the
115   * attempt was not successful.
116   */
117  @NotNull private static final String JSON_FIELD_FAILURE_REASON =
118       "failure-reason";
119
120
121
122  /**
123   * The name of the JSON field used to indicate whether the attempt was
124   * successful.
125   */
126  @NotNull private static final String JSON_FIELD_SUCCESSFUL = "successful";
127
128
129
130  /**
131   * The name of the JSON field used to hold the timestamp.
132   */
133  @NotNull private static final String JSON_FIELD_TIMESTAMP = "timestamp";
134
135
136
137  /**
138   * The serial version UID for this serializable class.
139   */
140  private static final long serialVersionUID = 6060214815221896077L;
141
142
143
144  // Indicates whether the authentication attempt was successful.
145  private final boolean successful;
146
147  // The JSON object providing an encoded representation of this attempt.
148  @NotNull private final JSONObject jsonObject;
149
150  // The number of additional authentication attempts on the same date (in the
151  // UTC time zone) as this attempt with the same values for the successful,
152  // authentication method, client IP address, and failure reason fields.
153  @Nullable private final Long additionalAttemptCount;
154
155  // The time that the authentication attempt occurred.
156  private final long timestamp;
157
158  // The name of the authentication method attempted by the client.
159  @NotNull  private final String authenticationMethod;
160
161  // The IP address of the client, if available.
162  @Nullable private final String clientIPAddress;
163
164  // A general reason that the authentication attempt failed, if available.
165  @Nullable private final String failureReason;
166
167
168
169  /**
170   * Creates a new recent login history attempt object with the provided
171   * information.
172   *
173   * @param  successful              Indicates whether the attempt was
174   *                                 successful.
175   * @param  timestamp               The time of the authentication attempt.
176   * @param  authenticationMethod    The name of the authentication method
177   *                                 used for the attempt.  This must not be
178   *                                 {@code null} or empty.
179   * @param  clientIPAddress         The IP address of the client that made the
180   *                                 authentication attempt.  This may be
181   *                                 {@code null} if no client IP address is
182   *                                 available.
183   * @param  failureReason           A general reason that the authentication
184   *                                 attempt failed.  It must be {@code null} if
185   *                                 the attempt succeeded and must not be
186   *                                 {@code null} if the attempt failed.  If
187   *                                 provided, the value should be one of the
188   *                                 {@code FAILURE_NAME_}* constants in the
189   *                                 {@link AuthenticationFailureReason} class.
190   * @param  additionalAttemptCount  The number of additional authentication
191   *                                 attempts that occurred on the same date (in
192   *                                 the UTC time zone) as the provided
193   *                                 timestamp with the same values for the
194   *                                 successful, authentication method, client
195   *                                 IP address, and failure reason fields.  It
196   *                                 may be {@code null} if this should not be
197   *                                 included (e.g., if information about
198   *                                 similar attempts should not be collapsed).
199   */
200  public RecentLoginHistoryAttempt(final boolean successful,
201              final long timestamp,
202              @NotNull final String authenticationMethod,
203              @Nullable final String clientIPAddress,
204              @Nullable final String failureReason,
205              @Nullable final Long additionalAttemptCount)
206  {
207    Validator.ensureNotNullOrEmpty(authenticationMethod,
208         "RecentLoginHistoryAttempt.<init>.authenticationMethod must not be " +
209              "null or empty.");
210
211    if (successful)
212    {
213      Validator.ensureTrue((failureReason == null),
214           "RecentLoginHistoryAttempt.<init>.failureReason must be null for " +
215                "successful authentication attempts.");
216    }
217    else
218    {
219      Validator.ensureNotNullOrEmpty(failureReason,
220           "RecentLoginHistoryAttempt.<init>.failureReason must not be null " +
221                "or empty for failed authentication attempts.");
222    }
223
224    this.successful = successful;
225    this.timestamp = timestamp;
226    this.authenticationMethod = authenticationMethod;
227    this.clientIPAddress = clientIPAddress;
228    this.failureReason = failureReason;
229    this.additionalAttemptCount = additionalAttemptCount;
230
231    jsonObject = encodeToJSON(successful, timestamp, authenticationMethod,
232         clientIPAddress, failureReason, additionalAttemptCount);
233  }
234
235
236
237  /**
238   * Creates a new recent login history attempt object that is decoded from the
239   * provided JSON object.
240   *
241   * @param  jsonObject  A JSON object containing an encoded representation of
242   *                     the attempt.  It must not be {@code null}.
243   *
244   * @throws  LDAPException  If a problem occurs while attempting to decode the
245   *                         provided JSON object as a recent login history
246   *                         attempt.
247   */
248  public RecentLoginHistoryAttempt(@NotNull final JSONObject jsonObject)
249         throws LDAPException
250  {
251    Validator.ensureNotNull(jsonObject,
252         "RecentLoginHistoryAttempt.<init>.jsonObject must not be null.");
253
254    this.jsonObject = jsonObject;
255
256    final Boolean successfulBoolean =
257         jsonObject.getFieldAsBoolean(JSON_FIELD_SUCCESSFUL);
258    if (successfulBoolean == null)
259    {
260      throw new LDAPException(ResultCode.DECODING_ERROR,
261           ERR_RECENT_LOGIN_HISTORY_ATTEMPT_MISSING_FIELD.get(
262                jsonObject.toSingleLineString(), JSON_FIELD_SUCCESSFUL));
263    }
264    else
265    {
266      successful = successfulBoolean;
267    }
268
269    final String timestampValue =
270         jsonObject.getFieldAsString(JSON_FIELD_TIMESTAMP);
271    if (timestampValue == null)
272    {
273      throw new LDAPException(ResultCode.DECODING_ERROR,
274           ERR_RECENT_LOGIN_HISTORY_ATTEMPT_MISSING_FIELD.get(
275                jsonObject.toSingleLineString(), JSON_FIELD_TIMESTAMP));
276    }
277
278    try
279    {
280      timestamp = StaticUtils.decodeRFC3339Time(timestampValue).getTime();
281    }
282    catch (final ParseException e)
283    {
284      Debug.debugException(e);
285      throw new LDAPException(ResultCode.DECODING_ERROR,
286           ERR_RECENT_LOGIN_HISTORY_ATTEMPT_MALFORMED_TIMESTAMP.get(
287                jsonObject.toSingleLineString(), timestampValue,
288                e.getMessage()),
289           e);
290    }
291
292    authenticationMethod =
293         jsonObject.getFieldAsString(JSON_FIELD_AUTHENTICATION_METHOD);
294    if (authenticationMethod == null)
295    {
296      throw new LDAPException(ResultCode.DECODING_ERROR,
297           ERR_RECENT_LOGIN_HISTORY_ATTEMPT_MISSING_FIELD.get(
298                jsonObject.toSingleLineString(),
299                JSON_FIELD_AUTHENTICATION_METHOD));
300    }
301
302    clientIPAddress = jsonObject.getFieldAsString(JSON_FIELD_CLIENT_IP_ADDRESS);
303
304    failureReason = jsonObject.getFieldAsString(JSON_FIELD_FAILURE_REASON);
305    if (successful)
306    {
307      if (failureReason != null)
308      {
309        throw new LDAPException(ResultCode.DECODING_ERROR,
310             ERR_RECENT_LOGIN_HISTORY_ATTEMPT_UNEXPECTED_FAILURE_REASON.get(
311                  jsonObject.toSingleLineString()));
312      }
313    }
314    else if (failureReason == null)
315    {
316      throw new LDAPException(ResultCode.DECODING_ERROR,
317           ERR_RECENT_LOGIN_HISTORY_ATTEMPT_MISSING_FAILURE_REASON.get(
318                jsonObject.toSingleLineString(), JSON_FIELD_FAILURE_REASON));
319    }
320
321    additionalAttemptCount =
322         jsonObject.getFieldAsLong(JSON_FIELD_ADDITIONAL_ATTEMPT_COUNT);
323  }
324
325
326
327  /**
328   * Encodes the provided information about a successful authentication attempt
329   * to a JSON object.
330   *
331   * @param  successful              Indicates whether the attempt was
332   *                                 successful.
333   * @param  timestamp               The time of the authentication attempt.
334   * @param  authenticationMethod    The name of the authentication method
335   *                                 used for the attempt.  This must not be
336   *                                 {@code null} or empty.
337   * @param  clientIPAddress         The IP address of the client that made the
338   *                                 authentication attempt.  This may be
339   *                                 {@code null} if no client IP address is
340   *                                 available.
341   * @param  failureReason           A general reason that the authentication
342   *                                 attempt failed.  It must be {@code null} if
343   *                                 the attempt succeeded and must not be
344   *                                 {@code null} if the attempt failed.  If
345   *                                 provided, the value should be one of the
346   *                                 {@code FAILURE_NAME_}* constants in the
347   *                                 {@link AuthenticationFailureReason} class.
348   * @param  additionalAttemptCount  The number of additional authentication
349   *                                 attempts that occurred on the same date (in
350   *                                 the UTC time zone) as the provided
351   *                                 timestamp with the same values for the
352   *                                 successful, authentication method, client
353   *                                 IP address, and failure reason fields.  It
354   *                                 may be {@code null} if this should not be
355   *                                 included (e.g., if information about
356   *                                 similar attempts should not be collapsed).
357   *
358   * @return  A JSON object containing the provided information.
359   */
360  @NotNull()
361  private static JSONObject encodeToJSON(final boolean successful,
362               final long timestamp,
363               @NotNull final String authenticationMethod,
364               @Nullable final String clientIPAddress,
365               @Nullable final String failureReason,
366               @Nullable final Long additionalAttemptCount)
367  {
368    final Map<String,JSONValue> fields = new LinkedHashMap<>(
369         StaticUtils.computeMapCapacity(6));
370
371    fields.put(JSON_FIELD_SUCCESSFUL, new JSONBoolean(successful));
372    fields.put(JSON_FIELD_TIMESTAMP,
373         new JSONString(StaticUtils.encodeRFC3339Time(timestamp)));
374    fields.put(JSON_FIELD_AUTHENTICATION_METHOD,
375         new JSONString(authenticationMethod));
376
377    if (clientIPAddress != null)
378    {
379      fields.put(JSON_FIELD_CLIENT_IP_ADDRESS, new JSONString(clientIPAddress));
380    }
381
382    if (failureReason != null)
383    {
384      fields.put(JSON_FIELD_FAILURE_REASON, new JSONString(failureReason));
385    }
386
387    if (additionalAttemptCount != null)
388    {
389      fields.put(JSON_FIELD_ADDITIONAL_ATTEMPT_COUNT,
390           new JSONNumber(additionalAttemptCount));
391    }
392
393    return new JSONObject(fields);
394  }
395
396
397
398  /**
399   * Indicates whether this recent login history attempt is for a successful
400   * login.
401   *
402   * @return  {@code true} if this recent login history attempt is for a
403   *          successful login, or {@code false} if it is for a failed login.
404   */
405  public boolean isSuccessful()
406  {
407    return successful;
408  }
409
410
411
412  /**
413   * Retrieves the time that the authentication attempt occurred.
414   *
415   * @return  The time that the authentication attempt occurred.
416   */
417  @NotNull()
418  public Date getTimestamp()
419  {
420    return new Date(timestamp);
421  }
422
423
424
425  /**
426   * Retrieves the name of the authentication method that the client used.  The
427   * value should generally be one of "simple" (for LDAP simple authentication),
428   * "internal" (if the authentication occurred internally within the server),
429   * or "SASL {mechanism}" (if the client authenticated via some SASL
430   * mechanism).
431   *
432   * @return  The name of the authentication method that the client used.
433   */
434  @NotNull()
435  public String getAuthenticationMethod()
436  {
437    return authenticationMethod;
438  }
439
440
441
442  /**
443   * Retrieves the IP address of the client that made the authentication
444   * attempt, if available.
445   *
446   * @return  The IP address of the client that made the authentication attempt,
447   *          or {@code null} if no client IP address is available (e.g.,
448   *          because the client authenticated through some internal mechanism).
449   */
450  @Nullable()
451  public String getClientIPAddress()
452  {
453    return clientIPAddress;
454  }
455
456
457
458  /**
459   * Retrieves a general reason that the authentication attempt failed, if
460   * appropriate.
461   *
462   * @return  A general reason that the authentication attempt failed, or
463   *          {@code null} if the attempt was successful.
464   */
465  @Nullable()
466  public String getFailureReason()
467  {
468    return failureReason;
469  }
470
471
472
473  /**
474   * Retrieves the number of additional authentication attempts that occurred on
475   * the same date (in the UTC time zone) as the timestamp for this attempt and
476   * had the same values for the successful, authentication method, client IP
477   * address, and failure reason fields.
478   *
479   * @return  The number of additional similar authentication attempts that
480   *          occurred on the same date as this attempt, or {@code null} if this
481   *          is not available (e.g., because the server is not configured to
482   *          collapse information about multiple similar attempts into a
483   *          single record).
484   */
485  @Nullable()
486  public Long getAdditionalAttemptCount()
487  {
488    return additionalAttemptCount;
489  }
490
491
492
493  /**
494   * Retrieves a JSON object with an encoded representation of this recent
495   * login history attempt.
496   *
497   * @return  A JSON object with an encoded representation of this recent long
498   *          history attempt.
499   */
500  @NotNull()
501  public JSONObject asJSONObject()
502  {
503    return jsonObject;
504  }
505
506
507
508  /**
509   * Indicates whether the provided object is logically equivalent to this
510   * recent login history attempt object.
511   *
512   * @param  o  The object for which to make the determination.
513   *
514   * @return  {@code true} if the provided object is logically equivalent to
515   *          this recent login history attempt object, or {@code false} if not.
516   */
517  @Override()
518  public boolean equals(@Nullable final Object o)
519  {
520    if (o == null)
521    {
522      return false;
523    }
524
525    if (o == this)
526    {
527      return true;
528    }
529
530    if (! (o instanceof RecentLoginHistoryAttempt))
531    {
532      return false;
533    }
534
535    final RecentLoginHistoryAttempt a = (RecentLoginHistoryAttempt) o;
536    if (successful != a.successful)
537    {
538      return false;
539    }
540
541    if (timestamp != a.timestamp)
542    {
543      return false;
544    }
545
546    if (! authenticationMethod.equalsIgnoreCase(a.authenticationMethod))
547    {
548      return false;
549    }
550
551    if (! Objects.equals(clientIPAddress, a.clientIPAddress))
552    {
553      return false;
554    }
555
556    if (! Objects.equals(failureReason, a.failureReason))
557    {
558      return false;
559    }
560
561    if (! Objects.equals(additionalAttemptCount, a.additionalAttemptCount))
562    {
563      return false;
564    }
565
566    return true;
567  }
568
569
570
571  /**
572   * Retrieves a hash code for this recent login history attempt.
573   *
574   * @return  A hash code for this recent login history attempt.
575   */
576  @Override()
577  public int hashCode()
578  {
579    int hashCode = (successful ? 1 : 0);
580    hashCode += (int) timestamp;
581    hashCode += StaticUtils.toLowerCase(authenticationMethod).hashCode();
582
583    if (clientIPAddress != null)
584    {
585      hashCode += StaticUtils.toLowerCase(clientIPAddress).hashCode();
586    }
587
588    if (failureReason != null)
589    {
590      hashCode += StaticUtils.toLowerCase(failureReason).hashCode();
591    }
592
593    if (additionalAttemptCount != null)
594    {
595      hashCode += additionalAttemptCount.hashCode();
596    }
597
598    return hashCode;
599  }
600
601
602
603  /**
604   * Retrieves an integer value that indicates the order of the provided recent
605   * login history attempt relative to this attempt in a sorted list.
606   *
607   * @param  a  The recent login history attempt to compare to this attempt.  It
608   *            must not be {@code null}.
609   *
610   * @return  A negative value integer if this attempt should be ordered before
611   *          the provided attempt in a sorted list, a positive integer if this
612   *          attempt should be ordered after the provided attempt, or zero if
613   *          they are logically equivalent.
614   */
615  @Override()
616  public int compareTo(@NotNull final RecentLoginHistoryAttempt a)
617  {
618    // Order first by timestamp, with newer timestamps coming before older.
619    if (timestamp > a.timestamp)
620    {
621      return -1;
622    }
623    else if (timestamp < a.timestamp)
624    {
625      return 1;
626    }
627
628    // Order successful attempts ahead of failed attempts.
629    if (successful != a.successful)
630    {
631      if (successful)
632      {
633        return -1;
634      }
635      else
636      {
637        return 1;
638      }
639    }
640
641    // Order based on the authentication method.
642    if (! authenticationMethod.equalsIgnoreCase(a.authenticationMethod))
643    {
644      return StaticUtils.toLowerCase(authenticationMethod).compareTo(
645           StaticUtils.toLowerCase(a.authenticationMethod));
646    }
647
648    // Order based on the additional attempt count, with a higher count coming
649    // before a lower/nonexistent count.
650    if (additionalAttemptCount == null)
651    {
652      if (a.additionalAttemptCount != null)
653      {
654        return 1;
655      }
656    }
657    else if (a.additionalAttemptCount == null)
658    {
659      return -1;
660    }
661    else if (additionalAttemptCount > a.additionalAttemptCount)
662    {
663      return -1;
664    }
665    else if (additionalAttemptCount < a.additionalAttemptCount)
666    {
667      return 1;
668    }
669
670    // Order based on the client IP address.  A null address will be ordered
671    // after a non-null address.
672    if (clientIPAddress == null)
673    {
674      if (a.clientIPAddress != null)
675      {
676        return 1;
677      }
678    }
679    else if (a.clientIPAddress == null)
680    {
681      return -1;
682    }
683    else if (! clientIPAddress.equalsIgnoreCase(a.clientIPAddress))
684    {
685      return StaticUtils.toLowerCase(clientIPAddress).compareTo(
686           StaticUtils.toLowerCase(a.clientIPAddress));
687    }
688
689    // Order based on the failure reason.  A null reason will be ordered after
690    // a non-null reason.
691    if ((failureReason != null) &&
692         (! failureReason.equalsIgnoreCase(a.failureReason)))
693    {
694      return StaticUtils.toLowerCase(failureReason).compareTo(
695           StaticUtils.toLowerCase(a.failureReason));
696    }
697
698    // If we've gotten here, then the records must be considered logically
699    // equivalent.
700    return 0;
701  }
702
703
704
705  /**
706   * Retrieves a string representation of this recent login history attempt.
707   *
708   * @return  A string representation of this recent login history attempt.
709   */
710  @Override()
711  @NotNull()
712  public String toString()
713  {
714    return jsonObject.toSingleLineString();
715  }
716}