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}