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}